Skip to content

Commit a767346

Browse files
committed
Extract import/path registry into py_import module
Move import and path registry functions from py.erl to new py_import module for better separation of concerns. The py_import module now provides init/0, ensure_imported/1,2, is_imported/1,2, all_imports/0, clear_imports/0, import_stats/0, import_list/0, add_path/1, add_paths/1, all_paths/0, clear_paths/0, and is_path_added/1. This is a breaking change - users must now use py_import: instead of py: for these functions.
1 parent 0788cbc commit a767346

6 files changed

Lines changed: 487 additions & 444 deletions

File tree

src/erlang_python_app.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
-export([start/2, stop/1]).
2121

2222
start(_StartType, _StartArgs) ->
23-
%% Initialize the import registry ETS table
24-
py:init_import_registry(),
23+
%% Initialize the import registry ETS tables
24+
py_import:init(),
2525
erlang_python_sup:start_link().
2626

2727
stop(_State) ->

src/py.erl

Lines changed: 0 additions & 334 deletions
Original file line numberDiff line numberDiff line change
@@ -56,23 +56,6 @@
5656
stream/4,
5757
stream_eval/1,
5858
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,
7659
version/0,
7760
memory_stats/0,
7861
gc/0,
@@ -175,12 +158,6 @@
175158
%% Process dictionary key for local Python environment
176159
-define(LOCAL_ENV_KEY, py_local_env).
177160

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-
184161
%% @doc Get or create a process-local Python environment for a context.
185162
%%
186163
%% Each Erlang process can have Python environments per interpreter.
@@ -350,317 +327,6 @@ exec(Ctx, Code) when is_pid(Ctx) ->
350327
EnvRef = get_local_env(Ctx),
351328
py_context:exec(Ctx, Code, EnvRef).
352329

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-
664330
%%% ============================================================================
665331
%%% Asynchronous API
666332
%%% ============================================================================

0 commit comments

Comments
 (0)