diff options
| author | shadchin <[email protected]> | 2022-04-18 12:39:32 +0300 |
|---|---|---|
| committer | shadchin <[email protected]> | 2022-04-18 12:39:32 +0300 |
| commit | d4be68e361f4258cf0848fc70018dfe37a2acc24 (patch) | |
| tree | 153e294cd97ac8b5d7a989612704a0c1f58e8ad4 /contrib/tools/python3/src/Lib/importlib | |
| parent | 260c02f5ccf242d9d9b8a873afaf6588c00237d6 (diff) | |
IGNIETFERRO-1816 Update Python 3 from 3.9.12 to 3.10.4
ref:9f96be6d02ee8044fdd6f124b799b270c20ce641
Diffstat (limited to 'contrib/tools/python3/src/Lib/importlib')
19 files changed, 2009 insertions, 979 deletions
diff --git a/contrib/tools/python3/src/Lib/importlib/__init__.py b/contrib/tools/python3/src/Lib/importlib/__init__.py index 0c73c505f98..ce61883288a 100644 --- a/contrib/tools/python3/src/Lib/importlib/__init__.py +++ b/contrib/tools/python3/src/Lib/importlib/__init__.py @@ -34,7 +34,7 @@ try: import _frozen_importlib_external as _bootstrap_external except ImportError: from . import _bootstrap_external - _bootstrap_external._setup(_bootstrap) + _bootstrap_external._set_bootstrap_module(_bootstrap) _bootstrap._bootstrap_external = _bootstrap_external else: _bootstrap_external.__name__ = 'importlib._bootstrap_external' @@ -54,7 +54,6 @@ _unpack_uint32 = _bootstrap_external._unpack_uint32 # Fully bootstrapped at this point, import whatever you like, circular # dependencies and startup overhead minimisation permitting :) -import types import warnings @@ -79,8 +78,8 @@ def find_loader(name, path=None): This function is deprecated in favor of importlib.util.find_spec(). """ - warnings.warn('Deprecated since Python 3.4. ' - 'Use importlib.util.find_spec() instead.', + warnings.warn('Deprecated since Python 3.4 and slated for removal in ' + 'Python 3.12; use importlib.util.find_spec() instead', DeprecationWarning, stacklevel=2) try: loader = sys.modules[name].__loader__ @@ -136,12 +135,13 @@ def reload(module): The module must have been successfully imported before. """ - if not module or not isinstance(module, types.ModuleType): - raise TypeError("reload() argument must be a module") try: name = module.__spec__.name except AttributeError: - name = module.__name__ + try: + name = module.__name__ + except AttributeError: + raise TypeError("reload() argument must be a module") if sys.modules.get(name) is not module: msg = "module {} not in sys.modules" diff --git a/contrib/tools/python3/src/Lib/importlib/_abc.py b/contrib/tools/python3/src/Lib/importlib/_abc.py new file mode 100644 index 00000000000..f80348fc7ff --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/_abc.py @@ -0,0 +1,54 @@ +"""Subset of importlib.abc used to reduce importlib.util imports.""" +from . import _bootstrap +import abc +import warnings + + +class Loader(metaclass=abc.ABCMeta): + + """Abstract base class for import loaders.""" + + def create_module(self, spec): + """Return a module to initialize and into which to load. + + This method should raise ImportError if anything prevents it + from creating a new module. It may return None to indicate + that the spec should create the new module. + """ + # By default, defer to default semantics for the new module. + return None + + # We don't define exec_module() here since that would break + # hasattr checks we do to support backward compatibility. + + def load_module(self, fullname): + """Return the loaded module. + + The module must be added to sys.modules and have import-related + attributes set properly. The fullname is a str. + + ImportError is raised on failure. + + This method is deprecated in favor of loader.exec_module(). If + exec_module() exists then it is used to provide a backwards-compatible + functionality for this method. + + """ + if not hasattr(self, 'exec_module'): + raise ImportError + # Warning implemented in _load_module_shim(). + return _bootstrap._load_module_shim(self, fullname) + + def module_repr(self, module): + """Return a module's repr. + + Used by the module type when the method does not raise + NotImplementedError. + + This method is deprecated. + + """ + warnings.warn("importlib.abc.Loader.module_repr() is deprecated and " + "slated for removal in Python 3.12", DeprecationWarning) + # The exception will cause ModuleType.__repr__ to ignore this method. + raise NotImplementedError diff --git a/contrib/tools/python3/src/Lib/importlib/_adapters.py b/contrib/tools/python3/src/Lib/importlib/_adapters.py new file mode 100644 index 00000000000..e72edd10705 --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/_adapters.py @@ -0,0 +1,83 @@ +from contextlib import suppress + +from . import abc + + +class SpecLoaderAdapter: + """ + Adapt a package spec to adapt the underlying loader. + """ + + def __init__(self, spec, adapter=lambda spec: spec.loader): + self.spec = spec + self.loader = adapter(spec) + + def __getattr__(self, name): + return getattr(self.spec, name) + + +class TraversableResourcesLoader: + """ + Adapt a loader to provide TraversableResources. + """ + + def __init__(self, spec): + self.spec = spec + + def get_resource_reader(self, name): + return DegenerateFiles(self.spec)._native() + + +class DegenerateFiles: + """ + Adapter for an existing or non-existant resource reader + to provide a degenerate .files(). + """ + + class Path(abc.Traversable): + def iterdir(self): + return iter(()) + + def is_dir(self): + return False + + is_file = exists = is_dir # type: ignore + + def joinpath(self, other): + return DegenerateFiles.Path() + + @property + def name(self): + return '' + + def open(self, mode='rb', *args, **kwargs): + raise ValueError() + + def __init__(self, spec): + self.spec = spec + + @property + def _reader(self): + with suppress(AttributeError): + return self.spec.loader.get_resource_reader(self.spec.name) + + def _native(self): + """ + Return the native reader if it supports files(). + """ + reader = self._reader + return reader if hasattr(reader, 'files') else self + + def __getattr__(self, attr): + return getattr(self._reader, attr) + + def files(self): + return DegenerateFiles.Path() + + +def wrap_spec(package): + """ + Construct a package spec with traversable compatibility + on the spec/loader/reader. + """ + return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) diff --git a/contrib/tools/python3/src/Lib/importlib/_bootstrap.py b/contrib/tools/python3/src/Lib/importlib/_bootstrap.py index e00b27ece26..527bc9c63c9 100644 --- a/contrib/tools/python3/src/Lib/importlib/_bootstrap.py +++ b/contrib/tools/python3/src/Lib/importlib/_bootstrap.py @@ -20,10 +20,23 @@ work. One should use importlib as the public-facing version of this module. # reference any injected objects! This includes not only global code but also # anything specified at the class level. +def _object_name(obj): + try: + return obj.__qualname__ + except AttributeError: + return type(obj).__qualname__ + # Bootstrap-related code ###################################################### +# Modules injected manually by _setup() +_thread = None +_warnings = None +_weakref = None + +# Import done by _install_external_importers() _bootstrap_external = None + def _wrap(new, old): """Simple substitute for functools.update_wrapper.""" for replace in ['__module__', '__name__', '__qualname__', '__doc__']: @@ -262,9 +275,12 @@ def _requires_frozen(fxn): def _load_module_shim(self, fullname): """Load the specified module into sys.modules and return it. - This method is deprecated. Use loader.exec_module instead. + This method is deprecated. Use loader.exec_module() instead. """ + msg = ("the load_module() method is deprecated and slated for removal in " + "Python 3.12; use exec_module() instead") + _warnings.warn(msg, DeprecationWarning) spec = spec_from_loader(fullname, self) if fullname in sys.modules: module = sys.modules[fullname] @@ -276,26 +292,16 @@ def _load_module_shim(self, fullname): # Module specifications ####################################################### def _module_repr(module): - # The implementation of ModuleType.__repr__(). + """The implementation of ModuleType.__repr__().""" loader = getattr(module, '__loader__', None) - if hasattr(loader, 'module_repr'): - # As soon as BuiltinImporter, FrozenImporter, and NamespaceLoader - # drop their implementations for module_repr. we can add a - # deprecation warning here. + if spec := getattr(module, "__spec__", None): + return _module_repr_from_spec(spec) + elif hasattr(loader, 'module_repr'): try: return loader.module_repr(module) except Exception: pass - try: - spec = module.__spec__ - except AttributeError: - pass - else: - if spec is not None: - return _module_repr_from_spec(spec) - - # We could use module.__class__.__name__ instead of 'module' in the - # various repr permutations. + # Fall through to a catch-all which always succeeds. try: name = module.__name__ except AttributeError: @@ -605,9 +611,9 @@ def _exec(spec, module): else: _init_module_attrs(spec, module, override=True) if not hasattr(spec.loader, 'exec_module'): - # (issue19713) Once BuiltinImporter and ExtensionFileLoader - # have exec_module() implemented, we can add a deprecation - # warning here. + msg = (f"{_object_name(spec.loader)}.exec_module() not found; " + "falling back to load_module()") + _warnings.warn(msg, ImportWarning) spec.loader.load_module(name) else: spec.loader.exec_module(module) @@ -620,9 +626,8 @@ def _exec(spec, module): def _load_backward_compatible(spec): - # (issue19713) Once BuiltinImporter and ExtensionFileLoader - # have exec_module() implemented, we can add a deprecation - # warning here. + # It is assumed that all callers have been warned about using load_module() + # appropriately before calling this function. try: spec.loader.load_module(spec.name) except: @@ -661,6 +666,9 @@ def _load_unlocked(spec): if spec.loader is not None: # Not a namespace package. if not hasattr(spec.loader, 'exec_module'): + msg = (f"{_object_name(spec.loader)}.exec_module() not found; " + "falling back to load_module()") + _warnings.warn(msg, ImportWarning) return _load_backward_compatible(spec) module = module_from_spec(spec) @@ -731,6 +739,8 @@ class BuiltinImporter: The method is deprecated. The import machinery does the job itself. """ + _warnings.warn("BuiltinImporter.module_repr() is deprecated and " + "slated for removal in Python 3.12", DeprecationWarning) return f'<module {module.__name__!r} ({BuiltinImporter._ORIGIN})>' @classmethod @@ -751,19 +761,22 @@ class BuiltinImporter: This method is deprecated. Use find_spec() instead. """ + _warnings.warn("BuiltinImporter.find_module() is deprecated and " + "slated for removal in Python 3.12; use find_spec() instead", + DeprecationWarning) spec = cls.find_spec(fullname, path) return spec.loader if spec is not None else None - @classmethod - def create_module(self, spec): + @staticmethod + def create_module(spec): """Create a built-in module""" if spec.name not in sys.builtin_module_names: raise ImportError('{!r} is not a built-in module'.format(spec.name), name=spec.name) return _call_with_frames_removed(_imp.create_builtin, spec) - @classmethod - def exec_module(self, module): + @staticmethod + def exec_module(module): """Exec a built-in module""" _call_with_frames_removed(_imp.exec_builtin, module) @@ -806,6 +819,8 @@ class FrozenImporter: The method is deprecated. The import machinery does the job itself. """ + _warnings.warn("FrozenImporter.module_repr() is deprecated and " + "slated for removal in Python 3.12", DeprecationWarning) return '<module {!r} ({})>'.format(m.__name__, FrozenImporter._ORIGIN) @classmethod @@ -822,10 +837,13 @@ class FrozenImporter: This method is deprecated. Use find_spec() instead. """ + _warnings.warn("FrozenImporter.find_module() is deprecated and " + "slated for removal in Python 3.12; use find_spec() instead", + DeprecationWarning) return cls if _imp.is_frozen(fullname) else None - @classmethod - def create_module(cls, spec): + @staticmethod + def create_module(spec): """Use default semantics for module creation.""" @staticmethod @@ -844,6 +862,7 @@ class FrozenImporter: This method is deprecated. Use exec_module() instead. """ + # Warning about deprecation implemented in _load_module_shim(). return _load_module_shim(cls, fullname) @classmethod @@ -890,8 +909,9 @@ def _resolve_name(name, package, level): def _find_spec_legacy(finder, name, path): - # This would be a good place for a DeprecationWarning if - # we ended up going that route. + msg = (f"{_object_name(finder)}.find_spec() not found; " + "falling back to find_module()") + _warnings.warn(msg, ImportWarning) loader = finder.find_module(name, path) if loader is None: return None diff --git a/contrib/tools/python3/src/Lib/importlib/_bootstrap_external.py b/contrib/tools/python3/src/Lib/importlib/_bootstrap_external.py index f3828b10e1c..49bcaea78d7 100644 --- a/contrib/tools/python3/src/Lib/importlib/_bootstrap_external.py +++ b/contrib/tools/python3/src/Lib/importlib/_bootstrap_external.py @@ -19,6 +19,9 @@ work. One should use importlib as the public-facing version of this module. # reference any injected objects! This includes not only global code but also # anything specified at the class level. +# Module injected manually by _set_bootstrap_module() +_bootstrap = None + # Import builtin modules import _imp import _io @@ -70,6 +73,8 @@ def _make_relax_case(): return False return _relax_case +_relax_case = _make_relax_case() + def _pack_uint32(x): """Convert a 32-bit integer to little-endian.""" @@ -337,6 +342,16 @@ _code_type = type(_write_atomic.__code__) # Python 3.9a2 3423 (add IS_OP, CONTAINS_OP and JUMP_IF_NOT_EXC_MATCH bytecodes #39156) # Python 3.9a2 3424 (simplify bytecodes for *value unpacking) # Python 3.9a2 3425 (simplify bytecodes for **value unpacking) +# Python 3.10a1 3430 (Make 'annotations' future by default) +# Python 3.10a1 3431 (New line number table format -- PEP 626) +# Python 3.10a2 3432 (Function annotation for MAKE_FUNCTION is changed from dict to tuple bpo-42202) +# Python 3.10a2 3433 (RERAISE restores f_lasti if oparg != 0) +# Python 3.10a6 3434 (PEP 634: Structural Pattern Matching) +# Python 3.10a7 3435 Use instruction offsets (as opposed to byte offsets). +# Python 3.10b1 3436 (Add GEN_START bytecode #43683) +# Python 3.10b1 3437 (Undo making 'annotations' future by default - We like to dance among core devs!) +# Python 3.10b1 3438 Safer line number table handling. +# Python 3.10b1 3439 (Add ROT_N) # # MAGIC must change whenever the bytecode emitted by the compiler may no @@ -346,13 +361,17 @@ _code_type = type(_write_atomic.__code__) # Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array # in PC/launcher.c must also be updated. -MAGIC_NUMBER = (3425).to_bytes(2, 'little') + b'\r\n' +MAGIC_NUMBER = (3439).to_bytes(2, 'little') + b'\r\n' _RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c _PYCACHE = '__pycache__' _OPT = 'opt-' -SOURCE_SUFFIXES = ['.py'] # _setup() adds .pyw as needed. +SOURCE_SUFFIXES = ['.py'] +if _MS_WINDOWS: + SOURCE_SUFFIXES.append('.pyw') + +EXTENSION_SUFFIXES = _imp.extension_suffixes() BYTECODE_SUFFIXES = ['.pyc'] # Deprecated. @@ -527,15 +546,18 @@ def _check_name(method): raise ImportError('loader for %s cannot handle %s' % (self.name, name), name=name) return method(self, name, *args, **kwargs) - try: + + # FIXME: @_check_name is used to define class methods before the + # _bootstrap module is set by _set_bootstrap_module(). + if _bootstrap is not None: _wrap = _bootstrap._wrap - except NameError: - # XXX yuck + else: def _wrap(new, old): for replace in ['__module__', '__name__', '__qualname__', '__doc__']: if hasattr(old, replace): setattr(new, replace, getattr(old, replace)) new.__dict__.update(old.__dict__) + _wrap(_check_name_wrapper, method) return _check_name_wrapper @@ -547,6 +569,9 @@ def _find_module_shim(self, fullname): This method is deprecated in favor of finder.find_spec(). """ + _warnings.warn("find_module() is deprecated and " + "slated for removal in Python 3.12; use find_spec() instead", + DeprecationWarning) # Call find_loader(). If it returns a string (indicating this # is a namespace package portion), generate a warning and # return None. @@ -718,6 +743,11 @@ def spec_from_file_location(name, location=None, *, loader=None, pass else: location = _os.fspath(location) + if not _path_isabs(location): + try: + location = _path_join(_os.getcwd(), location) + except OSError: + pass # If the location is on the filesystem, but doesn't actually exist, # we could return None here, indicating that the location is not @@ -771,10 +801,10 @@ class WindowsRegistryFinder: REGISTRY_KEY_DEBUG = ( 'Software\\Python\\PythonCore\\{sys_version}' '\\Modules\\{fullname}\\Debug') - DEBUG_BUILD = False # Changed in _setup() + DEBUG_BUILD = (_MS_WINDOWS and '_d.pyd' in EXTENSION_SUFFIXES) - @classmethod - def _open_registry(cls, key): + @staticmethod + def _open_registry(key): try: return winreg.OpenKey(winreg.HKEY_CURRENT_USER, key) except OSError: @@ -815,9 +845,12 @@ class WindowsRegistryFinder: def find_module(cls, fullname, path=None): """Find module named in the registry. - This method is deprecated. Use exec_module() instead. + This method is deprecated. Use find_spec() instead. """ + _warnings.warn("WindowsRegistryFinder.find_module() is deprecated and " + "slated for removal in Python 3.12; use find_spec() instead", + DeprecationWarning) spec = cls.find_spec(fullname, path) if spec is not None: return spec.loader @@ -850,7 +883,8 @@ class _LoaderBasics: _bootstrap._call_with_frames_removed(exec, code, module.__dict__) def load_module(self, fullname): - """This module is deprecated.""" + """This method is deprecated.""" + # Warning implemented in _load_module_shim(). return _bootstrap._load_module_shim(self, fullname) @@ -1025,7 +1059,7 @@ class FileLoader: """ # The only reason for this method is for the name check. # Issue #14857: Avoid the zero-argument form of super so the implementation - # of that form can be updated without breaking the frozen module + # of that form can be updated without breaking the frozen module. return super(FileLoader, self).load_module(fullname) @_check_name @@ -1042,32 +1076,10 @@ class FileLoader: with _io.FileIO(path, 'r') as file: return file.read() - # ResourceReader ABC API. - @_check_name def get_resource_reader(self, module): - if self.is_package(module): - return self - return None - - def open_resource(self, resource): - path = _path_join(_path_split(self.path)[0], resource) - return _io.FileIO(path, 'r') - - def resource_path(self, resource): - if not self.is_resource(resource): - raise FileNotFoundError - path = _path_join(_path_split(self.path)[0], resource) - return path - - def is_resource(self, name): - if path_sep in name: - return False - path = _path_join(_path_split(self.path)[0], name) - return _path_isfile(path) - - def contents(self): - return iter(_os.listdir(_path_split(self.path)[0])) + from importlib.readers import FileReader + return FileReader(self) class SourceFileLoader(FileLoader, SourceLoader): @@ -1140,10 +1152,6 @@ class SourcelessFileLoader(FileLoader, _LoaderBasics): return None -# Filled in by _setup(). -EXTENSION_SUFFIXES = [] - - class ExtensionFileLoader(FileLoader, _LoaderBasics): """Loader for extension modules. @@ -1154,11 +1162,6 @@ class ExtensionFileLoader(FileLoader, _LoaderBasics): def __init__(self, name, path): self.name = name - if not _path_isabs(path): - try: - path = _path_join(_os.getcwd(), path) - except OSError: - pass self.path = path def __eq__(self, other): @@ -1275,13 +1278,15 @@ class _NamespaceLoader: def __init__(self, name, path, path_finder): self._path = _NamespacePath(name, path, path_finder) - @classmethod - def module_repr(cls, module): + @staticmethod + def module_repr(module): """Return repr for the module. The method is deprecated. The import machinery does the job itself. """ + _warnings.warn("_NamespaceLoader.module_repr() is deprecated and " + "slated for removal in Python 3.12", DeprecationWarning) return '<module {!r} (namespace)>'.format(module.__name__) def is_package(self, fullname): @@ -1308,8 +1313,13 @@ class _NamespaceLoader: # The import system never calls this method. _bootstrap._verbose_message('namespace module loaded with path {!r}', self._path) + # Warning implemented in _load_module_shim(). return _bootstrap._load_module_shim(self, fullname) + def get_resource_reader(self, module): + from importlib.readers import NamespaceReader + return NamespaceReader(self._path) + # Finders ##################################################################### @@ -1317,8 +1327,8 @@ class PathFinder: """Meta path finder for sys.path and package __path__ attributes.""" - @classmethod - def invalidate_caches(cls): + @staticmethod + def invalidate_caches(): """Call the invalidate_caches() method on all path entry finders stored in sys.path_importer_caches (where implemented).""" for name, finder in list(sys.path_importer_cache.items()): @@ -1330,8 +1340,8 @@ class PathFinder: # https://bugs.python.org/issue45703 _NamespacePath._epoch += 1 - @classmethod - def _path_hooks(cls, path): + @staticmethod + def _path_hooks(path): """Search sys.path_hooks for a finder for 'path'.""" if sys.path_hooks is not None and not sys.path_hooks: _warnings.warn('sys.path_hooks is empty', ImportWarning) @@ -1370,8 +1380,14 @@ class PathFinder: # This would be a good place for a DeprecationWarning if # we ended up going that route. if hasattr(finder, 'find_loader'): + msg = (f"{_bootstrap._object_name(finder)}.find_spec() not found; " + "falling back to find_loader()") + _warnings.warn(msg, ImportWarning) loader, portions = finder.find_loader(fullname) else: + msg = (f"{_bootstrap._object_name(finder)}.find_spec() not found; " + "falling back to find_module()") + _warnings.warn(msg, ImportWarning) loader = finder.find_module(fullname) portions = [] if loader is not None: @@ -1444,13 +1460,16 @@ class PathFinder: This method is deprecated. Use find_spec() instead. """ + _warnings.warn("PathFinder.find_module() is deprecated and " + "slated for removal in Python 3.12; use find_spec() instead", + DeprecationWarning) spec = cls.find_spec(fullname, path) if spec is None: return None return spec.loader - @classmethod - def find_distributions(cls, *args, **kwargs): + @staticmethod + def find_distributions(*args, **kwargs): """ Find distributions. @@ -1501,6 +1520,9 @@ class FileFinder: This method is deprecated. Use find_spec() instead. """ + _warnings.warn("FileFinder.find_loader() is deprecated and " + "slated for removal in Python 3.12; use find_spec() instead", + DeprecationWarning) spec = self.find_spec(fullname) if spec is None: return None, [] @@ -1651,66 +1673,14 @@ def _get_supported_file_loaders(): return [extensions, source, bytecode] -def _setup(_bootstrap_module): - """Setup the path-based importers for importlib by importing needed - built-in modules and injecting them into the global namespace. - - Other components are extracted from the core bootstrap module. - - """ - global sys, _imp, _bootstrap +def _set_bootstrap_module(_bootstrap_module): + global _bootstrap _bootstrap = _bootstrap_module - sys = _bootstrap.sys - _imp = _bootstrap._imp - - self_module = sys.modules[__name__] - - # Directly load the os module (needed during bootstrap). - os_details = ('posix', ['/']), ('nt', ['\\', '/']) - for builtin_os, path_separators in os_details: - # Assumption made in _path_join() - assert all(len(sep) == 1 for sep in path_separators) - path_sep = path_separators[0] - if builtin_os in sys.modules: - os_module = sys.modules[builtin_os] - break - else: - try: - os_module = _bootstrap._builtin_from_name(builtin_os) - break - except ImportError: - continue - else: - raise ImportError('importlib requires posix or nt') - - setattr(self_module, '_os', os_module) - setattr(self_module, 'path_sep', path_sep) - setattr(self_module, 'path_separators', ''.join(path_separators)) - setattr(self_module, '_pathseps_with_colon', {f':{s}' for s in path_separators}) - - # Directly load built-in modules needed during bootstrap. - builtin_names = ['_io', '_warnings', 'marshal'] - if builtin_os == 'nt': - builtin_names.append('winreg') - for builtin_name in builtin_names: - if builtin_name not in sys.modules: - builtin_module = _bootstrap._builtin_from_name(builtin_name) - else: - builtin_module = sys.modules[builtin_name] - setattr(self_module, builtin_name, builtin_module) - - # Constants - setattr(self_module, '_relax_case', _make_relax_case()) - EXTENSION_SUFFIXES.extend(_imp.extension_suffixes()) - if builtin_os == 'nt': - SOURCE_SUFFIXES.append('.pyw') - if '_d.pyd' in EXTENSION_SUFFIXES: - WindowsRegistryFinder.DEBUG_BUILD = True def _install(_bootstrap_module): """Install the path-based import components.""" - _setup(_bootstrap_module) + _set_bootstrap_module(_bootstrap_module) supported_loaders = _get_supported_file_loaders() sys.path_hooks.extend([FileFinder.path_hook(*supported_loaders)]) sys.meta_path.append(PathFinder) diff --git a/contrib/tools/python3/src/Lib/importlib/_common.py b/contrib/tools/python3/src/Lib/importlib/_common.py index c1204f0b8f9..549fee379a4 100644 --- a/contrib/tools/python3/src/Lib/importlib/_common.py +++ b/contrib/tools/python3/src/Lib/importlib/_common.py @@ -1,28 +1,82 @@ import os import pathlib -import zipfile import tempfile import functools import contextlib +import types +import importlib +from typing import Union, Any, Optional +from .abc import ResourceReader, Traversable -def from_package(package): +from ._adapters import wrap_spec + +Package = Union[types.ModuleType, str] + + +def files(package): + # type: (Package) -> Traversable """ - Return a Traversable object for the given package. + Get a Traversable resource from a package + """ + return from_package(get_package(package)) + +def normalize_path(path): + # type: (Any) -> str + """Normalize a path by ensuring it is a string. + + If the resulting string contains path separators, an exception is raised. """ - return fallback_resources(package.__spec__) + str_path = str(path) + parent, file_name = os.path.split(str_path) + if parent: + raise ValueError(f'{path!r} must be only a file name') + return file_name -def fallback_resources(spec): - package_directory = pathlib.Path(spec.origin).parent - try: - archive_path = spec.loader.archive - rel_path = package_directory.relative_to(archive_path) - return zipfile.Path(archive_path, str(rel_path) + '/') - except Exception: - pass - return package_directory +def get_resource_reader(package): + # type: (types.ModuleType) -> Optional[ResourceReader] + """ + Return the package's loader if it's a ResourceReader. + """ + # We can't use + # a issubclass() check here because apparently abc.'s __subclasscheck__() + # hook wants to create a weak reference to the object, but + # zipimport.zipimporter does not support weak references, resulting in a + # TypeError. That seems terrible. + spec = package.__spec__ + reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore + if reader is None: + return None + return reader(spec.name) # type: ignore + + +def resolve(cand): + # type: (Package) -> types.ModuleType + return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand) + + +def get_package(package): + # type: (Package) -> types.ModuleType + """Take a package name or module object and return the module. + + Raise an exception if the resolved module is not a package. + """ + resolved = resolve(package) + if wrap_spec(resolved).submodule_search_locations is None: + raise TypeError(f'{package!r} is not a package') + return resolved + + +def from_package(package): + """ + Return a Traversable object for the given package. + + """ + spec = wrap_spec(package) + reader = spec.loader.get_resource_reader(spec.name) + return reader.files() @contextlib.contextmanager @@ -34,6 +88,7 @@ def _tempfile(reader, suffix=''): try: os.write(fd, reader()) os.close(fd) + del reader yield pathlib.Path(raw_path) finally: try: @@ -43,14 +98,12 @@ def _tempfile(reader, suffix=''): @functools.singledispatch def as_file(path): """ Given a Traversable object, return that object as a path on the local file system in a context manager. """ - with _tempfile(path.read_bytes, suffix=path.name) as local: - yield local + return _tempfile(path.read_bytes, suffix=path.name) @as_file.register(pathlib.Path) diff --git a/contrib/tools/python3/src/Lib/importlib/abc.py b/contrib/tools/python3/src/Lib/importlib/abc.py index b8a9bb1a21e..0b4a3f80717 100644 --- a/contrib/tools/python3/src/Lib/importlib/abc.py +++ b/contrib/tools/python3/src/Lib/importlib/abc.py @@ -1,5 +1,4 @@ """Abstract base classes related to import.""" -from . import _bootstrap from . import _bootstrap_external from . import machinery try: @@ -12,8 +11,10 @@ try: import _frozen_importlib_external except ImportError: _frozen_importlib_external = _bootstrap_external +from ._abc import Loader import abc import warnings +from typing import BinaryIO, Iterable, Text from typing import Protocol, runtime_checkable @@ -40,15 +41,27 @@ class Finder(metaclass=abc.ABCMeta): Deprecated since Python 3.3 """ + def __init__(self): + warnings.warn("the Finder ABC is deprecated and " + "slated for removal in Python 3.12; use MetaPathFinder " + "or PathEntryFinder instead", + DeprecationWarning) + @abc.abstractmethod def find_module(self, fullname, path=None): """An abstract method that should find a module. The fullname is a str and the optional path is a str or None. Returns a Loader object or None. """ + warnings.warn("importlib.abc.Finder along with its find_module() " + "method are deprecated and " + "slated for removal in Python 3.12; use " + "MetaPathFinder.find_spec() or " + "PathEntryFinder.find_spec() instead", + DeprecationWarning) -class MetaPathFinder(Finder): +class MetaPathFinder(metaclass=abc.ABCMeta): """Abstract base class for import finders on sys.meta_path.""" @@ -67,8 +80,8 @@ class MetaPathFinder(Finder): """ warnings.warn("MetaPathFinder.find_module() is deprecated since Python " - "3.4 in favor of MetaPathFinder.find_spec() " - "(available since 3.4)", + "3.4 in favor of MetaPathFinder.find_spec() and is " + "slated for removal in Python 3.12", DeprecationWarning, stacklevel=2) if not hasattr(self, 'find_spec'): @@ -85,7 +98,7 @@ _register(MetaPathFinder, machinery.BuiltinImporter, machinery.FrozenImporter, machinery.PathFinder, machinery.WindowsRegistryFinder) -class PathEntryFinder(Finder): +class PathEntryFinder(metaclass=abc.ABCMeta): """Abstract base class for path entry finders used by PathFinder.""" @@ -134,53 +147,6 @@ class PathEntryFinder(Finder): _register(PathEntryFinder, machinery.FileFinder) -class Loader(metaclass=abc.ABCMeta): - - """Abstract base class for import loaders.""" - - def create_module(self, spec): - """Return a module to initialize and into which to load. - - This method should raise ImportError if anything prevents it - from creating a new module. It may return None to indicate - that the spec should create the new module. - """ - # By default, defer to default semantics for the new module. - return None - - # We don't define exec_module() here since that would break - # hasattr checks we do to support backward compatibility. - - def load_module(self, fullname): - """Return the loaded module. - - The module must be added to sys.modules and have import-related - attributes set properly. The fullname is a str. - - ImportError is raised on failure. - - This method is deprecated in favor of loader.exec_module(). If - exec_module() exists then it is used to provide a backwards-compatible - functionality for this method. - - """ - if not hasattr(self, 'exec_module'): - raise ImportError - return _bootstrap._load_module_shim(self, fullname) - - def module_repr(self, module): - """Return a module's repr. - - Used by the module type when the method does not raise - NotImplementedError. - - This method is deprecated. - - """ - # The exception will cause ModuleType.__repr__ to ignore this method. - raise NotImplementedError - - class ResourceLoader(Loader): """Abstract base class for loaders which can return data from their @@ -344,49 +310,45 @@ _register(SourceLoader, machinery.SourceFileLoader) class ResourceReader(metaclass=abc.ABCMeta): - - """Abstract base class to provide resource-reading support. - - Loaders that support resource reading are expected to implement - the ``get_resource_reader(fullname)`` method and have it either return None - or an object compatible with this ABC. - """ + """Abstract base class for loaders to provide resource reading support.""" @abc.abstractmethod - def open_resource(self, resource): + def open_resource(self, resource: Text) -> BinaryIO: """Return an opened, file-like object for binary reading. - The 'resource' argument is expected to represent only a file name - and thus not contain any subdirectory components. - + The 'resource' argument is expected to represent only a file name. If the resource cannot be found, FileNotFoundError is raised. """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. raise FileNotFoundError @abc.abstractmethod - def resource_path(self, resource): + def resource_path(self, resource: Text) -> Text: """Return the file system path to the specified resource. - The 'resource' argument is expected to represent only a file name - and thus not contain any subdirectory components. - + The 'resource' argument is expected to represent only a file name. If the resource does not exist on the file system, raise FileNotFoundError. """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. raise FileNotFoundError @abc.abstractmethod - def is_resource(self, name): - """Return True if the named 'name' is consider a resource.""" + def is_resource(self, path: Text) -> bool: + """Return True if the named 'path' is a resource. + + Files are resources, directories are not. + """ raise FileNotFoundError @abc.abstractmethod - def contents(self): - """Return an iterable of strings over the contents of the package.""" - return [] - - -_register(ResourceReader, machinery.SourceFileLoader) + def contents(self) -> Iterable[str]: + """Return an iterable of entries in `package`.""" + raise FileNotFoundError @runtime_checkable @@ -402,26 +364,28 @@ class Traversable(Protocol): Yield Traversable objects in self """ - @abc.abstractmethod def read_bytes(self): """ Read contents of self as bytes """ + with self.open('rb') as strm: + return strm.read() - @abc.abstractmethod def read_text(self, encoding=None): """ - Read contents of self as bytes + Read contents of self as text """ + with self.open(encoding=encoding) as strm: + return strm.read() @abc.abstractmethod - def is_dir(self): + def is_dir(self) -> bool: """ Return True if self is a dir """ @abc.abstractmethod - def is_file(self): + def is_file(self) -> bool: """ Return True if self is a file """ @@ -432,11 +396,11 @@ class Traversable(Protocol): Return Traversable child in self """ - @abc.abstractmethod def __truediv__(self, child): """ Return Traversable child in self """ + return self.joinpath(child) @abc.abstractmethod def open(self, mode='r', *args, **kwargs): @@ -449,14 +413,18 @@ class Traversable(Protocol): """ @abc.abstractproperty - def name(self): - # type: () -> str + def name(self) -> str: """ The base name of this object without any parent references. """ class TraversableResources(ResourceReader): + """ + The required interface for providing traversable + resources. + """ + @abc.abstractmethod def files(self): """Return a Traversable object for the loaded package.""" @@ -468,7 +436,7 @@ class TraversableResources(ResourceReader): raise FileNotFoundError(resource) def is_resource(self, path): - return self.files().joinpath(path).isfile() + return self.files().joinpath(path).is_file() def contents(self): return (item.name for item in self.files().iterdir()) diff --git a/contrib/tools/python3/src/Lib/importlib/machinery.py b/contrib/tools/python3/src/Lib/importlib/machinery.py index 1b2b5c9b4f3..9a7757fb6e4 100644 --- a/contrib/tools/python3/src/Lib/importlib/machinery.py +++ b/contrib/tools/python3/src/Lib/importlib/machinery.py @@ -1,7 +1,5 @@ """The machinery of importlib: finders, loaders, hooks, etc.""" -import _imp - from ._bootstrap import ModuleSpec from ._bootstrap import BuiltinImporter from ._bootstrap import FrozenImporter diff --git a/contrib/tools/python3/src/Lib/importlib/metadata.py b/contrib/tools/python3/src/Lib/importlib/metadata.py deleted file mode 100644 index 647fd3e4dbf..00000000000 --- a/contrib/tools/python3/src/Lib/importlib/metadata.py +++ /dev/null @@ -1,604 +0,0 @@ -import io -import os -import re -import abc -import csv -import sys -import email -import pathlib -import zipfile -import operator -import functools -import itertools -import posixpath -import collections - -from configparser import ConfigParser -from contextlib import suppress -from importlib import import_module -from importlib.abc import MetaPathFinder -from itertools import starmap - - -__all__ = [ - 'Distribution', - 'DistributionFinder', - 'PackageNotFoundError', - 'distribution', - 'distributions', - 'entry_points', - 'files', - 'metadata', - 'requires', - 'version', - ] - - -class PackageNotFoundError(ModuleNotFoundError): - """The package was not found.""" - - -class EntryPoint( - collections.namedtuple('EntryPointBase', 'name value group')): - """An entry point as defined by Python packaging conventions. - - See `the packaging docs on entry points - <https://packaging.python.org/specifications/entry-points/>`_ - for more information. - - >>> ep = EntryPoint( - ... name=None, group=None, value='package.module:attr [extra1, extra2]') - >>> ep.module - 'package.module' - >>> ep.attr - 'attr' - >>> ep.extras - ['extra1', 'extra2'] - """ - - pattern = re.compile( - r'(?P<module>[\w.]+)\s*' - r'(:\s*(?P<attr>[\w.]+)\s*)?' - r'((?P<extras>\[.*\])\s*)?$' - ) - """ - A regular expression describing the syntax for an entry point, - which might look like: - - - module - - package.module - - package.module:attribute - - package.module:object.attribute - - package.module:attr [extra1, extra2] - - Other combinations are possible as well. - - The expression is lenient about whitespace around the ':', - following the attr, and following any extras. - """ - - def load(self): - """Load the entry point from its definition. If only a module - is indicated by the value, return that module. Otherwise, - return the named object. - """ - match = self.pattern.match(self.value) - module = import_module(match.group('module')) - attrs = filter(None, (match.group('attr') or '').split('.')) - return functools.reduce(getattr, attrs, module) - - @property - def module(self): - match = self.pattern.match(self.value) - return match.group('module') - - @property - def attr(self): - match = self.pattern.match(self.value) - return match.group('attr') - - @property - def extras(self): - match = self.pattern.match(self.value) - return re.findall(r'\w+', match.group('extras') or '') - - @classmethod - def _from_config(cls, config): - return [ - cls(name, value, group) - for group in config.sections() - for name, value in config.items(group) - ] - - @classmethod - def _from_text(cls, text): - config = ConfigParser(delimiters='=') - # case sensitive: https://stackoverflow.com/q/1611799/812183 - config.optionxform = str - try: - config.read_string(text) - except AttributeError: # pragma: nocover - # Python 2 has no read_string - config.readfp(io.StringIO(text)) - return EntryPoint._from_config(config) - - def __iter__(self): - """ - Supply iter so one may construct dicts of EntryPoints easily. - """ - return iter((self.name, self)) - - def __reduce__(self): - return ( - self.__class__, - (self.name, self.value, self.group), - ) - - -class PackagePath(pathlib.PurePosixPath): - """A reference to a path in a package""" - - def read_text(self, encoding='utf-8'): - with self.locate().open(encoding=encoding) as stream: - return stream.read() - - def read_binary(self): - with self.locate().open('rb') as stream: - return stream.read() - - def locate(self): - """Return a path-like object for this path""" - return self.dist.locate_file(self) - - -class FileHash: - def __init__(self, spec): - self.mode, _, self.value = spec.partition('=') - - def __repr__(self): - return '<FileHash mode: {} value: {}>'.format(self.mode, self.value) - - -class Distribution: - """A Python distribution package.""" - - @abc.abstractmethod - def read_text(self, filename): - """Attempt to load metadata file given by the name. - - :param filename: The name of the file in the distribution info. - :return: The text if found, otherwise None. - """ - - @abc.abstractmethod - def locate_file(self, path): - """ - Given a path to a file in this distribution, return a path - to it. - """ - - @classmethod - def from_name(cls, name): - """Return the Distribution for the given package name. - - :param name: The name of the distribution package to search for. - :return: The Distribution instance (or subclass thereof) for the named - package, if found. - :raises PackageNotFoundError: When the named package's distribution - metadata cannot be found. - """ - for resolver in cls._discover_resolvers(): - dists = resolver(DistributionFinder.Context(name=name)) - dist = next(iter(dists), None) - if dist is not None: - return dist - else: - raise PackageNotFoundError(name) - - @classmethod - def discover(cls, **kwargs): - """Return an iterable of Distribution objects for all packages. - - Pass a ``context`` or pass keyword arguments for constructing - a context. - - :context: A ``DistributionFinder.Context`` object. - :return: Iterable of Distribution objects for all packages. - """ - context = kwargs.pop('context', None) - if context and kwargs: - raise ValueError("cannot accept context and kwargs") - context = context or DistributionFinder.Context(**kwargs) - return itertools.chain.from_iterable( - resolver(context) - for resolver in cls._discover_resolvers() - ) - - @staticmethod - def at(path): - """Return a Distribution for the indicated metadata path - - :param path: a string or path-like object - :return: a concrete Distribution instance for the path - """ - return PathDistribution(pathlib.Path(path)) - - @staticmethod - def _discover_resolvers(): - """Search the meta_path for resolvers.""" - declared = ( - getattr(finder, 'find_distributions', None) - for finder in sys.meta_path - ) - return filter(None, declared) - - @classmethod - def _local(cls, root='.'): - from pep517 import build, meta - system = build.compat_system(root) - builder = functools.partial( - meta.build, - source_dir=root, - system=system, - ) - return PathDistribution(zipfile.Path(meta.build_as_zip(builder))) - - @property - def metadata(self): - """Return the parsed metadata for this Distribution. - - The returned object will have keys that name the various bits of - metadata. See PEP 566 for details. - """ - text = ( - self.read_text('METADATA') - or self.read_text('PKG-INFO') - # This last clause is here to support old egg-info files. Its - # effect is to just end up using the PathDistribution's self._path - # (which points to the egg-info file) attribute unchanged. - or self.read_text('') - ) - return email.message_from_string(text) - - @property - def version(self): - """Return the 'Version' metadata for the distribution package.""" - return self.metadata['Version'] - - @property - def entry_points(self): - return EntryPoint._from_text(self.read_text('entry_points.txt')) - - @property - def files(self): - """Files in this distribution. - - :return: List of PackagePath for this distribution or None - - Result is `None` if the metadata file that enumerates files - (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is - missing. - Result may be empty if the metadata exists but is empty. - """ - file_lines = self._read_files_distinfo() or self._read_files_egginfo() - - def make_file(name, hash=None, size_str=None): - result = PackagePath(name) - result.hash = FileHash(hash) if hash else None - result.size = int(size_str) if size_str else None - result.dist = self - return result - - return file_lines and list(starmap(make_file, csv.reader(file_lines))) - - def _read_files_distinfo(self): - """ - Read the lines of RECORD - """ - text = self.read_text('RECORD') - return text and text.splitlines() - - def _read_files_egginfo(self): - """ - SOURCES.txt might contain literal commas, so wrap each line - in quotes. - """ - text = self.read_text('SOURCES.txt') - return text and map('"{}"'.format, text.splitlines()) - - @property - def requires(self): - """Generated requirements specified for this Distribution""" - reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() - return reqs and list(reqs) - - def _read_dist_info_reqs(self): - return self.metadata.get_all('Requires-Dist') - - def _read_egg_info_reqs(self): - source = self.read_text('requires.txt') - return None if source is None else self._deps_from_requires_text(source) - - @classmethod - def _deps_from_requires_text(cls, source): - section_pairs = cls._read_sections(source.splitlines()) - sections = { - section: list(map(operator.itemgetter('line'), results)) - for section, results in - itertools.groupby(section_pairs, operator.itemgetter('section')) - } - return cls._convert_egg_info_reqs_to_simple_reqs(sections) - - @staticmethod - def _read_sections(lines): - section = None - for line in filter(None, lines): - section_match = re.match(r'\[(.*)\]$', line) - if section_match: - section = section_match.group(1) - continue - yield locals() - - @staticmethod - def _convert_egg_info_reqs_to_simple_reqs(sections): - """ - Historically, setuptools would solicit and store 'extra' - requirements, including those with environment markers, - in separate sections. More modern tools expect each - dependency to be defined separately, with any relevant - extras and environment markers attached directly to that - requirement. This method converts the former to the - latter. See _test_deps_from_requires_text for an example. - """ - def make_condition(name): - return name and 'extra == "{name}"'.format(name=name) - - def quoted_marker(section): - section = section or '' - extra, sep, markers = section.partition(':') - if extra and markers: - markers = f'({markers})' - conditions = list(filter(None, [markers, make_condition(extra)])) - return '; ' + ' and '.join(conditions) if conditions else '' - - def url_req_space(req): - """ - PEP 508 requires a space between the url_spec and the quoted_marker. - Ref python/importlib_metadata#357. - """ - # '@' is uniquely indicative of a url_req. - return ' ' * ('@' in req) - - for section, deps in sections.items(): - for dep in deps: - space = url_req_space(dep) - yield dep + space + quoted_marker(section) - - -class DistributionFinder(MetaPathFinder): - """ - A MetaPathFinder capable of discovering installed distributions. - """ - - class Context: - """ - Keyword arguments presented by the caller to - ``distributions()`` or ``Distribution.discover()`` - to narrow the scope of a search for distributions - in all DistributionFinders. - - Each DistributionFinder may expect any parameters - and should attempt to honor the canonical - parameters defined below when appropriate. - """ - - name = None - """ - Specific name for which a distribution finder should match. - A name of ``None`` matches all distributions. - """ - - def __init__(self, **kwargs): - vars(self).update(kwargs) - - @property - def path(self): - """ - The path that a distribution finder should search. - - Typically refers to Python package paths and defaults - to ``sys.path``. - """ - return vars(self).get('path', sys.path) - - @abc.abstractmethod - def find_distributions(self, context=Context()): - """ - Find distributions. - - Return an iterable of all Distribution instances capable of - loading the metadata for packages matching the ``context``, - a DistributionFinder.Context instance. - """ - - -class FastPath: - """ - Micro-optimized class for searching a path for - children. - """ - - def __init__(self, root): - self.root = root - self.base = os.path.basename(self.root).lower() - - def joinpath(self, child): - return pathlib.Path(self.root, child) - - def children(self): - with suppress(Exception): - return os.listdir(self.root or '.') - with suppress(Exception): - return self.zip_children() - return [] - - def zip_children(self): - zip_path = zipfile.Path(self.root) - names = zip_path.root.namelist() - self.joinpath = zip_path.joinpath - - return dict.fromkeys( - child.split(posixpath.sep, 1)[0] - for child in names - ) - - def is_egg(self, search): - base = self.base - return ( - base == search.versionless_egg_name - or base.startswith(search.prefix) - and base.endswith('.egg')) - - def search(self, name): - for child in self.children(): - n_low = child.lower() - if (n_low in name.exact_matches - or n_low.startswith(name.prefix) - and n_low.endswith(name.suffixes) - # legacy case: - or self.is_egg(name) and n_low == 'egg-info'): - yield self.joinpath(child) - - -class Prepared: - """ - A prepared search for metadata on a possibly-named package. - """ - normalized = '' - prefix = '' - suffixes = '.dist-info', '.egg-info' - exact_matches = [''][:0] - versionless_egg_name = '' - - def __init__(self, name): - self.name = name - if name is None: - return - self.normalized = name.lower().replace('-', '_') - self.prefix = self.normalized + '-' - self.exact_matches = [ - self.normalized + suffix for suffix in self.suffixes] - self.versionless_egg_name = self.normalized + '.egg' - - -class MetadataPathFinder(DistributionFinder): - @classmethod - def find_distributions(cls, context=DistributionFinder.Context()): - """ - Find distributions. - - Return an iterable of all Distribution instances capable of - loading the metadata for packages matching ``context.name`` - (or all names if ``None`` indicated) along the paths in the list - of directories ``context.path``. - """ - found = cls._search_paths(context.name, context.path) - return map(PathDistribution, found) - - @classmethod - def _search_paths(cls, name, paths): - """Find metadata directories in paths heuristically.""" - return itertools.chain.from_iterable( - path.search(Prepared(name)) - for path in map(FastPath, paths) - ) - - -class PathDistribution(Distribution): - def __init__(self, path): - """Construct a distribution from a path to the metadata directory. - - :param path: A pathlib.Path or similar object supporting - .joinpath(), __div__, .parent, and .read_text(). - """ - self._path = path - - def read_text(self, filename): - with suppress(FileNotFoundError, IsADirectoryError, KeyError, - NotADirectoryError, PermissionError): - return self._path.joinpath(filename).read_text(encoding='utf-8') - read_text.__doc__ = Distribution.read_text.__doc__ - - def locate_file(self, path): - return self._path.parent / path - - -def distribution(distribution_name): - """Get the ``Distribution`` instance for the named package. - - :param distribution_name: The name of the distribution package as a string. - :return: A ``Distribution`` instance (or subclass thereof). - """ - return Distribution.from_name(distribution_name) - - -def distributions(**kwargs): - """Get all ``Distribution`` instances in the current environment. - - :return: An iterable of ``Distribution`` instances. - """ - return Distribution.discover(**kwargs) - - -def metadata(distribution_name): - """Get the metadata for the named package. - - :param distribution_name: The name of the distribution package to query. - :return: An email.Message containing the parsed metadata. - """ - return Distribution.from_name(distribution_name).metadata - - -def version(distribution_name): - """Get the version string for the named package. - - :param distribution_name: The name of the distribution package to query. - :return: The version string for the package as defined in the package's - "Version" metadata key. - """ - return distribution(distribution_name).version - - -def entry_points(): - """Return EntryPoint objects for all installed packages. - - :return: EntryPoint objects for all installed packages. - """ - eps = itertools.chain.from_iterable( - dist.entry_points for dist in distributions()) - by_group = operator.attrgetter('group') - ordered = sorted(eps, key=by_group) - grouped = itertools.groupby(ordered, by_group) - return { - group: tuple(eps) - for group, eps in grouped - } - - -def files(distribution_name): - """Return a list of files for the named package. - - :param distribution_name: The name of the distribution package to query. - :return: List of files composing the distribution. - """ - return distribution(distribution_name).files - - -def requires(distribution_name): - """ - Return a list of requirements for the named package. - - :return: An iterator of requirements, suitable for - packaging.requirement.Requirement. - """ - return distribution(distribution_name).requires diff --git a/contrib/tools/python3/src/Lib/importlib/metadata/__init__.py b/contrib/tools/python3/src/Lib/importlib/metadata/__init__.py new file mode 100644 index 00000000000..b3063cd9915 --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/metadata/__init__.py @@ -0,0 +1,1045 @@ +import os +import re +import abc +import csv +import sys +import email +import pathlib +import zipfile +import operator +import textwrap +import warnings +import functools +import itertools +import posixpath +import collections + +from . import _adapters, _meta +from ._meta import PackageMetadata +from ._collections import FreezableDefaultDict, Pair +from ._functools import method_cache +from ._itertools import unique_everseen +from ._meta import PackageMetadata, SimplePath + +from contextlib import suppress +from importlib import import_module +from importlib.abc import MetaPathFinder +from itertools import starmap +from typing import List, Mapping, Optional, Union + + +__all__ = [ + 'Distribution', + 'DistributionFinder', + 'PackageMetadata', + 'PackageNotFoundError', + 'distribution', + 'distributions', + 'entry_points', + 'files', + 'metadata', + 'packages_distributions', + 'requires', + 'version', +] + + +class PackageNotFoundError(ModuleNotFoundError): + """The package was not found.""" + + def __str__(self): + return f"No package metadata was found for {self.name}" + + @property + def name(self): + (name,) = self.args + return name + + +class Sectioned: + """ + A simple entry point config parser for performance + + >>> for item in Sectioned.read(Sectioned._sample): + ... print(item) + Pair(name='sec1', value='# comments ignored') + Pair(name='sec1', value='a = 1') + Pair(name='sec1', value='b = 2') + Pair(name='sec2', value='a = 2') + + >>> res = Sectioned.section_pairs(Sectioned._sample) + >>> item = next(res) + >>> item.name + 'sec1' + >>> item.value + Pair(name='a', value='1') + >>> item = next(res) + >>> item.value + Pair(name='b', value='2') + >>> item = next(res) + >>> item.name + 'sec2' + >>> item.value + Pair(name='a', value='2') + >>> list(res) + [] + """ + + _sample = textwrap.dedent( + """ + [sec1] + # comments ignored + a = 1 + b = 2 + + [sec2] + a = 2 + """ + ).lstrip() + + @classmethod + def section_pairs(cls, text): + return ( + section._replace(value=Pair.parse(section.value)) + for section in cls.read(text, filter_=cls.valid) + if section.name is not None + ) + + @staticmethod + def read(text, filter_=None): + lines = filter(filter_, map(str.strip, text.splitlines())) + name = None + for value in lines: + section_match = value.startswith('[') and value.endswith(']') + if section_match: + name = value.strip('[]') + continue + yield Pair(name, value) + + @staticmethod + def valid(line): + return line and not line.startswith('#') + + +class EntryPoint( + collections.namedtuple('EntryPointBase', 'name value group')): + """An entry point as defined by Python packaging conventions. + + See `the packaging docs on entry points + <https://packaging.python.org/specifications/entry-points/>`_ + for more information. + + >>> ep = EntryPoint( + ... name=None, group=None, value='package.module:attr [extra1, extra2]') + >>> ep.module + 'package.module' + >>> ep.attr + 'attr' + >>> ep.extras + ['extra1', 'extra2'] + """ + + pattern = re.compile( + r'(?P<module>[\w.]+)\s*' + r'(:\s*(?P<attr>[\w.]+)\s*)?' + r'((?P<extras>\[.*\])\s*)?$' + ) + """ + A regular expression describing the syntax for an entry point, + which might look like: + + - module + - package.module + - package.module:attribute + - package.module:object.attribute + - package.module:attr [extra1, extra2] + + Other combinations are possible as well. + + The expression is lenient about whitespace around the ':', + following the attr, and following any extras. + """ + + dist: Optional['Distribution'] = None + + def load(self): + """Load the entry point from its definition. If only a module + is indicated by the value, return that module. Otherwise, + return the named object. + """ + match = self.pattern.match(self.value) + module = import_module(match.group('module')) + attrs = filter(None, (match.group('attr') or '').split('.')) + return functools.reduce(getattr, attrs, module) + + @property + def module(self): + match = self.pattern.match(self.value) + return match.group('module') + + @property + def attr(self): + match = self.pattern.match(self.value) + return match.group('attr') + + @property + def extras(self): + match = self.pattern.match(self.value) + return re.findall(r'\w+', match.group('extras') or '') + + def _for(self, dist): + self.dist = dist + return self + + def __iter__(self): + """ + Supply iter so one may construct dicts of EntryPoints by name. + """ + msg = ( + "Construction of dict of EntryPoints is deprecated in " + "favor of EntryPoints." + ) + warnings.warn(msg, DeprecationWarning) + return iter((self.name, self)) + + def __reduce__(self): + return ( + self.__class__, + (self.name, self.value, self.group), + ) + + def matches(self, **params): + """ + EntryPoint matches the given parameters. + + >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]') + >>> ep.matches(group='foo') + True + >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]') + True + >>> ep.matches(group='foo', name='other') + False + >>> ep.matches() + True + >>> ep.matches(extras=['extra1', 'extra2']) + True + >>> ep.matches(module='bing') + True + >>> ep.matches(attr='bong') + True + """ + attrs = (getattr(self, param) for param in params) + return all(map(operator.eq, params.values(), attrs)) + + +class DeprecatedList(list): + """ + Allow an otherwise immutable object to implement mutability + for compatibility. + + >>> recwarn = getfixture('recwarn') + >>> dl = DeprecatedList(range(3)) + >>> dl[0] = 1 + >>> dl.append(3) + >>> del dl[3] + >>> dl.reverse() + >>> dl.sort() + >>> dl.extend([4]) + >>> dl.pop(-1) + 4 + >>> dl.remove(1) + >>> dl += [5] + >>> dl + [6] + [1, 2, 5, 6] + >>> dl + (6,) + [1, 2, 5, 6] + >>> dl.insert(0, 0) + >>> dl + [0, 1, 2, 5] + >>> dl == [0, 1, 2, 5] + True + >>> dl == (0, 1, 2, 5) + True + >>> len(recwarn) + 1 + """ + + __slots__ = () + + _warn = functools.partial( + warnings.warn, + "EntryPoints list interface is deprecated. Cast to list if needed.", + DeprecationWarning, + stacklevel=2, + ) + + def __setitem__(self, *args, **kwargs): + self._warn() + return super().__setitem__(*args, **kwargs) + + def __delitem__(self, *args, **kwargs): + self._warn() + return super().__delitem__(*args, **kwargs) + + def append(self, *args, **kwargs): + self._warn() + return super().append(*args, **kwargs) + + def reverse(self, *args, **kwargs): + self._warn() + return super().reverse(*args, **kwargs) + + def extend(self, *args, **kwargs): + self._warn() + return super().extend(*args, **kwargs) + + def pop(self, *args, **kwargs): + self._warn() + return super().pop(*args, **kwargs) + + def remove(self, *args, **kwargs): + self._warn() + return super().remove(*args, **kwargs) + + def __iadd__(self, *args, **kwargs): + self._warn() + return super().__iadd__(*args, **kwargs) + + def __add__(self, other): + if not isinstance(other, tuple): + self._warn() + other = tuple(other) + return self.__class__(tuple(self) + other) + + def insert(self, *args, **kwargs): + self._warn() + return super().insert(*args, **kwargs) + + def sort(self, *args, **kwargs): + self._warn() + return super().sort(*args, **kwargs) + + def __eq__(self, other): + if not isinstance(other, tuple): + self._warn() + other = tuple(other) + + return tuple(self).__eq__(other) + + +class EntryPoints(DeprecatedList): + """ + An immutable collection of selectable EntryPoint objects. + """ + + __slots__ = () + + def __getitem__(self, name): # -> EntryPoint: + """ + Get the EntryPoint in self matching name. + """ + if isinstance(name, int): + warnings.warn( + "Accessing entry points by index is deprecated. " + "Cast to tuple if needed.", + DeprecationWarning, + stacklevel=2, + ) + return super().__getitem__(name) + try: + return next(iter(self.select(name=name))) + except StopIteration: + raise KeyError(name) + + def select(self, **params): + """ + Select entry points from self that match the + given parameters (typically group and/or name). + """ + return EntryPoints(ep for ep in self if ep.matches(**params)) + + @property + def names(self): + """ + Return the set of all names of all entry points. + """ + return set(ep.name for ep in self) + + @property + def groups(self): + """ + Return the set of all groups of all entry points. + + For coverage while SelectableGroups is present. + >>> EntryPoints().groups + set() + """ + return set(ep.group for ep in self) + + @classmethod + def _from_text_for(cls, text, dist): + return cls(ep._for(dist) for ep in cls._from_text(text)) + + @classmethod + def _from_text(cls, text): + return itertools.starmap(EntryPoint, cls._parse_groups(text or '')) + + @staticmethod + def _parse_groups(text): + return ( + (item.value.name, item.value.value, item.name) + for item in Sectioned.section_pairs(text) + ) + + +class Deprecated: + """ + Compatibility add-in for mapping to indicate that + mapping behavior is deprecated. + + >>> recwarn = getfixture('recwarn') + >>> class DeprecatedDict(Deprecated, dict): pass + >>> dd = DeprecatedDict(foo='bar') + >>> dd.get('baz', None) + >>> dd['foo'] + 'bar' + >>> list(dd) + ['foo'] + >>> list(dd.keys()) + ['foo'] + >>> 'foo' in dd + True + >>> list(dd.values()) + ['bar'] + >>> len(recwarn) + 1 + """ + + _warn = functools.partial( + warnings.warn, + "SelectableGroups dict interface is deprecated. Use select.", + DeprecationWarning, + stacklevel=2, + ) + + def __getitem__(self, name): + self._warn() + return super().__getitem__(name) + + def get(self, name, default=None): + self._warn() + return super().get(name, default) + + def __iter__(self): + self._warn() + return super().__iter__() + + def __contains__(self, *args): + self._warn() + return super().__contains__(*args) + + def keys(self): + self._warn() + return super().keys() + + def values(self): + self._warn() + return super().values() + + +class SelectableGroups(Deprecated, dict): + """ + A backward- and forward-compatible result from + entry_points that fully implements the dict interface. + """ + + @classmethod + def load(cls, eps): + by_group = operator.attrgetter('group') + ordered = sorted(eps, key=by_group) + grouped = itertools.groupby(ordered, by_group) + return cls((group, EntryPoints(eps)) for group, eps in grouped) + + @property + def _all(self): + """ + Reconstruct a list of all entrypoints from the groups. + """ + groups = super(Deprecated, self).values() + return EntryPoints(itertools.chain.from_iterable(groups)) + + @property + def groups(self): + return self._all.groups + + @property + def names(self): + """ + for coverage: + >>> SelectableGroups().names + set() + """ + return self._all.names + + def select(self, **params): + if not params: + return self + return self._all.select(**params) + + +class PackagePath(pathlib.PurePosixPath): + """A reference to a path in a package""" + + def read_text(self, encoding='utf-8'): + with self.locate().open(encoding=encoding) as stream: + return stream.read() + + def read_binary(self): + with self.locate().open('rb') as stream: + return stream.read() + + def locate(self): + """Return a path-like object for this path""" + return self.dist.locate_file(self) + + +class FileHash: + def __init__(self, spec): + self.mode, _, self.value = spec.partition('=') + + def __repr__(self): + return f'<FileHash mode: {self.mode} value: {self.value}>' + + +class Distribution: + """A Python distribution package.""" + + @abc.abstractmethod + def read_text(self, filename): + """Attempt to load metadata file given by the name. + + :param filename: The name of the file in the distribution info. + :return: The text if found, otherwise None. + """ + + @abc.abstractmethod + def locate_file(self, path): + """ + Given a path to a file in this distribution, return a path + to it. + """ + + @classmethod + def from_name(cls, name): + """Return the Distribution for the given package name. + + :param name: The name of the distribution package to search for. + :return: The Distribution instance (or subclass thereof) for the named + package, if found. + :raises PackageNotFoundError: When the named package's distribution + metadata cannot be found. + """ + for resolver in cls._discover_resolvers(): + dists = resolver(DistributionFinder.Context(name=name)) + dist = next(iter(dists), None) + if dist is not None: + return dist + else: + raise PackageNotFoundError(name) + + @classmethod + def discover(cls, **kwargs): + """Return an iterable of Distribution objects for all packages. + + Pass a ``context`` or pass keyword arguments for constructing + a context. + + :context: A ``DistributionFinder.Context`` object. + :return: Iterable of Distribution objects for all packages. + """ + context = kwargs.pop('context', None) + if context and kwargs: + raise ValueError("cannot accept context and kwargs") + context = context or DistributionFinder.Context(**kwargs) + return itertools.chain.from_iterable( + resolver(context) for resolver in cls._discover_resolvers() + ) + + @staticmethod + def at(path): + """Return a Distribution for the indicated metadata path + + :param path: a string or path-like object + :return: a concrete Distribution instance for the path + """ + return PathDistribution(pathlib.Path(path)) + + @staticmethod + def _discover_resolvers(): + """Search the meta_path for resolvers.""" + declared = ( + getattr(finder, 'find_distributions', None) for finder in sys.meta_path + ) + return filter(None, declared) + + @classmethod + def _local(cls, root='.'): + from pep517 import build, meta + + system = build.compat_system(root) + builder = functools.partial( + meta.build, + source_dir=root, + system=system, + ) + return PathDistribution(zipfile.Path(meta.build_as_zip(builder))) + + @property + def metadata(self) -> _meta.PackageMetadata: + """Return the parsed metadata for this Distribution. + + The returned object will have keys that name the various bits of + metadata. See PEP 566 for details. + """ + text = ( + self.read_text('METADATA') + or self.read_text('PKG-INFO') + # This last clause is here to support old egg-info files. Its + # effect is to just end up using the PathDistribution's self._path + # (which points to the egg-info file) attribute unchanged. + or self.read_text('') + ) + return _adapters.Message(email.message_from_string(text)) + + @property + def name(self): + """Return the 'Name' metadata for the distribution package.""" + return self.metadata['Name'] + + @property + def _normalized_name(self): + """Return a normalized version of the name.""" + return Prepared.normalize(self.name) + + @property + def version(self): + """Return the 'Version' metadata for the distribution package.""" + return self.metadata['Version'] + + @property + def entry_points(self): + return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) + + @property + def files(self): + """Files in this distribution. + + :return: List of PackagePath for this distribution or None + + Result is `None` if the metadata file that enumerates files + (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is + missing. + Result may be empty if the metadata exists but is empty. + """ + file_lines = self._read_files_distinfo() or self._read_files_egginfo() + + def make_file(name, hash=None, size_str=None): + result = PackagePath(name) + result.hash = FileHash(hash) if hash else None + result.size = int(size_str) if size_str else None + result.dist = self + return result + + return file_lines and list(starmap(make_file, csv.reader(file_lines))) + + def _read_files_distinfo(self): + """ + Read the lines of RECORD + """ + text = self.read_text('RECORD') + return text and text.splitlines() + + def _read_files_egginfo(self): + """ + SOURCES.txt might contain literal commas, so wrap each line + in quotes. + """ + text = self.read_text('SOURCES.txt') + return text and map('"{}"'.format, text.splitlines()) + + @property + def requires(self): + """Generated requirements specified for this Distribution""" + reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() + return reqs and list(reqs) + + def _read_dist_info_reqs(self): + return self.metadata.get_all('Requires-Dist') + + def _read_egg_info_reqs(self): + source = self.read_text('requires.txt') + return None if source is None else self._deps_from_requires_text(source) + + @classmethod + def _deps_from_requires_text(cls, source): + return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source)) + + @staticmethod + def _convert_egg_info_reqs_to_simple_reqs(sections): + """ + Historically, setuptools would solicit and store 'extra' + requirements, including those with environment markers, + in separate sections. More modern tools expect each + dependency to be defined separately, with any relevant + extras and environment markers attached directly to that + requirement. This method converts the former to the + latter. See _test_deps_from_requires_text for an example. + """ + + def make_condition(name): + return name and f'extra == "{name}"' + + def quoted_marker(section): + section = section or '' + extra, sep, markers = section.partition(':') + if extra and markers: + markers = f'({markers})' + conditions = list(filter(None, [markers, make_condition(extra)])) + return '; ' + ' and '.join(conditions) if conditions else '' + + def url_req_space(req): + """ + PEP 508 requires a space between the url_spec and the quoted_marker. + Ref python/importlib_metadata#357. + """ + # '@' is uniquely indicative of a url_req. + return ' ' * ('@' in req) + + for section in sections: + space = url_req_space(section.value) + yield section.value + space + quoted_marker(section.name) + + +class DistributionFinder(MetaPathFinder): + """ + A MetaPathFinder capable of discovering installed distributions. + """ + + class Context: + """ + Keyword arguments presented by the caller to + ``distributions()`` or ``Distribution.discover()`` + to narrow the scope of a search for distributions + in all DistributionFinders. + + Each DistributionFinder may expect any parameters + and should attempt to honor the canonical + parameters defined below when appropriate. + """ + + name = None + """ + Specific name for which a distribution finder should match. + A name of ``None`` matches all distributions. + """ + + def __init__(self, **kwargs): + vars(self).update(kwargs) + + @property + def path(self): + """ + The sequence of directory path that a distribution finder + should search. + + Typically refers to Python installed package paths such as + "site-packages" directories and defaults to ``sys.path``. + """ + return vars(self).get('path', sys.path) + + @abc.abstractmethod + def find_distributions(self, context=Context()): + """ + Find distributions. + + Return an iterable of all Distribution instances capable of + loading the metadata for packages matching the ``context``, + a DistributionFinder.Context instance. + """ + + +class FastPath: + """ + Micro-optimized class for searching a path for + children. + """ + + @functools.lru_cache() # type: ignore + def __new__(cls, root): + return super().__new__(cls) + + def __init__(self, root): + self.root = root + + def joinpath(self, child): + return pathlib.Path(self.root, child) + + def children(self): + with suppress(Exception): + return os.listdir(self.root or '.') + with suppress(Exception): + return self.zip_children() + return [] + + def zip_children(self): + zip_path = zipfile.Path(self.root) + names = zip_path.root.namelist() + self.joinpath = zip_path.joinpath + + return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names) + + def search(self, name): + return self.lookup(self.mtime).search(name) + + @property + def mtime(self): + with suppress(OSError): + return os.stat(self.root).st_mtime + self.lookup.cache_clear() + + @method_cache + def lookup(self, mtime): + return Lookup(self) + + +class Lookup: + def __init__(self, path: FastPath): + base = os.path.basename(path.root).lower() + base_is_egg = base.endswith(".egg") + self.infos = FreezableDefaultDict(list) + self.eggs = FreezableDefaultDict(list) + + for child in path.children(): + low = child.lower() + if low.endswith((".dist-info", ".egg-info")): + # rpartition is faster than splitext and suitable for this purpose. + name = low.rpartition(".")[0].partition("-")[0] + normalized = Prepared.normalize(name) + self.infos[normalized].append(path.joinpath(child)) + elif base_is_egg and low == "egg-info": + name = base.rpartition(".")[0].partition("-")[0] + legacy_normalized = Prepared.legacy_normalize(name) + self.eggs[legacy_normalized].append(path.joinpath(child)) + + self.infos.freeze() + self.eggs.freeze() + + def search(self, prepared): + infos = ( + self.infos[prepared.normalized] + if prepared + else itertools.chain.from_iterable(self.infos.values()) + ) + eggs = ( + self.eggs[prepared.legacy_normalized] + if prepared + else itertools.chain.from_iterable(self.eggs.values()) + ) + return itertools.chain(infos, eggs) + + +class Prepared: + """ + A prepared search for metadata on a possibly-named package. + """ + + normalized = None + legacy_normalized = None + + def __init__(self, name): + self.name = name + if name is None: + return + self.normalized = self.normalize(name) + self.legacy_normalized = self.legacy_normalize(name) + + @staticmethod + def normalize(name): + """ + PEP 503 normalization plus dashes as underscores. + """ + return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_') + + @staticmethod + def legacy_normalize(name): + """ + Normalize the package name as found in the convention in + older packaging tools versions and specs. + """ + return name.lower().replace('-', '_') + + def __bool__(self): + return bool(self.name) + + +class MetadataPathFinder(DistributionFinder): + @classmethod + def find_distributions(cls, context=DistributionFinder.Context()): + """ + Find distributions. + + Return an iterable of all Distribution instances capable of + loading the metadata for packages matching ``context.name`` + (or all names if ``None`` indicated) along the paths in the list + of directories ``context.path``. + """ + found = cls._search_paths(context.name, context.path) + return map(PathDistribution, found) + + @classmethod + def _search_paths(cls, name, paths): + """Find metadata directories in paths heuristically.""" + prepared = Prepared(name) + return itertools.chain.from_iterable( + path.search(prepared) for path in map(FastPath, paths) + ) + + def invalidate_caches(cls): + FastPath.__new__.cache_clear() + + +class PathDistribution(Distribution): + def __init__(self, path: SimplePath): + """Construct a distribution. + + :param path: SimplePath indicating the metadata directory. + """ + self._path = path + + def read_text(self, filename): + with suppress( + FileNotFoundError, + IsADirectoryError, + KeyError, + NotADirectoryError, + PermissionError, + ): + return self._path.joinpath(filename).read_text(encoding='utf-8') + + read_text.__doc__ = Distribution.read_text.__doc__ + + def locate_file(self, path): + return self._path.parent / path + + @property + def _normalized_name(self): + """ + Performance optimization: where possible, resolve the + normalized name from the file system path. + """ + stem = os.path.basename(str(self._path)) + return self._name_from_stem(stem) or super()._normalized_name + + def _name_from_stem(self, stem): + name, ext = os.path.splitext(stem) + if ext not in ('.dist-info', '.egg-info'): + return + name, sep, rest = stem.partition('-') + return name + + +def distribution(distribution_name): + """Get the ``Distribution`` instance for the named package. + + :param distribution_name: The name of the distribution package as a string. + :return: A ``Distribution`` instance (or subclass thereof). + """ + return Distribution.from_name(distribution_name) + + +def distributions(**kwargs): + """Get all ``Distribution`` instances in the current environment. + + :return: An iterable of ``Distribution`` instances. + """ + return Distribution.discover(**kwargs) + + +def metadata(distribution_name) -> _meta.PackageMetadata: + """Get the metadata for the named package. + + :param distribution_name: The name of the distribution package to query. + :return: A PackageMetadata containing the parsed metadata. + """ + return Distribution.from_name(distribution_name).metadata + + +def version(distribution_name): + """Get the version string for the named package. + + :param distribution_name: The name of the distribution package to query. + :return: The version string for the package as defined in the package's + "Version" metadata key. + """ + return distribution(distribution_name).version + + +def entry_points(**params) -> Union[EntryPoints, SelectableGroups]: + """Return EntryPoint objects for all installed packages. + + Pass selection parameters (group or name) to filter the + result to entry points matching those properties (see + EntryPoints.select()). + + For compatibility, returns ``SelectableGroups`` object unless + selection parameters are supplied. In the future, this function + will return ``EntryPoints`` instead of ``SelectableGroups`` + even when no selection parameters are supplied. + + For maximum future compatibility, pass selection parameters + or invoke ``.select`` with parameters on the result. + + :return: EntryPoints or SelectableGroups for all installed packages. + """ + norm_name = operator.attrgetter('_normalized_name') + unique = functools.partial(unique_everseen, key=norm_name) + eps = itertools.chain.from_iterable( + dist.entry_points for dist in unique(distributions()) + ) + return SelectableGroups.load(eps).select(**params) + + +def files(distribution_name): + """Return a list of files for the named package. + + :param distribution_name: The name of the distribution package to query. + :return: List of files composing the distribution. + """ + return distribution(distribution_name).files + + +def requires(distribution_name): + """ + Return a list of requirements for the named package. + + :return: An iterator of requirements, suitable for + packaging.requirement.Requirement. + """ + return distribution(distribution_name).requires + + +def packages_distributions() -> Mapping[str, List[str]]: + """ + Return a mapping of top-level packages to their + distributions. + + >>> import collections.abc + >>> pkgs = packages_distributions() + >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values()) + True + """ + pkg_to_dist = collections.defaultdict(list) + for dist in distributions(): + for pkg in (dist.read_text('top_level.txt') or '').split(): + pkg_to_dist[pkg].append(dist.metadata['Name']) + return dict(pkg_to_dist) diff --git a/contrib/tools/python3/src/Lib/importlib/metadata/_adapters.py b/contrib/tools/python3/src/Lib/importlib/metadata/_adapters.py new file mode 100644 index 00000000000..aa460d3eda5 --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/metadata/_adapters.py @@ -0,0 +1,68 @@ +import re +import textwrap +import email.message + +from ._text import FoldedCase + + +class Message(email.message.Message): + multiple_use_keys = set( + map( + FoldedCase, + [ + 'Classifier', + 'Obsoletes-Dist', + 'Platform', + 'Project-URL', + 'Provides-Dist', + 'Provides-Extra', + 'Requires-Dist', + 'Requires-External', + 'Supported-Platform', + 'Dynamic', + ], + ) + ) + """ + Keys that may be indicated multiple times per PEP 566. + """ + + def __new__(cls, orig: email.message.Message): + res = super().__new__(cls) + vars(res).update(vars(orig)) + return res + + def __init__(self, *args, **kwargs): + self._headers = self._repair_headers() + + # suppress spurious error from mypy + def __iter__(self): + return super().__iter__() + + def _repair_headers(self): + def redent(value): + "Correct for RFC822 indentation" + if not value or '\n' not in value: + return value + return textwrap.dedent(' ' * 8 + value) + + headers = [(key, redent(value)) for key, value in vars(self)['_headers']] + if self._payload: + headers.append(('Description', self.get_payload())) + return headers + + @property + def json(self): + """ + Convert PackageMetadata to a JSON-compatible format + per PEP 0566. + """ + + def transform(key): + value = self.get_all(key) if key in self.multiple_use_keys else self[key] + if key == 'Keywords': + value = re.split(r'\s+', value) + tk = key.lower().replace('-', '_') + return tk, value + + return dict(map(transform, map(FoldedCase, self))) diff --git a/contrib/tools/python3/src/Lib/importlib/metadata/_collections.py b/contrib/tools/python3/src/Lib/importlib/metadata/_collections.py new file mode 100644 index 00000000000..cf0954e1a30 --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/metadata/_collections.py @@ -0,0 +1,30 @@ +import collections + + +# from jaraco.collections 3.3 +class FreezableDefaultDict(collections.defaultdict): + """ + Often it is desirable to prevent the mutation of + a default dict after its initial construction, such + as to prevent mutation during iteration. + + >>> dd = FreezableDefaultDict(list) + >>> dd[0].append('1') + >>> dd.freeze() + >>> dd[1] + [] + >>> len(dd) + 1 + """ + + def __missing__(self, key): + return getattr(self, '_frozen', super().__missing__)(key) + + def freeze(self): + self._frozen = lambda key: self.default_factory() + + +class Pair(collections.namedtuple('Pair', 'name value')): + @classmethod + def parse(cls, text): + return cls(*map(str.strip, text.split("=", 1))) diff --git a/contrib/tools/python3/src/Lib/importlib/metadata/_functools.py b/contrib/tools/python3/src/Lib/importlib/metadata/_functools.py new file mode 100644 index 00000000000..73f50d00bc0 --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/metadata/_functools.py @@ -0,0 +1,85 @@ +import types +import functools + + +# from jaraco.functools 3.3 +def method_cache(method, cache_wrapper=None): + """ + Wrap lru_cache to support storing the cache data in the object instances. + + Abstracts the common paradigm where the method explicitly saves an + underscore-prefixed protected property on first call and returns that + subsequently. + + >>> class MyClass: + ... calls = 0 + ... + ... @method_cache + ... def method(self, value): + ... self.calls += 1 + ... return value + + >>> a = MyClass() + >>> a.method(3) + 3 + >>> for x in range(75): + ... res = a.method(x) + >>> a.calls + 75 + + Note that the apparent behavior will be exactly like that of lru_cache + except that the cache is stored on each instance, so values in one + instance will not flush values from another, and when an instance is + deleted, so are the cached values for that instance. + + >>> b = MyClass() + >>> for x in range(35): + ... res = b.method(x) + >>> b.calls + 35 + >>> a.method(0) + 0 + >>> a.calls + 75 + + Note that if method had been decorated with ``functools.lru_cache()``, + a.calls would have been 76 (due to the cached value of 0 having been + flushed by the 'b' instance). + + Clear the cache with ``.cache_clear()`` + + >>> a.method.cache_clear() + + Same for a method that hasn't yet been called. + + >>> c = MyClass() + >>> c.method.cache_clear() + + Another cache wrapper may be supplied: + + >>> cache = functools.lru_cache(maxsize=2) + >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache) + >>> a = MyClass() + >>> a.method2() + 3 + + Caution - do not subsequently wrap the method with another decorator, such + as ``@property``, which changes the semantics of the function. + + See also + http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ + for another implementation and additional justification. + """ + cache_wrapper = cache_wrapper or functools.lru_cache() + + def wrapper(self, *args, **kwargs): + # it's the first call, replace the method with a cached, bound method + bound_method = types.MethodType(method, self) + cached_method = cache_wrapper(bound_method) + setattr(self, method.__name__, cached_method) + return cached_method(*args, **kwargs) + + # Support cache clear even before cache has been created. + wrapper.cache_clear = lambda: None + + return wrapper diff --git a/contrib/tools/python3/src/Lib/importlib/metadata/_itertools.py b/contrib/tools/python3/src/Lib/importlib/metadata/_itertools.py new file mode 100644 index 00000000000..dd45f2f0966 --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/metadata/_itertools.py @@ -0,0 +1,19 @@ +from itertools import filterfalse + + +def unique_everseen(iterable, key=None): + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen = set() + seen_add = seen.add + if key is None: + for element in filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element + else: + for element in iterable: + k = key(element) + if k not in seen: + seen_add(k) + yield element diff --git a/contrib/tools/python3/src/Lib/importlib/metadata/_meta.py b/contrib/tools/python3/src/Lib/importlib/metadata/_meta.py new file mode 100644 index 00000000000..1a6edbf957d --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/metadata/_meta.py @@ -0,0 +1,47 @@ +from typing import Any, Dict, Iterator, List, Protocol, TypeVar, Union + + +_T = TypeVar("_T") + + +class PackageMetadata(Protocol): + def __len__(self) -> int: + ... # pragma: no cover + + def __contains__(self, item: str) -> bool: + ... # pragma: no cover + + def __getitem__(self, key: str) -> str: + ... # pragma: no cover + + def __iter__(self) -> Iterator[str]: + ... # pragma: no cover + + def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]: + """ + Return all values associated with a possibly multi-valued key. + """ + + @property + def json(self) -> Dict[str, Union[str, List[str]]]: + """ + A JSON-compatible form of the metadata. + """ + + +class SimplePath(Protocol): + """ + A minimal subset of pathlib.Path required by PathDistribution. + """ + + def joinpath(self) -> 'SimplePath': + ... # pragma: no cover + + def __div__(self) -> 'SimplePath': + ... # pragma: no cover + + def parent(self) -> 'SimplePath': + ... # pragma: no cover + + def read_text(self) -> str: + ... # pragma: no cover diff --git a/contrib/tools/python3/src/Lib/importlib/metadata/_text.py b/contrib/tools/python3/src/Lib/importlib/metadata/_text.py new file mode 100644 index 00000000000..766979d93c1 --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/metadata/_text.py @@ -0,0 +1,99 @@ +import re + +from ._functools import method_cache + + +# from jaraco.text 3.5 +class FoldedCase(str): + """ + A case insensitive string class; behaves just like str + except compares equal when the only variation is case. + + >>> s = FoldedCase('hello world') + + >>> s == 'Hello World' + True + + >>> 'Hello World' == s + True + + >>> s != 'Hello World' + False + + >>> s.index('O') + 4 + + >>> s.split('O') + ['hell', ' w', 'rld'] + + >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta'])) + ['alpha', 'Beta', 'GAMMA'] + + Sequence membership is straightforward. + + >>> "Hello World" in [s] + True + >>> s in ["Hello World"] + True + + You may test for set inclusion, but candidate and elements + must both be folded. + + >>> FoldedCase("Hello World") in {s} + True + >>> s in {FoldedCase("Hello World")} + True + + String inclusion works as long as the FoldedCase object + is on the right. + + >>> "hello" in FoldedCase("Hello World") + True + + But not if the FoldedCase object is on the left: + + >>> FoldedCase('hello') in 'Hello World' + False + + In that case, use in_: + + >>> FoldedCase('hello').in_('Hello World') + True + + >>> FoldedCase('hello') > FoldedCase('Hello') + False + """ + + def __lt__(self, other): + return self.lower() < other.lower() + + def __gt__(self, other): + return self.lower() > other.lower() + + def __eq__(self, other): + return self.lower() == other.lower() + + def __ne__(self, other): + return self.lower() != other.lower() + + def __hash__(self): + return hash(self.lower()) + + def __contains__(self, other): + return super(FoldedCase, self).lower().__contains__(other.lower()) + + def in_(self, other): + "Does self appear in other?" + return self in FoldedCase(other) + + # cache lower since it's likely to be called frequently. + @method_cache + def lower(self): + return super(FoldedCase, self).lower() + + def index(self, sub): + return self.lower().index(sub.lower()) + + def split(self, splitter=' ', maxsplit=0): + pattern = re.compile(re.escape(splitter), re.I) + return pattern.split(self, maxsplit) diff --git a/contrib/tools/python3/src/Lib/importlib/readers.py b/contrib/tools/python3/src/Lib/importlib/readers.py new file mode 100644 index 00000000000..41089c071d8 --- /dev/null +++ b/contrib/tools/python3/src/Lib/importlib/readers.py @@ -0,0 +1,123 @@ +import collections +import zipfile +import pathlib +from . import abc + + +def remove_duplicates(items): + return iter(collections.OrderedDict.fromkeys(items)) + + +class FileReader(abc.TraversableResources): + def __init__(self, loader): + self.path = pathlib.Path(loader.path).parent + + def resource_path(self, resource): + """ + Return the file system path to prevent + `resources.path()` from creating a temporary + copy. + """ + return str(self.path.joinpath(resource)) + + def files(self): + return self.path + + +class ZipReader(abc.TraversableResources): + def __init__(self, loader, module): + _, _, name = module.rpartition('.') + self.prefix = loader.prefix.replace('\\', '/') + name + '/' + self.archive = loader.archive + + def open_resource(self, resource): + try: + return super().open_resource(resource) + except KeyError as exc: + raise FileNotFoundError(exc.args[0]) + + def is_resource(self, path): + # workaround for `zipfile.Path.is_file` returning true + # for non-existent paths. + target = self.files().joinpath(path) + return target.is_file() and target.exists() + + def files(self): + return zipfile.Path(self.archive, self.prefix) + + +class MultiplexedPath(abc.Traversable): + """ + Given a series of Traversable objects, implement a merged + version of the interface across all objects. Useful for + namespace packages which may be multihomed at a single + name. + """ + + def __init__(self, *paths): + self._paths = list(map(pathlib.Path, remove_duplicates(paths))) + if not self._paths: + message = 'MultiplexedPath must contain at least one path' + raise FileNotFoundError(message) + if not all(path.is_dir() for path in self._paths): + raise NotADirectoryError('MultiplexedPath only supports directories') + + def iterdir(self): + visited = [] + for path in self._paths: + for file in path.iterdir(): + if file.name in visited: + continue + visited.append(file.name) + yield file + + def read_bytes(self): + raise FileNotFoundError(f'{self} is not a file') + + def read_text(self, *args, **kwargs): + raise FileNotFoundError(f'{self} is not a file') + + def is_dir(self): + return True + + def is_file(self): + return False + + def joinpath(self, child): + # first try to find child in current paths + for file in self.iterdir(): + if file.name == child: + return file + # if it does not exist, construct it with the first path + return self._paths[0] / child + + __truediv__ = joinpath + + def open(self, *args, **kwargs): + raise FileNotFoundError(f'{self} is not a file') + + @property + def name(self): + return self._paths[0].name + + def __repr__(self): + paths = ', '.join(f"'{path}'" for path in self._paths) + return f'MultiplexedPath({paths})' + + +class NamespaceReader(abc.TraversableResources): + def __init__(self, namespace_path): + if 'NamespacePath' not in str(namespace_path): + raise ValueError('Invalid path') + self.path = MultiplexedPath(*list(namespace_path)) + + def resource_path(self, resource): + """ + Return the file system path to prevent + `resources.path()` from creating a temporary + copy. + """ + return str(self.path.joinpath(resource)) + + def files(self): + return self.path diff --git a/contrib/tools/python3/src/Lib/importlib/resources.py b/contrib/tools/python3/src/Lib/importlib/resources.py index b803a01c91d..8a98663ff8e 100644 --- a/contrib/tools/python3/src/Lib/importlib/resources.py +++ b/contrib/tools/python3/src/Lib/importlib/resources.py @@ -1,22 +1,26 @@ import os +import io -from . import abc as resources_abc from . import _common -from ._common import as_file -from contextlib import contextmanager, suppress -from importlib import import_module +from ._common import as_file, files +from .abc import ResourceReader +from contextlib import suppress from importlib.abc import ResourceLoader +from importlib.machinery import ModuleSpec from io import BytesIO, TextIOWrapper from pathlib import Path from types import ModuleType -from typing import ContextManager, Iterable, Optional, Union +from typing import ContextManager, Iterable, Union from typing import cast from typing.io import BinaryIO, TextIO +from collections.abc import Sequence +from functools import singledispatch __all__ = [ 'Package', 'Resource', + 'ResourceReader', 'as_file', 'contents', 'files', @@ -26,99 +30,57 @@ __all__ = [ 'path', 'read_binary', 'read_text', - ] +] Package = Union[str, ModuleType] Resource = Union[str, os.PathLike] -def _resolve(name) -> ModuleType: - """If name is a string, resolve to a module.""" - if hasattr(name, '__spec__'): - return name - return import_module(name) - - -def _get_package(package) -> ModuleType: - """Take a package name or module object and return the module. - - If a name, the module is imported. If the resolved module - object is not a package, raise an exception. - """ - module = _resolve(package) - if module.__spec__.submodule_search_locations is None: - raise TypeError('{!r} is not a package'.format(package)) - return module - - -def _normalize_path(path) -> str: - """Normalize a path by ensuring it is a string. - - If the resulting string contains path separators, an exception is raised. - """ - parent, file_name = os.path.split(path) - if parent: - raise ValueError('{!r} must be only a file name'.format(path)) - return file_name - - -def _get_resource_reader( - package: ModuleType) -> Optional[resources_abc.ResourceReader]: - # Return the package's loader if it's a ResourceReader. We can't use - # a issubclass() check here because apparently abc.'s __subclasscheck__() - # hook wants to create a weak reference to the object, but - # zipimport.zipimporter does not support weak references, resulting in a - # TypeError. That seems terrible. - spec = package.__spec__ - if hasattr(spec.loader, 'get_resource_reader'): - return cast(resources_abc.ResourceReader, - spec.loader.get_resource_reader(spec.name)) - return None - - -def _check_location(package): - if package.__spec__.origin is None or not package.__spec__.has_location: - raise FileNotFoundError(f'Package has no location {package!r}') - - def open_binary(package: Package, resource: Resource) -> BinaryIO: """Return a file-like object opened for binary reading of the resource.""" - resource = _normalize_path(resource) - package = _get_package(package) - reader = _get_resource_reader(package) + resource = _common.normalize_path(resource) + package = _common.get_package(package) + reader = _common.get_resource_reader(package) if reader is not None: return reader.open_resource(resource) - absolute_package_path = os.path.abspath( - package.__spec__.origin or 'non-existent file') - package_path = os.path.dirname(absolute_package_path) - full_path = os.path.join(package_path, resource) - try: - return open(full_path, mode='rb') - except OSError: - # Just assume the loader is a resource loader; all the relevant - # importlib.machinery loaders are and an AttributeError for - # get_data() will make it clear what is needed from the loader. - loader = cast(ResourceLoader, package.__spec__.loader) - data = None - if hasattr(package.__spec__.loader, 'get_data'): - with suppress(OSError): - data = loader.get_data(full_path) - if data is None: - package_name = package.__spec__.name - message = '{!r} resource not found in {!r}'.format( - resource, package_name) - raise FileNotFoundError(message) - return BytesIO(data) + spec = cast(ModuleSpec, package.__spec__) + # Using pathlib doesn't work well here due to the lack of 'strict' + # argument for pathlib.Path.resolve() prior to Python 3.6. + if spec.submodule_search_locations is not None: + paths = spec.submodule_search_locations + elif spec.origin is not None: + paths = [os.path.dirname(os.path.abspath(spec.origin))] + + for package_path in paths: + full_path = os.path.join(package_path, resource) + try: + return open(full_path, mode='rb') + except OSError: + # Just assume the loader is a resource loader; all the relevant + # importlib.machinery loaders are and an AttributeError for + # get_data() will make it clear what is needed from the loader. + loader = cast(ResourceLoader, spec.loader) + data = None + if hasattr(spec.loader, 'get_data'): + with suppress(OSError): + data = loader.get_data(full_path) + if data is not None: + return BytesIO(data) + raise FileNotFoundError(f'{resource!r} resource not found in {spec.name!r}') -def open_text(package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict') -> TextIO: + +def open_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> TextIO: """Return a file-like object opened for text reading of the resource.""" return TextIOWrapper( - open_binary(package, resource), encoding=encoding, errors=errors) + open_binary(package, resource), encoding=encoding, errors=errors + ) def read_binary(package: Package, resource: Resource) -> bytes: @@ -127,10 +89,12 @@ def read_binary(package: Package, resource: Resource) -> bytes: return fp.read() -def read_text(package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict') -> str: +def read_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> str: """Return the decoded string of the resource. The decoding-related arguments have the same semantics as those of @@ -140,16 +104,10 @@ def read_text(package: Package, return fp.read() -def files(package: Package) -> resources_abc.Traversable: - """ - Get a Traversable resource from a package - """ - return _common.from_package(_get_package(package)) - - def path( - package: Package, resource: Resource, - ) -> 'ContextManager[Path]': + package: Package, + resource: Resource, +) -> 'ContextManager[Path]': """A context manager providing a file path object to the resource. If the resource does not already exist on its own on the file system, @@ -158,23 +116,30 @@ def path( raised if the file was deleted prior to the context manager exiting). """ - reader = _get_resource_reader(_get_package(package)) + reader = _common.get_resource_reader(_common.get_package(package)) return ( - _path_from_reader(reader, resource) - if reader else - _common.as_file(files(package).joinpath(_normalize_path(resource))) + _path_from_reader(reader, _common.normalize_path(resource)) + if reader + else _common.as_file( + _common.files(package).joinpath(_common.normalize_path(resource)) ) + ) -@contextmanager def _path_from_reader(reader, resource): - norm_resource = _normalize_path(resource) + return _path_from_resource_path(reader, resource) or _path_from_open_resource( + reader, resource + ) + + +def _path_from_resource_path(reader, resource): with suppress(FileNotFoundError): - yield Path(reader.resource_path(norm_resource)) - return - opener_reader = reader.open_resource(norm_resource) - with _common._tempfile(opener_reader.read, suffix=norm_resource) as res: - yield res + return Path(reader.resource_path(resource)) + + +def _path_from_open_resource(reader, resource): + saved = io.BytesIO(reader.open_resource(resource).read()) + return _common._tempfile(saved.read, suffix=resource) def is_resource(package: Package, name: str) -> bool: @@ -182,9 +147,9 @@ def is_resource(package: Package, name: str) -> bool: Directories are *not* resources. """ - package = _get_package(package) - _normalize_path(name) - reader = _get_resource_reader(package) + package = _common.get_package(package) + _common.normalize_path(name) + reader = _common.get_resource_reader(package) if reader is not None: return reader.is_resource(name) package_contents = set(contents(package)) @@ -200,16 +165,21 @@ def contents(package: Package) -> Iterable[str]: not considered resources. Use `is_resource()` on each entry returned here to check if it is a resource or not. """ - package = _get_package(package) - reader = _get_resource_reader(package) + package = _common.get_package(package) + reader = _common.get_resource_reader(package) if reader is not None: - return reader.contents() - # Is the package a namespace package? By definition, namespace packages - # cannot have resources. - namespace = ( - package.__spec__.origin is None or - package.__spec__.origin == 'namespace' - ) - if namespace or not package.__spec__.has_location: - return () - return list(item.name for item in _common.from_package(package).iterdir()) + return _ensure_sequence(reader.contents()) + transversable = _common.from_package(package) + if transversable.is_dir(): + return list(item.name for item in transversable.iterdir()) + return [] + + +@singledispatch +def _ensure_sequence(iterable): + return list(iterable) + + +@_ensure_sequence.register(Sequence) +def _(iterable): + return iterable diff --git a/contrib/tools/python3/src/Lib/importlib/util.py b/contrib/tools/python3/src/Lib/importlib/util.py index 269a6fa930a..8623c89840c 100644 --- a/contrib/tools/python3/src/Lib/importlib/util.py +++ b/contrib/tools/python3/src/Lib/importlib/util.py @@ -1,5 +1,5 @@ """Utility code for constructing importers, etc.""" -from . import abc +from ._abc import Loader from ._bootstrap import module_from_spec from ._bootstrap import _resolve_name from ._bootstrap import spec_from_loader @@ -149,7 +149,8 @@ def set_package(fxn): """ @functools.wraps(fxn) def set_package_wrapper(*args, **kwargs): - warnings.warn('The import system now takes care of this automatically.', + warnings.warn('The import system now takes care of this automatically; ' + 'this decorator is slated for removal in Python 3.12', DeprecationWarning, stacklevel=2) module = fxn(*args, **kwargs) if getattr(module, '__package__', None) is None: @@ -168,7 +169,8 @@ def set_loader(fxn): """ @functools.wraps(fxn) def set_loader_wrapper(self, *args, **kwargs): - warnings.warn('The import system now takes care of this automatically.', + warnings.warn('The import system now takes care of this automatically; ' + 'this decorator is slated for removal in Python 3.12', DeprecationWarning, stacklevel=2) module = fxn(self, *args, **kwargs) if getattr(module, '__loader__', None) is None: @@ -195,7 +197,8 @@ def module_for_loader(fxn): the second argument. """ - warnings.warn('The import system now takes care of this automatically.', + warnings.warn('The import system now takes care of this automatically; ' + 'this decorator is slated for removal in Python 3.12', DeprecationWarning, stacklevel=2) @functools.wraps(fxn) def module_for_loader_wrapper(self, fullname, *args, **kwargs): @@ -232,7 +235,6 @@ class _LazyModule(types.ModuleType): # Figure out exactly what attributes were mutated between the creation # of the module and now. attrs_then = self.__spec__.loader_state['__dict__'] - original_type = self.__spec__.loader_state['__class__'] attrs_now = self.__dict__ attrs_updated = {} for key, value in attrs_now.items(): @@ -263,7 +265,7 @@ class _LazyModule(types.ModuleType): delattr(self, attr) -class LazyLoader(abc.Loader): +class LazyLoader(Loader): """A loader that creates a module which defers loading until attribute access.""" |
