diff options
Diffstat (limited to 'Lib/importlib/util.py')
| -rw-r--r-- | Lib/importlib/util.py | 106 | 
1 files changed, 97 insertions, 9 deletions
| diff --git a/Lib/importlib/util.py b/Lib/importlib/util.py index 6d73b1d7b6..e1fa07a664 100644 --- a/Lib/importlib/util.py +++ b/Lib/importlib/util.py @@ -1,17 +1,19 @@  """Utility code for constructing importers, etc.""" - -from ._bootstrap import MAGIC_NUMBER -from ._bootstrap import cache_from_source -from ._bootstrap import decode_source -from ._bootstrap import source_from_cache -from ._bootstrap import spec_from_loader -from ._bootstrap import spec_from_file_location +from . import abc +from ._bootstrap import module_from_spec  from ._bootstrap import _resolve_name +from ._bootstrap import spec_from_loader  from ._bootstrap import _find_spec +from ._bootstrap_external import MAGIC_NUMBER +from ._bootstrap_external import cache_from_source +from ._bootstrap_external import decode_source +from ._bootstrap_external import source_from_cache +from ._bootstrap_external import spec_from_file_location  from contextlib import contextmanager  import functools  import sys +import types  import warnings @@ -54,7 +56,7 @@ def _find_spec_from_path(name, path=None):          try:              spec = module.__spec__          except AttributeError: -            raise ValueError('{}.__spec__ is not set'.format(name)) +            raise ValueError('{}.__spec__ is not set'.format(name)) from None          else:              if spec is None:                  raise ValueError('{}.__spec__ is None'.format(name)) @@ -94,7 +96,7 @@ def find_spec(name, package=None):          try:              spec = module.__spec__          except AttributeError: -            raise ValueError('{}.__spec__ is not set'.format(name)) +            raise ValueError('{}.__spec__ is not set'.format(name)) from None          else:              if spec is None:                  raise ValueError('{}.__spec__ is None'.format(name)) @@ -200,3 +202,89 @@ def module_for_loader(fxn):              return fxn(self, module, *args, **kwargs)      return module_for_loader_wrapper + + +class _Module(types.ModuleType): + +    """A subclass of the module type to allow __class__ manipulation.""" + + +class _LazyModule(types.ModuleType): + +    """A subclass of the module type which triggers loading upon attribute access.""" + +    def __getattribute__(self, attr): +        """Trigger the load of the module and return the attribute.""" +        # All module metadata must be garnered from __spec__ in order to avoid +        # using mutated values. +        # Stop triggering this method. +        self.__class__ = _Module +        # Get the original name to make sure no object substitution occurred +        # in sys.modules. +        original_name = self.__spec__.name +        # Figure out exactly what attributes were mutated between the creation +        # of the module and now. +        attrs_then = self.__spec__.loader_state +        attrs_now = self.__dict__ +        attrs_updated = {} +        for key, value in attrs_now.items(): +            # Code that set the attribute may have kept a reference to the +            # assigned object, making identity more important than equality. +            if key not in attrs_then: +                attrs_updated[key] = value +            elif id(attrs_now[key]) != id(attrs_then[key]): +                attrs_updated[key] = value +        self.__spec__.loader.exec_module(self) +        # If exec_module() was used directly there is no guarantee the module +        # object was put into sys.modules. +        if original_name in sys.modules: +            if id(self) != id(sys.modules[original_name]): +                msg = ('module object for {!r} substituted in sys.modules ' +                       'during a lazy load') +                raise ValueError(msg.format(original_name)) +        # Update after loading since that's what would happen in an eager +        # loading situation. +        self.__dict__.update(attrs_updated) +        return getattr(self, attr) + +    def __delattr__(self, attr): +        """Trigger the load and then perform the deletion.""" +        # To trigger the load and raise an exception if the attribute +        # doesn't exist. +        self.__getattribute__(attr) +        delattr(self, attr) + + +class LazyLoader(abc.Loader): + +    """A loader that creates a module which defers loading until attribute access.""" + +    @staticmethod +    def __check_eager_loader(loader): +        if not hasattr(loader, 'exec_module'): +            raise TypeError('loader must define exec_module()') + +    @classmethod +    def factory(cls, loader): +        """Construct a callable which returns the eager loader made lazy.""" +        cls.__check_eager_loader(loader) +        return lambda *args, **kwargs: cls(loader(*args, **kwargs)) + +    def __init__(self, loader): +        self.__check_eager_loader(loader) +        self.loader = loader + +    def create_module(self, spec): +        """Create a module which can have its __class__ manipulated.""" +        return _Module(spec.name) + +    def exec_module(self, module): +        """Make the module load lazily.""" +        module.__spec__.loader = self.loader +        module.__loader__ = self.loader +        # Don't need to worry about deep-copying as trying to set an attribute +        # on an object would have triggered the load, +        # e.g. ``module.__spec__.loader = None`` would trigger a load from +        # trying to access module.__spec__. +        module.__spec__.loader_state = module.__dict__.copy() +        module.__class__ = _LazyModule | 
