Skip to content

Commit a2f0e04

Browse files
committed
Apply imports and paths immediately to running interpreters
ensure_imported/1,2 and add_path/1 now apply changes immediately to all running interpreters (contexts and event loops), not just future ones. - Add apply_import_to_interpreters/1 and apply_path_to_interpreters/1 - Apply to all contexts via py_context_router:contexts() - Apply to main event loop via py_event_loop:get_loop() - Apply to pool event loops via py_event_loop_pool:get_all_loops() - Add tests verifying immediate application
1 parent 786e2b7 commit a2f0e04

2 files changed

Lines changed: 173 additions & 36 deletions

File tree

src/py_import.erl

Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@
1515
%%% @doc Import and path registry for Python interpreters.
1616
%%%
1717
%%% This module manages the global import and path registries that are
18-
%%% applied to all Python interpreters. When new interpreters are created,
19-
%%% they automatically get all registered imports and paths applied.
18+
%%% applied to all Python interpreters. Imports and paths are applied
19+
%%% immediately to all running interpreters and stored for new interpreters.
2020
%%%
2121
%%% == Examples ==
2222
%%%
2323
%%% ```
24-
%%% %% Register modules for import in all interpreters
24+
%%% %% Register modules for import in all interpreters (immediate + future)
2525
%%% ok = py_import:ensure_imported(json).
2626
%%% ok = py_import:ensure_imported(math, sqrt).
2727
%%%
28-
%%% %% Add paths to sys.path in all interpreters
28+
%%% %% Add paths to sys.path in all interpreters (immediate + future)
2929
%%% ok = py_import:add_path("/path/to/my/modules").
3030
%%%
3131
%%% %% Check registry contents
@@ -99,48 +99,42 @@ init() ->
9999

100100
%% @doc Register a module for import in all interpreters.
101101
%%
102-
%% Adds the module to the global import registry. When new interpreters
103-
%% are created, they will automatically import all registered modules.
104-
%% The module will be imported lazily when first used.
102+
%% Imports the module immediately in all running interpreters and adds it
103+
%% to the registry for future interpreters.
105104
%%
106105
%% The `__main__' module is never cached.
107106
%%
108107
%% Example:
109108
%% ```
110109
%% ok = py_import:ensure_imported(json),
111-
%% {ok, Result} = py:call(json, dumps, [Data]). %% Module imported on first use
110+
%% {ok, Result} = py:call(json, dumps, [Data]).
112111
%% '''
113112
%%
114113
%% @param Module Python module name
115114
%% @returns ok | {error, Reason}
116115
-spec ensure_imported(py_module()) -> ok | {error, term()}.
117116
ensure_imported(Module) ->
118117
ModuleBin = ensure_binary(Module),
119-
%% Reject __main__
120118
case ModuleBin of
121119
<<"__main__">> ->
122120
{error, main_not_cacheable};
123121
_ ->
124-
%% Add to global registry - module will be imported lazily
125-
case ets:info(?IMPORT_REGISTRY) of
126-
undefined -> ok;
127-
_ -> ets:insert(?IMPORT_REGISTRY, {ModuleBin, all})
128-
end,
122+
ets:insert(?IMPORT_REGISTRY, {ModuleBin, all}),
123+
apply_import_to_interpreters(ModuleBin),
129124
ok
130125
end.
131126

132127
%% @doc Register a module/function for import in all interpreters.
133128
%%
134-
%% Adds the module/function to the global import registry. When new
135-
%% interpreters are created, they will automatically import the module.
136-
%% The module will be imported lazily when first used.
129+
%% Imports the module immediately in all running interpreters and adds it
130+
%% to the registry for future interpreters.
137131
%%
138132
%% The `__main__' module is never cached.
139133
%%
140134
%% Example:
141135
%% ```
142136
%% ok = py_import:ensure_imported(json, dumps),
143-
%% {ok, Result} = py:call(json, dumps, [Data]). %% Module imported on first use
137+
%% {ok, Result} = py:call(json, dumps, [Data]).
144138
%% '''
145139
%%
146140
%% @param Module Python module name
@@ -150,16 +144,12 @@ ensure_imported(Module) ->
150144
ensure_imported(Module, Func) ->
151145
ModuleBin = ensure_binary(Module),
152146
FuncBin = ensure_binary(Func),
153-
%% Reject __main__
154147
case ModuleBin of
155148
<<"__main__">> ->
156149
{error, main_not_cacheable};
157150
_ ->
158-
%% Add to global registry - module will be imported lazily
159-
case ets:info(?IMPORT_REGISTRY) of
160-
undefined -> ok;
161-
_ -> ets:insert(?IMPORT_REGISTRY, {ModuleBin, FuncBin})
162-
end,
151+
ets:insert(?IMPORT_REGISTRY, {ModuleBin, FuncBin}),
152+
apply_import_to_interpreters(ModuleBin),
163153
ok
164154
end.
165155

@@ -268,9 +258,8 @@ import_list() ->
268258

269259
%% @doc Add a path to sys.path in all interpreters.
270260
%%
271-
%% Adds the path to the global path registry. When new interpreters
272-
%% are created, they will automatically have this path in sys.path.
273-
%% The path is inserted at the beginning of sys.path to take precedence.
261+
%% Adds the path immediately to sys.path in all running interpreters
262+
%% (contexts and event loops) and stores it for future interpreters.
274263
%%
275264
%% Example:
276265
%% ```
@@ -283,13 +272,9 @@ import_list() ->
283272
-spec add_path(string() | binary() | atom()) -> ok.
284273
add_path(Path) ->
285274
PathBin = ensure_binary(Path),
286-
case ets:info(?PATH_REGISTRY) of
287-
undefined -> ok;
288-
_ ->
289-
%% Use monotonic time as key to preserve insertion order
290-
Key = erlang:monotonic_time(),
291-
ets:insert(?PATH_REGISTRY, {Key, PathBin})
292-
end,
275+
Key = erlang:monotonic_time(),
276+
ets:insert(?PATH_REGISTRY, {Key, PathBin}),
277+
apply_path_to_interpreters(PathBin),
293278
ok.
294279

