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