Skip to content

Commit a33f3f9

Browse files
authored
Make lazy_load.load partially thread-safe (#90)
1 parent 0d4fb38 commit a33f3f9

File tree

3 files changed

+92
-59
lines changed

3 files changed

+92
-59
lines changed

lazy_loader/__init__.py

Lines changed: 66 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@
99
import importlib.util
1010
import os
1111
import sys
12+
import threading
1213
import types
1314
import warnings
1415

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

1718

19+
threadlock = threading.Lock()
20+
21+
1822
def attach(package_name, submodules=None, submod_attrs=None):
1923
"""Attach lazily loaded submodules, functions, or other attributes.
2024
@@ -179,66 +183,69 @@ def myfunc():
179183
Actual loading of the module occurs upon first attribute request.
180184
181185
"""
182-
module = sys.modules.get(fullname)
183-
have_module = module is not None
184-
185-
# Most common, short-circuit
186-
if have_module and require is None:
187-
return module
188-
189-
if "." in fullname:
190-
msg = (
191-
"subpackages can technically be lazily loaded, but it causes the "
192-
"package to be eagerly loaded even if it is already lazily loaded."
193-
"So, you probably shouldn't use subpackages with this lazy feature."
194-
)
195-
warnings.warn(msg, RuntimeWarning)
196-
197-
spec = None
198-
if not have_module:
199-
spec = importlib.util.find_spec(fullname)
200-
have_module = spec is not None
201-
202-
if not have_module:
203-
not_found_message = f"No module named '{fullname}'"
204-
elif require is not None:
205-
try:
206-
have_module = _check_requirement(require)
207-
except ModuleNotFoundError as e:
208-
raise ValueError(
209-
f"Found module '{fullname}' but cannot test requirement '{require}'. "
210-
"Requirements must match distribution name, not module name."
211-
) from e
212-
213-
not_found_message = f"No distribution can be found matching '{require}'"
214-
215-
if not have_module:
216-
if error_on_import:
217-
raise ModuleNotFoundError(not_found_message)
218-
import inspect
219-
220-
try:
221-
parent = inspect.stack()[1]
222-
frame_data = {
223-
"filename": parent.filename,
224-
"lineno": parent.lineno,
225-
"function": parent.function,
226-
"code_context": parent.code_context,
227-
}
228-
return DelayedImportErrorModule(
229-
frame_data,
230-
"DelayedImportErrorModule",
231-
message=not_found_message,
186+
with threadlock:
187+
module = sys.modules.get(fullname)
188+
have_module = module is not None
189+
190+
# Most common, short-circuit
191+
if have_module and require is None:
192+
return module
193+
194+
if "." in fullname:
195+
msg = (
196+
"subpackages can technically be lazily loaded, but it causes the "
197+
"package to be eagerly loaded even if it is already lazily loaded."
198+
"So, you probably shouldn't use subpackages with this lazy feature."
232199
)
233-
finally:
234-
del parent
235-
236-
if spec is not None:
237-
module = importlib.util.module_from_spec(spec)
238-
sys.modules[fullname] = module
239-
240-
loader = importlib.util.LazyLoader(spec.loader)
241-
loader.exec_module(module)
200+
warnings.warn(msg, RuntimeWarning)
201+
202+
spec = None
203+
204+
if not have_module:
205+
spec = importlib.util.find_spec(fullname)
206+
have_module = spec is not None
207+
208+
if not have_module:
209+
not_found_message = f"No module named '{fullname}'"
210+
elif require is not None:
211+
try:
212+
have_module = _check_requirement(require)
213+
except ModuleNotFoundError as e:
214+
raise ValueError(
215+
f"Found module '{fullname}' but cannot test "
216+
"requirement '{require}'. "
217+
"Requirements must match distribution name, not module name."
218+
) from e
219+
220+
not_found_message = f"No distribution can be found matching '{require}'"
221+
222+
if not have_module:
223+
if error_on_import:
224+
raise ModuleNotFoundError(not_found_message)
225+
import inspect
226+
227+
try:
228+
parent = inspect.stack()[1]
229+
frame_data = {
230+
"filename": parent.filename,
231+
"lineno": parent.lineno,
232+
"function": parent.function,
233+
"code_context": parent.code_context,
234+
}
235+
return DelayedImportErrorModule(
236+
frame_data,
237+
"DelayedImportErrorModule",
238+
message=not_found_message,
239+
)
240+
finally:
241+
del parent
242+
243+
if spec is not None:
244+
module = importlib.util.module_from_spec(spec)
245+
sys.modules[fullname] = module
246+
247+
loader = importlib.util.LazyLoader(spec.loader)
248+
loader.exec_module(module)
242249

243250
return module
244251

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
from unittest import mock
@@ -172,3 +174,14 @@ def test_require_kwarg():
172174
# raise a ValueError
173175
with pytest.raises(ValueError):
174176
lazy.load("math", require="somepkg >= 1.0")
177+
178+
179+
def test_parallel_load():
180+
pytest.importorskip("numpy")
181+
182+
subprocess.run(
183+
[
184+
sys.executable,
185+
os.path.join(os.path.dirname(__file__), "import_np_parallel.py"),
186+
]
187+
)

0 commit comments

Comments
 (0)