• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Utility code for constructing importers, etc."""
2from ._abc import Loader
3from ._bootstrap import module_from_spec
4from ._bootstrap import _resolve_name
5from ._bootstrap import spec_from_loader
6from ._bootstrap import _find_spec
7from ._bootstrap_external import MAGIC_NUMBER
8from ._bootstrap_external import _RAW_MAGIC_NUMBER
9from ._bootstrap_external import cache_from_source
10from ._bootstrap_external import decode_source
11from ._bootstrap_external import source_from_cache
12from ._bootstrap_external import spec_from_file_location
13
14from contextlib import contextmanager
15import _imp
16import functools
17import sys
18import types
19import warnings
20
21
22def source_hash(source_bytes):
23    "Return the hash of *source_bytes* as used in hash-based pyc files."
24    return _imp.source_hash(_RAW_MAGIC_NUMBER, source_bytes)
25
26
27def resolve_name(name, package):
28    """Resolve a relative module name to an absolute one."""
29    if not name.startswith('.'):
30        return name
31    elif not package:
32        raise ImportError(f'no package specified for {repr(name)} '
33                          '(required for relative module names)')
34    level = 0
35    for character in name:
36        if character != '.':
37            break
38        level += 1
39    return _resolve_name(name[level:], package, level)
40
41
42def _find_spec_from_path(name, path=None):
43    """Return the spec for the specified module.
44
45    First, sys.modules is checked to see if the module was already imported. If
46    so, then sys.modules[name].__spec__ is returned. If that happens to be
47    set to None, then ValueError is raised. If the module is not in
48    sys.modules, then sys.meta_path is searched for a suitable spec with the
49    value of 'path' given to the finders. None is returned if no spec could
50    be found.
51
52    Dotted names do not have their parent packages implicitly imported. You will
53    most likely need to explicitly import all parent packages in the proper
54    order for a submodule to get the correct spec.
55
56    """
57    if name not in sys.modules:
58        return _find_spec(name, path)
59    else:
60        module = sys.modules[name]
61        if module is None:
62            return None
63        try:
64            spec = module.__spec__
65        except AttributeError:
66            raise ValueError('{}.__spec__ is not set'.format(name)) from None
67        else:
68            if spec is None:
69                raise ValueError('{}.__spec__ is None'.format(name))
70            return spec
71
72
73def find_spec(name, package=None):
74    """Return the spec for the specified module.
75
76    First, sys.modules is checked to see if the module was already imported. If
77    so, then sys.modules[name].__spec__ is returned. If that happens to be
78    set to None, then ValueError is raised. If the module is not in
79    sys.modules, then sys.meta_path is searched for a suitable spec with the
80    value of 'path' given to the finders. None is returned if no spec could
81    be found.
82
83    If the name is for submodule (contains a dot), the parent module is
84    automatically imported.
85
86    The name and package arguments work the same as importlib.import_module().
87    In other words, relative module names (with leading dots) work.
88
89    """
90    fullname = resolve_name(name, package) if name.startswith('.') else name
91    if fullname not in sys.modules:
92        parent_name = fullname.rpartition('.')[0]
93        if parent_name:
94            parent = __import__(parent_name, fromlist=['__path__'])
95            try:
96                parent_path = parent.__path__
97            except AttributeError as e:
98                raise ModuleNotFoundError(
99                    f"__path__ attribute not found on {parent_name!r} "
100                    f"while trying to find {fullname!r}", name=fullname) from e
101        else:
102            parent_path = None
103        return _find_spec(fullname, parent_path)
104    else:
105        module = sys.modules[fullname]
106        if module is None:
107            return None
108        try:
109            spec = module.__spec__
110        except AttributeError:
111            raise ValueError('{}.__spec__ is not set'.format(name)) from None
112        else:
113            if spec is None:
114                raise ValueError('{}.__spec__ is None'.format(name))
115            return spec
116
117
118@contextmanager
119def _module_to_load(name):
120    is_reload = name in sys.modules
121
122    module = sys.modules.get(name)
123    if not is_reload:
124        # This must be done before open() is called as the 'io' module
125        # implicitly imports 'locale' and would otherwise trigger an
126        # infinite loop.
127        module = type(sys)(name)
128        # This must be done before putting the module in sys.modules
129        # (otherwise an optimization shortcut in import.c becomes wrong)
130        module.__initializing__ = True
131        sys.modules[name] = module
132    try:
133        yield module
134    except Exception:
135        if not is_reload:
136            try:
137                del sys.modules[name]
138            except KeyError:
139                pass
140    finally:
141        module.__initializing__ = False
142
143
144def set_package(fxn):
145    """Set __package__ on the returned module.
146
147    This function is deprecated.
148
149    """
150    @functools.wraps(fxn)
151    def set_package_wrapper(*args, **kwargs):
152        warnings.warn('The import system now takes care of this automatically; '
153                      'this decorator is slated for removal in Python 3.12',
154                      DeprecationWarning, stacklevel=2)
155        module = fxn(*args, **kwargs)
156        if getattr(module, '__package__', None) is None:
157            module.__package__ = module.__name__
158            if not hasattr(module, '__path__'):
159                module.__package__ = module.__package__.rpartition('.')[0]
160        return module
161    return set_package_wrapper
162
163
164def set_loader(fxn):
165    """Set __loader__ on the returned module.
166
167    This function is deprecated.
168
169    """
170    @functools.wraps(fxn)
171    def set_loader_wrapper(self, *args, **kwargs):
172        warnings.warn('The import system now takes care of this automatically; '
173                      'this decorator is slated for removal in Python 3.12',
174                      DeprecationWarning, stacklevel=2)
175        module = fxn(self, *args, **kwargs)
176        if getattr(module, '__loader__', None) is None:
177            module.__loader__ = self
178        return module
179    return set_loader_wrapper
180
181
182def module_for_loader(fxn):
183    """Decorator to handle selecting the proper module for loaders.
184
185    The decorated function is passed the module to use instead of the module
186    name. The module passed in to the function is either from sys.modules if
187    it already exists or is a new module. If the module is new, then __name__
188    is set the first argument to the method, __loader__ is set to self, and
189    __package__ is set accordingly (if self.is_package() is defined) will be set
190    before it is passed to the decorated function (if self.is_package() does
191    not work for the module it will be set post-load).
192
193    If an exception is raised and the decorator created the module it is
194    subsequently removed from sys.modules.
195
196    The decorator assumes that the decorated function takes the module name as
197    the second argument.
198
199    """
200    warnings.warn('The import system now takes care of this automatically; '
201                  'this decorator is slated for removal in Python 3.12',
202                  DeprecationWarning, stacklevel=2)
203    @functools.wraps(fxn)
204    def module_for_loader_wrapper(self, fullname, *args, **kwargs):
205        with _module_to_load(fullname) as module:
206            module.__loader__ = self
207            try:
208                is_package = self.is_package(fullname)
209            except (ImportError, AttributeError):
210                pass
211            else:
212                if is_package:
213                    module.__package__ = fullname
214                else:
215                    module.__package__ = fullname.rpartition('.')[0]
216            # If __package__ was not set above, __import__() will do it later.
217            return fxn(self, module, *args, **kwargs)
218
219    return module_for_loader_wrapper
220
221
222class _LazyModule(types.ModuleType):
223
224    """A subclass of the module type which triggers loading upon attribute access."""
225
226    def __getattribute__(self, attr):
227        """Trigger the load of the module and return the attribute."""
228        # All module metadata must be garnered from __spec__ in order to avoid
229        # using mutated values.
230        # Stop triggering this method.
231        self.__class__ = types.ModuleType
232        # Get the original name to make sure no object substitution occurred
233        # in sys.modules.
234        original_name = self.__spec__.name
235        # Figure out exactly what attributes were mutated between the creation
236        # of the module and now.
237        attrs_then = self.__spec__.loader_state['__dict__']
238        attrs_now = self.__dict__
239        attrs_updated = {}
240        for key, value in attrs_now.items():
241            # Code that set the attribute may have kept a reference to the
242            # assigned object, making identity more important than equality.
243            if key not in attrs_then:
244                attrs_updated[key] = value
245            elif id(attrs_now[key]) != id(attrs_then[key]):
246                attrs_updated[key] = value
247        self.__spec__.loader.exec_module(self)
248        # If exec_module() was used directly there is no guarantee the module
249        # object was put into sys.modules.
250        if original_name in sys.modules:
251            if id(self) != id(sys.modules[original_name]):
252                raise ValueError(f"module object for {original_name!r} "
253                                  "substituted in sys.modules during a lazy "
254                                  "load")
255        # Update after loading since that's what would happen in an eager
256        # loading situation.
257        self.__dict__.update(attrs_updated)
258        return getattr(self, attr)
259
260    def __delattr__(self, attr):
261        """Trigger the load and then perform the deletion."""
262        # To trigger the load and raise an exception if the attribute
263        # doesn't exist.
264        self.__getattribute__(attr)
265        delattr(self, attr)
266
267
268class LazyLoader(Loader):
269
270    """A loader that creates a module which defers loading until attribute access."""
271
272    @staticmethod
273    def __check_eager_loader(loader):
274        if not hasattr(loader, 'exec_module'):
275            raise TypeError('loader must define exec_module()')
276
277    @classmethod
278    def factory(cls, loader):
279        """Construct a callable which returns the eager loader made lazy."""
280        cls.__check_eager_loader(loader)
281        return lambda *args, **kwargs: cls(loader(*args, **kwargs))
282
283    def __init__(self, loader):
284        self.__check_eager_loader(loader)
285        self.loader = loader
286
287    def create_module(self, spec):
288        return self.loader.create_module(spec)
289
290    def exec_module(self, module):
291        """Make the module load lazily."""
292        module.__spec__.loader = self.loader
293        module.__loader__ = self.loader
294        # Don't need to worry about deep-copying as trying to set an attribute
295        # on an object would have triggered the load,
296        # e.g. ``module.__spec__.loader = None`` would trigger a load from
297        # trying to access module.__spec__.
298        loader_state = {}
299        loader_state['__dict__'] = module.__dict__.copy()
300        loader_state['__class__'] = module.__class__
301        module.__spec__.loader_state = loader_state
302        module.__class__ = _LazyModule
303