Skip to content

Commit dd07cc3

Browse files
committed
ENH: Implement load_requirement
1 parent 43d6a63 commit dd07cc3

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
@@ -12,6 +12,11 @@
1212
import sys
1313
import types
1414

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

1722

@@ -98,17 +103,18 @@ def __dir__():
98103

99104

100105
class DelayedImportErrorModule(types.ModuleType):
101-
def __init__(self, frame_data, *args, **kwargs):
106+
def __init__(self, frame_data, *args, message=None, **kwargs):
102107
self.__frame_data = frame_data
108+
self.__message = message or f"No module named '{frame_data['spec']}'"
103109
super().__init__(*args, **kwargs)
104110

105111
def __getattr__(self, x):
106-
if x in ("__class__", "__file__", "__frame_data"):
112+
if x in ("__class__", "__file__", "__frame_data", "__message"):
107113
super().__getattr__(x)
108114
else:
109115
fd = self.__frame_data
110116
raise ModuleNotFoundError(
111-
f"No module named '{fd['spec']}'\n\n"
117+
f"{self.__message}\n\n"
112118
"This error is lazily reported, having originally occured in\n"
113119
f' File {fd["filename"]}, line {fd["lineno"]}, in {fd["function"]}\n\n'
114120
f'----> {"".join(fd["code_context"]).strip()}'
@@ -166,20 +172,33 @@ def myfunc():
166172
pass
167173

168174
spec = importlib.util.find_spec(fullname)
175+
return _module_from_spec(
176+
spec,
177+
fullname,
178+
f"No module named '{fullname}'",
179+
error_on_import,
180+
)
181+
182+
183+
def _module_from_spec(spec, fullname, failure_message, error_on_import):
184+
"""Return lazy module, DelayedImportErrorModule, or raise error"""
169185
if spec is None:
170186
if error_on_import:
171-
raise ModuleNotFoundError(f"No module named '{fullname}'")
187+
raise ModuleNotFoundError(failure_message)
172188
else:
173189
try:
174-
parent = inspect.stack()[1]
190+
parent = inspect.stack()[2]
175191
frame_data = {
176-
"spec": fullname,
177192
"filename": parent.filename,
178193
"lineno": parent.lineno,
179194
"function": parent.function,
180195
"code_context": parent.code_context,
181196
}
182-
return DelayedImportErrorModule(frame_data, "DelayedImportErrorModule")
197+
return DelayedImportErrorModule(
198+
frame_data,
199+
"DelayedImportErrorModule",
200+
message=failure_message,
201+
)
183202
finally:
184203
del parent
185204

@@ -250,3 +269,40 @@ def attach_stub(package_name: str, filename: str):
250269
visitor = _StubVisitor()
251270
visitor.visit(stub_node)
252271
return attach(package_name, visitor._submodules, visitor._submod_attrs)
272+
273+
274+
def load_requirement(requirement, fullname=None, error_on_import=False):
275+
# Old style lazy loading to avoid polluting sys.modules
276+
import packaging.requirements
277+
278+
req = packaging.requirements.Requirement(requirement)
279+
280+
if fullname is None:
281+
fullname = req.name
282+
283+
not_found_msg = f"No module named '{fullname}'"
284+
285+
module = sys.modules.get(fullname)
286+
have_mod = module is not None
287+
if not have_mod:
288+
spec = importlib.util.find_spec(fullname)
289+
have_mod = spec is not None
290+
291+
if have_mod and req.specifier:
292+
# Note: req.name is the distribution name, not the module name
293+
try:
294+
version = importlib_metadata.version(req.name)
295+
except importlib_metadata.PackageNotFoundError as e:
296+
raise ValueError(
297+
f"Found module '{fullname}' but cannot test requirement '{req}'. "
298+
"Requirements must match distribution name, not module name."
299+
) from e
300+
have_mod = any(req.specifier.filter((version,)))
301+
if not have_mod:
302+
spec = None
303+
not_found_msg = f"No distribution can be found matching '{req}'"
304+
305+
if have_mod and module is not None:
306+
return module, have_mod
307+
308+
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
@@ -19,6 +19,10 @@ classifiers = [
1919
"Programming Language :: Python :: 3.10",
2020
"Programming Language :: Python :: 3.11",
2121
]
22+
dependencies = [
23+
"packaging",
24+
"importlib_metadata; python_version < '3.9'",
25+
]
2226
dynamic = ["description"]
2327

2428

0 commit comments

Comments
 (0)