Skip to content

Commit df17386

Browse files
committed
RF: Rewrite load_requirement as argument to load, add have_module() function
1 parent 46da604 commit df17386

File tree

1 file changed

+61
-77
lines changed

1 file changed

+61
-77
lines changed

lazy_loader/__init__.py

Lines changed: 61 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ def __dir__():
104104

105105

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

112112
def __getattr__(self, x):
@@ -122,7 +122,7 @@ def __getattr__(self, x):
122122
)
123123

124124

125-
def load(fullname, error_on_import=False):
125+
def load(fullname, *, require=None, error_on_import=False):
126126
"""Return a lazily imported proxy for a module.
127127
128128
We often see the following pattern::
@@ -177,10 +177,12 @@ def myfunc():
177177
Actual loading of the module occurs upon first attribute request.
178178
179179
"""
180-
try:
181-
return sys.modules[fullname]
182-
except KeyError:
183-
pass
180+
module = sys.modules.get(fullname)
181+
have_module = module is not None
182+
183+
# Most common, short-circuit
184+
if have_module and require is None:
185+
return module
184186

185187
if "." in fullname:
186188
msg = (
@@ -190,46 +192,65 @@ def myfunc():
190192
)
191193
warnings.warn(msg, RuntimeWarning)
192194

193-
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-
)
195+
spec = None
196+
if not have_module:
197+
spec = importlib.util.find_spec(fullname)
198+
have_module = spec is not None
199+
200+
if not have_module:
201+
not_found_message = f"No module named '{fullname}'"
202+
elif require is not None:
203+
# Old style lazy loading to avoid polluting sys.modules
204+
import packaging.requirements
205+
206+
req = packaging.requirements.Requirement(require)
207+
try:
208+
have_module = req.specifier.contains(
209+
importlib_metadata.version(req.name),
210+
prereleases=True,
211+
)
212+
except importlib_metadata.PackageNotFoundError as e:
213+
raise ValueError(
214+
f"Found module '{fullname}' but cannot test requirement '{require}'. "
215+
"Requirements must match distribution name, not module name."
216+
) from e
200217

218+
if not have_module:
219+
not_found_message = f"No distribution can be found matching '{require}'"
201220

202-
def _module_from_spec(spec, fullname, failure_message, error_on_import):
203-
"""Return lazy module, DelayedImportErrorModule, or raise error"""
204-
if spec is None:
221+
if not have_module:
205222
if error_on_import:
206-
raise ModuleNotFoundError(failure_message)
207-
else:
208-
try:
209-
parent = inspect.stack()[2]
210-
frame_data = {
211-
"filename": parent.filename,
212-
"lineno": parent.lineno,
213-
"function": parent.function,
214-
"code_context": parent.code_context,
215-
}
216-
return DelayedImportErrorModule(
217-
frame_data,
218-
"DelayedImportErrorModule",
219-
message=failure_message,
220-
)
221-
finally:
222-
del parent
223-
224-
module = importlib.util.module_from_spec(spec)
225-
sys.modules[fullname] = module
226-
227-
loader = importlib.util.LazyLoader(spec.loader)
228-
loader.exec_module(module)
223+
raise ModuleNotFoundError(not_found_message)
224+
try:
225+
parent = inspect.stack()[1]
226+
frame_data = {
227+
"filename": parent.filename,
228+
"lineno": parent.lineno,
229+
"function": parent.function,
230+
"code_context": parent.code_context,
231+
}
232+
return DelayedImportErrorModule(
233+
frame_data,
234+
"DelayedImportErrorModule",
235+
message=not_found_message,
236+
)
237+
finally:
238+
del parent
239+
240+
if spec is not None:
241+
module = importlib.util.module_from_spec(spec)
242+
sys.modules[fullname] = module
243+
244+
loader = importlib.util.LazyLoader(spec.loader)
245+
loader.exec_module(module)
229246

230247
return module
231248

232249

250+
def have_module(module_like: types.ModuleType) -> bool:
251+
return not isinstance(module_like, DelayedImportErrorModule)
252+
253+
233254
class _StubVisitor(ast.NodeVisitor):
234255
"""AST visitor to parse a stub file for submodules and submod_attrs."""
235256

@@ -288,40 +309,3 @@ def attach_stub(package_name: str, filename: str):
288309
visitor = _StubVisitor()
289310
visitor.visit(stub_node)
290311
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

0 commit comments

Comments
 (0)