|
56 | 56 | stream/4, |
57 | 57 | stream_eval/1, |
58 | 58 | stream_eval/2, |
59 | | - %% Module import caching |
60 | | - ensure_imported/1, |
61 | | - ensure_imported/2, |
62 | | - is_imported/1, |
63 | | - is_imported/2, |
64 | | - import_stats/0, |
65 | | - import_list/0, |
66 | | - %% Import registry (global list applied to all interpreters) |
67 | | - init_import_registry/0, |
68 | | - all_imports/0, |
69 | | - clear_imports/0, |
70 | | - %% Path registry (sys.path additions applied to all interpreters) |
71 | | - add_path/1, |
72 | | - add_paths/1, |
73 | | - all_paths/0, |
74 | | - clear_paths/0, |
75 | | - is_path_added/1, |
76 | 59 | version/0, |
77 | 60 | memory_stats/0, |
78 | 61 | gc/0, |
|
175 | 158 | %% Process dictionary key for local Python environment |
176 | 159 | -define(LOCAL_ENV_KEY, py_local_env). |
177 | 160 |
|
178 | | -%% ETS table for global import registry |
179 | | --define(IMPORT_REGISTRY, py_import_registry). |
180 | | - |
181 | | -%% ETS table for global path registry (sys.path additions) |
182 | | --define(PATH_REGISTRY, py_path_registry). |
183 | | - |
184 | 161 | %% @doc Get or create a process-local Python environment for a context. |
185 | 162 | %% |
186 | 163 | %% Each Erlang process can have Python environments per interpreter. |
@@ -350,317 +327,6 @@ exec(Ctx, Code) when is_pid(Ctx) -> |
350 | 327 | EnvRef = get_local_env(Ctx), |
351 | 328 | py_context:exec(Ctx, Code, EnvRef). |
352 | 329 |
|
353 | | -%%% ============================================================================ |
354 | | -%%% Module Import Caching |
355 | | -%%% ============================================================================ |
356 | | - |
357 | | -%% @doc Initialize the import and path registry ETS tables. |
358 | | -%% |
359 | | -%% This is called automatically during application startup. |
360 | | -%% Safe to call multiple times - does nothing if already initialized. |
361 | | -%% |
362 | | -%% @returns ok |
363 | | --spec init_import_registry() -> ok. |
364 | | -init_import_registry() -> |
365 | | - case ets:info(?IMPORT_REGISTRY) of |
366 | | - undefined -> |
367 | | - %% Use bag type to allow multiple entries with same module name |
368 | | - %% e.g., {<<"json">>, all} and {<<"json">>, <<"dumps">>} |
369 | | - ets:new(?IMPORT_REGISTRY, [bag, public, named_table]), |
370 | | - ok; |
371 | | - _ -> |
372 | | - ok |
373 | | - end, |
374 | | - case ets:info(?PATH_REGISTRY) of |
375 | | - undefined -> |
376 | | - %% Use set type - paths are unique, ordered by insertion |
377 | | - ets:new(?PATH_REGISTRY, [ordered_set, public, named_table]), |
378 | | - ok; |
379 | | - _ -> |
380 | | - ok |
381 | | - end. |
382 | | - |
383 | | -%% @doc Register a module for import in all interpreters. |
384 | | -%% |
385 | | -%% Adds the module to the global import registry. When new interpreters |
386 | | -%% are created, they will automatically import all registered modules. |
387 | | -%% The module will be imported lazily when first used. |
388 | | -%% |
389 | | -%% The `__main__' module is never cached. |
390 | | -%% |
391 | | -%% Example: |
392 | | -%% ``` |
393 | | -%% ok = py:ensure_imported(json), |
394 | | -%% {ok, Result} = py:call(json, dumps, [Data]). %% Module imported on first use |
395 | | -%% ''' |
396 | | -%% |
397 | | -%% @param Module Python module name |
398 | | -%% @returns ok | {error, Reason} |
399 | | --spec ensure_imported(py_module()) -> ok | {error, term()}. |
400 | | -ensure_imported(Module) -> |
401 | | - ModuleBin = ensure_binary(Module), |
402 | | - %% Reject __main__ |
403 | | - case ModuleBin of |
404 | | - <<"__main__">> -> |
405 | | - {error, main_not_cacheable}; |
406 | | - _ -> |
407 | | - %% Add to global registry - module will be imported lazily |
408 | | - case ets:info(?IMPORT_REGISTRY) of |
409 | | - undefined -> ok; |
410 | | - _ -> ets:insert(?IMPORT_REGISTRY, {ModuleBin, all}) |
411 | | - end, |
412 | | - ok |
413 | | - end. |
414 | | - |
415 | | -%% @doc Register a module/function for import in all interpreters. |
416 | | -%% |
417 | | -%% Adds the module/function to the global import registry. When new |
418 | | -%% interpreters are created, they will automatically import the module. |
419 | | -%% The module will be imported lazily when first used. |
420 | | -%% |
421 | | -%% The `__main__' module is never cached. |
422 | | -%% |
423 | | -%% Example: |
424 | | -%% ``` |
425 | | -%% ok = py:ensure_imported(json, dumps), |
426 | | -%% {ok, Result} = py:call(json, dumps, [Data]). %% Module imported on first use |
427 | | -%% ''' |
428 | | -%% |
429 | | -%% @param Module Python module name |
430 | | -%% @param Func Function name to register |
431 | | -%% @returns ok | {error, Reason} |
432 | | --spec ensure_imported(py_module(), py_func()) -> ok | {error, term()}. |
433 | | -ensure_imported(Module, Func) -> |
434 | | - ModuleBin = ensure_binary(Module), |
435 | | - FuncBin = ensure_binary(Func), |
436 | | - %% Reject __main__ |
437 | | - case ModuleBin of |
438 | | - <<"__main__">> -> |
439 | | - {error, main_not_cacheable}; |
440 | | - _ -> |
441 | | - %% Add to global registry - module will be imported lazily |
442 | | - case ets:info(?IMPORT_REGISTRY) of |
443 | | - undefined -> ok; |
444 | | - _ -> ets:insert(?IMPORT_REGISTRY, {ModuleBin, FuncBin}) |
445 | | - end, |
446 | | - ok |
447 | | - end. |
448 | | - |
449 | | -%% @doc Check if a module is registered in the import registry. |
450 | | -%% |
451 | | -%% @param Module Python module name |
452 | | -%% @returns true if module is registered, false otherwise |
453 | | --spec is_imported(py_module()) -> boolean(). |
454 | | -is_imported(Module) -> |
455 | | - ModuleBin = ensure_binary(Module), |
456 | | - case ets:info(?IMPORT_REGISTRY) of |
457 | | - undefined -> false; |
458 | | - _ -> ets:member(?IMPORT_REGISTRY, ModuleBin) |
459 | | - end. |
460 | | - |
461 | | -%% @doc Check if a module/function is registered in the import registry. |
462 | | -%% |
463 | | -%% @param Module Python module name |
464 | | -%% @param Func Function name |
465 | | -%% @returns true if module/function is registered, false otherwise |
466 | | --spec is_imported(py_module(), py_func()) -> boolean(). |
467 | | -is_imported(Module, Func) -> |
468 | | - ModuleBin = ensure_binary(Module), |
469 | | - FuncBin = ensure_binary(Func), |
470 | | - case ets:info(?IMPORT_REGISTRY) of |
471 | | - undefined -> false; |
472 | | - _ -> |
473 | | - case ets:lookup(?IMPORT_REGISTRY, ModuleBin) of |
474 | | - [{_, all}] -> true; |
475 | | - [{_, FuncBin}] -> true; |
476 | | - _ -> false |
477 | | - end |
478 | | - end. |
479 | | - |
480 | | -%% @doc Get all registered imports from the global registry. |
481 | | -%% |
482 | | -%% Returns a list of {Module, Func | all} tuples representing all |
483 | | -%% modules/functions registered for automatic import. |
484 | | -%% |
485 | | -%% Example: |
486 | | -%% ``` |
487 | | -%% ok = py:ensure_imported(json), |
488 | | -%% ok = py:ensure_imported(math, sqrt), |
489 | | -%% [{<<"json">>, all}, {<<"math">>, <<"sqrt">>}] = py:all_imports(). |
490 | | -%% ''' |
491 | | -%% |
492 | | -%% @returns List of {Module, Func | all} tuples |
493 | | --spec all_imports() -> [{binary(), binary() | all}]. |
494 | | -all_imports() -> |
495 | | - case ets:info(?IMPORT_REGISTRY) of |
496 | | - undefined -> []; |
497 | | - _ -> ets:tab2list(?IMPORT_REGISTRY) |
498 | | - end. |
499 | | - |
500 | | -%% @doc Clear all registered imports from the global registry. |
501 | | -%% |
502 | | -%% Removes all entries from the registry. |
503 | | -%% Does not affect already-running interpreters. |
504 | | -%% |
505 | | -%% @returns ok |
506 | | --spec clear_imports() -> ok. |
507 | | -clear_imports() -> |
508 | | - case ets:info(?IMPORT_REGISTRY) of |
509 | | - undefined -> ok; |
510 | | - _ -> ets:delete_all_objects(?IMPORT_REGISTRY) |
511 | | - end, |
512 | | - ok. |
513 | | - |
514 | | -%%% ============================================================================ |
515 | | -%%% Path Registry (sys.path additions) |
516 | | -%%% ============================================================================ |
517 | | - |
518 | | -%% @doc Add a path to sys.path in all interpreters. |
519 | | -%% |
520 | | -%% Adds the path to the global path registry. When new interpreters |
521 | | -%% are created, they will automatically have this path in sys.path. |
522 | | -%% The path is inserted at the beginning of sys.path to take precedence. |
523 | | -%% |
524 | | -%% Example: |
525 | | -%% ``` |
526 | | -%% ok = py:add_path("/path/to/my/modules"), |
527 | | -%% {ok, Result} = py:call(mymodule, myfunc, []). |
528 | | -%% ''' |
529 | | -%% |
530 | | -%% @param Path Directory path to add (string, binary, or atom) |
531 | | -%% @returns ok |
532 | | --spec add_path(string() | binary() | atom()) -> ok. |
533 | | -add_path(Path) -> |
534 | | - PathBin = ensure_binary(Path), |
535 | | - case ets:info(?PATH_REGISTRY) of |
536 | | - undefined -> ok; |
537 | | - _ -> |
538 | | - %% Use monotonic time as key to preserve insertion order |
539 | | - Key = erlang:monotonic_time(), |
540 | | - ets:insert(?PATH_REGISTRY, {Key, PathBin}) |
541 | | - end, |
542 | | - ok. |
543 | | - |
544 | | -%% @doc Add multiple paths to sys.path in all interpreters. |
545 | | -%% |
546 | | -%% Adds all paths to the global path registry. Paths are added in order, |
547 | | -%% so the first path in the list will be first in sys.path. |
548 | | -%% |
549 | | -%% Example: |
550 | | -%% ``` |
551 | | -%% ok = py:add_paths(["/path/to/lib1", "/path/to/lib2"]), |
552 | | -%% {ok, Result} = py:call(mymodule, myfunc, []). |
553 | | -%% ''' |
554 | | -%% |
555 | | -%% @param Paths List of directory paths to add |
556 | | -%% @returns ok |
557 | | --spec add_paths([string() | binary() | atom()]) -> ok. |
558 | | -add_paths(Paths) when is_list(Paths) -> |
559 | | - lists:foreach(fun add_path/1, Paths), |
560 | | - ok. |
561 | | - |
562 | | -%% @doc Get all registered paths from the global registry. |
563 | | -%% |
564 | | -%% Returns a list of paths in the order they were added. |
565 | | -%% |
566 | | -%% Example: |
567 | | -%% ``` |
568 | | -%% ok = py:add_path("/path/to/modules"), |
569 | | -%% [<<"/path/to/modules">>] = py:all_paths(). |
570 | | -%% ''' |
571 | | -%% |
572 | | -%% @returns List of paths as binaries |
573 | | --spec all_paths() -> [binary()]. |
574 | | -all_paths() -> |
575 | | - case ets:info(?PATH_REGISTRY) of |
576 | | - undefined -> []; |
577 | | - _ -> |
578 | | - %% ordered_set returns in key order (monotonic time = insertion order) |
579 | | - [Path || {_Key, Path} <- ets:tab2list(?PATH_REGISTRY)] |
580 | | - end. |
581 | | - |
582 | | -%% @doc Clear all registered paths from the global registry. |
583 | | -%% |
584 | | -%% Removes all entries from the path registry. |
585 | | -%% Does not affect already-running interpreters. |
586 | | -%% |
587 | | -%% @returns ok |
588 | | --spec clear_paths() -> ok. |
589 | | -clear_paths() -> |
590 | | - case ets:info(?PATH_REGISTRY) of |
591 | | - undefined -> ok; |
592 | | - _ -> ets:delete_all_objects(?PATH_REGISTRY) |
593 | | - end, |
594 | | - ok. |
595 | | - |
596 | | -%% @doc Check if a path is registered in the path registry. |
597 | | -%% |
598 | | -%% @param Path Directory path to check |
599 | | -%% @returns true if path is registered, false otherwise |
600 | | --spec is_path_added(string() | binary() | atom()) -> boolean(). |
601 | | -is_path_added(Path) -> |
602 | | - PathBin = ensure_binary(Path), |
603 | | - case ets:info(?PATH_REGISTRY) of |
604 | | - undefined -> false; |
605 | | - _ -> |
606 | | - case ets:match(?PATH_REGISTRY, {'_', PathBin}) of |
607 | | - [] -> false; |
608 | | - _ -> true |
609 | | - end |
610 | | - end. |
611 | | - |
612 | | -%% @doc Get import registry statistics. |
613 | | -%% |
614 | | -%% Returns a map with the count of registered imports. |
615 | | -%% |
616 | | -%% Example: |
617 | | -%% ``` |
618 | | -%% {ok, #{count => 5}} = py:import_stats(). |
619 | | -%% ''' |
620 | | -%% |
621 | | -%% @returns {ok, Stats} where Stats is a map with registry metrics |
622 | | --spec import_stats() -> {ok, map()} | {error, term()}. |
623 | | -import_stats() -> |
624 | | - Count = case ets:info(?IMPORT_REGISTRY) of |
625 | | - undefined -> 0; |
626 | | - _ -> ets:info(?IMPORT_REGISTRY, size) |
627 | | - end, |
628 | | - {ok, #{count => Count}}. |
629 | | - |
630 | | -%% @doc List all registered imports. |
631 | | -%% |
632 | | -%% Returns a map of modules to their registered functions. |
633 | | -%% Module names are binary keys, function lists are the values. |
634 | | -%% An empty list means only the module is registered (no specific functions). |
635 | | -%% |
636 | | -%% Example: |
637 | | -%% ``` |
638 | | -%% ok = py:import(json), |
639 | | -%% ok = py:import(json, dumps), |
640 | | -%% ok = py:import(json, loads), |
641 | | -%% ok = py:import(math), |
642 | | -%% {ok, #{<<"json">> => [<<"dumps">>, <<"loads">>], |
643 | | -%% <<"math">> => []}} = py:import_list(). |
644 | | -%% ''' |
645 | | -%% |
646 | | -%% @returns {ok, #{Module => [Func]}} map of modules to functions |
647 | | --spec import_list() -> {ok, #{binary() => [binary()]}} | {error, term()}. |
648 | | -import_list() -> |
649 | | - Imports = all_imports(), |
650 | | - %% Group by module |
651 | | - Map = lists:foldl(fun({Module, FuncOrAll}, Acc) -> |
652 | | - Existing = maps:get(Module, Acc, []), |
653 | | - case FuncOrAll of |
654 | | - all -> |
655 | | - %% Module-level import, don't add to function list |
656 | | - maps:put(Module, Existing, Acc); |
657 | | - Func -> |
658 | | - %% Function-level import |
659 | | - maps:put(Module, [Func | Existing], Acc) |
660 | | - end |
661 | | - end, #{}, Imports), |
662 | | - {ok, Map}. |
663 | | - |
664 | 330 | %%% ============================================================================ |
665 | 331 | %%% Asynchronous API |
666 | 332 | %%% ============================================================================ |
|
0 commit comments