diff --git a/lazy_loader/__init__.py b/lazy_loader/__init__.py index 28ed5b1..b1962af 100644 --- a/lazy_loader/__init__.py +++ b/lazy_loader/__init__.py @@ -6,10 +6,13 @@ """ import importlib import importlib.util +import inspect import os import sys import types +__all__ = ["attach", "load"] + def attach(package_name, submodules=None, submod_attrs=None): """Attach lazily loaded submodules, functions, or other attributes. @@ -83,6 +86,24 @@ def __dir__(): return __getattr__, __dir__, list(__all__) +class DelayedImportErrorModule(types.ModuleType): + def __init__(self, frame_data, *args, **kwargs): + self.__frame_data = frame_data + super().__init__(*args, **kwargs) + + def __getattr__(self, x): + if x in ("__class__", "__file__", "__frame_data"): + super().__getattr__(x) + else: + fd = self.__frame_data + raise ModuleNotFoundError( + f"No module named '{fd['spec']}'\n\n" + "This error is lazily reported, having originally occured in\n" + f' File {fd["filename"]}, line {fd["lineno"]}, in {fd["function"]}\n\n' + f'----> {"".join(fd["code_context"]).strip()}' + ) + + def load(fullname, error_on_import=False): """Return a lazily imported proxy for a module. @@ -138,13 +159,18 @@ def myfunc(): if error_on_import: raise ModuleNotFoundError(f"No module named '{fullname}'") else: - spec = importlib.util.spec_from_loader(fullname, loader=None) - module = importlib.util.module_from_spec(spec) - tmp_loader = importlib.machinery.SourceFileLoader(module, path=None) - loader = DelayedImportErrorLoader(tmp_loader) - loader.exec_module(module) - # dont add to sys.modules. The module wasn't found. - return module + try: + parent = inspect.stack()[1] + frame_data = { + "spec": fullname, + "filename": parent.filename, + "lineno": parent.lineno, + "function": parent.function, + "code_context": parent.code_context, + } + return DelayedImportErrorModule(frame_data, "DelayedImportErrorModule") + finally: + del parent module = importlib.util.module_from_spec(spec) sys.modules[fullname] = module @@ -153,20 +179,3 @@ def myfunc(): loader.exec_module(module) return module - - -class DelayedImportErrorLoader(importlib.util.LazyLoader): - def exec_module(self, module): - super().exec_module(module) - module.__class__ = DelayedImportErrorModule - - -class DelayedImportErrorModule(types.ModuleType): - def __getattribute__(self, attr): - """Trigger a ModuleNotFoundError upon attribute access""" - spec = super().__getattribute__("__spec__") - # allows isinstance and type functions to work without raising error - if attr in ["__class__"]: - return super().__getattribute__("__class__") - - raise ModuleNotFoundError(f"No module named '{spec.name}'")