Skip to content

Commit 0644862

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

File tree

1 file changed

+62
-78
lines changed

1 file changed

+62
-78
lines changed

lazy_loader/__init__.py

Lines changed: 62 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ def __dir__():
103103

104104

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

111111
def __getattr__(self, x):
@@ -121,7 +121,7 @@ def __getattr__(self, x):
121121
)
122122

123123

124-
def load(fullname, error_on_import=False):
124+
def load(fullname, *, require=None, error_on_import=False):
125125
"""Return a lazily imported proxy for a module.
126126
127127
We often see the following pattern::
@@ -166,51 +166,72 @@ def myfunc():
166166
Actual loading of the module occurs upon first attribute request.
167167
168168
"""
169-
try:
170-
return sys.modules[fullname]
171-
except KeyError:
172-
pass
173-
174-
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-
)
169+
module = sys.modules.get(fullname)
170+
have_module = module is not None
171+
172+
# Most common, short-circuit
173+
if have_module and require is None:
174+
return module
175+
176+
spec = None
177+
if not have_module:
178+
spec = importlib.util.find_spec(fullname)
179+
have_module = spec is not None
180+
181+
if not have_module:
182+
not_found_message = f"No module named '{fullname}'"
183+
elif require is not None:
184+
# Old style lazy loading to avoid polluting sys.modules
185+
import packaging.requirements
186+
187+
req = packaging.requirements.Requirement(require)
188+
try:
189+
have_module = req.specifier.contains(
190+
importlib_metadata.version(req.name),
191+
prereleases=True,
192+
)
193+
except importlib_metadata.PackageNotFoundError as e:
194+
raise ValueError(
195+
f"Found module '{fullname}' but cannot test requirement '{require}'. "
196+
"Requirements must match distribution name, not module name."
197+
) from e
181198

199+
if not have_module:
200+
not_found_message = f"No distribution can be found matching '{require}'"
182201

183-
def _module_from_spec(spec, fullname, failure_message, error_on_import):
184-
"""Return lazy module, DelayedImportErrorModule, or raise error"""
185-
if spec is None:
202+
if not have_module:
186203
if error_on_import:
187-
raise ModuleNotFoundError(failure_message)
188-
else:
189-
try:
190-
parent = inspect.stack()[2]
191-
frame_data = {
192-
"filename": parent.filename,
193-
"lineno": parent.lineno,
194-
"function": parent.function,
195-
"code_context": parent.code_context,
196-
}
197-
return DelayedImportErrorModule(
198-
frame_data,
199-
"DelayedImportErrorModule",
200-
message=failure_message,
201-
)
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)
204+
raise ModuleNotFoundError(not_found_message)
205+
try:
206+
parent = inspect.stack()[1]
207+
frame_data = {
208+
"filename": parent.filename,
209+
"lineno": parent.lineno,
210+
"function": parent.function,
211+
"code_context": parent.code_context,
212+
}
213+
return DelayedImportErrorModule(
214+
frame_data,
215+
"DelayedImportErrorModule",
216+
message=not_found_message,
217+
)
218+
finally:
219+
del parent
220+
221+
if spec is not None:
222+
module = importlib.util.module_from_spec(spec)
223+
sys.modules[fullname] = module
224+
225+
loader = importlib.util.LazyLoader(spec.loader)
226+
loader.exec_module(module)
210227

211228
return module
212229

213230

231+
def have_module(module_like: types.ModuleType) -> bool:
232+
return not isinstance(module_like, DelayedImportErrorModule)
233+
234+
214235
class _StubVisitor(ast.NodeVisitor):
215236
"""AST visitor to parse a stub file for submodules and submod_attrs."""
216237

@@ -269,40 +290,3 @@ def attach_stub(package_name: str, filename: str):
269290
visitor = _StubVisitor()
270291
visitor.visit(stub_node)
271292
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

0 commit comments

Comments
 (0)