Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Simplify delayed exception handling and improve reporting #4

Merged
merged 1 commit into from
Mar 8, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 33 additions & 24 deletions lazy_loader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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}'")