|
12 | 12 | import sys
|
13 | 13 | import types
|
14 | 14 |
|
| 15 | +try: |
| 16 | + import importlib_metadata |
| 17 | +except ImportError: |
| 18 | + import importlib.metadata as importlib_metadata |
| 19 | + |
15 | 20 | __all__ = ["attach", "load", "attach_stub"]
|
16 | 21 |
|
17 | 22 |
|
@@ -98,17 +103,18 @@ def __dir__():
|
98 | 103 |
|
99 | 104 |
|
100 | 105 | class DelayedImportErrorModule(types.ModuleType):
|
101 |
| - def __init__(self, frame_data, *args, **kwargs): |
| 106 | + def __init__(self, frame_data, *args, message=None, **kwargs): |
102 | 107 | self.__frame_data = frame_data
|
| 108 | + self.__message = message or f"No module named '{frame_data['spec']}'" |
103 | 109 | super().__init__(*args, **kwargs)
|
104 | 110 |
|
105 | 111 | def __getattr__(self, x):
|
106 |
| - if x in ("__class__", "__file__", "__frame_data"): |
| 112 | + if x in ("__class__", "__file__", "__frame_data", "__message"): |
107 | 113 | super().__getattr__(x)
|
108 | 114 | else:
|
109 | 115 | fd = self.__frame_data
|
110 | 116 | raise ModuleNotFoundError(
|
111 |
| - f"No module named '{fd['spec']}'\n\n" |
| 117 | + f"{self.__message}\n\n" |
112 | 118 | "This error is lazily reported, having originally occured in\n"
|
113 | 119 | f' File {fd["filename"]}, line {fd["lineno"]}, in {fd["function"]}\n\n'
|
114 | 120 | f'----> {"".join(fd["code_context"]).strip()}'
|
@@ -166,20 +172,33 @@ def myfunc():
|
166 | 172 | pass
|
167 | 173 |
|
168 | 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 | + ) |
| 181 | + |
| 182 | + |
| 183 | +def _module_from_spec(spec, fullname, failure_message, error_on_import): |
| 184 | + """Return lazy module, DelayedImportErrorModule, or raise error""" |
169 | 185 | if spec is None:
|
170 | 186 | if error_on_import:
|
171 |
| - raise ModuleNotFoundError(f"No module named '{fullname}'") |
| 187 | + raise ModuleNotFoundError(failure_message) |
172 | 188 | else:
|
173 | 189 | try:
|
174 |
| - parent = inspect.stack()[1] |
| 190 | + parent = inspect.stack()[2] |
175 | 191 | frame_data = {
|
176 |
| - "spec": fullname, |
177 | 192 | "filename": parent.filename,
|
178 | 193 | "lineno": parent.lineno,
|
179 | 194 | "function": parent.function,
|
180 | 195 | "code_context": parent.code_context,
|
181 | 196 | }
|
182 |
| - return DelayedImportErrorModule(frame_data, "DelayedImportErrorModule") |
| 197 | + return DelayedImportErrorModule( |
| 198 | + frame_data, |
| 199 | + "DelayedImportErrorModule", |
| 200 | + message=failure_message, |
| 201 | + ) |
183 | 202 | finally:
|
184 | 203 | del parent
|
185 | 204 |
|
@@ -250,3 +269,40 @@ def attach_stub(package_name: str, filename: str):
|
250 | 269 | visitor = _StubVisitor()
|
251 | 270 | visitor.visit(stub_node)
|
252 | 271 | 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