Skip to content

Commit 46da604

Browse files
committed
ENH: Implement load_requirement
1 parent 79ccf2d commit 46da604

File tree

2 files changed

+67
-7
lines changed

2 files changed

+67
-7
lines changed

lazy_loader/__init__.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
import types
1414
import warnings
1515

16+
try:
17+
import importlib_metadata
18+
except ImportError:
19+
import importlib.metadata as importlib_metadata
20+
1621
__all__ = ["attach", "load", "attach_stub"]
1722

1823

@@ -99,17 +104,18 @@ def __dir__():
99104

100105

101106
class DelayedImportErrorModule(types.ModuleType):
102-
def __init__(self, frame_data, *args, **kwargs):
107+
def __init__(self, frame_data, *args, message=None, **kwargs):
103108
self.__frame_data = frame_data
109+
self.__message = message or f"No module named '{frame_data['spec']}'"
104110
super().__init__(*args, **kwargs)
105111

106112
def __getattr__(self, x):
107-
if x in ("__class__", "__file__", "__frame_data"):
113+
if x in ("__class__", "__file__", "__frame_data", "__message"):
108114
super().__getattr__(x)
109115
else:
110116
fd = self.__frame_data
111117
raise ModuleNotFoundError(
112-
f"No module named '{fd['spec']}'\n\n"
118+
f"{self.__message}\n\n"
113119
"This error is lazily reported, having originally occured in\n"
114120
f' File {fd["filename"]}, line {fd["lineno"]}, in {fd["function"]}\n\n'
115121
f'----> {"".join(fd["code_context"]).strip()}'
@@ -185,20 +191,33 @@ def myfunc():
185191
warnings.warn(msg, RuntimeWarning)
186192

187193
spec = importlib.util.find_spec(fullname)
194+
return _module_from_spec(
195+
spec,
196+
fullname,
197+
f"No module named '{fullname}'",
198+
error_on_import,
199+
)
200+
201+
202+
def _module_from_spec(spec, fullname, failure_message, error_on_import):
203+
"""Return lazy module, DelayedImportErrorModule, or raise error"""
188204
if spec is None:
189205
if error_on_import:
190-
raise ModuleNotFoundError(f"No module named '{fullname}'")
206+
raise ModuleNotFoundError(failure_message)
191207
else:
192208
try:
193-
parent = inspect.stack()[1]
209+
parent = inspect.stack()[2]
194210
frame_data = {
195-
"spec": fullname,
196211
"filename": parent.filename,
197212
"lineno": parent.lineno,
198213
"function": parent.function,
199214
"code_context": parent.code_context,
200215
}
201-
return DelayedImportErrorModule(frame_data, "DelayedImportErrorModule")
216+
return DelayedImportErrorModule(
217+
frame_data,
218+
"DelayedImportErrorModule",
219+
message=failure_message,
220+
)
202221
finally:
203222
del parent
204223

@@ -269,3 +288,40 @@ def attach_stub(package_name: str, filename: str):
269288
visitor = _StubVisitor()
270289
visitor.visit(stub_node)
271290
return attach(package_name, visitor._submodules, visitor._submod_attrs)
291+
292+
293+
def load_requirement(requirement, fullname=None, error_on_import=False):
294+
# Old style lazy loading to avoid polluting sys.modules
295+
import packaging.requirements
296+
297+
req = packaging.requirements.Requirement(requirement)
298+
299+
if fullname is None:
300+
fullname = req.name
301+
302+
not_found_msg = f"No module named '{fullname}'"
303+
304+
module = sys.modules.get(fullname)
305+
have_mod = module is not None
306+
if not have_mod:
307+
spec = importlib.util.find_spec(fullname)
308+
have_mod = spec is not None
309+
310+
if have_mod and req.specifier:
311+
# Note: req.name is the distribution name, not the module name
312+
try:
313+
version = importlib_metadata.version(req.name)
314+
except importlib_metadata.PackageNotFoundError as e:
315+
raise ValueError(
316+
f"Found module '{fullname}' but cannot test requirement '{req}'. "
317+
"Requirements must match distribution name, not module name."
318+
) from e
319+
have_mod = any(req.specifier.filter((version,)))
320+
if not have_mod:
321+
spec = None
322+
not_found_msg = f"No distribution can be found matching '{req}'"
323+
324+
if have_mod and module is not None:
325+
return module, have_mod
326+
327+
return _module_from_spec(spec, fullname, not_found_msg, error_on_import), have_mod

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ classifiers = [
2121
"Programming Language :: Python :: 3.12",
2222
]
2323
description = "Makes it easy to load subpackages and functions on demand."
24+
dependencies = [
25+
"packaging",
26+
"importlib_metadata; python_version < '3.9'",
27+
]
2428

2529
[project.optional-dependencies]
2630
test = ["pytest >= 7.4", "pytest-cov >= 4.1"]

0 commit comments

Comments
 (0)