295280
%% @doc Add multiple paths to sys.path in all interpreters.
@@ -367,3 +352,75 @@ is_path_added(Path) ->
367352
%% @private
368353
ensure_binary(S) ->
369354
py_util:to_binary(S).
355+
356+
%% @private Apply import to all running interpreters (contexts + event loops)
357+
apply_import_to_interpreters(ModuleBin) ->
358+
Imports = [{ModuleBin, all}],
359+
%% Apply to all contexts
360+
lists:foreach(
361+
fun(Ctx) ->
362+
try
363+
Ref = py_context:get_nif_ref(Ctx),
364+
py_nif:interp_apply_imports(Ref, Imports)
365+
catch _:_ -> ok
366+
end
367+
end,
368+
get_all_contexts()
369+
),
370+
%% Apply to main event loop
371+
case py_event_loop:get_loop() of
372+
{ok, LoopRef} ->
373+
catch py_nif:interp_apply_imports(LoopRef, Imports);
374+
_ -> ok
375+
end,
376+
%% Apply to all pool event loops
377+
case py_event_loop_pool:get_all_loops() of
378+
{ok, Loops} ->
379+
lists:foreach(
380+
fun({LoopRef, _WorkerPid}) ->
381+
catch py_nif:interp_apply_imports(LoopRef, Imports)
382+
end,
383+
Loops
384+
);
385+
_ -> ok
386+
end,
387+
ok.
388+
389+
%% @private Apply path to all running interpreters (contexts + event loops)
390+
apply_path_to_interpreters(PathBin) ->
391+
Paths = [PathBin],
392+
%% Apply to all contexts
393+
lists:foreach(
394+
fun(Ctx) ->
395+
try
396+
Ref = py_context:get_nif_ref(Ctx),
397+
py_nif:interp_apply_paths(Ref, Paths)
398+
catch _:_ -> ok
399+
end
400+
end,
401+
get_all_contexts()
402+
),
403+
%% Apply to main event loop
404+
case py_event_loop:get_loop() of
405+
{ok, LoopRef} ->
406+
catch py_nif:interp_apply_paths(LoopRef, Paths);
407+
_ -> ok
408+
end,
409+
%% Apply to all pool event loops
410+
case py_event_loop_pool:get_all_loops() of
411+
{ok, Loops} ->
412+
lists:foreach(
413+
fun({LoopRef, _WorkerPid}) ->
414+
catch py_nif:interp_apply_paths(LoopRef, Paths)
415+
end,
416+
Loops
417+
);
418+
_ -> ok
419+
end,
420+
ok.
421+
422+
%% @private Get all context pids from all pools
423+
get_all_contexts() ->
424+
DefaultCtxs = try py_context_router:contexts() catch _:_ -> [] end,
425+
%% Could add other pools here if needed
426+
DefaultCtxs.

