Skip to content

Commit d56c717

Browse files
committed
Make lazy_load.load thread-safe
Closes #88
1 parent bf82b68 commit d56c717

File tree

3 files changed

+69
-36
lines changed

3 files changed

+69
-36
lines changed

lazy_loader/__init__.py

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
import inspect
1111
import os
1212
import sys
13+
import threading
1314
import types
1415
import warnings
1516

1617
__all__ = ["attach", "load", "attach_stub"]
1718

1819

20+
threadlock = threading.Lock()
21+
22+
1923
def attach(package_name, submodules=None, submod_attrs=None):
2024
"""Attach lazily loaded submodules, functions, or other attributes.
2125
@@ -171,42 +175,45 @@ def myfunc():
171175
Actual loading of the module occurs upon first attribute request.
172176
173177
"""
174-
try:
175-
return sys.modules[fullname]
176-
except KeyError:
177-
pass
178-
179-
if "." in fullname:
180-
msg = (
181-
"subpackages can technically be lazily loaded, but it causes the "
182-
"package to be eagerly loaded even if it is already lazily loaded."
183-
"So, you probably shouldn't use subpackages with this lazy feature."
184-
)
185-
warnings.warn(msg, RuntimeWarning)
186-
187-
spec = importlib.util.find_spec(fullname)
188-
if spec is None:
189-
if error_on_import:
190-
raise ModuleNotFoundError(f"No module named '{fullname}'")
191-
else:
192-
try:
193-
parent = inspect.stack()[1]
194-
frame_data = {
195-
"spec": fullname,
196-
"filename": parent.filename,
197-
"lineno": parent.lineno,
198-
"function": parent.function,
199-
"code_context": parent.code_context,
200-
}
201-
return DelayedImportErrorModule(frame_data, "DelayedImportErrorModule")
202-
finally:
203-
del parent
204-
205-
module = importlib.util.module_from_spec(spec)
206-
sys.modules[fullname] = module
207-
208-
loader = importlib.util.LazyLoader(spec.loader)
209-
loader.exec_module(module)
178+
with threadlock:
179+
try:
180+
return sys.modules[fullname]
181+
except KeyError:
182+
pass
183+
184+
if "." in fullname:
185+
msg = (
186+
"subpackages can technically be lazily loaded, but it causes the "
187+
"package to be eagerly loaded even if it is already lazily loaded."
188+
"So, you probably shouldn't use subpackages with this lazy feature."
189+
)
190+
warnings.warn(msg, RuntimeWarning)
191+
192+
spec = importlib.util.find_spec(fullname)
193+
if spec is None:
194+
if error_on_import:
195+
raise ModuleNotFoundError(f"No module named '{fullname}'")
196+
else:
197+
try:
198+
parent = inspect.stack()[1]
199+
frame_data = {
200+
"spec": fullname,
201+
"filename": parent.filename,
202+
"lineno": parent.lineno,
203+
"function": parent.function,
204+
"code_context": parent.code_context,
205+
}
206+
return DelayedImportErrorModule(
207+
frame_data, "DelayedImportErrorModule"
208+
)
209+
finally:
210+
del parent
211+
212+
module = importlib.util.module_from_spec(spec)
213+
sys.modules[fullname] = module
214+
215+
loader = importlib.util.LazyLoader(spec.loader)
216+
loader.exec_module(module)
210217

211218
return module
212219

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import threading
2+
import time
3+
4+
import lazy_loader as lazy
5+
6+
7+
def import_np():
8+
time.sleep(0.5)
9+
lazy.load("numpy")
10+
11+
12+
for _ in range(10):
13+
threading.Thread(target=import_np).start()

lazy_loader/tests/test_lazy_loader.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import importlib
2+
import os
3+
import subprocess
24
import sys
35
import types
46

@@ -149,3 +151,14 @@ def test_stub_loading_errors(tmp_path):
149151

150152
with pytest.raises(ValueError, match="Cannot load imports from non-existent stub"):
151153
lazy.attach_stub("name", "not a file")
154+
155+
156+
def test_parallel_load():
157+
pytest.importorskip("numpy")
158+
159+
subprocess.run(
160+
[
161+
sys.executable,
162+
os.path.join(os.path.dirname(__file__), "import_np_parallel.py"),
163+
]
164+
)

0 commit comments

Comments
 (0)