|
13 | 13 | import types |
14 | 14 | import warnings |
15 | 15 |
|
| 16 | +try: |
| 17 | + import importlib_metadata |
| 18 | +except ImportError: |
| 19 | + import importlib.metadata as importlib_metadata |
| 20 | + |
16 | 21 | __all__ = ["attach", "load", "attach_stub"] |
17 | 22 |
|
18 | 23 |
|
@@ -99,17 +104,18 @@ def __dir__(): |
99 | 104 |
|
100 | 105 |
|
101 | 106 | class DelayedImportErrorModule(types.ModuleType): |
102 | | - def __init__(self, frame_data, *args, **kwargs): |
| 107 | + def __init__(self, frame_data, *args, message=None, **kwargs): |
103 | 108 | self.__frame_data = frame_data |
| 109 | + self.__message = message or f"No module named '{frame_data['spec']}'" |
104 | 110 | super().__init__(*args, **kwargs) |
105 | 111 |
|
106 | 112 | def __getattr__(self, x): |
107 | | - if x in ("__class__", "__file__", "__frame_data"): |
| 113 | + if x in ("__class__", "__file__", "__frame_data", "__message"): |
108 | 114 | super().__getattr__(x) |
109 | 115 | else: |
110 | 116 | fd = self.__frame_data |
111 | 117 | raise ModuleNotFoundError( |
112 | | - f"No module named '{fd['spec']}'\n\n" |
| 118 | + f"{self.__message}\n\n" |
113 | 119 | "This error is lazily reported, having originally occured in\n" |
114 | 120 | f' File {fd["filename"]}, line {fd["lineno"]}, in {fd["function"]}\n\n' |
115 | 121 | f'----> {"".join(fd["code_context"]).strip()}' |
@@ -185,20 +191,33 @@ def myfunc(): |
185 | 191 | warnings.warn(msg, RuntimeWarning) |
186 | 192 |
|
187 | 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 | + ) |
| 200 | + |
| 201 | + |
| 202 | +def _module_from_spec(spec, fullname, failure_message, error_on_import): |
| 203 | + """Return lazy module, DelayedImportErrorModule, or raise error""" |
188 | 204 | if spec is None: |
189 | 205 | if error_on_import: |
190 | | - raise ModuleNotFoundError(f"No module named '{fullname}'") |
| 206 | + raise ModuleNotFoundError(failure_message) |
191 | 207 | else: |
192 | 208 | try: |
193 | | - parent = inspect.stack()[1] |
| 209 | + parent = inspect.stack()[2] |
194 | 210 | frame_data = { |
195 | | - "spec": fullname, |
196 | 211 | "filename": parent.filename, |
197 | 212 | "lineno": parent.lineno, |
198 | 213 | "function": parent.function, |
199 | 214 | "code_context": parent.code_context, |
200 | 215 | } |
201 | | - return DelayedImportErrorModule(frame_data, "DelayedImportErrorModule") |
| 216 | + return DelayedImportErrorModule( |
| 217 | + frame_data, |
| 218 | + "DelayedImportErrorModule", |
| 219 | + message=failure_message, |
| 220 | + ) |
202 | 221 | finally: |
203 | 222 | del parent |
204 | 223 |
|
@@ -269,3 +288,40 @@ def attach_stub(package_name: str, filename: str): |
269 | 288 | visitor = _StubVisitor() |
270 | 289 | visitor.visit(stub_node) |
271 | 290 | 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