test/py_import_SUITE.erl

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@
4242
registry_import_in_sys_modules_test/1,
4343
context_import_in_sys_modules_test/1,
4444
%% Path registry tests
45-
add_path_test/1
45+
add_path_test/1,
46+
%% Immediate application tests
47+
import_applies_to_running_interpreter_test/1,
48+
path_applies_to_running_interpreter_test/1
4649
]).
4750

4851
all() ->
@@ -76,7 +79,10 @@ groups() ->
7679
registry_import_in_sys_modules_test,
7780
context_import_in_sys_modules_test,
7881
%% Path registry tests
79-
add_path_test
82+
add_path_test,
83+
%% Immediate application tests
84+
import_applies_to_running_interpreter_test,
85+
path_applies_to_running_interpreter_test
8086
]}].
8187

8288
init_per_suite(Config) ->
@@ -765,3 +771,77 @@ add_path_test(Config) ->
765771
ok = py_import:clear_paths(),
766772

767773
ct:pal("add_path successfully registers paths and enables module imports").
774+
775+
%% ============================================================================
776+
%% Immediate Application Tests
777+
%% ============================================================================
778+
779+
%% @doc Test that ensure_imported applies immediately to running interpreters
780+
%%
781+
%% This verifies that calling ensure_imported on an already-running interpreter
782+
%% makes the module available without needing to create a new context.
783+
import_applies_to_running_interpreter_test(_Config) ->
784+
%% Clear registry
785+
ok = py_import:clear_imports(),
786+
787+
%% Verify 'zipfile' is NOT in sys.modules yet
788+
ok = py:exec(<<"import sys; _zipfile_before = 'zipfile' in sys.modules">>),
789+
{ok, BeforeImport} = py:eval(<<"_zipfile_before">>),
790+
?assertEqual(false, BeforeImport),
791+
792+
%% Now call ensure_imported - should apply immediately
793+
ok = py_import:ensure_imported(zipfile),
794+
795+
%% Verify 'zipfile' IS now in sys.modules (without creating new context)
796+
ok = py:exec(<<"import sys; _zipfile_after = 'zipfile' in sys.modules">>),
797+
{ok, AfterImport} = py:eval(<<"_zipfile_after">>),
798+
?assertEqual(true, AfterImport),
799+
800+
%% Verify we can call functions from zipfile
801+
{ok, _} = py:call(zipfile, 'is_zipfile', [<<"/nonexistent">>]),
802+
803+
ct:pal("ensure_imported applies immediately to running interpreter").
804+
805+
%% @doc Test that add_path applies immediately to running interpreters
806+
%%
807+
%% This verifies that calling add_path on an already-running interpreter
808+
%% makes the path available in sys.path without needing to create a new context.
809+
path_applies_to_running_interpreter_test(Config) ->
810+
%% Clear paths
811+
ok = py_import:clear_paths(),
812+
813+
%% Create test module in priv_dir
814+
PrivDir = ?config(priv_dir, Config),
815+
ModuleDir = filename:join(PrivDir, "immediate_path_test"),
816+
ok = filelib:ensure_dir(filename:join(ModuleDir, "dummy")),
817+
818+
%% Write a simple Python module
819+
ModulePath = filename:join(ModuleDir, "immediate_test_mod.py"),
820+
ModuleContent = <<"IMMEDIATE_TEST_VALUE = 42\n">>,
821+
ok = file:write_file(ModulePath, ModuleContent),
822+
823+
ModuleDirBin = list_to_binary(ModuleDir),
824+
825+
%% Verify path is NOT in sys.path yet
826+
CheckCode = <<"import sys; _path_before = '", ModuleDirBin/binary, "' in sys.path">>,
827+
ok = py:exec(CheckCode),
828+
{ok, BeforePath} = py:eval(<<"_path_before">>),
829+
?assertEqual(false, BeforePath),
830+
831+
%% Now call add_path - should apply immediately
832+
ok = py_import:add_path(ModuleDir),
833+
834+
%% Verify path IS now in sys.path (without creating new context)
835+
CheckAfterCode = <<"import sys; _path_after = '", ModuleDirBin/binary, "' in sys.path">>,
836+
ok = py:exec(CheckAfterCode),
837+
{ok, AfterPath} = py:eval(<<"_path_after">>),
838+
?assertEqual(true, AfterPath),
839+
840+
%% Verify we can import and use the module
841+
{ok, Value} = py:eval(<<"__import__('immediate_test_mod').IMMEDIATE_TEST_VALUE">>),
842+
?assertEqual(42, Value),
843+
844+
%% Clean up
845+
ok = py_import:clear_paths(),
846+
847+
ct:pal("add_path applies immediately to running interpreter").

0 commit comments

Comments
 (0)