mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Update importlib to CPython 3.15 with native bootstrap callback (#9169)
* Refresh canonical importlib and preserve native import callback Cache the original builtin __import__ callback so CPython 3.15 importlib's hierarchical locking uses the existing native parent-initialization path. Refresh the complete canonical importlib and related test/helper families from CPython d220409a13b77de7a6c30abe16ea90db01528edc through update_lib. Preserve both nondecorator importlib guards in the updater. Add explicit hash-pinned removal of reviewed obsolete canonical files, with full preflight, dry-run, idempotence and refusal tests. Preserve empty fixtures. Assisted-by: OpenAI Codex:model version unavailable * Complete canonical inspect and types closure for importlib metadata Refresh whole canonical inspect/types production and related test families through update_lib after hosted CI exposed stale __cached__ consumers. Add coroutine and async-generator state getters, with closed-state priority for reentrant frame-local finalizers, and expose the existing frame-locals proxy type with its CPython class documentation. Preserve the optional _datetime and unittest import blocks through a narrow migration adapter. Retain canonical assertions and existing guards; record the missing LazyImportType expectation before adding a conditional expected failure, and capability-skip only the two absent-_testcapi CLI fixtures. Assisted-by: OpenAI Codex:model version unavailable * Reject canonical removal junctions and read UTF-8 manifests Reject junctions alongside symlinks in the existing path-component preflight and revalidation. Decode removal manifests explicitly as UTF-8. Add regressions for whole-batch refusal, revalidation, and Unicode dry-run/apply/idempotence. Assisted-by: OpenAI Codex:model version unavailable
This commit is contained in:
1 parent
aac1f4cb46
commit
fc9f4a9541
121 files changed
+3540
-3001
No files matched your search
Vendored
-18
@@ -19,21 +19,3 @@ class Loader(metaclass=abc.ABCMeta):
|
||||
|
||||
# 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)
|
||||
Vendored
+99
-115
@@ -424,6 +424,64 @@ class _ModuleLockManager:
|
||||
self._lock.release()
|
||||
|
||||
|
||||
def _get_module_chain(name):
|
||||
"""Return the chain of dotted-name prefixes from root to leaf.
|
||||
|
||||
For example: 'a.b.c' -> ['a', 'a.b', 'a.b.c']
|
||||
"""
|
||||
parts = name.split('.')
|
||||
return ['.'.join(parts[:i+1]) for i in range(len(parts))]
|
||||
|
||||
|
||||
class _HierarchicalLockManager:
|
||||
"""Manages acquisition of multiple module locks in hierarchical order.
|
||||
|
||||
This prevents deadlocks by ensuring all threads acquire locks in the
|
||||
same order (parent modules before child modules).
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._module_chain = _get_module_chain(name)
|
||||
self._locks = []
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
for module_name in self._module_chain:
|
||||
# Only acquire lock if module is not already fully loaded
|
||||
module = sys.modules.get(module_name)
|
||||
if (module is None or
|
||||
getattr(getattr(module, "__spec__", None),
|
||||
"_initializing", False)):
|
||||
lock = _get_module_lock(module_name)
|
||||
try:
|
||||
lock.acquire()
|
||||
except _DeadlockError:
|
||||
if module_name == self._name:
|
||||
raise
|
||||
# The parent is being initialised by a thread that
|
||||
# is (transitively) waiting on a lock we hold.
|
||||
# Apply the same policy as _lock_unlock_module():
|
||||
# accept a partially-initialised parent for circular
|
||||
# imports rather than failing the whole chain.
|
||||
continue
|
||||
self._locks.append((module_name, lock))
|
||||
except:
|
||||
# __exit__ is not called when __enter__ raises (e.g. _DeadlockError
|
||||
# on the leaf lock, or KeyboardInterrupt), so release whatever we
|
||||
# already hold to avoid permanently leaking held module locks.
|
||||
for module_name, lock in reversed(self._locks):
|
||||
lock.release()
|
||||
self._locks.clear()
|
||||
raise
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
for module_name, lock in reversed(self._locks):
|
||||
lock.release()
|
||||
self._locks.clear()
|
||||
|
||||
|
||||
# The following two functions are for consumption by Python/import.c.
|
||||
|
||||
def _get_module_lock(name):
|
||||
@@ -521,24 +579,6 @@ def _requires_frozen(fxn):
|
||||
return _requires_frozen_wrapper
|
||||
|
||||
|
||||
# Typically used by loader classes as a method replacement.
|
||||
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.
|
||||
|
||||
"""
|
||||
msg = ("the load_module() method is deprecated and slated for removal in "
|
||||
"Python 3.15; use exec_module() instead")
|
||||
_warnings.warn(msg, DeprecationWarning)
|
||||
spec = spec_from_loader(fullname, self)
|
||||
if fullname in sys.modules:
|
||||
module = sys.modules[fullname]
|
||||
_exec(spec, module)
|
||||
return sys.modules[fullname]
|
||||
else:
|
||||
return _load(spec)
|
||||
|
||||
# Module specifications #######################################################
|
||||
|
||||
def _module_repr(module):
|
||||
@@ -583,8 +623,7 @@ class ModuleSpec:
|
||||
`has_location` indicates that a spec's "origin" reflects a location.
|
||||
When this is True, `__file__` attribute of the module is set.
|
||||
|
||||
`cached` is the location of the cached bytecode file, if any. It
|
||||
corresponds to the `__cached__` attribute.
|
||||
`cached` is the location of the cached bytecode file, if any.
|
||||
|
||||
`submodule_search_locations` is the sequence of path entries to
|
||||
search when importing submodules. If set, is_package should be
|
||||
@@ -717,10 +756,6 @@ def _spec_from_module(module, loader=None, origin=None):
|
||||
origin = getattr(loader, '_ORIGIN', None)
|
||||
if not origin and location is not None:
|
||||
origin = location
|
||||
try:
|
||||
cached = module.__cached__
|
||||
except AttributeError:
|
||||
cached = None
|
||||
try:
|
||||
submodule_search_locations = list(module.__path__)
|
||||
except AttributeError:
|
||||
@@ -728,7 +763,7 @@ def _spec_from_module(module, loader=None, origin=None):
|
||||
|
||||
spec = ModuleSpec(name, loader, origin=origin)
|
||||
spec._set_fileattr = False if location is None else (origin == location)
|
||||
spec.cached = cached
|
||||
spec.cached = None
|
||||
spec.submodule_search_locations = submodule_search_locations
|
||||
return spec
|
||||
|
||||
@@ -763,8 +798,7 @@ def _init_module_attrs(spec, module, *, override=False):
|
||||
# should also be None for consistency. While a bit of a hack,
|
||||
# this is the best place to ensure this consistency.
|
||||
#
|
||||
# See # https://docs.python.org/3/library/importlib.html#importlib.abc.Loader.load_module
|
||||
# and bpo-32305
|
||||
# See bpo-32305
|
||||
module.__file__ = None
|
||||
try:
|
||||
module.__loader__ = loader
|
||||
@@ -789,7 +823,7 @@ def _init_module_attrs(spec, module, *, override=False):
|
||||
module.__path__ = spec.submodule_search_locations
|
||||
except AttributeError:
|
||||
pass
|
||||
# __file__/__cached__
|
||||
# __file__
|
||||
if spec.has_location:
|
||||
if override or getattr(module, '__file__', None) is None:
|
||||
try:
|
||||
@@ -797,12 +831,6 @@ def _init_module_attrs(spec, module, *, override=False):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if override or getattr(module, '__cached__', None) is None:
|
||||
if spec.cached is not None:
|
||||
try:
|
||||
module.__cached__ = spec.cached
|
||||
except AttributeError:
|
||||
pass
|
||||
return module
|
||||
|
||||
|
||||
@@ -844,7 +872,7 @@ def _module_repr_from_spec(spec):
|
||||
return f'<module {spec.name!r} ({spec.origin})>'
|
||||
|
||||
|
||||
# Used by importlib.reload() and _load_module_shim().
|
||||
# Used by importlib.reload().
|
||||
def _exec(spec, module):
|
||||
"""Execute the spec's specified module in an existing module's namespace."""
|
||||
name = spec.name
|
||||
@@ -860,13 +888,7 @@ def _exec(spec, module):
|
||||
_init_module_attrs(spec, module, override=True)
|
||||
else:
|
||||
_init_module_attrs(spec, module, override=True)
|
||||
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)
|
||||
spec.loader.load_module(name)
|
||||
else:
|
||||
spec.loader.exec_module(module)
|
||||
spec.loader.exec_module(module)
|
||||
finally:
|
||||
# Update the order of insertion into sys.modules for module
|
||||
# clean-up at shutdown.
|
||||
@@ -874,53 +896,8 @@ def _exec(spec, module):
|
||||
sys.modules[spec.name] = module
|
||||
return module
|
||||
|
||||
|
||||
def _load_backward_compatible(spec):
|
||||
# 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:
|
||||
if spec.name in sys.modules:
|
||||
module = sys.modules.pop(spec.name)
|
||||
sys.modules[spec.name] = module
|
||||
raise
|
||||
# The module must be in sys.modules at this point!
|
||||
# Move it to the end of sys.modules.
|
||||
module = sys.modules.pop(spec.name)
|
||||
sys.modules[spec.name] = module
|
||||
if getattr(module, '__loader__', None) is None:
|
||||
try:
|
||||
module.__loader__ = spec.loader
|
||||
except AttributeError:
|
||||
pass
|
||||
if getattr(module, '__package__', None) is None:
|
||||
try:
|
||||
# Since module.__path__ may not line up with
|
||||
# spec.submodule_search_paths, we can't necessarily rely
|
||||
# on spec.parent here.
|
||||
module.__package__ = module.__name__
|
||||
if not hasattr(module, '__path__'):
|
||||
module.__package__ = spec.name.rpartition('.')[0]
|
||||
except AttributeError:
|
||||
pass
|
||||
if getattr(module, '__spec__', None) is None:
|
||||
try:
|
||||
module.__spec__ = spec
|
||||
except AttributeError:
|
||||
pass
|
||||
return module
|
||||
|
||||
def _load_unlocked(spec):
|
||||
# A helper for direct use by the import system.
|
||||
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)
|
||||
|
||||
# This must be done before putting the module in sys.modules
|
||||
@@ -954,8 +931,7 @@ def _load_unlocked(spec):
|
||||
|
||||
return module
|
||||
|
||||
# A method used during testing of _load_unlocked() and by
|
||||
# _load_module_shim().
|
||||
# A method used during testing of _load_unlocked().
|
||||
def _load(spec):
|
||||
"""Return a new module object, loaded by the spec's loader.
|
||||
|
||||
@@ -1020,8 +996,6 @@ class BuiltinImporter:
|
||||
"""Return False as built-in modules are never packages."""
|
||||
return False
|
||||
|
||||
load_module = classmethod(_load_module_shim)
|
||||
|
||||
|
||||
class FrozenImporter:
|
||||
|
||||
@@ -1178,25 +1152,6 @@ class FrozenImporter:
|
||||
code = _call_with_frames_removed(_imp.get_frozen_object, name)
|
||||
exec(code, module.__dict__)
|
||||
|
||||
@classmethod
|
||||
def load_module(cls, fullname):
|
||||
"""Load a frozen module.
|
||||
|
||||
This method is deprecated. Use exec_module() instead.
|
||||
|
||||
"""
|
||||
# Warning about deprecation implemented in _load_module_shim().
|
||||
module = _load_module_shim(cls, fullname)
|
||||
info = _imp.find_frozen(fullname)
|
||||
assert info is not None
|
||||
_, ispkg, origname = info
|
||||
module.__origname__ = origname
|
||||
vars(module).pop('__file__', None)
|
||||
if ispkg:
|
||||
module.__path__ = []
|
||||
cls._fix_up_module(module)
|
||||
return module
|
||||
|
||||
@classmethod
|
||||
@_requires_frozen
|
||||
def get_code(cls, fullname):
|
||||
@@ -1305,8 +1260,16 @@ def _sanity_check(name, package, level):
|
||||
|
||||
_ERR_MSG_PREFIX = 'No module named '
|
||||
|
||||
def _find_and_load_unlocked(name, import_):
|
||||
def _find_and_load_unlocked(name, import_, *, lazy_submodule=False):
|
||||
path = None
|
||||
sys.audit(
|
||||
"import",
|
||||
name,
|
||||
path,
|
||||
getattr(sys, "path", None),
|
||||
getattr(sys, "meta_path", None),
|
||||
getattr(sys, "path_hooks", None)
|
||||
)
|
||||
parent = name.rpartition('.')[0]
|
||||
parent_spec = None
|
||||
if parent:
|
||||
@@ -1320,6 +1283,8 @@ def _find_and_load_unlocked(name, import_):
|
||||
try:
|
||||
path = parent_module.__path__
|
||||
except AttributeError:
|
||||
if lazy_submodule:
|
||||
return None
|
||||
msg = f'{_ERR_MSG_PREFIX}{name!r}; {parent!r} is not a package'
|
||||
raise ModuleNotFoundError(msg, name=name) from None
|
||||
parent_spec = parent_module.__spec__
|
||||
@@ -1332,6 +1297,8 @@ def _find_and_load_unlocked(name, import_):
|
||||
child = name.rpartition('.')[2]
|
||||
spec = _find_spec(name, path)
|
||||
if spec is None:
|
||||
if lazy_submodule:
|
||||
return None
|
||||
raise ModuleNotFoundError(f'{_ERR_MSG_PREFIX}{name!r}', name=name)
|
||||
else:
|
||||
if parent_spec:
|
||||
@@ -1351,13 +1318,19 @@ def _find_and_load_unlocked(name, import_):
|
||||
except AttributeError:
|
||||
msg = f"Cannot set an attribute on {parent!r} for child module {child!r}"
|
||||
_warnings.warn(msg, ImportWarning)
|
||||
# Set attributes to lazy submodules on the module.
|
||||
try:
|
||||
_imp._set_lazy_attributes(module, name)
|
||||
except Exception as e:
|
||||
msg = f"Cannot set lazy attributes on {name!r}: {e!r}"
|
||||
_warnings.warn(msg, ImportWarning)
|
||||
return module
|
||||
|
||||
|
||||
_NEEDS_LOADING = object()
|
||||
|
||||
|
||||
def _find_and_load(name, import_):
|
||||
def _find_and_load(name, import_, *, lazy_submodule=False):
|
||||
"""Find and load the module."""
|
||||
|
||||
# Optimization: we avoid unneeded module locking if the module
|
||||
@@ -1365,10 +1338,17 @@ def _find_and_load(name, import_):
|
||||
module = sys.modules.get(name, _NEEDS_LOADING)
|
||||
if (module is _NEEDS_LOADING or
|
||||
getattr(getattr(module, "__spec__", None), "_initializing", False)):
|
||||
with _ModuleLockManager(name):
|
||||
|
||||
if '.' in name:
|
||||
lock_manager = _HierarchicalLockManager(name)
|
||||
else:
|
||||
lock_manager = _ModuleLockManager(name)
|
||||
|
||||
with lock_manager:
|
||||
module = sys.modules.get(name, _NEEDS_LOADING)
|
||||
if module is _NEEDS_LOADING:
|
||||
return _find_and_load_unlocked(name, import_)
|
||||
return _find_and_load_unlocked(
|
||||
name, import_, lazy_submodule=lazy_submodule)
|
||||
|
||||
# Optimization: only call _bootstrap._lock_unlock_module() if
|
||||
# module.__spec__._initializing is True.
|
||||
@@ -1382,7 +1362,7 @@ def _find_and_load(name, import_):
|
||||
# to preserve normal semantics: the caller gets the exception from
|
||||
# the actual import failure rather than a synthetic error.
|
||||
if sys.modules.get(name) is not module:
|
||||
return _find_and_load(name, import_)
|
||||
return _find_and_load(name, import_, lazy_submodule=lazy_submodule)
|
||||
|
||||
if module is None:
|
||||
message = f'import of {name} halted; None in sys.modules'
|
||||
@@ -1391,6 +1371,10 @@ def _find_and_load(name, import_):
|
||||
return module
|
||||
|
||||
|
||||
def _find_and_load_lazy_submodule(name, import_):
|
||||
return _find_and_load(name, import_, lazy_submodule=True)
|
||||
|
||||
|
||||
def _gcd_import(name, package=None, level=0):
|
||||
"""Import and return the module based on its name, the package the call is
|
||||
being made from, and the level adjustment.
|
||||
|
||||
Vendored
+85
-60
@@ -236,7 +236,7 @@ BYTECODE_SUFFIXES = ['.pyc']
|
||||
# Deprecated.
|
||||
DEBUG_BYTECODE_SUFFIXES = OPTIMIZED_BYTECODE_SUFFIXES = BYTECODE_SUFFIXES
|
||||
|
||||
def cache_from_source(path, debug_override=None, *, optimization=None):
|
||||
def cache_from_source(path, *, optimization=None):
|
||||
"""Given the path to a .py file, return the path to its .pyc file.
|
||||
|
||||
The .py file does not need to exist; this simply returns the path to the
|
||||
@@ -247,20 +247,9 @@ def cache_from_source(path, debug_override=None, *, optimization=None):
|
||||
of the argument is taken and verified to be alphanumeric (else ValueError
|
||||
is raised).
|
||||
|
||||
The debug_override parameter is deprecated. If debug_override is not None,
|
||||
a True value is the same as setting 'optimization' to the empty string
|
||||
while a False value is equivalent to setting 'optimization' to '1'.
|
||||
|
||||
If sys.implementation.cache_tag is None then NotImplementedError is raised.
|
||||
|
||||
"""
|
||||
if debug_override is not None:
|
||||
_warnings.warn('the debug_override parameter is deprecated; use '
|
||||
"'optimization' instead", DeprecationWarning)
|
||||
if optimization is not None:
|
||||
message = 'debug_override or optimization must be set to None'
|
||||
raise TypeError(message)
|
||||
optimization = '' if debug_override else 1
|
||||
path = _os.fspath(path)
|
||||
head, tail = _path_split(path)
|
||||
base, sep, rest = tail.rpartition('.')
|
||||
@@ -548,8 +537,7 @@ def decode_source(source_bytes):
|
||||
import tokenize # To avoid bootstrap issues.
|
||||
source_bytes_readline = _io.BytesIO(source_bytes).readline
|
||||
encoding = tokenize.detect_encoding(source_bytes_readline)
|
||||
newline_decoder = _io.IncrementalNewlineDecoder(None, True)
|
||||
return newline_decoder.decode(source_bytes.decode(encoding[0]))
|
||||
return _io.TextIOWrapper(_io.BytesIO(source_bytes), encoding=encoding[0], newline=None).read()
|
||||
|
||||
|
||||
# Module specifications #######################################################
|
||||
@@ -758,11 +746,6 @@ class _LoaderBasics:
|
||||
'get_code() returns None')
|
||||
_bootstrap._call_with_frames_removed(exec, code, module.__dict__)
|
||||
|
||||
def load_module(self, fullname):
|
||||
"""This method is deprecated."""
|
||||
# Warning implemented in _load_module_shim().
|
||||
return _bootstrap._load_module_shim(self, fullname)
|
||||
|
||||
|
||||
class SourceLoader(_LoaderBasics):
|
||||
|
||||
@@ -815,13 +798,14 @@ class SourceLoader(_LoaderBasics):
|
||||
name=fullname) from exc
|
||||
return decode_source(source_bytes)
|
||||
|
||||
def source_to_code(self, data, path, *, _optimize=-1):
|
||||
def source_to_code(self, data, path, fullname=None, *, _optimize=-1):
|
||||
"""Return the code object compiled from source.
|
||||
|
||||
The 'data' argument can be any object type that compile() supports.
|
||||
"""
|
||||
return _bootstrap._call_with_frames_removed(compile, data, path, 'exec',
|
||||
dont_inherit=True, optimize=_optimize)
|
||||
dont_inherit=True, optimize=_optimize,
|
||||
module=fullname)
|
||||
|
||||
def get_code(self, fullname):
|
||||
"""Concrete implementation of InspectLoader.get_code.
|
||||
@@ -890,7 +874,7 @@ class SourceLoader(_LoaderBasics):
|
||||
source_path=source_path)
|
||||
if source_bytes is None:
|
||||
source_bytes = self.get_data(source_path)
|
||||
code_object = self.source_to_code(source_bytes, source_path)
|
||||
code_object = self.source_to_code(source_bytes, source_path, fullname)
|
||||
_bootstrap._verbose_message('code object from {}', source_path)
|
||||
if (not sys.dont_write_bytecode and bytecode_path is not None and
|
||||
source_mtime is not None):
|
||||
@@ -927,18 +911,6 @@ class FileLoader:
|
||||
def __hash__(self):
|
||||
return hash(self.name) ^ hash(self.path)
|
||||
|
||||
@_check_name
|
||||
def load_module(self, fullname):
|
||||
"""Load a module from a file.
|
||||
|
||||
This method is deprecated. Use exec_module() instead.
|
||||
|
||||
"""
|
||||
# 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.
|
||||
return super(FileLoader, self).load_module(fullname)
|
||||
|
||||
@_check_name
|
||||
def get_filename(self, fullname):
|
||||
"""Return the path to the source file as found by the finder."""
|
||||
@@ -1082,12 +1054,18 @@ class ExtensionFileLoader(FileLoader, _LoaderBasics):
|
||||
return self.path
|
||||
|
||||
|
||||
class _NamespacePath:
|
||||
"""Represents a namespace package's path. It uses the module name
|
||||
to find its parent module, and from there it looks up the parent's
|
||||
__path__. When this changes, the module's own path is recomputed,
|
||||
using path_finder. For top-level modules, the parent module's path
|
||||
is sys.path."""
|
||||
class NamespacePath:
|
||||
"""Represents a namespace package's path.
|
||||
|
||||
It uses the module *name* to find its parent module, and from there it looks
|
||||
up the parent's __path__. When this changes, the module's own path is
|
||||
recomputed, using *path_finder*. The initial value is set to *path*.
|
||||
|
||||
For top-level modules, the parent module's path is sys.path.
|
||||
|
||||
*path_finder* should be a callable with the same signature as
|
||||
MetaPathFinder.find_spec((fullname, path, target=None) -> spec).
|
||||
"""
|
||||
|
||||
# When invalidate_caches() is called, this epoch is incremented
|
||||
# https://bugs.python.org/issue45703
|
||||
@@ -1112,7 +1090,15 @@ class _NamespacePath:
|
||||
|
||||
def _get_parent_path(self):
|
||||
parent_module_name, path_attr_name = self._find_parent_path_names()
|
||||
return getattr(sys.modules[parent_module_name], path_attr_name)
|
||||
try:
|
||||
module = sys.modules[parent_module_name]
|
||||
except KeyError as e:
|
||||
raise ModuleNotFoundError(
|
||||
f"{parent_module_name!r} must be imported before finding {self._name!r}.",
|
||||
name=parent_module_name,
|
||||
) from e
|
||||
else:
|
||||
return getattr(module, path_attr_name)
|
||||
|
||||
def _recalculate(self):
|
||||
# If the parent's path has changed, recalculate _path
|
||||
@@ -1141,7 +1127,7 @@ class _NamespacePath:
|
||||
return len(self._recalculate())
|
||||
|
||||
def __repr__(self):
|
||||
return f'_NamespacePath({self._path!r})'
|
||||
return f'NamespacePath({self._path!r})'
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self._recalculate()
|
||||
@@ -1150,12 +1136,16 @@ class _NamespacePath:
|
||||
self._path.append(item)
|
||||
|
||||
|
||||
# For backwards-compatibility for anyone desperate enough to get at the class back in the day.
|
||||
_NamespacePath = NamespacePath
|
||||
|
||||
|
||||
# This class is actually exposed publicly in a namespace package's __loader__
|
||||
# attribute, so it should be available through a non-private name.
|
||||
# https://github.com/python/cpython/issues/92054
|
||||
class NamespaceLoader:
|
||||
def __init__(self, name, path, path_finder):
|
||||
self._path = _NamespacePath(name, path, path_finder)
|
||||
self._path = NamespacePath(name, path, path_finder)
|
||||
|
||||
def is_package(self, fullname):
|
||||
return True
|
||||
@@ -1164,7 +1154,7 @@ class NamespaceLoader:
|
||||
return ''
|
||||
|
||||
def get_code(self, fullname):
|
||||
return compile('', '<string>', 'exec', dont_inherit=True)
|
||||
return compile('', '<string>', 'exec', dont_inherit=True, module=fullname)
|
||||
|
||||
def create_module(self, spec):
|
||||
"""Use default semantics for module creation."""
|
||||
@@ -1172,18 +1162,6 @@ class NamespaceLoader:
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
def load_module(self, fullname):
|
||||
"""Load a namespace module.
|
||||
|
||||
This method is deprecated. Use exec_module() instead.
|
||||
|
||||
"""
|
||||
# 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)
|
||||
@@ -1210,9 +1188,9 @@ class PathFinder:
|
||||
del sys.path_importer_cache[name]
|
||||
elif hasattr(finder, 'invalidate_caches'):
|
||||
finder.invalidate_caches()
|
||||
# Also invalidate the caches of _NamespacePaths
|
||||
# Also invalidate the caches of NamespacePaths
|
||||
# https://bugs.python.org/issue45703
|
||||
_NamespacePath._epoch += 1
|
||||
NamespacePath._epoch += 1
|
||||
|
||||
from importlib.metadata import MetadataPathFinder
|
||||
MetadataPathFinder.invalidate_caches()
|
||||
@@ -1298,13 +1276,30 @@ class PathFinder:
|
||||
# We found at least one namespace path. Return a spec which
|
||||
# can create the namespace package.
|
||||
spec.origin = None
|
||||
spec.submodule_search_locations = _NamespacePath(fullname, namespace_path, cls._get_spec)
|
||||
spec.submodule_search_locations = NamespacePath(fullname, namespace_path, cls._get_spec)
|
||||
return spec
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return spec
|
||||
|
||||
@classmethod
|
||||
def discover(cls, parent=None):
|
||||
if parent is None:
|
||||
path = sys.path
|
||||
elif parent.submodule_search_locations is None:
|
||||
raise ValueError(f'{parent} is not a package module')
|
||||
else:
|
||||
path = parent.submodule_search_locations
|
||||
|
||||
for entry in set(path):
|
||||
if not isinstance(entry, str):
|
||||
continue
|
||||
if (finder := cls._path_importer_cache(entry)) is None:
|
||||
continue
|
||||
if discover := getattr(finder, 'discover', None):
|
||||
yield from discover(parent)
|
||||
|
||||
@staticmethod
|
||||
def find_distributions(*args, **kwargs):
|
||||
"""
|
||||
@@ -1454,6 +1449,37 @@ class FileFinder:
|
||||
|
||||
return path_hook_for_FileFinder
|
||||
|
||||
def _find_children(self):
|
||||
with _os.scandir(self.path) as scan_iterator:
|
||||
while True:
|
||||
try:
|
||||
entry = next(scan_iterator)
|
||||
if entry.name == _PYCACHE:
|
||||
continue
|
||||
# packages
|
||||
if entry.is_dir() and '.' not in entry.name:
|
||||
yield entry.name
|
||||
# files
|
||||
if entry.is_file():
|
||||
yield from {
|
||||
entry.name.removesuffix(suffix)
|
||||
for suffix, _ in self._loaders
|
||||
if entry.name.endswith(suffix)
|
||||
}
|
||||
except OSError:
|
||||
pass # ignore exceptions from next(scan_iterator) and os.DirEntry
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
def discover(self, parent=None):
|
||||
if parent and parent.submodule_search_locations is None:
|
||||
raise ValueError(f'{parent} is not a package module')
|
||||
|
||||
module_prefix = f'{parent.name}.' if parent else ''
|
||||
for child_name in self._find_children():
|
||||
if spec := self.find_spec(module_prefix + child_name):
|
||||
yield spec
|
||||
|
||||
def __repr__(self):
|
||||
return f'FileFinder({self.path!r})'
|
||||
|
||||
@@ -1525,7 +1551,6 @@ def _fix_up_module(ns, name, pathname, cpathname=None):
|
||||
ns['__spec__'] = spec
|
||||
ns['__loader__'] = loader
|
||||
ns['__file__'] = pathname
|
||||
ns['__cached__'] = cpathname
|
||||
except Exception:
|
||||
# Not important enough to report.
|
||||
pass
|
||||
|
||||
Vendored
+24
-7
@@ -45,6 +45,16 @@ class MetaPathFinder(metaclass=abc.ABCMeta):
|
||||
This method is used by importlib.invalidate_caches().
|
||||
"""
|
||||
|
||||
def discover(self, parent=None):
|
||||
"""An optional method which searches for possible specs with given *parent*
|
||||
module spec. If *parent* is *None*, MetaPathFinder.discover will search
|
||||
for top-level modules.
|
||||
|
||||
Returns an iterable of possible specs.
|
||||
"""
|
||||
return ()
|
||||
|
||||
|
||||
_register(MetaPathFinder, machinery.BuiltinImporter, machinery.FrozenImporter,
|
||||
machinery.PathFinder, machinery.WindowsRegistryFinder)
|
||||
|
||||
@@ -58,6 +68,15 @@ class PathEntryFinder(metaclass=abc.ABCMeta):
|
||||
This method is used by PathFinder.invalidate_caches().
|
||||
"""
|
||||
|
||||
def discover(self, parent=None):
|
||||
"""An optional method which searches for possible specs with given
|
||||
*parent* module spec. If *parent* is *None*, PathEntryFinder.discover
|
||||
will search for top-level modules.
|
||||
|
||||
Returns an iterable of possible specs.
|
||||
"""
|
||||
return ()
|
||||
|
||||
_register(PathEntryFinder, machinery.FileFinder)
|
||||
|
||||
|
||||
@@ -108,7 +127,7 @@ class InspectLoader(Loader):
|
||||
source = self.get_source(fullname)
|
||||
if source is None:
|
||||
return None
|
||||
return self.source_to_code(source)
|
||||
return self.source_to_code(source, '<string>', fullname)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_source(self, fullname):
|
||||
@@ -120,15 +139,14 @@ class InspectLoader(Loader):
|
||||
raise ImportError
|
||||
|
||||
@staticmethod
|
||||
def source_to_code(data, path='<string>'):
|
||||
def source_to_code(data, path='<string>', fullname=None):
|
||||
"""Compile 'data' into a code object.
|
||||
|
||||
The 'data' argument can be anything that compile() can handle. The'path'
|
||||
argument should be where the data was retrieved (when applicable)."""
|
||||
return compile(data, path, 'exec', dont_inherit=True)
|
||||
return compile(data, path, 'exec', dont_inherit=True, module=fullname)
|
||||
|
||||
exec_module = _bootstrap_external._LoaderBasics.exec_module
|
||||
load_module = _bootstrap_external._LoaderBasics.load_module
|
||||
|
||||
_register(InspectLoader, machinery.BuiltinImporter, machinery.FrozenImporter, machinery.NamespaceLoader)
|
||||
|
||||
@@ -163,9 +181,8 @@ class ExecutionLoader(InspectLoader):
|
||||
try:
|
||||
path = self.get_filename(fullname)
|
||||
except ImportError:
|
||||
return self.source_to_code(source)
|
||||
else:
|
||||
return self.source_to_code(source, path)
|
||||
path = '<string>'
|
||||
return self.source_to_code(source, path, fullname)
|
||||
|
||||
_register(
|
||||
ExecutionLoader,
|
||||
|
||||
Vendored
+1
@@ -16,6 +16,7 @@ from ._bootstrap_external import SourcelessFileLoader
|
||||
from ._bootstrap_external import ExtensionFileLoader
|
||||
from ._bootstrap_external import AppleFrameworkLoader
|
||||
from ._bootstrap_external import NamespaceLoader
|
||||
from ._bootstrap_external import NamespacePath
|
||||
|
||||
|
||||
def all_suffixes():
|
||||
|
||||
Vendored
+198
-85
@@ -1,40 +1,50 @@
|
||||
"""
|
||||
APIs exposing metadata from third-party Python packages.
|
||||
|
||||
This codebase is shared between importlib.metadata in the stdlib
|
||||
and importlib_metadata in PyPI. See
|
||||
https://github.com/python/importlib_metadata/wiki/Development-Methodology
|
||||
for more detail.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import abc
|
||||
import sys
|
||||
import json
|
||||
import collections
|
||||
import email
|
||||
import types
|
||||
import inspect
|
||||
import pathlib
|
||||
import zipfile
|
||||
import operator
|
||||
import textwrap
|
||||
import warnings
|
||||
import functools
|
||||
import itertools
|
||||
import operator
|
||||
import os
|
||||
import pathlib
|
||||
import posixpath
|
||||
import collections
|
||||
|
||||
from . import _meta
|
||||
from ._collections import FreezableDefaultDict, Pair
|
||||
from ._functools import method_cache, pass_none
|
||||
from ._itertools import always_iterable, unique_everseen
|
||||
from ._meta import PackageMetadata, SimplePath
|
||||
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
import types
|
||||
from collections.abc import Iterable, Mapping
|
||||
from contextlib import suppress
|
||||
from importlib import import_module
|
||||
from importlib.abc import MetaPathFinder
|
||||
from itertools import starmap
|
||||
from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast
|
||||
from typing import Any
|
||||
|
||||
from . import _meta
|
||||
from ._collections import FreezableDefaultDict, Pair
|
||||
from ._context import ExceptionTrap
|
||||
from ._functools import method_cache, noop, pass_none, passthrough
|
||||
from ._itertools import always_iterable, bucket, unique_everseen
|
||||
from ._meta import PackageMetadata, SimplePath
|
||||
from ._typing import md_none
|
||||
|
||||
__all__ = [
|
||||
'Distribution',
|
||||
'DistributionFinder',
|
||||
'PackageMetadata',
|
||||
'PackageNotFoundError',
|
||||
'PackagePath',
|
||||
'MetadataNotFound',
|
||||
'SimplePath',
|
||||
'distribution',
|
||||
'distributions',
|
||||
'entry_points',
|
||||
@@ -53,11 +63,15 @@ class PackageNotFoundError(ModuleNotFoundError):
|
||||
return f"No package metadata was found for {self.name}"
|
||||
|
||||
@property
|
||||
def name(self) -> str: # type: ignore[override]
|
||||
def name(self) -> str: # type: ignore[override] # make readonly
|
||||
(name,) = self.args
|
||||
return name
|
||||
|
||||
|
||||
class MetadataNotFound(FileNotFoundError):
|
||||
"""No metadata file is present in the distribution."""
|
||||
|
||||
|
||||
class Sectioned:
|
||||
"""
|
||||
A simple entry point config parser for performance
|
||||
@@ -123,6 +137,12 @@ class Sectioned:
|
||||
return line and not line.startswith('#')
|
||||
|
||||
|
||||
class _EntryPointMatch(types.SimpleNamespace):
|
||||
module: str
|
||||
attr: str
|
||||
extras: str
|
||||
|
||||
|
||||
class EntryPoint:
|
||||
"""An entry point as defined by Python packaging conventions.
|
||||
|
||||
@@ -138,6 +158,30 @@ class EntryPoint:
|
||||
'attr'
|
||||
>>> ep.extras
|
||||
['extra1', 'extra2']
|
||||
|
||||
If the value package or module are not valid identifiers, a
|
||||
ValueError is raised on access.
|
||||
|
||||
>>> EntryPoint(name=None, group=None, value='invalid-name').module
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: ('Invalid object reference...invalid-name...
|
||||
>>> EntryPoint(name=None, group=None, value='invalid-name').attr
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: ('Invalid object reference...invalid-name...
|
||||
>>> EntryPoint(name=None, group=None, value='invalid-name').extras
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: ('Invalid object reference...invalid-name...
|
||||
|
||||
The same thing happens on construction.
|
||||
|
||||
>>> EntryPoint(name=None, group=None, value='invalid-name')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: ('Invalid object reference...invalid-name...
|
||||
|
||||
"""
|
||||
|
||||
pattern = re.compile(
|
||||
@@ -165,38 +209,44 @@ class EntryPoint:
|
||||
value: str
|
||||
group: str
|
||||
|
||||
dist: Optional[Distribution] = None
|
||||
dist: Distribution | None = None
|
||||
|
||||
def __init__(self, name: str, value: str, group: str) -> None:
|
||||
vars(self).update(name=name, value=value, group=group)
|
||||
self.module
|
||||
|
||||
def load(self) -> Any:
|
||||
"""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 = cast(Match, self.pattern.match(self.value))
|
||||
module = import_module(match.group('module'))
|
||||
attrs = filter(None, (match.group('attr') or '').split('.'))
|
||||
module = import_module(self.module)
|
||||
attrs = filter(None, (self.attr or '').split('.'))
|
||||
return functools.reduce(getattr, attrs, module)
|
||||
|
||||
@property
|
||||
def module(self) -> str:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return match.group('module')
|
||||
return self._match.module
|
||||
|
||||
@property
|
||||
def attr(self) -> str:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return match.group('attr')
|
||||
return self._match.attr
|
||||
|
||||
@property
|
||||
def extras(self) -> List[str]:
|
||||
def extras(self) -> list[str]:
|
||||
return re.findall(r'\w+', self._match.extras or '')
|
||||
|
||||
@functools.cached_property
|
||||
def _match(self) -> _EntryPointMatch:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return re.findall(r'\w+', match.group('extras') or '')
|
||||
if not match:
|
||||
raise ValueError(
|
||||
'Invalid object reference. '
|
||||
'See https://packaging.python.org'
|
||||
'/en/latest/specifications/entry-points/#data-model',
|
||||
self.value,
|
||||
)
|
||||
return _EntryPointMatch(**match.groupdict())
|
||||
|
||||
def _for(self, dist):
|
||||
vars(self).update(dist=dist)
|
||||
@@ -222,9 +272,26 @@ class EntryPoint:
|
||||
>>> ep.matches(attr='bong')
|
||||
True
|
||||
"""
|
||||
self._disallow_dist(params)
|
||||
attrs = (getattr(self, param) for param in params)
|
||||
return all(map(operator.eq, params.values(), attrs))
|
||||
|
||||
@staticmethod
|
||||
def _disallow_dist(params):
|
||||
"""
|
||||
Querying by dist is not allowed (dist objects are not comparable).
|
||||
>>> EntryPoint(name='fan', value='fav', group='fag').matches(dist='foo')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: "dist" is not suitable for matching...
|
||||
"""
|
||||
if "dist" in params:
|
||||
raise ValueError(
|
||||
'"dist" is not suitable for matching. '
|
||||
"Instead, use Distribution.entry_points.select() on a "
|
||||
"located distribution."
|
||||
)
|
||||
|
||||
def _key(self):
|
||||
return self.name, self.value, self.group
|
||||
|
||||
@@ -254,7 +321,7 @@ class EntryPoints(tuple):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
|
||||
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int
|
||||
"""
|
||||
Get the EntryPoint in self matching name.
|
||||
"""
|
||||
@@ -278,14 +345,14 @@ class EntryPoints(tuple):
|
||||
return EntryPoints(ep for ep in self if ep.matches(**params))
|
||||
|
||||
@property
|
||||
def names(self) -> Set[str]:
|
||||
def names(self) -> set[str]:
|
||||
"""
|
||||
Return the set of all names of all entry points.
|
||||
"""
|
||||
return {ep.name for ep in self}
|
||||
|
||||
@property
|
||||
def groups(self) -> Set[str]:
|
||||
def groups(self) -> set[str]:
|
||||
"""
|
||||
Return the set of all groups of all entry points.
|
||||
"""
|
||||
@@ -306,11 +373,11 @@ class EntryPoints(tuple):
|
||||
class PackagePath(pathlib.PurePosixPath):
|
||||
"""A reference to a path in a package"""
|
||||
|
||||
hash: Optional[FileHash]
|
||||
hash: FileHash | None
|
||||
size: int
|
||||
dist: Distribution
|
||||
|
||||
def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
|
||||
def read_text(self, encoding: str = 'utf-8') -> str:
|
||||
return self.locate().read_text(encoding=encoding)
|
||||
|
||||
def read_binary(self) -> bytes:
|
||||
@@ -329,27 +396,7 @@ class FileHash:
|
||||
return f'<FileHash mode: {self.mode} value: {self.value}>'
|
||||
|
||||
|
||||
class DeprecatedNonAbstract:
|
||||
# Required until Python 3.14
|
||||
def __new__(cls, *args, **kwargs):
|
||||
all_names = {
|
||||
name for subclass in inspect.getmro(cls) for name in vars(subclass)
|
||||
}
|
||||
abstract = {
|
||||
name
|
||||
for name in all_names
|
||||
if getattr(getattr(cls, name), '__isabstractmethod__', False)
|
||||
}
|
||||
if abstract:
|
||||
warnings.warn(
|
||||
f"Unimplemented abstract methods {abstract}",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return super().__new__(cls)
|
||||
|
||||
|
||||
class Distribution(DeprecatedNonAbstract):
|
||||
class Distribution(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
An abstract Python distribution package.
|
||||
|
||||
@@ -361,7 +408,7 @@ class Distribution(DeprecatedNonAbstract):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def read_text(self, filename) -> Optional[str]:
|
||||
def read_text(self, filename) -> str | None:
|
||||
"""Attempt to load metadata file given by the name.
|
||||
|
||||
Python distribution metadata is organized by blobs of text
|
||||
@@ -388,6 +435,17 @@ class Distribution(DeprecatedNonAbstract):
|
||||
"""
|
||||
Given a path to a file in this distribution, return a SimplePath
|
||||
to it.
|
||||
|
||||
This method is used by callers of ``Distribution.files()`` to
|
||||
locate files within the distribution. If it's possible for a
|
||||
Distribution to represent files in the distribution as
|
||||
``SimplePath`` objects, it should implement this method
|
||||
to resolve such objects.
|
||||
|
||||
Some Distribution providers may elect not to resolve SimplePath
|
||||
objects within the distribution by raising a
|
||||
NotImplementedError, but consumers of such a Distribution would
|
||||
be unable to invoke ``Distribution.files()``.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -404,13 +462,13 @@ class Distribution(DeprecatedNonAbstract):
|
||||
if not name:
|
||||
raise ValueError("A distribution name is required.")
|
||||
try:
|
||||
return next(iter(cls.discover(name=name)))
|
||||
return next(iter(cls._prefer_valid(cls.discover(name=name))))
|
||||
except StopIteration:
|
||||
raise PackageNotFoundError(name)
|
||||
raise PackageNotFoundError(name) from None
|
||||
|
||||
@classmethod
|
||||
def discover(
|
||||
cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
|
||||
cls, *, context: DistributionFinder.Context | None = None, **kwargs
|
||||
) -> Iterable[Distribution]:
|
||||
"""Return an iterable of Distribution objects for all packages.
|
||||
|
||||
@@ -428,6 +486,21 @@ class Distribution(DeprecatedNonAbstract):
|
||||
resolver(context) for resolver in cls._discover_resolvers()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]:
|
||||
"""
|
||||
Prefer (move to the front) distributions that have metadata.
|
||||
|
||||
Ref python/importlib_resources#489.
|
||||
"""
|
||||
|
||||
has_metadata = ExceptionTrap(MetadataNotFound).passes(
|
||||
operator.attrgetter('metadata')
|
||||
)
|
||||
|
||||
buckets = bucket(dists, has_metadata)
|
||||
return itertools.chain(buckets[True], buckets[False])
|
||||
|
||||
@staticmethod
|
||||
def at(path: str | os.PathLike[str]) -> Distribution:
|
||||
"""Return a Distribution for the indicated metadata path.
|
||||
@@ -455,11 +528,11 @@ class Distribution(DeprecatedNonAbstract):
|
||||
|
||||
Custom providers may provide the METADATA file or override this
|
||||
property.
|
||||
"""
|
||||
# deferred for performance (python/cpython#109829)
|
||||
from . import _adapters
|
||||
|
||||
opt_text = (
|
||||
:raises MetadataNotFound: If no metadata file is present.
|
||||
"""
|
||||
|
||||
text = (
|
||||
self.read_text('METADATA')
|
||||
or self.read_text('PKG-INFO')
|
||||
# This last clause is here to support old egg-info files. Its
|
||||
@@ -467,9 +540,21 @@ class Distribution(DeprecatedNonAbstract):
|
||||
# (which points to the egg-info file) attribute unchanged.
|
||||
or self.read_text('')
|
||||
)
|
||||
text = cast(str, opt_text)
|
||||
return self._assemble_message(self._ensure_metadata_present(text))
|
||||
|
||||
@staticmethod
|
||||
def _assemble_message(text: str) -> _meta.PackageMetadata:
|
||||
# deferred for performance (python/cpython#109829)
|
||||
from . import _adapters
|
||||
|
||||
return _adapters.Message(email.message_from_string(text))
|
||||
|
||||
def _ensure_metadata_present(self, text: str | None) -> str:
|
||||
if text is not None:
|
||||
return text
|
||||
|
||||
raise MetadataNotFound('No package metadata was found.')
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the 'Name' metadata for the distribution package."""
|
||||
@@ -496,7 +581,7 @@ class Distribution(DeprecatedNonAbstract):
|
||||
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
|
||||
|
||||
@property
|
||||
def files(self) -> Optional[List[PackagePath]]:
|
||||
def files(self) -> list[PackagePath] | None:
|
||||
"""Files in this distribution.
|
||||
|
||||
:return: List of PackagePath for this distribution or None
|
||||
@@ -589,7 +674,7 @@ class Distribution(DeprecatedNonAbstract):
|
||||
return text and map('"{}"'.format, text.splitlines())
|
||||
|
||||
@property
|
||||
def requires(self) -> Optional[List[str]]:
|
||||
def requires(self) -> list[str] | None:
|
||||
"""Generated requirements specified for this Distribution"""
|
||||
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
|
||||
return reqs and list(reqs)
|
||||
@@ -645,6 +730,9 @@ class Distribution(DeprecatedNonAbstract):
|
||||
return self._load_json('direct_url.json')
|
||||
|
||||
def _load_json(self, filename):
|
||||
# Deferred for performance (python/importlib_metadata#503)
|
||||
import json
|
||||
|
||||
return pass_none(json.loads)(
|
||||
self.read_text(filename),
|
||||
object_hook=lambda data: types.SimpleNamespace(**data),
|
||||
@@ -692,7 +780,7 @@ class DistributionFinder(MetaPathFinder):
|
||||
vars(self).update(kwargs)
|
||||
|
||||
@property
|
||||
def path(self) -> List[str]:
|
||||
def path(self) -> list[str]:
|
||||
"""
|
||||
The sequence of directory path that a distribution finder
|
||||
should search.
|
||||
@@ -713,6 +801,20 @@ class DistributionFinder(MetaPathFinder):
|
||||
"""
|
||||
|
||||
|
||||
@passthrough
|
||||
def _clear_after_fork(cached):
|
||||
"""Ensure ``func`` clears cached state after ``fork`` when supported.
|
||||
|
||||
``FastPath`` caches zip-backed ``pathlib.Path`` objects that retain a
|
||||
reference to the parent's open ``ZipFile`` handle. Re-using a cached
|
||||
instance in a forked child can therefore resurrect invalid file pointers
|
||||
and trigger ``BadZipFile``/``OSError`` failures (python/importlib_metadata#520).
|
||||
Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process
|
||||
on its own cache.
|
||||
"""
|
||||
getattr(os, 'register_at_fork', noop)(after_in_child=cached.cache_clear)
|
||||
|
||||
|
||||
class FastPath:
|
||||
"""
|
||||
Micro-optimized class for searching a root for children.
|
||||
@@ -729,7 +831,8 @@ class FastPath:
|
||||
True
|
||||
"""
|
||||
|
||||
@functools.lru_cache() # type: ignore
|
||||
@_clear_after_fork # type: ignore[misc]
|
||||
@functools.lru_cache()
|
||||
def __new__(cls, root):
|
||||
return super().__new__(cls)
|
||||
|
||||
@@ -747,6 +850,9 @@ class FastPath:
|
||||
return []
|
||||
|
||||
def zip_children(self):
|
||||
# deferred for performance (python/importlib_metadata#502)
|
||||
import zipfile
|
||||
|
||||
zip_path = zipfile.Path(self.root)
|
||||
names = zip_path.root.namelist()
|
||||
self.joinpath = zip_path.joinpath
|
||||
@@ -841,7 +947,7 @@ class Prepared:
|
||||
normalized = None
|
||||
legacy_normalized = None
|
||||
|
||||
def __init__(self, name: Optional[str]):
|
||||
def __init__(self, name: str | None):
|
||||
self.name = name
|
||||
if name is None:
|
||||
return
|
||||
@@ -852,8 +958,15 @@ class Prepared:
|
||||
def normalize(name):
|
||||
"""
|
||||
PEP 503 normalization plus dashes as underscores.
|
||||
|
||||
Specifically avoids ``re.sub`` as prescribed for performance
|
||||
benefits (see python/cpython#143658).
|
||||
"""
|
||||
return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
|
||||
value = name.lower().replace("-", "_").replace(".", "_")
|
||||
# Condense repeats
|
||||
while "__" in value:
|
||||
value = value.replace("__", "_")
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def legacy_normalize(name):
|
||||
@@ -904,7 +1017,7 @@ class PathDistribution(Distribution):
|
||||
"""
|
||||
self._path = path
|
||||
|
||||
def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
|
||||
def read_text(self, filename: str | os.PathLike[str]) -> str | None:
|
||||
with suppress(
|
||||
FileNotFoundError,
|
||||
IsADirectoryError,
|
||||
@@ -973,6 +1086,7 @@ def metadata(distribution_name: str) -> _meta.PackageMetadata:
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
:return: A PackageMetadata containing the parsed metadata.
|
||||
:raises MetadataNotFound: If no metadata file is present in the distribution.
|
||||
"""
|
||||
return Distribution.from_name(distribution_name).metadata
|
||||
|
||||
@@ -1011,7 +1125,7 @@ def entry_points(**params) -> EntryPoints:
|
||||
return EntryPoints(eps).select(**params)
|
||||
|
||||
|
||||
def files(distribution_name: str) -> Optional[List[PackagePath]]:
|
||||
def files(distribution_name: str) -> list[PackagePath] | None:
|
||||
"""Return a list of files for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
@@ -1020,7 +1134,7 @@ def files(distribution_name: str) -> Optional[List[PackagePath]]:
|
||||
return distribution(distribution_name).files
|
||||
|
||||
|
||||
def requires(distribution_name: str) -> Optional[List[str]]:
|
||||
def requires(distribution_name: str) -> list[str] | None:
|
||||
"""
|
||||
Return a list of requirements for the named package.
|
||||
|
||||
@@ -1030,7 +1144,7 @@ def requires(distribution_name: str) -> Optional[List[str]]:
|
||||
return distribution(distribution_name).requires
|
||||
|
||||
|
||||
def packages_distributions() -> Mapping[str, List[str]]:
|
||||
def packages_distributions() -> Mapping[str, list[str]]:
|
||||
"""
|
||||
Return a mapping of top-level packages to their
|
||||
distributions.
|
||||
@@ -1051,7 +1165,7 @@ def _top_level_declared(dist):
|
||||
return (dist.read_text('top_level.txt') or '').split()
|
||||
|
||||
|
||||
def _topmost(name: PackagePath) -> Optional[str]:
|
||||
def _topmost(name: PackagePath) -> str | None:
|
||||
"""
|
||||
Return the top-most parent as long as there is a parent.
|
||||
"""
|
||||
@@ -1077,11 +1191,10 @@ def _get_toplevel_name(name: PackagePath) -> str:
|
||||
>>> _get_toplevel_name(PackagePath('foo.dist-info'))
|
||||
'foo.dist-info'
|
||||
"""
|
||||
return _topmost(name) or (
|
||||
# python/typeshed#10328
|
||||
inspect.getmodulename(name) # type: ignore
|
||||
or str(name)
|
||||
)
|
||||
# Defer import of inspect for performance (python/cpython#118761)
|
||||
import inspect
|
||||
|
||||
return _topmost(name) or inspect.getmodulename(name) or str(name)
|
||||
|
||||
|
||||
def _top_level_inferred(dist):
|
||||
|
||||
Vendored
+62
-15
@@ -1,22 +1,59 @@
|
||||
import functools
|
||||
import warnings
|
||||
import email.message
|
||||
import email.policy
|
||||
import re
|
||||
import textwrap
|
||||
import email.message
|
||||
|
||||
from ._text import FoldedCase
|
||||
|
||||
|
||||
# Do not remove prior to 2024-01-01 or Python 3.14
|
||||
_warn = functools.partial(
|
||||
warnings.warn,
|
||||
"Implicit None on return values is deprecated and will raise KeyErrors.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
class RawPolicy(email.policy.EmailPolicy):
|
||||
def fold(self, name, value):
|
||||
folded = self.linesep.join(
|
||||
textwrap
|
||||
.indent(value, prefix=' ' * 8, predicate=lambda line: True)
|
||||
.lstrip()
|
||||
.splitlines()
|
||||
)
|
||||
return f'{name}: {folded}{self.linesep}'
|
||||
|
||||
|
||||
class Message(email.message.Message):
|
||||
r"""
|
||||
Specialized Message subclass to handle metadata naturally.
|
||||
|
||||
Reads values that may have newlines in them and converts the
|
||||
payload to the Description.
|
||||
|
||||
>>> msg_text = textwrap.dedent('''
|
||||
... Name: Foo
|
||||
... Version: 3.0
|
||||
... License: blah
|
||||
... de-blah
|
||||
... <BLANKLINE>
|
||||
... First line of description.
|
||||
... Second line of description.
|
||||
... <BLANKLINE>
|
||||
... Fourth line!
|
||||
... ''').lstrip().replace('<BLANKLINE>', '')
|
||||
>>> msg = Message(email.message_from_string(msg_text))
|
||||
>>> msg['Description']
|
||||
'First line of description.\nSecond line of description.\n\nFourth line!\n'
|
||||
|
||||
Message should render even if values contain newlines.
|
||||
|
||||
>>> print(msg)
|
||||
Name: Foo
|
||||
Version: 3.0
|
||||
License: blah
|
||||
de-blah
|
||||
Description: First line of description.
|
||||
Second line of description.
|
||||
<BLANKLINE>
|
||||
Fourth line!
|
||||
<BLANKLINE>
|
||||
<BLANKLINE>
|
||||
"""
|
||||
|
||||
multiple_use_keys = set(
|
||||
map(
|
||||
FoldedCase,
|
||||
@@ -52,26 +89,36 @@ class Message(email.message.Message):
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""
|
||||
Warn users that a ``KeyError`` can be expected when a
|
||||
missing key is supplied. Ref python/importlib_metadata#371.
|
||||
Override parent behavior to typical dict behavior.
|
||||
|
||||
``email.message.Message`` will emit None values for missing
|
||||
keys. Typical mappings, including this ``Message``, will raise
|
||||
a key error for missing keys.
|
||||
|
||||
Ref python/importlib_metadata#371.
|
||||
"""
|
||||
res = super().__getitem__(item)
|
||||
if res is None:
|
||||
_warn()
|
||||
raise KeyError(item)
|
||||
return res
|
||||
|
||||
def _repair_headers(self):
|
||||
def redent(value):
|
||||
"Correct for RFC822 indentation"
|
||||
if not value or '\n' not in value:
|
||||
indent = ' ' * 8
|
||||
if not value or '\n' + indent not in value:
|
||||
return value
|
||||
return textwrap.dedent(' ' * 8 + value)
|
||||
return textwrap.dedent(indent + value)
|
||||
|
||||
headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
|
||||
if self._payload:
|
||||
headers.append(('Description', self.get_payload()))
|
||||
self.set_payload('')
|
||||
return headers
|
||||
|
||||
def as_string(self):
|
||||
return super().as_string(policy=RawPolicy())
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""
|
||||
|
||||
+5
-1
@@ -1,4 +1,5 @@
|
||||
import collections
|
||||
import typing
|
||||
|
||||
|
||||
# from jaraco.collections 3.3
|
||||
@@ -24,7 +25,10 @@ class FreezableDefaultDict(collections.defaultdict):
|
||||
self._frozen = lambda key: self.default_factory()
|
||||
|
||||
|
||||
class Pair(collections.namedtuple('Pair', 'name value')):
|
||||
class Pair(typing.NamedTuple):
|
||||
name: str
|
||||
value: str
|
||||
|
||||
@classmethod
|
||||
def parse(cls, text):
|
||||
return cls(*map(str.strip, text.split("=", 1)))
|
||||
Vendored
+118
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import operator
|
||||
|
||||
|
||||
# from jaraco.context 6.1
|
||||
class ExceptionTrap:
|
||||
"""
|
||||
A context manager that will catch certain exceptions and provide an
|
||||
indication they occurred.
|
||||
|
||||
>>> with ExceptionTrap() as trap:
|
||||
... raise Exception()
|
||||
>>> bool(trap)
|
||||
True
|
||||
|
||||
>>> with ExceptionTrap() as trap:
|
||||
... pass
|
||||
>>> bool(trap)
|
||||
False
|
||||
|
||||
>>> with ExceptionTrap(ValueError) as trap:
|
||||
... raise ValueError("1 + 1 is not 3")
|
||||
>>> bool(trap)
|
||||
True
|
||||
>>> trap.value
|
||||
ValueError('1 + 1 is not 3')
|
||||
>>> trap.tb
|
||||
<traceback object at ...>
|
||||
|
||||
>>> with ExceptionTrap(ValueError) as trap:
|
||||
... raise Exception()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Exception
|
||||
|
||||
>>> bool(trap)
|
||||
False
|
||||
"""
|
||||
|
||||
exc_info = None, None, None
|
||||
|
||||
def __init__(self, exceptions=(Exception,)):
|
||||
self.exceptions = exceptions
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self.exc_info[0]
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.exc_info[1]
|
||||
|
||||
@property
|
||||
def tb(self):
|
||||
return self.exc_info[2]
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
type = exc_info[0]
|
||||
matches = type and issubclass(type, self.exceptions)
|
||||
if matches:
|
||||
self.exc_info = exc_info
|
||||
return matches
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.type)
|
||||
|
||||
def raises(self, func, *, _test=bool):
|
||||
"""
|
||||
Wrap func and replace the result with the truth
|
||||
value of the trap (True if an exception occurred).
|
||||
|
||||
First, give the decorator an alias to support Python 3.8
|
||||
Syntax.
|
||||
|
||||
>>> raises = ExceptionTrap(ValueError).raises
|
||||
|
||||
Now decorate a function that always fails.
|
||||
|
||||
>>> @raises
|
||||
... def fail():
|
||||
... raise ValueError('failed')
|
||||
>>> fail()
|
||||
True
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
with ExceptionTrap(self.exceptions) as trap:
|
||||
func(*args, **kwargs)
|
||||
return _test(trap)
|
||||
|
||||
return wrapper
|
||||
|
||||
def passes(self, func):
|
||||
"""
|
||||
Wrap func and replace the result with the truth
|
||||
value of the trap (True if no exception).
|
||||
|
||||
First, give the decorator an alias to support Python 3.8
|
||||
Syntax.
|
||||
|
||||
>>> passes = ExceptionTrap(ValueError).passes
|
||||
|
||||
Now decorate a function that always fails.
|
||||
|
||||
>>> @passes
|
||||
... def fail():
|
||||
... raise ValueError('failed')
|
||||
|
||||
>>> fail()
|
||||
False
|
||||
"""
|
||||
return self.raises(func, _test=operator.not_)
|
||||
Vendored
+33
-1
@@ -1,5 +1,7 @@
|
||||
import types
|
||||
import functools
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
# from jaraco.functools 3.3
|
||||
@@ -102,3 +104,33 @@ def pass_none(func):
|
||||
return func(param, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# From jaraco.functools 4.4
|
||||
def noop(*args, **kwargs):
|
||||
"""
|
||||
A no-operation function that does nothing.
|
||||
|
||||
>>> noop(1, 2, three=3)
|
||||
"""
|
||||
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
|
||||
# From jaraco.functools 4.4
|
||||
def passthrough(func: Callable[..., object]) -> Callable[[_T], _T]:
|
||||
"""
|
||||
Wrap the function to always return the first parameter.
|
||||
|
||||
>>> passthrough(print)('3')
|
||||
3
|
||||
'3'
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(first: _T, *args, **kwargs) -> _T:
|
||||
func(first, *args, **kwargs)
|
||||
return first
|
||||
|
||||
return wrapper # type: ignore[return-value]
|
||||
Vendored
+98
@@ -1,3 +1,4 @@
|
||||
from collections import defaultdict, deque
|
||||
from itertools import filterfalse
|
||||
|
||||
|
||||
@@ -71,3 +72,100 @@ def always_iterable(obj, base_type=(str, bytes)):
|
||||
return iter(obj)
|
||||
except TypeError:
|
||||
return iter((obj,))
|
||||
|
||||
|
||||
# Copied from more_itertools 10.3
|
||||
class bucket:
|
||||
"""Wrap *iterable* and return an object that buckets the iterable into
|
||||
child iterables based on a *key* function.
|
||||
|
||||
>>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3']
|
||||
>>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character
|
||||
>>> sorted(list(s)) # Get the keys
|
||||
['a', 'b', 'c']
|
||||
>>> a_iterable = s['a']
|
||||
>>> next(a_iterable)
|
||||
'a1'
|
||||
>>> next(a_iterable)
|
||||
'a2'
|
||||
>>> list(s['b'])
|
||||
['b1', 'b2', 'b3']
|
||||
|
||||
The original iterable will be advanced and its items will be cached until
|
||||
they are used by the child iterables. This may require significant storage.
|
||||
|
||||
By default, attempting to select a bucket to which no items belong will
|
||||
exhaust the iterable and cache all values.
|
||||
If you specify a *validator* function, selected buckets will instead be
|
||||
checked against it.
|
||||
|
||||
>>> from itertools import count
|
||||
>>> it = count(1, 2) # Infinite sequence of odd numbers
|
||||
>>> key = lambda x: x % 10 # Bucket by last digit
|
||||
>>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only
|
||||
>>> s = bucket(it, key=key, validator=validator)
|
||||
>>> 2 in s
|
||||
False
|
||||
>>> list(s[2])
|
||||
[]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, iterable, key, validator=None):
|
||||
self._it = iter(iterable)
|
||||
self._key = key
|
||||
self._cache = defaultdict(deque)
|
||||
self._validator = validator or (lambda x: True)
|
||||
|
||||
def __contains__(self, value):
|
||||
if not self._validator(value):
|
||||
return False
|
||||
|
||||
try:
|
||||
item = next(self[value])
|
||||
except StopIteration:
|
||||
return False
|
||||
else:
|
||||
self._cache[value].appendleft(item)
|
||||
|
||||
return True
|
||||
|
||||
def _get_values(self, value):
|
||||
"""
|
||||
Helper to yield items from the parent iterator that match *value*.
|
||||
Items that don't match are stored in the local cache as they
|
||||
are encountered.
|
||||
"""
|
||||
while True:
|
||||
# If we've cached some items that match the target value, emit
|
||||
# the first one and evict it from the cache.
|
||||
if self._cache[value]:
|
||||
yield self._cache[value].popleft()
|
||||
# Otherwise we need to advance the parent iterator to search for
|
||||
# a matching item, caching the rest.
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
item = next(self._it)
|
||||
except StopIteration:
|
||||
return
|
||||
item_value = self._key(item)
|
||||
if item_value == value:
|
||||
yield item
|
||||
break
|
||||
elif self._validator(item_value):
|
||||
self._cache[item_value].append(item)
|
||||
|
||||
def __iter__(self):
|
||||
for item in self._it:
|
||||
item_value = self._key(item)
|
||||
if self._validator(item_value):
|
||||
self._cache[item_value].append(item)
|
||||
|
||||
yield from self._cache.keys()
|
||||
|
||||
def __getitem__(self, value):
|
||||
if not self._validator(value):
|
||||
return iter(())
|
||||
|
||||
return self._get_values(value)
|
||||
Vendored
+14
-10
@@ -1,9 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Protocol
|
||||
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
|
||||
|
||||
from collections.abc import Iterator
|
||||
from typing import (
|
||||
Any,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
overload,
|
||||
)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
@@ -20,25 +24,25 @@ class PackageMetadata(Protocol):
|
||||
@overload
|
||||
def get(
|
||||
self, name: str, failobj: None = None
|
||||
) -> Optional[str]: ... # pragma: no cover
|
||||
) -> str | None: ... # pragma: no cover
|
||||
|
||||
@overload
|
||||
def get(self, name: str, failobj: _T) -> Union[str, _T]: ... # pragma: no cover
|
||||
def get(self, name: str, failobj: _T) -> str | _T: ... # pragma: no cover
|
||||
|
||||
# overload per python/importlib_metadata#435
|
||||
@overload
|
||||
def get_all(
|
||||
self, name: str, failobj: None = None
|
||||
) -> Optional[List[Any]]: ... # pragma: no cover
|
||||
) -> list[Any] | None: ... # pragma: no cover
|
||||
|
||||
@overload
|
||||
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
|
||||
def get_all(self, name: str, failobj: _T) -> list[Any] | _T:
|
||||
"""
|
||||
Return all values associated with a possibly multi-valued key.
|
||||
"""
|
||||
|
||||
@property
|
||||
def json(self) -> Dict[str, Union[str, List[str]]]:
|
||||
def json(self) -> dict[str, str | list[str]]:
|
||||
"""
|
||||
A JSON-compatible form of the metadata.
|
||||
"""
|
||||
@@ -50,11 +54,11 @@ class SimplePath(Protocol):
|
||||
"""
|
||||
|
||||
def joinpath(
|
||||
self, other: Union[str, os.PathLike[str]]
|
||||
self, other: str | os.PathLike[str]
|
||||
) -> SimplePath: ... # pragma: no cover
|
||||
|
||||
def __truediv__(
|
||||
self, other: Union[str, os.PathLike[str]]
|
||||
self, other: str | os.PathLike[str]
|
||||
) -> SimplePath: ... # pragma: no cover
|
||||
|
||||
@property
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
import functools
|
||||
import typing
|
||||
|
||||
from ._meta import PackageMetadata
|
||||
|
||||
md_none = functools.partial(typing.cast, PackageMetadata)
|
||||
"""
|
||||
Suppress type errors for optional metadata.
|
||||
|
||||
Although Distribution.metadata can return None when metadata is corrupt
|
||||
and thus None, allow callers to assume it's not None and crash if
|
||||
that's the case.
|
||||
|
||||
# python/importlib_metadata#493
|
||||
"""
|
||||
Vendored
+2
-5
@@ -8,12 +8,11 @@ for more detail.
|
||||
"""
|
||||
|
||||
from ._common import (
|
||||
Anchor,
|
||||
Package,
|
||||
as_file,
|
||||
files,
|
||||
Package,
|
||||
Anchor,
|
||||
)
|
||||
|
||||
from ._functional import (
|
||||
contents,
|
||||
is_resource,
|
||||
@@ -23,10 +22,8 @@ from ._functional import (
|
||||
read_binary,
|
||||
read_text,
|
||||
)
|
||||
|
||||
from .abc import ResourceReader
|
||||
|
||||
|
||||
__all__ = [
|
||||
'Package',
|
||||
'Anchor',
|
||||
|
||||
Vendored
+21
-41
@@ -1,54 +1,20 @@
|
||||
import contextlib
|
||||
import functools
|
||||
import importlib
|
||||
import inspect
|
||||
import itertools
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
import functools
|
||||
import contextlib
|
||||
import types
|
||||
import importlib
|
||||
import inspect
|
||||
import warnings
|
||||
import itertools
|
||||
from typing import Optional, cast
|
||||
|
||||
from typing import Union, Optional, cast
|
||||
from .abc import ResourceReader, Traversable
|
||||
|
||||
Package = Union[types.ModuleType, str]
|
||||
Package = types.ModuleType | str
|
||||
Anchor = Package
|
||||
|
||||
|
||||
def package_to_anchor(func):
|
||||
"""
|
||||
Replace 'package' parameter as 'anchor' and warn about the change.
|
||||
|
||||
Other errors should fall through.
|
||||
|
||||
>>> files('a', 'b')
|
||||
Traceback (most recent call last):
|
||||
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
|
||||
|
||||
Remove this compatibility in Python 3.14.
|
||||
"""
|
||||
undefined = object()
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(anchor=undefined, package=undefined):
|
||||
if package is not undefined:
|
||||
if anchor is not undefined:
|
||||
return func(anchor, package)
|
||||
warnings.warn(
|
||||
"First parameter to files is renamed to 'anchor'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return func(package)
|
||||
elif anchor is undefined:
|
||||
return func()
|
||||
return func(anchor)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@package_to_anchor
|
||||
def files(anchor: Optional[Anchor] = None) -> Traversable:
|
||||
"""
|
||||
Get a Traversable resource for an anchor.
|
||||
@@ -105,6 +71,19 @@ def _infer_caller():
|
||||
return next(callers).frame
|
||||
|
||||
|
||||
def _assert_spec(package: types.ModuleType) -> None:
|
||||
"""
|
||||
Provide a nicer error message when package is ``__main__``
|
||||
and its ``__spec__`` is ``None``
|
||||
(https://docs.python.org/3/reference/import.html#main-spec).
|
||||
"""
|
||||
if package.__spec__ is None:
|
||||
raise TypeError(
|
||||
f"Cannot access resources for '{package.__name__}' "
|
||||
"as it does not appear to correspond to an importable module (its __spec__ is None)."
|
||||
)
|
||||
|
||||
|
||||
def from_package(package: types.ModuleType):
|
||||
"""
|
||||
Return a Traversable object for the given package.
|
||||
@@ -113,6 +92,7 @@ def from_package(package: types.ModuleType):
|
||||
# deferred for performance (python/cpython#109829)
|
||||
from ._adapters import wrap_spec
|
||||
|
||||
_assert_spec(package)
|
||||
spec = wrap_spec(package)
|
||||
reader = spec.loader.get_resource_reader(spec.name)
|
||||
return reader.files()
|
||||
|
||||
+6
-3
@@ -2,8 +2,8 @@
|
||||
|
||||
import warnings
|
||||
|
||||
from ._common import files, as_file
|
||||
|
||||
from ._common import as_file, files
|
||||
from .abc import TraversalError
|
||||
|
||||
_MISSING = object()
|
||||
|
||||
@@ -42,7 +42,10 @@ def is_resource(anchor, *path_names):
|
||||
|
||||
Otherwise returns ``False``.
|
||||
"""
|
||||
return _get_resource(anchor, path_names).is_file()
|
||||
try:
|
||||
return _get_resource(anchor, path_names).is_file()
|
||||
except TraversalError:
|
||||
return False
|
||||
|
||||
|
||||
def contents(anchor, *path_names):
|
||||
|
||||
Vendored
+26
-10
@@ -1,14 +1,22 @@
|
||||
import abc
|
||||
import io
|
||||
import itertools
|
||||
import os
|
||||
import pathlib
|
||||
from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional
|
||||
from typing import runtime_checkable, Protocol
|
||||
from typing import Union
|
||||
from collections.abc import Iterable, Iterator
|
||||
from typing import (
|
||||
Any,
|
||||
BinaryIO,
|
||||
Literal,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Protocol,
|
||||
Text,
|
||||
TextIO,
|
||||
overload,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
|
||||
StrPath = Union[str, os.PathLike[str]]
|
||||
StrPath = str | os.PathLike[str]
|
||||
|
||||
__all__ = ["ResourceReader", "Traversable", "TraversableResources"]
|
||||
|
||||
@@ -82,11 +90,13 @@ class Traversable(Protocol):
|
||||
with self.open('rb') as strm:
|
||||
return strm.read()
|
||||
|
||||
def read_text(self, encoding: Optional[str] = None) -> str:
|
||||
def read_text(
|
||||
self, encoding: Optional[str] = None, errors: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
Read contents of self as text
|
||||
"""
|
||||
with self.open(encoding=encoding) as strm:
|
||||
with self.open(encoding=encoding, errors=errors) as strm:
|
||||
return strm.read()
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -132,8 +142,14 @@ class Traversable(Protocol):
|
||||
"""
|
||||
return self.joinpath(child)
|
||||
|
||||
@overload
|
||||
def open(self, mode: Literal['r'] = 'r', *args: Any, **kwargs: Any) -> TextIO: ...
|
||||
|
||||
@overload
|
||||
def open(self, mode: Literal['rb'], *args: Any, **kwargs: Any) -> BinaryIO: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def open(self, mode='r', *args, **kwargs):
|
||||
def open(self, mode: str = 'r', *args: Any, **kwargs: Any) -> TextIO | BinaryIO:
|
||||
"""
|
||||
mode may be 'r' or 'rb' to open as text or binary. Return a handle
|
||||
suitable for reading (same as pathlib.Path.open).
|
||||
@@ -160,7 +176,7 @@ class TraversableResources(ResourceReader):
|
||||
def files(self) -> "Traversable":
|
||||
"""Return a Traversable object for the loaded package."""
|
||||
|
||||
def open_resource(self, resource: StrPath) -> io.BufferedReader:
|
||||
def open_resource(self, resource: StrPath) -> BinaryIO:
|
||||
return self.files().joinpath(resource).open('rb')
|
||||
|
||||
def resource_path(self, resource: Any) -> NoReturn:
|
||||
|
||||
Vendored
+1
-2
@@ -3,15 +3,14 @@ from __future__ import annotations
|
||||
import collections
|
||||
import contextlib
|
||||
import itertools
|
||||
import pathlib
|
||||
import operator
|
||||
import pathlib
|
||||
import re
|
||||
import warnings
|
||||
import zipfile
|
||||
from collections.abc import Iterator
|
||||
|
||||
from . import abc
|
||||
|
||||
from ._itertools import only
|
||||
|
||||
|
||||
|
||||
Vendored
+3
-3
@@ -5,7 +5,7 @@ Interface adapters for low-level readers.
|
||||
import abc
|
||||
import io
|
||||
import itertools
|
||||
from typing import BinaryIO, List
|
||||
from typing import BinaryIO
|
||||
|
||||
from .abc import Traversable, TraversableResources
|
||||
|
||||
@@ -24,14 +24,14 @@ class SimpleReader(abc.ABC):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def children(self) -> List['SimpleReader']:
|
||||
def children(self) -> list['SimpleReader']:
|
||||
"""
|
||||
Obtain an iterable of SimpleReader for available
|
||||
child containers (e.g. directories).
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def resources(self) -> List[str]:
|
||||
def resources(self) -> list[str]:
|
||||
"""
|
||||
Obtain available named resources for this virtual package.
|
||||
"""
|
||||
|
||||
Vendored
+151
-65
@@ -153,9 +153,9 @@ import importlib.machinery
|
||||
import itertools
|
||||
import linecache
|
||||
import os
|
||||
import re
|
||||
lazy import re
|
||||
import sys
|
||||
import tokenize
|
||||
lazy import tokenize
|
||||
import token
|
||||
import types
|
||||
import functools
|
||||
@@ -163,9 +163,9 @@ import builtins
|
||||
from keyword import iskeyword
|
||||
from operator import attrgetter
|
||||
from collections import namedtuple, OrderedDict
|
||||
from weakref import ref as make_weakref
|
||||
from _weakref import ref as make_weakref
|
||||
|
||||
# Create constants for the compiler flags in Include/code.h
|
||||
# Create constants for the compiler flags in Include/cpython/code.h
|
||||
# We try to get them from dis to avoid duplication
|
||||
mod_dict = globals()
|
||||
for k, v in dis.COMPILER_FLAG_NAMES.items():
|
||||
@@ -421,7 +421,6 @@ def iscode(object):
|
||||
co_freevars tuple of names of free variables
|
||||
co_posonlyargcount number of positional only arguments
|
||||
co_kwonlyargcount number of keyword only arguments (not including ** arg)
|
||||
co_lnotab encoded mapping of line numbers to bytecode indices
|
||||
co_name name with which this code object was defined
|
||||
co_names tuple of names other than arguments and function locals
|
||||
co_nlocals number of local variables
|
||||
@@ -712,8 +711,8 @@ def _findclass(func):
|
||||
return None
|
||||
return cls
|
||||
|
||||
def _finddoc(obj):
|
||||
if isclass(obj):
|
||||
def _finddoc(obj, *, search_in_class=True):
|
||||
if search_in_class and isclass(obj):
|
||||
for base in obj.__mro__:
|
||||
if base is not object:
|
||||
try:
|
||||
@@ -753,6 +752,12 @@ def _finddoc(obj):
|
||||
cls = _findclass(obj.fget)
|
||||
if cls is None or getattr(cls, name) is not obj:
|
||||
return None
|
||||
# Should be tested before ismethoddescriptor()
|
||||
elif isinstance(obj, functools.cached_property):
|
||||
name = obj.attrname
|
||||
cls = _findclass(obj.func)
|
||||
if cls is None or getattr(cls, name) is not obj:
|
||||
return None
|
||||
elif ismethoddescriptor(obj) or isdatadescriptor(obj):
|
||||
name = obj.__name__
|
||||
cls = obj.__objclass__
|
||||
@@ -773,19 +778,37 @@ def _finddoc(obj):
|
||||
return doc
|
||||
return None
|
||||
|
||||
def getdoc(object):
|
||||
def _getowndoc(obj):
|
||||
"""Get the documentation string for an object if it is not
|
||||
inherited from its class."""
|
||||
try:
|
||||
doc = object.__getattribute__(obj, '__doc__')
|
||||
if doc is None:
|
||||
return None
|
||||
if obj is not type:
|
||||
typedoc = type(obj).__doc__
|
||||
if isinstance(typedoc, str) and typedoc == doc:
|
||||
return None
|
||||
return doc
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def getdoc(object, *, fallback_to_class_doc=True, inherit_class_doc=True):
|
||||
"""Get the documentation string for an object.
|
||||
|
||||
All tabs are expanded to spaces. To clean up docstrings that are
|
||||
indented to line up with blocks of code, any whitespace than can be
|
||||
uniformly removed from the second line onwards is removed."""
|
||||
try:
|
||||
doc = object.__doc__
|
||||
except AttributeError:
|
||||
return None
|
||||
if fallback_to_class_doc:
|
||||
try:
|
||||
doc = object.__doc__
|
||||
except AttributeError:
|
||||
return None
|
||||
else:
|
||||
doc = _getowndoc(object)
|
||||
if doc is None:
|
||||
try:
|
||||
doc = _finddoc(object)
|
||||
doc = _finddoc(object, search_in_class=inherit_class_doc)
|
||||
except (AttributeError, TypeError):
|
||||
return None
|
||||
if not isinstance(doc, str):
|
||||
@@ -1236,11 +1259,10 @@ def getargs(co):
|
||||
FullArgSpec = namedtuple('FullArgSpec',
|
||||
'args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations')
|
||||
|
||||
def getfullargspec(func):
|
||||
def getfullargspec(func, *, annotation_format=Format.VALUE):
|
||||
"""Get the names and default values of a callable object's parameters.
|
||||
|
||||
A tuple of seven things is returned:
|
||||
(args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations).
|
||||
A FullArgSpec namedtuple is returned, which has the following attributes:
|
||||
'args' is a list of the parameter names.
|
||||
'varargs' and 'varkw' are the names of the * and ** parameters or None.
|
||||
'defaults' is an n-tuple of the default values of the last n parameters.
|
||||
@@ -1248,6 +1270,9 @@ def getfullargspec(func):
|
||||
'kwonlydefaults' is a dictionary mapping names from kwonlyargs to defaults.
|
||||
'annotations' is a dictionary mapping parameter names to annotations.
|
||||
|
||||
The *annotation_format* parameter controls the format of the annotations.
|
||||
See the annotationlib documentation for details.
|
||||
|
||||
Notable differences from inspect.signature():
|
||||
- the "self" parameter is always reported, even for bound methods
|
||||
- wrapper chains defined by __wrapped__ *not* unwrapped automatically
|
||||
@@ -1273,7 +1298,8 @@ def getfullargspec(func):
|
||||
follow_wrapper_chains=False,
|
||||
skip_bound_arg=False,
|
||||
sigcls=Signature,
|
||||
eval_str=False)
|
||||
eval_str=False,
|
||||
annotation_format=annotation_format)
|
||||
except Exception as ex:
|
||||
# Most of the times 'signature' will raise ValueError.
|
||||
# But, it can also raise AttributeError, and, maybe something
|
||||
@@ -1615,7 +1641,6 @@ def getframeinfo(frame, context=1):
|
||||
|
||||
def getlineno(frame):
|
||||
"""Get the line number from a frame object, allowing for optimization."""
|
||||
# FrameType.f_lineno is now a descriptor that grovels co_lnotab
|
||||
return frame.f_lineno
|
||||
|
||||
_FrameInfo = namedtuple('_FrameInfo', ('frame',) + Traceback._fields)
|
||||
@@ -1709,7 +1734,8 @@ def _shadowed_dict_from_weakref_mro_tuple(*weakref_mro):
|
||||
class_dict = dunder_dict['__dict__']
|
||||
if not (type(class_dict) is types.GetSetDescriptorType and
|
||||
class_dict.__name__ == "__dict__" and
|
||||
class_dict.__objclass__ is entry):
|
||||
(class_dict.__objclass__ is object or
|
||||
class_dict.__objclass__ is entry)):
|
||||
return class_dict
|
||||
return _sentinel
|
||||
|
||||
@@ -1793,13 +1819,7 @@ def getgeneratorstate(generator):
|
||||
GEN_SUSPENDED: Currently suspended at a yield expression.
|
||||
GEN_CLOSED: Execution has completed.
|
||||
"""
|
||||
if generator.gi_running:
|
||||
return GEN_RUNNING
|
||||
if generator.gi_suspended:
|
||||
return GEN_SUSPENDED
|
||||
if generator.gi_frame is None:
|
||||
return GEN_CLOSED
|
||||
return GEN_CREATED
|
||||
return generator.gi_state
|
||||
|
||||
|
||||
def getgeneratorlocals(generator):
|
||||
@@ -1835,13 +1855,7 @@ def getcoroutinestate(coroutine):
|
||||
CORO_SUSPENDED: Currently suspended at an await expression.
|
||||
CORO_CLOSED: Execution has completed.
|
||||
"""
|
||||
if coroutine.cr_running:
|
||||
return CORO_RUNNING
|
||||
if coroutine.cr_suspended:
|
||||
return CORO_SUSPENDED
|
||||
if coroutine.cr_frame is None:
|
||||
return CORO_CLOSED
|
||||
return CORO_CREATED
|
||||
return coroutine.cr_state
|
||||
|
||||
|
||||
def getcoroutinelocals(coroutine):
|
||||
@@ -1874,13 +1888,7 @@ def getasyncgenstate(agen):
|
||||
AGEN_SUSPENDED: Currently suspended at a yield expression.
|
||||
AGEN_CLOSED: Execution has completed.
|
||||
"""
|
||||
if agen.ag_running:
|
||||
return AGEN_RUNNING
|
||||
if agen.ag_suspended:
|
||||
return AGEN_SUSPENDED
|
||||
if agen.ag_frame is None:
|
||||
return AGEN_CLOSED
|
||||
return AGEN_CREATED
|
||||
return agen.ag_state
|
||||
|
||||
|
||||
def getasyncgenlocals(agen):
|
||||
@@ -2121,7 +2129,6 @@ def _signature_strip_non_python_syntax(signature):
|
||||
|
||||
current_parameter = 0
|
||||
OP = token.OP
|
||||
ERRORTOKEN = token.ERRORTOKEN
|
||||
|
||||
# token stream always starts with ENCODING token, skip it
|
||||
t = next(token_stream)
|
||||
@@ -2198,7 +2205,8 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True):
|
||||
except NameError:
|
||||
raise ValueError
|
||||
|
||||
if isinstance(value, (str, int, float, bytes, bool, type(None))):
|
||||
if isinstance(value, (str, int, float, bytes, bool, type(None),
|
||||
sentinel)):
|
||||
return ast.Constant(value)
|
||||
raise ValueError
|
||||
|
||||
@@ -3356,22 +3364,113 @@ class BufferFlags(enum.IntFlag):
|
||||
WRITE = 0x200
|
||||
|
||||
|
||||
def _get_details_for_cli(module, nominal_target, resolved_target):
|
||||
# Determine if the given module name is an alias for another module,
|
||||
# or if it is reexporting a name that is actually defined elsewhere
|
||||
resolved_module = getmodule(resolved_target)
|
||||
if resolved_module is not None and resolved_module is not module:
|
||||
# Referenced target indicates it was defined somewhere else,
|
||||
# so report the details of that module rather than the lookup module
|
||||
module = resolved_module
|
||||
reported_module_name = module.__name__
|
||||
# Ensure the reported source file reflects the actual defining location
|
||||
try:
|
||||
source_file = getsourcefile(resolved_target)
|
||||
except Exception:
|
||||
try:
|
||||
source_file = getsourcefile(module)
|
||||
except Exception:
|
||||
source_file = None
|
||||
# Determine if the nominal target location is its defining location
|
||||
if resolved_target is module:
|
||||
reported_target = reported_module_name
|
||||
else:
|
||||
reported_qualname = getattr(resolved_target, "__qualname__", None)
|
||||
if not reported_qualname:
|
||||
reported_qualname = nominal_target.partition(":")[2]
|
||||
reported_target = f"{reported_module_name}:{reported_qualname}"
|
||||
# Special case for looking up functions in frozen modules
|
||||
if source_file == f"<frozen {reported_module_name}>":
|
||||
source_file = module.__file__
|
||||
# Populate the actual details to be reported
|
||||
details = {
|
||||
"target": reported_target,
|
||||
"origin": module.__spec__.origin,
|
||||
"cached": module.__spec__.cached,
|
||||
"source": source_file,
|
||||
}
|
||||
if reported_target != nominal_target:
|
||||
details["alias"] = nominal_target
|
||||
error = None
|
||||
if not source_file:
|
||||
if module.__name__ in sys.builtin_module_names:
|
||||
error = "No source code available for builtin module"
|
||||
else:
|
||||
error = "No source code available for defining module"
|
||||
if resolved_target is module:
|
||||
details["loader"] = repr(module.__spec__.loader)
|
||||
if hasattr(module, '__path__'):
|
||||
details["submodule_paths"] = str(module.__path__)
|
||||
elif source_file:
|
||||
try:
|
||||
__, lineno = findsource(resolved_target)
|
||||
except Exception:
|
||||
error = "Failed to retrieve source code for given target"
|
||||
else:
|
||||
details["lineno"] = lineno
|
||||
if error:
|
||||
details["error"] = error
|
||||
return details
|
||||
|
||||
def _render_details_for_cli(details):
|
||||
resolved_target = details["target"]
|
||||
alias = details.get("alias")
|
||||
if alias:
|
||||
rendered_target = f'{resolved_target} (looked up as "{alias}")'
|
||||
else:
|
||||
rendered_target = resolved_target
|
||||
lines = [
|
||||
f'Target: {rendered_target}',
|
||||
f'Origin: {details["origin"]}',
|
||||
f'Source: {details["source"]}',
|
||||
f'Cached: {details["cached"]}',
|
||||
]
|
||||
loader = details.get("loader")
|
||||
if loader:
|
||||
lines.append(f'Loader: {loader}')
|
||||
submodule_paths = details.get("submodule_paths")
|
||||
if submodule_paths:
|
||||
lines.append(f'Submodule search paths: {submodule_paths}')
|
||||
else:
|
||||
error = details.get("error")
|
||||
if error:
|
||||
# The error is only informational when retrieving object details
|
||||
lines.append(error)
|
||||
else:
|
||||
lines.append(f'Line: {details["lineno"]}')
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _main():
|
||||
""" Logic for inspecting an object given at command line """
|
||||
import argparse
|
||||
import importlib
|
||||
|
||||
parser = argparse.ArgumentParser(color=True)
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'object',
|
||||
help="The object to be analysed. "
|
||||
"It supports the 'module:qualname' syntax")
|
||||
"It supports the `module:qualname` syntax")
|
||||
parser.add_argument(
|
||||
'-d', '--details', action='store_true',
|
||||
help='Display info about the module rather than its source code')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# We don't use `pkgutil.resolve_name` here because we want to obtain
|
||||
# references to both the module *and* the fully resolved target object
|
||||
target = args.object
|
||||
mod_name, has_attrs, attrs = target.partition(":")
|
||||
try:
|
||||
@@ -3389,29 +3488,16 @@ def _main():
|
||||
for part in parts:
|
||||
obj = getattr(obj, part)
|
||||
|
||||
if module.__name__ in sys.builtin_module_names:
|
||||
print("Can't get info for builtin modules.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
details = _get_details_for_cli(module, target, obj)
|
||||
if args.details:
|
||||
print('Target: {}'.format(target))
|
||||
print('Origin: {}'.format(getsourcefile(module)))
|
||||
print('Cached: {}'.format(module.__cached__))
|
||||
if obj is module:
|
||||
print('Loader: {}'.format(repr(module.__loader__)))
|
||||
if hasattr(module, '__path__'):
|
||||
print('Submodule search path: {}'.format(module.__path__))
|
||||
else:
|
||||
try:
|
||||
__, lineno = findsource(obj)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
print('Line: {}'.format(lineno))
|
||||
|
||||
print('\n')
|
||||
print(_render_details_for_cli(details))
|
||||
else:
|
||||
print(getsource(obj))
|
||||
# Attempt to render target source details
|
||||
error = details.get("error")
|
||||
if error:
|
||||
sys.exit(error)
|
||||
else:
|
||||
print(getsource(obj))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Vendored
+28
-6
@@ -4,6 +4,7 @@ import importlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import os
|
||||
import py_compile
|
||||
import shutil
|
||||
import sys
|
||||
import textwrap
|
||||
@@ -49,20 +50,31 @@ def forget(modname):
|
||||
# combinations of PEP 3147/488 and legacy pyc files.
|
||||
unlink(source + 'c')
|
||||
for opt in ('', 1, 2):
|
||||
unlink(importlib.util.cache_from_source(source, optimization=opt))
|
||||
try:
|
||||
unlink(importlib.util.cache_from_source(source, optimization=opt))
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
|
||||
def make_legacy_pyc(source):
|
||||
def make_legacy_pyc(source, allow_compile=False):
|
||||
"""Move a PEP 3147/488 pyc file to its legacy pyc location.
|
||||
|
||||
:param source: The file system path to the source file. The source file
|
||||
does not need to exist, however the PEP 3147/488 pyc file must exist.
|
||||
does not need to exist, however the PEP 3147/488 pyc file must exist or
|
||||
allow_compile must be set.
|
||||
:param allow_compile: If True, uses py_compile to create a .pyc if it does
|
||||
not exist. This should be passed as True if cache_tag may be None.
|
||||
:return: The file system path to the legacy pyc file.
|
||||
"""
|
||||
pyc_file = importlib.util.cache_from_source(source)
|
||||
assert source.endswith('.py')
|
||||
legacy_pyc = source + 'c'
|
||||
shutil.move(pyc_file, legacy_pyc)
|
||||
try:
|
||||
pyc_file = importlib.util.cache_from_source(source)
|
||||
shutil.move(pyc_file, legacy_pyc)
|
||||
except FileNotFoundError, NotImplementedError:
|
||||
if not allow_compile:
|
||||
raise
|
||||
py_compile.compile(source, legacy_pyc, doraise=True)
|
||||
return legacy_pyc
|
||||
|
||||
|
||||
@@ -313,7 +325,7 @@ def ready_to_import(name=None, source=""):
|
||||
sys.modules.pop(name, None)
|
||||
|
||||
|
||||
def ensure_lazy_imports(imported_module, modules_to_block):
|
||||
def ensure_lazy_imports(imported_module, modules_to_block, *, additional_code=None):
|
||||
"""Test that when imported_module is imported, none of the modules in
|
||||
modules_to_block are imported as a side effect."""
|
||||
modules_to_block = frozenset(modules_to_block)
|
||||
@@ -331,6 +343,16 @@ def ensure_lazy_imports(imported_module, modules_to_block):
|
||||
raise AssertionError(f'unexpectedly imported after importing {imported_module}: {{after}}')
|
||||
"""
|
||||
)
|
||||
if additional_code:
|
||||
script += additional_code
|
||||
script += textwrap.dedent(
|
||||
f"""
|
||||
if unexpected := modules_to_block & sys.modules.keys():
|
||||
after = ", ".join(unexpected)
|
||||
raise AssertionError(f'unexpectedly imported after additional code: {{after}}')
|
||||
"""
|
||||
)
|
||||
|
||||
from .script_helper import assert_python_ok
|
||||
assert_python_ok("-S", "-c", script)
|
||||
|
||||
|
||||
Vendored
+47
-1
@@ -1,6 +1,7 @@
|
||||
import collections.abc
|
||||
import contextlib
|
||||
import errno
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -433,7 +434,10 @@ if sys.platform.startswith("win"):
|
||||
file=sys.__stderr__)
|
||||
mode = 0
|
||||
if stat.S_ISDIR(mode):
|
||||
_waitfor(_rmtree_inner, fullname, waitall=True)
|
||||
# Do not follow junctions, which os.lstat() reports
|
||||
# as directories.
|
||||
if not os.path.isjunction(fullname):
|
||||
_waitfor(_rmtree_inner, fullname, waitall=True)
|
||||
_force_run(fullname, os.rmdir, fullname)
|
||||
else:
|
||||
_force_run(fullname, os.unlink, fullname)
|
||||
@@ -806,6 +810,48 @@ class EnvironmentVarGuard(collections.abc.MutableMapping):
|
||||
os.environ = self._environ
|
||||
|
||||
|
||||
def without_source_date_epoch(fxn):
|
||||
"""Runs function with SOURCE_DATE_EPOCH unset."""
|
||||
@functools.wraps(fxn)
|
||||
def wrapper(*args, **kwargs):
|
||||
with EnvironmentVarGuard() as env:
|
||||
env.unset('SOURCE_DATE_EPOCH')
|
||||
return fxn(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
_MISSING = sentinel("MISSING")
|
||||
|
||||
def with_source_date_epoch(fxn=_MISSING, *, epoch=123456789):
|
||||
"""Runs function with SOURCE_DATE_EPOCH set to *epoch*."""
|
||||
if fxn is _MISSING:
|
||||
return functools.partial(with_source_date_epoch, epoch=epoch)
|
||||
|
||||
@functools.wraps(fxn)
|
||||
def wrapper(*args, **kwargs):
|
||||
with EnvironmentVarGuard() as env:
|
||||
env['SOURCE_DATE_EPOCH'] = str(epoch)
|
||||
return fxn(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
# Run tests with SOURCE_DATE_EPOCH set or unset explicitly.
|
||||
class SourceDateEpochTestMeta(type(unittest.TestCase)):
|
||||
def __new__(mcls, name, bases, dct, *, source_date_epoch):
|
||||
cls = super().__new__(mcls, name, bases, dct)
|
||||
|
||||
for attr in dir(cls):
|
||||
if attr.startswith('test_'):
|
||||
meth = getattr(cls, attr)
|
||||
if source_date_epoch:
|
||||
wrapper = with_source_date_epoch(meth)
|
||||
else:
|
||||
wrapper = without_source_date_epoch(meth)
|
||||
setattr(cls, attr, wrapper)
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
try:
|
||||
if support.MS_WINDOWS:
|
||||
import ctypes
|
||||
|
||||
Vendored
+241
-99
@@ -15,6 +15,7 @@ import marshal
|
||||
import os
|
||||
import py_compile
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
@@ -23,6 +24,7 @@ import textwrap
|
||||
import threading
|
||||
import time
|
||||
import types
|
||||
import warnings
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import _imp
|
||||
@@ -52,7 +54,7 @@ from test.support.os_helper import (
|
||||
TESTFN, rmtree, temp_umask, TESTFN_UNENCODABLE)
|
||||
from test.support import script_helper
|
||||
from test.support import threading_helper
|
||||
from test.test_importlib.util import uncache
|
||||
from test.test_importlib.util import uncache, temporary_pycache_prefix
|
||||
from types import ModuleType
|
||||
try:
|
||||
import _testsinglephase
|
||||
@@ -64,8 +66,10 @@ except ImportError:
|
||||
_testmultiphase = None
|
||||
try:
|
||||
import _interpreters
|
||||
import concurrent.interpreters
|
||||
except ModuleNotFoundError:
|
||||
_interpreters = None
|
||||
concurrent = None
|
||||
try:
|
||||
import _testinternalcapi
|
||||
except ImportError:
|
||||
@@ -73,7 +77,7 @@ except ImportError:
|
||||
|
||||
|
||||
skip_if_dont_write_bytecode = unittest.skipIf(
|
||||
sys.dont_write_bytecode,
|
||||
sys.dont_write_bytecode or sys.implementation.cache_tag is None,
|
||||
"test meaningful only when writing bytecode")
|
||||
|
||||
|
||||
@@ -363,11 +367,11 @@ class ImportTests(unittest.TestCase):
|
||||
def test_import_null_byte_in_name_raises_ModuleNotFoundError(self):
|
||||
# gh-150633: module names containing null bytes should not
|
||||
# lead to duplicates in sys.modules
|
||||
before = set(sys.modules.keys())
|
||||
before = set(sys.modules)
|
||||
with self.assertRaises(ModuleNotFoundError):
|
||||
__import__('zipimport\x00junk')
|
||||
|
||||
self.assertEqual(set(sys.modules.keys()), before)
|
||||
self.assertEqual(set(sys.modules), before)
|
||||
|
||||
def test_from_import_missing_module_raises_ModuleNotFoundError(self):
|
||||
with self.assertRaises(ModuleNotFoundError):
|
||||
@@ -422,7 +426,6 @@ class ImportTests(unittest.TestCase):
|
||||
self.assertIsNotNone(cm.exception)
|
||||
|
||||
def test_from_import_star_invalid_type(self):
|
||||
import re
|
||||
with ready_to_import() as (name, path):
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write("__all__ = [b'invalid_type']")
|
||||
@@ -511,7 +514,7 @@ class ImportTests(unittest.TestCase):
|
||||
try:
|
||||
# Compile & remove .py file; we only need .pyc.
|
||||
# Bytecode must be relocated from the PEP 3147 bytecode-only location.
|
||||
py_compile.compile(filename)
|
||||
make_legacy_pyc(filename, allow_compile=True)
|
||||
finally:
|
||||
unlink(filename)
|
||||
|
||||
@@ -521,7 +524,6 @@ class ImportTests(unittest.TestCase):
|
||||
|
||||
namespace = {}
|
||||
try:
|
||||
make_legacy_pyc(filename)
|
||||
# This used to crash.
|
||||
exec('import ' + module, None, namespace)
|
||||
finally:
|
||||
@@ -786,7 +788,7 @@ class ImportTests(unittest.TestCase):
|
||||
pydname = importlib.util.find_spec("_sqlite3").origin
|
||||
depname = os.path.join(
|
||||
os.path.dirname(pydname),
|
||||
"sqlite3{}.dll".format("_d" if "_d" in pydname else ""))
|
||||
"sqlite3{}.dll".format("_d" if "_d" in os.path.basename(pydname) else ""))
|
||||
|
||||
with os_helper.temp_dir() as tmp:
|
||||
tmp2 = os.path.join(tmp, "DLLs")
|
||||
@@ -1261,6 +1263,44 @@ os.does_not_exist
|
||||
origin = "a\x00b"
|
||||
_imp.create_dynamic(Spec2())
|
||||
|
||||
def test_create_builtin(self):
|
||||
class Spec:
|
||||
pass
|
||||
spec = Spec()
|
||||
|
||||
spec.name = "sys"
|
||||
self.assertIs(_imp.create_builtin(spec), sys)
|
||||
|
||||
spec.name = None
|
||||
with self.assertRaisesRegex(TypeError, 'name must be string, not NoneType'):
|
||||
_imp.create_builtin(spec)
|
||||
|
||||
# gh-142029
|
||||
spec.name = "nonexistent_lib"
|
||||
with self.assertRaises(ModuleNotFoundError):
|
||||
_imp.create_builtin(spec)
|
||||
|
||||
# gh-142029
|
||||
spec.name = ""
|
||||
with self.assertRaisesRegex(ValueError, 'name must not be empty'):
|
||||
_imp.create_builtin(spec)
|
||||
|
||||
def test_filter_syntax_warnings_by_module(self):
|
||||
module_re = r'test\.test_import\.data\.syntax_warnings\z'
|
||||
unload('test.test_import.data.syntax_warnings')
|
||||
with (os_helper.temp_dir() as tmpdir,
|
||||
temporary_pycache_prefix(tmpdir),
|
||||
warnings.catch_warnings(record=True) as wlog):
|
||||
warnings.simplefilter('error')
|
||||
warnings.filterwarnings('always', module=module_re)
|
||||
warnings.filterwarnings('error', module='syntax_warnings')
|
||||
import test.test_import.data.syntax_warnings
|
||||
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
|
||||
filename = test.test_import.data.syntax_warnings.__file__
|
||||
for wm in wlog:
|
||||
self.assertEqual(wm.filename, filename)
|
||||
self.assertIs(wm.category, SyntaxWarning)
|
||||
|
||||
|
||||
@skip_if_dont_write_bytecode
|
||||
class FilePermissionTests(unittest.TestCase):
|
||||
@@ -1369,7 +1409,10 @@ func_filename = func.__code__.co_filename
|
||||
"""
|
||||
dir_name = os.path.abspath(TESTFN)
|
||||
file_name = os.path.join(dir_name, module_name) + os.extsep + "py"
|
||||
compiled_name = importlib.util.cache_from_source(file_name)
|
||||
try:
|
||||
compiled_name = importlib.util.cache_from_source(file_name)
|
||||
except NotImplementedError:
|
||||
compiled_name = None
|
||||
|
||||
def setUp(self):
|
||||
self.sys_path = sys.path[:]
|
||||
@@ -1387,7 +1430,8 @@ func_filename = func.__code__.co_filename
|
||||
else:
|
||||
unload(self.module_name)
|
||||
unlink(self.file_name)
|
||||
unlink(self.compiled_name)
|
||||
if self.compiled_name:
|
||||
unlink(self.compiled_name)
|
||||
rmtree(self.dir_name)
|
||||
|
||||
def import_module(self):
|
||||
@@ -1406,6 +1450,8 @@ func_filename = func.__code__.co_filename
|
||||
self.assertEqual(mod.code_filename, self.file_name)
|
||||
self.assertEqual(mod.func_filename, self.file_name)
|
||||
|
||||
@unittest.skipIf(sys.implementation.cache_tag is None,
|
||||
'requires sys.implementation.cache_tag is not None')
|
||||
def test_incorrect_code_name(self):
|
||||
py_compile.compile(self.file_name, dfile="another_module.py")
|
||||
mod = self.import_module()
|
||||
@@ -1415,9 +1461,9 @@ func_filename = func.__code__.co_filename
|
||||
|
||||
def test_module_without_source(self):
|
||||
target = "another_module.py"
|
||||
py_compile.compile(self.file_name, dfile=target)
|
||||
pyc_file = self.file_name + 'c'
|
||||
py_compile.compile(self.file_name, pyc_file, dfile=target)
|
||||
os.remove(self.file_name)
|
||||
pyc_file = make_legacy_pyc(self.file_name)
|
||||
importlib.invalidate_caches()
|
||||
mod = self.import_module()
|
||||
self.assertEqual(mod.module_filename, pyc_file)
|
||||
@@ -1425,8 +1471,9 @@ func_filename = func.__code__.co_filename
|
||||
self.assertEqual(mod.func_filename, target)
|
||||
|
||||
def test_foreign_code(self):
|
||||
py_compile.compile(self.file_name)
|
||||
with open(self.compiled_name, "rb") as f:
|
||||
compiled_name = self.compiled_name or (self.file_name + 'c')
|
||||
py_compile.compile(self.file_name, compiled_name)
|
||||
with open(compiled_name, "rb") as f:
|
||||
header = f.read(16)
|
||||
code = marshal.load(f)
|
||||
constants = list(code.co_consts)
|
||||
@@ -1434,9 +1481,11 @@ func_filename = func.__code__.co_filename
|
||||
pos = constants.index(1000)
|
||||
constants[pos] = foreign_code
|
||||
code = code.replace(co_consts=tuple(constants))
|
||||
with open(self.compiled_name, "wb") as f:
|
||||
with open(compiled_name, "wb") as f:
|
||||
f.write(header)
|
||||
marshal.dump(code, f)
|
||||
if not self.compiled_name:
|
||||
os.remove(self.file_name)
|
||||
mod = self.import_module()
|
||||
self.assertEqual(mod.constant.co_filename, foreign_code.co_filename)
|
||||
|
||||
@@ -1621,6 +1670,11 @@ class PycacheTests(unittest.TestCase):
|
||||
unlink(self.source)
|
||||
|
||||
def setUp(self):
|
||||
# These tests assume bytecode is written next to the source in a
|
||||
# local __pycache__ directory, so neutralize any pycache prefix (e.g.
|
||||
# when the test suite is run with PYTHONPYCACHEPREFIX set).
|
||||
self._orig_pycache_prefix = sys.pycache_prefix
|
||||
sys.pycache_prefix = None
|
||||
self.source = TESTFN + '.py'
|
||||
self._clean()
|
||||
with open(self.source, 'w', encoding='utf-8') as fp:
|
||||
@@ -1632,6 +1686,7 @@ class PycacheTests(unittest.TestCase):
|
||||
assert sys.path[0] == os.curdir, 'Unexpected sys.path[0]'
|
||||
del sys.path[0]
|
||||
self._clean()
|
||||
sys.pycache_prefix = self._orig_pycache_prefix
|
||||
|
||||
@skip_if_dont_write_bytecode
|
||||
def test_import_pyc_path(self):
|
||||
@@ -1691,78 +1746,6 @@ class PycacheTests(unittest.TestCase):
|
||||
finally:
|
||||
os.remove(pyc_file)
|
||||
|
||||
def test___cached__(self):
|
||||
# Modules now also have an __cached__ that points to the pyc file.
|
||||
m = __import__(TESTFN)
|
||||
pyc_file = importlib.util.cache_from_source(TESTFN + '.py')
|
||||
self.assertEqual(m.__cached__, os.path.join(os.getcwd(), pyc_file))
|
||||
|
||||
@skip_if_dont_write_bytecode
|
||||
def test___cached___legacy_pyc(self):
|
||||
# Like test___cached__() except that for backward compatibility,
|
||||
# when the pyc file lives where the py file would have been (and named
|
||||
# without the tag), it is importable. The __cached__ of the imported
|
||||
# module is the pyc location.
|
||||
__import__(TESTFN)
|
||||
# pyc_file gets removed in _clean() via tearDown().
|
||||
pyc_file = make_legacy_pyc(self.source)
|
||||
os.remove(self.source)
|
||||
unload(TESTFN)
|
||||
importlib.invalidate_caches()
|
||||
m = __import__(TESTFN)
|
||||
self.assertEqual(m.__cached__,
|
||||
os.path.join(os.getcwd(), os.path.relpath(pyc_file)))
|
||||
|
||||
@skip_if_dont_write_bytecode
|
||||
def test_package___cached__(self):
|
||||
# Like test___cached__ but for packages.
|
||||
def cleanup():
|
||||
rmtree('pep3147')
|
||||
unload('pep3147.foo')
|
||||
unload('pep3147')
|
||||
os.mkdir('pep3147')
|
||||
self.addCleanup(cleanup)
|
||||
# Touch the __init__.py
|
||||
with open(os.path.join('pep3147', '__init__.py'), 'wb'):
|
||||
pass
|
||||
with open(os.path.join('pep3147', 'foo.py'), 'wb'):
|
||||
pass
|
||||
importlib.invalidate_caches()
|
||||
m = __import__('pep3147.foo')
|
||||
init_pyc = importlib.util.cache_from_source(
|
||||
os.path.join('pep3147', '__init__.py'))
|
||||
self.assertEqual(m.__cached__, os.path.join(os.getcwd(), init_pyc))
|
||||
foo_pyc = importlib.util.cache_from_source(os.path.join('pep3147', 'foo.py'))
|
||||
self.assertEqual(sys.modules['pep3147.foo'].__cached__,
|
||||
os.path.join(os.getcwd(), foo_pyc))
|
||||
|
||||
def test_package___cached___from_pyc(self):
|
||||
# Like test___cached__ but ensuring __cached__ when imported from a
|
||||
# PEP 3147 pyc file.
|
||||
def cleanup():
|
||||
rmtree('pep3147')
|
||||
unload('pep3147.foo')
|
||||
unload('pep3147')
|
||||
os.mkdir('pep3147')
|
||||
self.addCleanup(cleanup)
|
||||
# Touch the __init__.py
|
||||
with open(os.path.join('pep3147', '__init__.py'), 'wb'):
|
||||
pass
|
||||
with open(os.path.join('pep3147', 'foo.py'), 'wb'):
|
||||
pass
|
||||
importlib.invalidate_caches()
|
||||
m = __import__('pep3147.foo')
|
||||
unload('pep3147.foo')
|
||||
unload('pep3147')
|
||||
importlib.invalidate_caches()
|
||||
m = __import__('pep3147.foo')
|
||||
init_pyc = importlib.util.cache_from_source(
|
||||
os.path.join('pep3147', '__init__.py'))
|
||||
self.assertEqual(m.__cached__, os.path.join(os.getcwd(), init_pyc))
|
||||
foo_pyc = importlib.util.cache_from_source(os.path.join('pep3147', 'foo.py'))
|
||||
self.assertEqual(sys.modules['pep3147.foo'].__cached__,
|
||||
os.path.join(os.getcwd(), foo_pyc))
|
||||
|
||||
def test_recompute_pyc_same_second(self):
|
||||
# Even when the source file doesn't change timestamp, a change in
|
||||
# source size is enough to trigger recomputation of the pyc file.
|
||||
@@ -2031,10 +2014,6 @@ class ImportTracebackTests(unittest.TestCase):
|
||||
# away from the traceback.
|
||||
self.create_module("foo", "")
|
||||
importlib = sys.modules['_frozen_importlib_external']
|
||||
if 'load_module' in vars(importlib.SourceLoader):
|
||||
old_exec_module = importlib.SourceLoader.exec_module
|
||||
else:
|
||||
old_exec_module = None
|
||||
try:
|
||||
def exec_module(*args):
|
||||
1/0
|
||||
@@ -2047,10 +2026,7 @@ class ImportTracebackTests(unittest.TestCase):
|
||||
self.fail("ZeroDivisionError should have been raised")
|
||||
self.assert_traceback(tb, [__file__, '<frozen importlib', __file__])
|
||||
finally:
|
||||
if old_exec_module is None:
|
||||
del importlib.SourceLoader.exec_module
|
||||
else:
|
||||
importlib.SourceLoader.exec_module = old_exec_module
|
||||
del importlib.SourceLoader.exec_module
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; subprocess fails on Windows
|
||||
@unittest.skipUnless(TESTFN_UNENCODABLE, 'need TESTFN_UNENCODABLE')
|
||||
@@ -2479,6 +2455,21 @@ class SubinterpImportTests(unittest.TestCase):
|
||||
self.check_compatible_here(
|
||||
modname, filename, strict=False, isolated=False)
|
||||
|
||||
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
|
||||
def test_testmultiphase_exec_multiple(self):
|
||||
modname = '_testmultiphase_exec_multiple'
|
||||
filename = _testmultiphase.__file__
|
||||
module = import_extension_from_file(modname, filename,
|
||||
put_in_sys_modules=False)
|
||||
# All three exec's were called.
|
||||
self.assertEqual(module.a, 1)
|
||||
self.assertEqual(module.b, 2)
|
||||
self.assertEqual(module.c, 3)
|
||||
# They were called in order.
|
||||
keys = list(module.__dict__)
|
||||
self.assertLess(keys.index('a'), keys.index('b'))
|
||||
self.assertLess(keys.index('b'), keys.index('c'))
|
||||
|
||||
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
|
||||
def test_python_compat(self):
|
||||
module = 'threading'
|
||||
@@ -2552,8 +2543,9 @@ class SubinterpImportTests(unittest.TestCase):
|
||||
excsnap = _interpreters.run_string(interpid, script)
|
||||
self.assertIsNot(excsnap, None)
|
||||
|
||||
@requires_subinterpreters
|
||||
@unittest.skip("TODO: RUSTPYTHON; test requires _testsinglephase module")
|
||||
@cpython_only
|
||||
@requires_subinterpreters
|
||||
def test_pyinit_function_raises_exception(self):
|
||||
# gh-144601: PyInit functions that raised exceptions would cause a
|
||||
# crash when imported from a subinterpreter.
|
||||
@@ -2783,9 +2775,6 @@ class SinglephaseInitTests(unittest.TestCase):
|
||||
# This is essentially copied from the old imp module.
|
||||
from importlib._bootstrap import _load
|
||||
loader = self.LOADER(name, path)
|
||||
|
||||
# Issue bpo-24748: Skip the sys.modules check in _load_module_shim;
|
||||
# always load new extension.
|
||||
spec = importlib.util.spec_from_file_location(name, path,
|
||||
loader=loader)
|
||||
return _load(spec)
|
||||
@@ -3268,6 +3257,7 @@ class SinglephaseInitTests(unittest.TestCase):
|
||||
# * m_copy was copied from interp2 (was from interp1)
|
||||
# * module's global state was updated, not reset
|
||||
|
||||
@unittest.skip("gh-131229: This is suddenly very flaky")
|
||||
@no_rerun(reason="rerun not possible; module state is never cleared (see gh-102251)")
|
||||
@requires_subinterpreters
|
||||
def test_basic_multiple_interpreters_deleted_no_reset(self):
|
||||
@@ -3403,6 +3393,158 @@ class SinglephaseInitTests(unittest.TestCase):
|
||||
# * module's global state was initialized, not reset
|
||||
|
||||
|
||||
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
|
||||
class ModexportTests(unittest.TestCase):
|
||||
def test_from_modexport(self):
|
||||
modname = '_test_from_modexport'
|
||||
filename = _testmultiphase.__file__
|
||||
module = import_extension_from_file(modname, filename,
|
||||
put_in_sys_modules=False)
|
||||
|
||||
self.assertEqual(module.__name__, modname)
|
||||
|
||||
@requires_subinterpreters
|
||||
def test_from_modexport_gil_used(self):
|
||||
# Test that a module with Py_MOD_GIL_USED (re-)enables the GIL.
|
||||
# Do this in a new interpreter to avoid interfering with global state.
|
||||
modname = '_test_from_modexport_gil_used'
|
||||
filename = _testmultiphase.__file__
|
||||
interp = concurrent.interpreters.create()
|
||||
self.addCleanup(interp.close)
|
||||
queue = concurrent.interpreters.create_queue()
|
||||
interp.prepare_main(
|
||||
modname=modname,
|
||||
filename=filename,
|
||||
queue=queue,
|
||||
)
|
||||
enabled_before = sys._is_gil_enabled()
|
||||
interp.exec(f"""if True:
|
||||
import sys
|
||||
from test.support.warnings_helper import check_warnings
|
||||
from {__name__} import import_extension_from_file
|
||||
with check_warnings((".*GIL..has been enabled.*", RuntimeWarning),
|
||||
quiet=True):
|
||||
module = import_extension_from_file(modname, filename,
|
||||
put_in_sys_modules=False)
|
||||
queue.put(module.__name__)
|
||||
queue.put(sys._is_gil_enabled())
|
||||
""")
|
||||
|
||||
self.assertEqual(queue.get(), modname)
|
||||
self.assertEqual(queue.get(), True)
|
||||
self.assertTrue(queue.empty())
|
||||
|
||||
self.assertEqual(enabled_before, sys._is_gil_enabled())
|
||||
|
||||
def test_from_modexport_null(self):
|
||||
modname = '_test_from_modexport_null'
|
||||
filename = _testmultiphase.__file__
|
||||
with self.assertRaises(SystemError):
|
||||
import_extension_from_file(modname, filename,
|
||||
put_in_sys_modules=False)
|
||||
|
||||
def test_from_modexport_exception(self):
|
||||
modname = '_test_from_modexport_exception'
|
||||
filename = _testmultiphase.__file__
|
||||
with self.assertRaises(ValueError):
|
||||
import_extension_from_file(modname, filename,
|
||||
put_in_sys_modules=False)
|
||||
|
||||
def test_from_modexport_create_nonmodule(self):
|
||||
modname = '_test_from_modexport_create_nonmodule'
|
||||
filename = _testmultiphase.__file__
|
||||
module = import_extension_from_file(modname, filename,
|
||||
put_in_sys_modules=False)
|
||||
self.assertIsInstance(module, str)
|
||||
|
||||
@requires_subinterpreters
|
||||
def test_from_modexport_create_nonmodule_gil_used(self):
|
||||
# Test that a module with Py_MOD_GIL_USED (re-)enables the GIL.
|
||||
# Do this in a new interpreter to avoid interfering with global state.
|
||||
modname = '_test_from_modexport_create_nonmodule_gil_used'
|
||||
filename = _testmultiphase.__file__
|
||||
interp = concurrent.interpreters.create()
|
||||
self.addCleanup(interp.close)
|
||||
queue = concurrent.interpreters.create_queue()
|
||||
interp.prepare_main(
|
||||
modname=modname,
|
||||
filename=filename,
|
||||
queue=queue,
|
||||
)
|
||||
enabled_before = sys._is_gil_enabled()
|
||||
interp.exec(f"""if True:
|
||||
import sys
|
||||
from test.support.warnings_helper import check_warnings
|
||||
from {__name__} import import_extension_from_file
|
||||
with check_warnings((".*GIL..has been enabled.*", RuntimeWarning),
|
||||
quiet=True):
|
||||
module = import_extension_from_file(modname, filename,
|
||||
put_in_sys_modules=False)
|
||||
queue.put(module)
|
||||
queue.put(sys._is_gil_enabled())
|
||||
""")
|
||||
|
||||
self.assertIsInstance(queue.get(), str)
|
||||
self.assertEqual(queue.get(), True)
|
||||
self.assertTrue(queue.empty())
|
||||
|
||||
self.assertEqual(enabled_before, sys._is_gil_enabled())
|
||||
|
||||
def test_from_modexport_smoke(self):
|
||||
# General positive test for sundry features
|
||||
# (PyModule_FromSlotsAndSpec tests exercise these more carefully)
|
||||
modname = '_test_from_modexport_smoke'
|
||||
filename = _testmultiphase.__file__
|
||||
module = import_extension_from_file(modname, filename,
|
||||
put_in_sys_modules=False)
|
||||
self.assertEqual(module.__doc__, "the expected docstring")
|
||||
self.assertEqual(module.number, 147)
|
||||
self.assertEqual(module.get_state_int(), 258)
|
||||
self.assertGreater(module.get_test_token(), 0)
|
||||
|
||||
def test_from_modexport_smoke_token(self):
|
||||
_testcapi = import_module("_testcapi")
|
||||
|
||||
modname = '_test_from_modexport_smoke'
|
||||
filename = _testmultiphase.__file__
|
||||
module = import_extension_from_file(modname, filename,
|
||||
put_in_sys_modules=False)
|
||||
token = module.get_test_token()
|
||||
self.assertEqual(_testcapi.pymodule_get_token(module), token)
|
||||
|
||||
tp = module.Example
|
||||
self.assertEqual(_testcapi.pytype_getmodulebytoken(tp, token), module)
|
||||
class Sub(tp):
|
||||
pass
|
||||
self.assertEqual(_testcapi.pytype_getmodulebytoken(Sub, token), module)
|
||||
|
||||
def test_from_modexport_empty_slots(self):
|
||||
# Module to test that Py_mod_abi is mandatory for PyModExport
|
||||
modname = '_test_from_modexport_empty_slots'
|
||||
filename = _testmultiphase.__file__
|
||||
with self.assertRaises(SystemError):
|
||||
import_extension_from_file(
|
||||
modname, filename, put_in_sys_modules=False)
|
||||
|
||||
@requires_gil_enabled("this module re-enables GIL")
|
||||
def test_from_modexport_minimal_slots(self):
|
||||
# Module to test that:
|
||||
# - no slots except Py_mod_abi is mandatory for PyModExport
|
||||
# - the slots array is used as the default token
|
||||
modname = '_test_from_modexport_minimal_slots'
|
||||
filename = _testmultiphase.__file__
|
||||
module = import_extension_from_file(
|
||||
modname, filename, put_in_sys_modules=False)
|
||||
|
||||
self.assertEqual(module.__name__, modname)
|
||||
self.assertEqual(module.__doc__, None)
|
||||
|
||||
_testcapi = import_module("_testcapi")
|
||||
smoke_mod = import_extension_from_file(
|
||||
'_test_from_modexport_smoke', filename, put_in_sys_modules=False)
|
||||
self.assertEqual(_testcapi.pymodule_get_token(module),
|
||||
smoke_mod.get_modexport_minimal_slots())
|
||||
|
||||
@cpython_only
|
||||
class TestMagicNumber(unittest.TestCase):
|
||||
def test_magic_number_endianness(self):
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import sys
|
||||
|
||||
class MyMod(object):
|
||||
__slots__ = ['__builtins__', '__cached__', '__doc__',
|
||||
'__file__', '__loader__', '__name__',
|
||||
'__package__', '__path__', '__spec__']
|
||||
__slots__ = ['__builtins__', '__doc__', '__file__',
|
||||
'__loader__', '__name__', '__package__',
|
||||
'__path__', '__spec__']
|
||||
def __init__(self):
|
||||
for attr in self.__slots__:
|
||||
setattr(self, attr, globals()[attr])
|
||||
|
||||
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
import contextlib
|
||||
|
||||
|
||||
# from jaraco.context 4.3
|
||||
class suppress(contextlib.suppress, contextlib.ContextDecorator):
|
||||
"""
|
||||
A version of contextlib.suppress with decorator support.
|
||||
|
||||
>>> @suppress(KeyError)
|
||||
... def key_error():
|
||||
... {}['']
|
||||
>>> key_error()
|
||||
"""
|
||||
Vendored
-109
@@ -1,109 +0,0 @@
|
||||
# from jaraco.path 3.5
|
||||
|
||||
import functools
|
||||
import pathlib
|
||||
from typing import Dict, Union
|
||||
|
||||
try:
|
||||
from typing import Protocol, runtime_checkable
|
||||
except ImportError: # pragma: no cover
|
||||
# Python 3.7
|
||||
from typing_extensions import Protocol, runtime_checkable # type: ignore
|
||||
|
||||
|
||||
FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TreeMaker(Protocol):
|
||||
def __truediv__(self, *args, **kwargs):
|
||||
... # pragma: no cover
|
||||
|
||||
def mkdir(self, **kwargs):
|
||||
... # pragma: no cover
|
||||
|
||||
def write_text(self, content, **kwargs):
|
||||
... # pragma: no cover
|
||||
|
||||
def write_bytes(self, content):
|
||||
... # pragma: no cover
|
||||
|
||||
|
||||
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
|
||||
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore
|
||||
|
||||
|
||||
def build(
|
||||
spec: FilesSpec,
|
||||
prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore
|
||||
):
|
||||
"""
|
||||
Build a set of files/directories, as described by the spec.
|
||||
|
||||
Each key represents a pathname, and the value represents
|
||||
the content. Content may be a nested directory.
|
||||
|
||||
>>> spec = {
|
||||
... 'README.txt': "A README file",
|
||||
... "foo": {
|
||||
... "__init__.py": "",
|
||||
... "bar": {
|
||||
... "__init__.py": "",
|
||||
... },
|
||||
... "baz.py": "# Some code",
|
||||
... }
|
||||
... }
|
||||
>>> target = getfixture('tmp_path')
|
||||
>>> build(spec, target)
|
||||
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
|
||||
'# Some code'
|
||||
"""
|
||||
for name, contents in spec.items():
|
||||
create(contents, _ensure_tree_maker(prefix) / name)
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
def create(content: Union[str, bytes, FilesSpec], path):
|
||||
path.mkdir(exist_ok=True)
|
||||
build(content, prefix=path) # type: ignore
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: bytes, path):
|
||||
path.write_bytes(content)
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: str, path):
|
||||
path.write_text(content, encoding='utf-8')
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: str, path):
|
||||
path.write_text(content, encoding='utf-8')
|
||||
|
||||
|
||||
class Recording:
|
||||
"""
|
||||
A TreeMaker object that records everything that would be written.
|
||||
|
||||
>>> r = Recording()
|
||||
>>> build({'foo': {'foo1.txt': 'yes'}, 'bar.txt': 'abc'}, r)
|
||||
>>> r.record
|
||||
['foo/foo1.txt', 'bar.txt']
|
||||
"""
|
||||
|
||||
def __init__(self, loc=pathlib.PurePosixPath(), record=None):
|
||||
self.loc = loc
|
||||
self.record = record if record is not None else []
|
||||
|
||||
def __truediv__(self, other):
|
||||
return Recording(self.loc / other, self.record)
|
||||
|
||||
def write_text(self, content, **kwargs):
|
||||
self.record.append(str(self.loc))
|
||||
|
||||
write_bytes = write_text
|
||||
|
||||
def mkdir(self, **kwargs):
|
||||
return
|
||||
Vendored
-16
@@ -40,22 +40,6 @@ class FinderTests(metaclass=abc.ABCMeta):
|
||||
|
||||
class LoaderTests(metaclass=abc.ABCMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def test_module(self):
|
||||
"""A module should load without issue.
|
||||
|
||||
After the loader returns the module should be in sys.modules.
|
||||
|
||||
Attributes to verify:
|
||||
|
||||
* __file__
|
||||
* __loader__
|
||||
* __name__
|
||||
* No __path__
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def test_package(self):
|
||||
"""Loading a package should work.
|
||||
|
||||
-64
@@ -7,70 +7,6 @@ import types
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
@unittest.skipIf(util.BUILTINS.good_name is None, 'no reasonable builtin module')
|
||||
class LoaderTests(abc.LoaderTests):
|
||||
|
||||
"""Test load_module() for built-in modules."""
|
||||
|
||||
def setUp(self):
|
||||
self.verification = {'__name__': 'errno', '__package__': '',
|
||||
'__loader__': self.machinery.BuiltinImporter}
|
||||
|
||||
def verify(self, module):
|
||||
"""Verify that the module matches against what it should have."""
|
||||
self.assertIsInstance(module, types.ModuleType)
|
||||
for attr, value in self.verification.items():
|
||||
self.assertEqual(getattr(module, attr), value)
|
||||
self.assertIn(module.__name__, sys.modules)
|
||||
|
||||
def load_module(self, name):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
return self.machinery.BuiltinImporter.load_module(name)
|
||||
|
||||
def test_module(self):
|
||||
# Common case.
|
||||
with util.uncache(util.BUILTINS.good_name):
|
||||
module = self.load_module(util.BUILTINS.good_name)
|
||||
self.verify(module)
|
||||
|
||||
# Built-in modules cannot be a package.
|
||||
test_package = test_lacking_parent = None
|
||||
|
||||
# No way to force an import failure.
|
||||
test_state_after_failure = None
|
||||
|
||||
def test_module_reuse(self):
|
||||
# Test that the same module is used in a reload.
|
||||
with util.uncache(util.BUILTINS.good_name):
|
||||
module1 = self.load_module(util.BUILTINS.good_name)
|
||||
module2 = self.load_module(util.BUILTINS.good_name)
|
||||
self.assertIs(module1, module2)
|
||||
|
||||
def test_unloadable(self):
|
||||
name = 'dssdsdfff'
|
||||
assert name not in sys.builtin_module_names
|
||||
with self.assertRaises(ImportError) as cm:
|
||||
self.load_module(name)
|
||||
self.assertEqual(cm.exception.name, name)
|
||||
|
||||
def test_already_imported(self):
|
||||
# Using the name of a module already imported but not a built-in should
|
||||
# still fail.
|
||||
module_name = 'builtin_reload_test'
|
||||
assert module_name not in sys.builtin_module_names
|
||||
with util.uncache(module_name):
|
||||
module = types.ModuleType(module_name)
|
||||
sys.modules[module_name] = module
|
||||
with self.assertRaises(ImportError) as cm:
|
||||
self.load_module(module_name)
|
||||
self.assertEqual(cm.exception.name, module_name)
|
||||
|
||||
|
||||
(Frozen_LoaderTests,
|
||||
Source_LoaderTests
|
||||
) = util.test_both(LoaderTests, machinery=machinery)
|
||||
|
||||
|
||||
@unittest.skipIf(util.BUILTINS.good_name is None, 'no reasonable builtin module')
|
||||
class InspectLoaderTests:
|
||||
|
||||
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
+24
-1
@@ -1,10 +1,11 @@
|
||||
from test.support import is_apple_mobile
|
||||
from test.support import is_apple_mobile, Py_GIL_DISABLED
|
||||
from test.test_importlib import abc, util
|
||||
|
||||
machinery = util.import_importlib('importlib.machinery')
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
|
||||
class FinderTests(abc.FinderTests):
|
||||
@@ -59,6 +60,28 @@ class FinderTests(abc.FinderTests):
|
||||
def test_failure(self):
|
||||
self.assertIsNone(self.find_spec('asdfjkl;'))
|
||||
|
||||
def test_abi3_extension_suffixes(self):
|
||||
suffixes = self.machinery.EXTENSION_SUFFIXES
|
||||
platform = sysconfig.get_config_var("SOABI_PLATFORM")
|
||||
if 'win32' in sys.platform:
|
||||
# Either "_d.pyd" or ".pyd" must be in suffixes
|
||||
self.assertTrue({"_d.pyd", ".pyd"}.intersection(suffixes))
|
||||
elif 'cygwin' in sys.platform:
|
||||
pass
|
||||
else:
|
||||
if Py_GIL_DISABLED:
|
||||
self.assertNotIn(".abi3.so", suffixes)
|
||||
else:
|
||||
self.assertIn(".abi3.so", suffixes)
|
||||
self.assertIn(".abi3t.so", suffixes)
|
||||
|
||||
if platform:
|
||||
if Py_GIL_DISABLED:
|
||||
self.assertNotIn(f".abi3-{platform}.so", suffixes)
|
||||
else:
|
||||
self.assertIn(f".abi3-{platform}.so", suffixes)
|
||||
self.assertIn(f".abi3t-{platform}.so", suffixes)
|
||||
|
||||
|
||||
(Frozen_FinderTests,
|
||||
Source_FinderTests
|
||||
|
||||
-114
@@ -35,11 +35,6 @@ class LoaderTests:
|
||||
|
||||
self.loader = self.LoaderClass(util.EXTENSIONS.name, util.EXTENSIONS.file_path)
|
||||
|
||||
def load_module(self, fullname):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
return self.loader.load_module(fullname)
|
||||
|
||||
def test_equality(self):
|
||||
other = self.LoaderClass(util.EXTENSIONS.name, util.EXTENSIONS.file_path)
|
||||
self.assertEqual(self.loader, other)
|
||||
@@ -48,25 +43,6 @@ class LoaderTests:
|
||||
other = self.LoaderClass('_' + util.EXTENSIONS.name, util.EXTENSIONS.file_path)
|
||||
self.assertNotEqual(self.loader, other)
|
||||
|
||||
def test_load_module_API(self):
|
||||
# Test the default argument for load_module().
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
self.loader.load_module()
|
||||
self.loader.load_module(None)
|
||||
with self.assertRaises(ImportError):
|
||||
self.load_module('XXX')
|
||||
|
||||
def test_module(self):
|
||||
with util.uncache(util.EXTENSIONS.name):
|
||||
module = self.load_module(util.EXTENSIONS.name)
|
||||
for attr, value in [('__name__', util.EXTENSIONS.name),
|
||||
('__file__', util.EXTENSIONS.file_path),
|
||||
('__package__', '')]:
|
||||
self.assertEqual(getattr(module, attr), value)
|
||||
self.assertIn(util.EXTENSIONS.name, sys.modules)
|
||||
self.assertIsInstance(module.__loader__, self.LoaderClass)
|
||||
|
||||
# No extension module as __init__ available for testing.
|
||||
test_package = None
|
||||
|
||||
@@ -76,18 +52,6 @@ class LoaderTests:
|
||||
# No easy way to trigger a failure after a successful import.
|
||||
test_state_after_failure = None
|
||||
|
||||
def test_unloadable(self):
|
||||
name = 'asdfjkl;'
|
||||
with self.assertRaises(ImportError) as cm:
|
||||
self.load_module(name)
|
||||
self.assertEqual(cm.exception.name, name)
|
||||
|
||||
def test_module_reuse(self):
|
||||
with util.uncache(util.EXTENSIONS.name):
|
||||
module1 = self.load_module(util.EXTENSIONS.name)
|
||||
module2 = self.load_module(util.EXTENSIONS.name)
|
||||
self.assertIs(module1, module2)
|
||||
|
||||
def test_is_package(self):
|
||||
self.assertFalse(self.loader.is_package(util.EXTENSIONS.name))
|
||||
for suffix in self.machinery.EXTENSION_SUFFIXES:
|
||||
@@ -126,11 +90,6 @@ class SinglePhaseExtensionModuleTests(abc.LoaderTests):
|
||||
|
||||
self.loader = self.LoaderClass(self.name, self.spec.origin)
|
||||
|
||||
def load_module(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
return self.loader.load_module(self.name)
|
||||
|
||||
def load_module_by_name(self, fullname):
|
||||
# Load a module from the test extension by name.
|
||||
origin = self.spec.origin
|
||||
@@ -140,19 +99,6 @@ class SinglePhaseExtensionModuleTests(abc.LoaderTests):
|
||||
loader.exec_module(module)
|
||||
return module
|
||||
|
||||
def test_module(self):
|
||||
# Test loading an extension module.
|
||||
with util.uncache(self.name):
|
||||
module = self.load_module()
|
||||
for attr, value in [('__name__', self.name),
|
||||
('__file__', self.spec.origin),
|
||||
('__package__', '')]:
|
||||
self.assertEqual(getattr(module, attr), value)
|
||||
with self.assertRaises(AttributeError):
|
||||
module.__path__
|
||||
self.assertIs(module, sys.modules[self.name])
|
||||
self.assertIsInstance(module.__loader__, self.LoaderClass)
|
||||
|
||||
# No extension module as __init__ available for testing.
|
||||
test_package = None
|
||||
|
||||
@@ -213,12 +159,6 @@ class MultiPhaseExtensionModuleTests(abc.LoaderTests):
|
||||
assert self.spec
|
||||
self.loader = self.LoaderClass(self.name, self.spec.origin)
|
||||
|
||||
def load_module(self):
|
||||
# Load the module from the test extension.
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
return self.loader.load_module(self.name)
|
||||
|
||||
def load_module_by_name(self, fullname):
|
||||
# Load a module from the test extension by name.
|
||||
origin = self.spec.origin
|
||||
@@ -237,60 +177,6 @@ class MultiPhaseExtensionModuleTests(abc.LoaderTests):
|
||||
# Handling failure on reload is the up to the module.
|
||||
test_state_after_failure = None
|
||||
|
||||
def test_module(self):
|
||||
# Test loading an extension module.
|
||||
with util.uncache(self.name):
|
||||
module = self.load_module()
|
||||
for attr, value in [('__name__', self.name),
|
||||
('__file__', self.spec.origin),
|
||||
('__package__', '')]:
|
||||
self.assertEqual(getattr(module, attr), value)
|
||||
with self.assertRaises(AttributeError):
|
||||
module.__path__
|
||||
self.assertIs(module, sys.modules[self.name])
|
||||
self.assertIsInstance(module.__loader__, self.LoaderClass)
|
||||
|
||||
def test_functionality(self):
|
||||
# Test basic functionality of stuff defined in an extension module.
|
||||
with util.uncache(self.name):
|
||||
module = self.load_module()
|
||||
self.assertIsInstance(module, types.ModuleType)
|
||||
ex = module.Example()
|
||||
self.assertEqual(ex.demo('abcd'), 'abcd')
|
||||
self.assertEqual(ex.demo(), None)
|
||||
with self.assertRaises(AttributeError):
|
||||
ex.abc
|
||||
ex.abc = 0
|
||||
self.assertEqual(ex.abc, 0)
|
||||
self.assertEqual(module.foo(9, 9), 18)
|
||||
self.assertIsInstance(module.Str(), str)
|
||||
self.assertEqual(module.Str(1) + '23', '123')
|
||||
with self.assertRaises(module.error):
|
||||
raise module.error()
|
||||
self.assertEqual(module.int_const, 1969)
|
||||
self.assertEqual(module.str_const, 'something different')
|
||||
|
||||
def test_reload(self):
|
||||
# Test that reload didn't re-set the module's attributes.
|
||||
with util.uncache(self.name):
|
||||
module = self.load_module()
|
||||
ex_class = module.Example
|
||||
importlib.reload(module)
|
||||
self.assertIs(ex_class, module.Example)
|
||||
|
||||
def test_try_registration(self):
|
||||
# Assert that the PyState_{Find,Add,Remove}Module C API doesn't work.
|
||||
with util.uncache(self.name):
|
||||
module = self.load_module()
|
||||
with self.subTest('PyState_FindModule'):
|
||||
self.assertEqual(module.call_state_registration_func(0), None)
|
||||
with self.subTest('PyState_AddModule'):
|
||||
with self.assertRaises(SystemError):
|
||||
module.call_state_registration_func(1)
|
||||
with self.subTest('PyState_RemoveModule'):
|
||||
with self.assertRaises(SystemError):
|
||||
module.call_state_registration_func(2)
|
||||
|
||||
def test_load_submodule(self):
|
||||
# Test loading a simulated submodule.
|
||||
module = self.load_module_by_name('pkg.' + self.name)
|
||||
|
||||
Vendored
-382
@@ -1,382 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import copy
|
||||
import shutil
|
||||
import pathlib
|
||||
import tempfile
|
||||
import textwrap
|
||||
import functools
|
||||
import contextlib
|
||||
|
||||
from test.support.os_helper import FS_NONASCII
|
||||
from test.support import requires_zlib
|
||||
|
||||
from . import _path
|
||||
from ._path import FilesSpec
|
||||
|
||||
|
||||
try:
|
||||
from importlib import resources # type: ignore
|
||||
|
||||
getattr(resources, 'files')
|
||||
getattr(resources, 'as_file')
|
||||
except (ImportError, AttributeError):
|
||||
import importlib_resources as resources # type: ignore
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def tempdir():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
yield pathlib.Path(tmpdir)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def save_cwd():
|
||||
orig = os.getcwd()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.chdir(orig)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def tempdir_as_cwd():
|
||||
with tempdir() as tmp:
|
||||
with save_cwd():
|
||||
os.chdir(str(tmp))
|
||||
yield tmp
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def install_finder(finder):
|
||||
sys.meta_path.append(finder)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.meta_path.remove(finder)
|
||||
|
||||
|
||||
class Fixtures:
|
||||
def setUp(self):
|
||||
self.fixtures = contextlib.ExitStack()
|
||||
self.addCleanup(self.fixtures.close)
|
||||
|
||||
|
||||
class SiteDir(Fixtures):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.site_dir = self.fixtures.enter_context(tempdir())
|
||||
|
||||
|
||||
class OnSysPath(Fixtures):
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def add_sys_path(dir):
|
||||
sys.path[:0] = [str(dir)]
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.path.remove(str(dir))
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.fixtures.enter_context(self.add_sys_path(self.site_dir))
|
||||
|
||||
|
||||
class DistInfoPkg(OnSysPath, SiteDir):
|
||||
files: FilesSpec = {
|
||||
"distinfo_pkg-1.0.0.dist-info": {
|
||||
"METADATA": """
|
||||
Name: distinfo-pkg
|
||||
Author: Steven Ma
|
||||
Version: 1.0.0
|
||||
Requires-Dist: wheel >= 1.0
|
||||
Requires-Dist: pytest; extra == 'test'
|
||||
Keywords: sample package
|
||||
|
||||
Once upon a time
|
||||
There was a distinfo pkg
|
||||
""",
|
||||
"RECORD": "mod.py,sha256=abc,20\n",
|
||||
"entry_points.txt": """
|
||||
[entries]
|
||||
main = mod:main
|
||||
ns:sub = mod:main
|
||||
""",
|
||||
},
|
||||
"mod.py": """
|
||||
def main():
|
||||
print("hello world")
|
||||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(DistInfoPkg.files, self.site_dir)
|
||||
|
||||
def make_uppercase(self):
|
||||
"""
|
||||
Rewrite metadata with everything uppercase.
|
||||
"""
|
||||
shutil.rmtree(self.site_dir / "distinfo_pkg-1.0.0.dist-info")
|
||||
files = copy.deepcopy(DistInfoPkg.files)
|
||||
info = files["distinfo_pkg-1.0.0.dist-info"]
|
||||
info["METADATA"] = info["METADATA"].upper()
|
||||
build_files(files, self.site_dir)
|
||||
|
||||
|
||||
class DistInfoPkgWithDot(OnSysPath, SiteDir):
|
||||
files: FilesSpec = {
|
||||
"pkg_dot-1.0.0.dist-info": {
|
||||
"METADATA": """
|
||||
Name: pkg.dot
|
||||
Version: 1.0.0
|
||||
""",
|
||||
},
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(DistInfoPkgWithDot.files, self.site_dir)
|
||||
|
||||
|
||||
class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
|
||||
files: FilesSpec = {
|
||||
"pkg.dot-1.0.0.dist-info": {
|
||||
"METADATA": """
|
||||
Name: pkg.dot
|
||||
Version: 1.0.0
|
||||
""",
|
||||
},
|
||||
"pkg.lot.egg-info": {
|
||||
"METADATA": """
|
||||
Name: pkg.lot
|
||||
Version: 1.0.0
|
||||
""",
|
||||
},
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(DistInfoPkgWithDotLegacy.files, self.site_dir)
|
||||
|
||||
|
||||
class DistInfoPkgOffPath(SiteDir):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(DistInfoPkg.files, self.site_dir)
|
||||
|
||||
|
||||
class EggInfoPkg(OnSysPath, SiteDir):
|
||||
files: FilesSpec = {
|
||||
"egginfo_pkg.egg-info": {
|
||||
"PKG-INFO": """
|
||||
Name: egginfo-pkg
|
||||
Author: Steven Ma
|
||||
License: Unknown
|
||||
Version: 1.0.0
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
Keywords: sample package
|
||||
Description: Once upon a time
|
||||
There was an egginfo package
|
||||
""",
|
||||
"SOURCES.txt": """
|
||||
mod.py
|
||||
egginfo_pkg.egg-info/top_level.txt
|
||||
""",
|
||||
"entry_points.txt": """
|
||||
[entries]
|
||||
main = mod:main
|
||||
""",
|
||||
"requires.txt": """
|
||||
wheel >= 1.0; python_version >= "2.7"
|
||||
[test]
|
||||
pytest
|
||||
""",
|
||||
"top_level.txt": "mod\n",
|
||||
},
|
||||
"mod.py": """
|
||||
def main():
|
||||
print("hello world")
|
||||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoPkg.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir):
|
||||
files: FilesSpec = {
|
||||
"egg_with_module_pkg.egg-info": {
|
||||
"PKG-INFO": "Name: egg_with_module-pkg",
|
||||
# SOURCES.txt is made from the source archive, and contains files
|
||||
# (setup.py) that are not present after installation.
|
||||
"SOURCES.txt": """
|
||||
egg_with_module.py
|
||||
setup.py
|
||||
egg_with_module_pkg.egg-info/PKG-INFO
|
||||
egg_with_module_pkg.egg-info/SOURCES.txt
|
||||
egg_with_module_pkg.egg-info/top_level.txt
|
||||
""",
|
||||
# installed-files.txt is written by pip, and is a strictly more
|
||||
# accurate source than SOURCES.txt as to the installed contents of
|
||||
# the package.
|
||||
"installed-files.txt": """
|
||||
../egg_with_module.py
|
||||
PKG-INFO
|
||||
SOURCES.txt
|
||||
top_level.txt
|
||||
""",
|
||||
# missing top_level.txt (to trigger fallback to installed-files.txt)
|
||||
},
|
||||
"egg_with_module.py": """
|
||||
def main():
|
||||
print("hello world")
|
||||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoPkgPipInstalledNoToplevel.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir):
|
||||
files: FilesSpec = {
|
||||
"egg_with_no_modules_pkg.egg-info": {
|
||||
"PKG-INFO": "Name: egg_with_no_modules-pkg",
|
||||
# SOURCES.txt is made from the source archive, and contains files
|
||||
# (setup.py) that are not present after installation.
|
||||
"SOURCES.txt": """
|
||||
setup.py
|
||||
egg_with_no_modules_pkg.egg-info/PKG-INFO
|
||||
egg_with_no_modules_pkg.egg-info/SOURCES.txt
|
||||
egg_with_no_modules_pkg.egg-info/top_level.txt
|
||||
""",
|
||||
# installed-files.txt is written by pip, and is a strictly more
|
||||
# accurate source than SOURCES.txt as to the installed contents of
|
||||
# the package.
|
||||
"installed-files.txt": """
|
||||
PKG-INFO
|
||||
SOURCES.txt
|
||||
top_level.txt
|
||||
""",
|
||||
# top_level.txt correctly reflects that no modules are installed
|
||||
"top_level.txt": b"\n",
|
||||
},
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoPkgPipInstalledNoModules.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
class EggInfoPkgSourcesFallback(OnSysPath, SiteDir):
|
||||
files: FilesSpec = {
|
||||
"sources_fallback_pkg.egg-info": {
|
||||
"PKG-INFO": "Name: sources_fallback-pkg",
|
||||
# SOURCES.txt is made from the source archive, and contains files
|
||||
# (setup.py) that are not present after installation.
|
||||
"SOURCES.txt": """
|
||||
sources_fallback.py
|
||||
setup.py
|
||||
sources_fallback_pkg.egg-info/PKG-INFO
|
||||
sources_fallback_pkg.egg-info/SOURCES.txt
|
||||
""",
|
||||
# missing installed-files.txt (i.e. not installed by pip) and
|
||||
# missing top_level.txt (to trigger fallback to SOURCES.txt)
|
||||
},
|
||||
"sources_fallback.py": """
|
||||
def main():
|
||||
print("hello world")
|
||||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoPkgSourcesFallback.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
class EggInfoFile(OnSysPath, SiteDir):
|
||||
files: FilesSpec = {
|
||||
"egginfo_file.egg-info": """
|
||||
Metadata-Version: 1.0
|
||||
Name: egginfo_file
|
||||
Version: 0.1
|
||||
Summary: An example package
|
||||
Home-page: www.example.com
|
||||
Author: Eric Haffa-Vee
|
||||
Author-email: eric@example.coms
|
||||
License: UNKNOWN
|
||||
Description: UNKNOWN
|
||||
Platform: UNKNOWN
|
||||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoFile.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
# dedent all text strings before writing
|
||||
orig = _path.create.registry[str]
|
||||
_path.create.register(str, lambda content, path: orig(DALS(content), path))
|
||||
|
||||
|
||||
build_files = _path.build
|
||||
|
||||
|
||||
def build_record(file_defs):
|
||||
return ''.join(f'{name},,\n' for name in record_names(file_defs))
|
||||
|
||||
|
||||
def record_names(file_defs):
|
||||
recording = _path.Recording()
|
||||
_path.build(file_defs, recording)
|
||||
return recording.record
|
||||
|
||||
|
||||
class FileBuilder:
|
||||
def unicode_filename(self):
|
||||
return FS_NONASCII or self.skip("File system does not support non-ascii.")
|
||||
|
||||
|
||||
def DALS(str):
|
||||
"Dedent and left-strip"
|
||||
return textwrap.dedent(str).lstrip()
|
||||
|
||||
|
||||
@requires_zlib()
|
||||
class ZipFixtures:
|
||||
root = 'test.test_importlib.data'
|
||||
|
||||
def _fixture_on_path(self, filename):
|
||||
pkg_file = resources.files(self.root).joinpath(filename)
|
||||
file = self.resources.enter_context(resources.as_file(pkg_file))
|
||||
assert file.name.startswith('example'), file.name
|
||||
sys.path.insert(0, str(file))
|
||||
self.resources.callback(sys.path.pop, 0)
|
||||
|
||||
def setUp(self):
|
||||
# Add self.zip_name to the front of sys.path.
|
||||
self.resources = contextlib.ExitStack()
|
||||
self.addCleanup(self.resources.close)
|
||||
|
||||
|
||||
def parameterize(*args_set):
|
||||
"""Run test method with a series of parameters."""
|
||||
|
||||
def wrapper(func):
|
||||
@functools.wraps(func)
|
||||
def _inner(self):
|
||||
for args in args_set:
|
||||
with self.subTest(**args):
|
||||
func(self, **args)
|
||||
|
||||
return _inner
|
||||
|
||||
return wrapper
|
||||
-46
@@ -27,13 +27,6 @@ class BadSpecFinderLoader:
|
||||
raise ImportError('I cannot be loaded!')
|
||||
|
||||
|
||||
class BadLoaderFinder:
|
||||
@classmethod
|
||||
def load_module(cls, fullname):
|
||||
if fullname == SUBMOD_NAME:
|
||||
raise ImportError('I cannot be loaded!')
|
||||
|
||||
|
||||
class APITest:
|
||||
|
||||
"""Test API-specific details for __import__ (e.g. raising the right
|
||||
@@ -93,45 +86,6 @@ class APITest:
|
||||
self.assertEqual(cm.exception.name, SUBMOD_NAME)
|
||||
|
||||
|
||||
class OldAPITests(APITest):
|
||||
bad_finder_loader = BadLoaderFinder
|
||||
|
||||
def test_raises_ModuleNotFoundError(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", ImportWarning)
|
||||
super().test_raises_ModuleNotFoundError()
|
||||
|
||||
def test_name_requires_rparition(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", ImportWarning)
|
||||
super().test_name_requires_rparition()
|
||||
|
||||
def test_negative_level(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", ImportWarning)
|
||||
super().test_negative_level()
|
||||
|
||||
def test_nonexistent_fromlist_entry(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", ImportWarning)
|
||||
super().test_nonexistent_fromlist_entry()
|
||||
|
||||
def test_fromlist_load_error_propagates(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", ImportWarning)
|
||||
super().test_fromlist_load_error_propagates
|
||||
|
||||
def test_blocked_fromlist(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", ImportWarning)
|
||||
super().test_blocked_fromlist()
|
||||
|
||||
|
||||
(Frozen_OldAPITests,
|
||||
Source_OldAPITests
|
||||
) = util.test_both(OldAPITests, __import__=util.__import__)
|
||||
|
||||
|
||||
class SpecAPITests(APITest):
|
||||
bad_finder_loader = BadSpecFinderLoader
|
||||
|
||||
|
||||
+3
-4
@@ -19,8 +19,7 @@ class FixUpModuleTests:
|
||||
ns = {"__spec__": spec}
|
||||
_bootstrap_external._fix_up_module(ns, name, path)
|
||||
|
||||
expected = {"__spec__": spec, "__loader__": loader, "__file__": path,
|
||||
"__cached__": None}
|
||||
expected = {"__spec__": spec, "__loader__": loader, "__file__": path}
|
||||
self.assertEqual(ns, expected)
|
||||
|
||||
def test_no_loader_no_spec_but_sourceless(self):
|
||||
@@ -29,7 +28,7 @@ class FixUpModuleTests:
|
||||
ns = {}
|
||||
_bootstrap_external._fix_up_module(ns, name, path, path)
|
||||
|
||||
expected = {"__file__": path, "__cached__": path}
|
||||
expected = {"__file__": path}
|
||||
|
||||
for key, val in expected.items():
|
||||
with self.subTest(f"{key}: {val}"):
|
||||
@@ -51,7 +50,7 @@ class FixUpModuleTests:
|
||||
ns = {}
|
||||
_bootstrap_external._fix_up_module(ns, name, path)
|
||||
|
||||
expected = {"__file__": path, "__cached__": None}
|
||||
expected = {"__file__": path}
|
||||
|
||||
for key, val in expected.items():
|
||||
with self.subTest(f"{key}: {val}"):
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
+23
-21
@@ -1,9 +1,14 @@
|
||||
# from jaraco.path 3.7
|
||||
# from jaraco.path 3.7.2
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import pathlib
|
||||
from typing import Dict, Protocol, Union
|
||||
from typing import runtime_checkable
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
class Symlink(str):
|
||||
@@ -12,29 +17,25 @@ class Symlink(str):
|
||||
"""
|
||||
|
||||
|
||||
FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore
|
||||
FilesSpec = Mapping[str, Union[str, bytes, Symlink, 'FilesSpec']]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TreeMaker(Protocol):
|
||||
def __truediv__(self, *args, **kwargs): ... # pragma: no cover
|
||||
|
||||
def mkdir(self, **kwargs): ... # pragma: no cover
|
||||
|
||||
def write_text(self, content, **kwargs): ... # pragma: no cover
|
||||
|
||||
def write_bytes(self, content): ... # pragma: no cover
|
||||
|
||||
def symlink_to(self, target): ... # pragma: no cover
|
||||
def __truediv__(self, other, /) -> Self: ...
|
||||
def mkdir(self, *, exist_ok) -> object: ...
|
||||
def write_text(self, content, /, *, encoding) -> object: ...
|
||||
def write_bytes(self, content, /) -> object: ...
|
||||
def symlink_to(self, target, /) -> object: ...
|
||||
|
||||
|
||||
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
|
||||
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore
|
||||
def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker:
|
||||
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj)
|
||||
|
||||
|
||||
def build(
|
||||
spec: FilesSpec,
|
||||
prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore
|
||||
prefix: str | TreeMaker = pathlib.Path(),
|
||||
):
|
||||
"""
|
||||
Build a set of files/directories, as described by the spec.
|
||||
@@ -66,23 +67,24 @@ def build(
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
def create(content: Union[str, bytes, FilesSpec], path):
|
||||
def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None:
|
||||
path.mkdir(exist_ok=True)
|
||||
build(content, prefix=path) # type: ignore
|
||||
# Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union
|
||||
build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: bytes, path):
|
||||
def _(content: bytes, path: TreeMaker) -> None:
|
||||
path.write_bytes(content)
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: str, path):
|
||||
def _(content: str, path: TreeMaker) -> None:
|
||||
path.write_text(content, encoding='utf-8')
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: Symlink, path):
|
||||
def _(content: Symlink, path: TreeMaker) -> None:
|
||||
path.symlink_to(content)
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
+8
-16
@@ -1,11 +1,12 @@
|
||||
import sys
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import pathlib
|
||||
import textwrap
|
||||
import functools
|
||||
import contextlib
|
||||
import copy
|
||||
import functools
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
import textwrap
|
||||
from importlib import resources
|
||||
|
||||
from test.support import import_helper
|
||||
from test.support import os_helper
|
||||
@@ -15,15 +16,6 @@ from . import _path
|
||||
from ._path import FilesSpec
|
||||
|
||||
|
||||
try:
|
||||
from importlib import resources # type: ignore
|
||||
|
||||
getattr(resources, 'files')
|
||||
getattr(resources, 'as_file')
|
||||
except (ImportError, AttributeError):
|
||||
import importlib_resources as resources # type: ignore
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def tmp_path():
|
||||
"""
|
||||
|
||||
+38
-14
@@ -1,14 +1,12 @@
|
||||
import importlib
|
||||
import re
|
||||
import textwrap
|
||||
import unittest
|
||||
import warnings
|
||||
import importlib
|
||||
import contextlib
|
||||
|
||||
from . import fixtures
|
||||
from importlib.metadata import (
|
||||
Distribution,
|
||||
PackageNotFoundError,
|
||||
Prepared,
|
||||
distribution,
|
||||
entry_points,
|
||||
files,
|
||||
@@ -17,12 +15,7 @@ from importlib.metadata import (
|
||||
version,
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_known_deprecation():
|
||||
with warnings.catch_warnings(record=True) as ctx:
|
||||
warnings.simplefilter('default', category=DeprecationWarning)
|
||||
yield ctx
|
||||
from . import fixtures
|
||||
|
||||
|
||||
class APITests(
|
||||
@@ -153,13 +146,13 @@ class APITests(
|
||||
classifiers = md.get_all('Classifier')
|
||||
assert 'Topic :: Software Development :: Libraries' in classifiers
|
||||
|
||||
def test_missing_key_legacy(self):
|
||||
def test_missing_key(self):
|
||||
"""
|
||||
Requesting a missing key will still return None, but warn.
|
||||
Requesting a missing key raises KeyError.
|
||||
"""
|
||||
md = metadata('distinfo-pkg')
|
||||
with suppress_known_deprecation():
|
||||
assert md['does-not-exist'] is None
|
||||
with self.assertRaises(KeyError):
|
||||
md['does-not-exist']
|
||||
|
||||
def test_get_key(self):
|
||||
"""
|
||||
@@ -321,3 +314,34 @@ class InvalidateCache(unittest.TestCase):
|
||||
def test_invalidate_cache(self):
|
||||
# No externally observable behavior, but ensures test coverage...
|
||||
importlib.invalidate_caches()
|
||||
|
||||
|
||||
class PreparedTests(unittest.TestCase):
|
||||
@fixtures.parameterize(
|
||||
# Simple
|
||||
dict(input='sample', expected='sample'),
|
||||
# Mixed case
|
||||
dict(input='Sample', expected='sample'),
|
||||
dict(input='SAMPLE', expected='sample'),
|
||||
dict(input='SaMpLe', expected='sample'),
|
||||
# Separator conversions
|
||||
dict(input='sample-pkg', expected='sample_pkg'),
|
||||
dict(input='sample.pkg', expected='sample_pkg'),
|
||||
dict(input='sample_pkg', expected='sample_pkg'),
|
||||
# Multiple separators
|
||||
dict(input='sample---pkg', expected='sample_pkg'),
|
||||
dict(input='sample___pkg', expected='sample_pkg'),
|
||||
dict(input='sample...pkg', expected='sample_pkg'),
|
||||
# Mixed separators
|
||||
dict(input='sample-._pkg', expected='sample_pkg'),
|
||||
dict(input='sample_.-pkg', expected='sample_pkg'),
|
||||
# Complex
|
||||
dict(input='Sample__Pkg-name.foo', expected='sample_pkg_name_foo'),
|
||||
dict(input='Sample__Pkg.name__foo', expected='sample_pkg_name_foo'),
|
||||
# Uppercase with separators
|
||||
dict(input='SAMPLE-PKG', expected='sample_pkg'),
|
||||
dict(input='Sample.Pkg', expected='sample_pkg'),
|
||||
dict(input='SAMPLE_PKG', expected='sample_pkg'),
|
||||
)
|
||||
def test_normalize(self, input, expected):
|
||||
self.assertEqual(Prepared.normalize(input), expected)
|
||||
+44
-19
@@ -1,23 +1,18 @@
|
||||
import re
|
||||
import pickle
|
||||
import unittest
|
||||
import warnings
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import contextlib
|
||||
from test.support import os_helper
|
||||
import pickle
|
||||
import re
|
||||
import unittest
|
||||
|
||||
try:
|
||||
import pyfakefs.fake_filesystem_unittest as ffs
|
||||
except ImportError:
|
||||
from .stubs import fake_filesystem_unittest as ffs
|
||||
from test.support import os_helper
|
||||
|
||||
from . import fixtures
|
||||
from ._context import suppress
|
||||
from ._path import Symlink
|
||||
from importlib.metadata import (
|
||||
Distribution,
|
||||
EntryPoint,
|
||||
MetadataNotFound,
|
||||
PackageNotFoundError,
|
||||
_unique,
|
||||
distributions,
|
||||
@@ -27,12 +22,8 @@ from importlib.metadata import (
|
||||
version,
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_known_deprecation():
|
||||
with warnings.catch_warnings(record=True) as ctx:
|
||||
warnings.simplefilter('default', category=DeprecationWarning)
|
||||
yield ctx
|
||||
from . import fixtures
|
||||
from ._path import Symlink
|
||||
|
||||
|
||||
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
|
||||
@@ -59,9 +50,6 @@ class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
|
||||
|
||||
assert "metadata" in str(ctx.exception)
|
||||
|
||||
# expected to fail until ABC is enforced
|
||||
@suppress(AssertionError)
|
||||
@suppress_known_deprecation()
|
||||
def test_abc_enforced(self):
|
||||
with self.assertRaises(TypeError):
|
||||
type('DistributionSubclass', (Distribution,), {})()
|
||||
@@ -146,6 +134,43 @@ class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.Test
|
||||
assert len(after) == len(before)
|
||||
|
||||
|
||||
class InvalidMetadataTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||
@staticmethod
|
||||
def make_pkg(name, files=dict(METADATA="VERSION: 1.0")):
|
||||
"""
|
||||
Create metadata for a dist-info package with name and files.
|
||||
"""
|
||||
return {
|
||||
f'{name}.dist-info': files,
|
||||
}
|
||||
|
||||
def test_valid_dists_preferred(self):
|
||||
"""
|
||||
Dists with metadata should be preferred when discovered by name.
|
||||
|
||||
Ref python/importlib_metadata#489.
|
||||
"""
|
||||
# create three dists with the valid one in the middle (lexicographically)
|
||||
# such that on most file systems, the valid one is never naturally first.
|
||||
fixtures.build_files(self.make_pkg('foo-4.0', files={}), self.site_dir)
|
||||
fixtures.build_files(self.make_pkg('foo-4.1'), self.site_dir)
|
||||
fixtures.build_files(self.make_pkg('foo-4.2', files={}), self.site_dir)
|
||||
dist = Distribution.from_name('foo')
|
||||
assert dist.version == "1.0"
|
||||
|
||||
def test_missing_metadata(self):
|
||||
"""
|
||||
Dists with a missing metadata file should raise ``MetadataNotFound``.
|
||||
|
||||
Ref python/importlib_metadata#493 and python/cpython#143387.
|
||||
"""
|
||||
fixtures.build_files(self.make_pkg('foo-4.3', files={}), self.site_dir)
|
||||
with self.assertRaises(MetadataNotFound):
|
||||
Distribution.from_name('foo').metadata
|
||||
with self.assertRaises(MetadataNotFound):
|
||||
metadata('foo')
|
||||
|
||||
|
||||
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||
@staticmethod
|
||||
def pkg_with_non_ascii_description(site_dir):
|
||||
|
||||
+40
-1
@@ -1,8 +1,12 @@
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from . import fixtures
|
||||
from test.support import requires_fork, warnings_helper
|
||||
|
||||
from importlib.metadata import (
|
||||
FastPath,
|
||||
PackageNotFoundError,
|
||||
distribution,
|
||||
distributions,
|
||||
@@ -11,6 +15,8 @@ from importlib.metadata import (
|
||||
version,
|
||||
)
|
||||
|
||||
from . import fixtures
|
||||
|
||||
|
||||
class TestZip(fixtures.ZipFixtures, unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -46,6 +52,39 @@ class TestZip(fixtures.ZipFixtures, unittest.TestCase):
|
||||
dists = list(distributions(path=sys.path[:1]))
|
||||
assert len(dists) == 1
|
||||
|
||||
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
|
||||
@requires_fork()
|
||||
@unittest.skipUnless(
|
||||
hasattr(os, 'register_at_fork')
|
||||
and 'fork' in multiprocessing.get_all_start_methods(),
|
||||
'requires fork-based multiprocessing support',
|
||||
)
|
||||
def test_fastpath_cache_cleared_in_forked_child(self):
|
||||
zip_path = sys.path[0]
|
||||
|
||||
FastPath(zip_path)
|
||||
assert FastPath.__new__.cache_info().currsize >= 1
|
||||
|
||||
ctx = multiprocessing.get_context('fork')
|
||||
parent_conn, child_conn = ctx.Pipe()
|
||||
|
||||
def child(conn, root):
|
||||
try:
|
||||
before = FastPath.__new__.cache_info().currsize
|
||||
FastPath(root)
|
||||
after = FastPath.__new__.cache_info().currsize
|
||||
conn.send((before, after))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
proc = ctx.Process(target=child, args=(child_conn, zip_path))
|
||||
proc.start()
|
||||
child_conn.close()
|
||||
cache_sizes = parent_conn.recv()
|
||||
proc.join()
|
||||
|
||||
self.assertEqual(cache_sizes, (0, 1))
|
||||
|
||||
|
||||
class TestEgg(TestZip):
|
||||
def setUp(self):
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
This directory should not be a package, but should share a name with an
|
||||
unrelated subpackage.
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
|
||||
+6
-10
@@ -1,10 +1,6 @@
|
||||
import pathlib
|
||||
import functools
|
||||
|
||||
from typing import Dict, Union
|
||||
from typing import runtime_checkable
|
||||
from typing import Protocol
|
||||
|
||||
import pathlib
|
||||
from typing import Protocol, Union, runtime_checkable
|
||||
|
||||
####
|
||||
# from jaraco.path 3.7.1
|
||||
@@ -16,7 +12,7 @@ class Symlink(str):
|
||||
"""
|
||||
|
||||
|
||||
FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']]
|
||||
FilesSpec = dict[str, Union[str, bytes, Symlink, 'FilesSpec']]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@@ -32,13 +28,13 @@ class TreeMaker(Protocol):
|
||||
def symlink_to(self, target): ... # pragma: no cover
|
||||
|
||||
|
||||
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
|
||||
def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker:
|
||||
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value]
|
||||
|
||||
|
||||
def build(
|
||||
spec: FilesSpec,
|
||||
prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment]
|
||||
prefix: str | TreeMaker = pathlib.Path(), # type: ignore[assignment]
|
||||
):
|
||||
"""
|
||||
Build a set of files/directories, as described by the spec.
|
||||
@@ -70,7 +66,7 @@ def build(
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
def create(content: Union[str, bytes, FilesSpec], path):
|
||||
def create(content: str | bytes | FilesSpec, path):
|
||||
path.mkdir(exist_ok=True)
|
||||
build(content, prefix=path) # type: ignore[arg-type]
|
||||
|
||||
|
||||
Whitespace-only changes.
Binary file not shown.
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
Hello, UTF-8 world!
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1 +0,0 @@
|
||||
one resource
|
||||
@@ -1 +0,0 @@
|
||||
a resource
|
||||
Whitespace-only changes.
@@ -1 +0,0 @@
|
||||
two resource
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
Hello, UTF-8 world!
|
||||
+23
-30
@@ -1,8 +1,6 @@
|
||||
import importlib.resources as resources
|
||||
import io
|
||||
import unittest
|
||||
|
||||
from importlib import resources
|
||||
|
||||
from importlib.resources._adapters import (
|
||||
CompatibilityFiles,
|
||||
wrap_spec,
|
||||
@@ -26,51 +24,46 @@ class CompatibilityFilesTests(unittest.TestCase):
|
||||
return resources.files(self.package)
|
||||
|
||||
def test_spec_path_iter(self):
|
||||
self.assertEqual(
|
||||
sorted(path.name for path in self.files.iterdir()),
|
||||
['a', 'b', 'c'],
|
||||
)
|
||||
assert sorted(path.name for path in self.files.iterdir()) == ['a', 'b', 'c']
|
||||
|
||||
def test_child_path_iter(self):
|
||||
self.assertEqual(list((self.files / 'a').iterdir()), [])
|
||||
assert list((self.files / 'a').iterdir()) == []
|
||||
|
||||
def test_orphan_path_iter(self):
|
||||
self.assertEqual(list((self.files / 'a' / 'a').iterdir()), [])
|
||||
self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), [])
|
||||
assert list((self.files / 'a' / 'a').iterdir()) == []
|
||||
assert list((self.files / 'a' / 'a' / 'a').iterdir()) == []
|
||||
|
||||
def test_spec_path_is(self):
|
||||
self.assertFalse(self.files.is_file())
|
||||
self.assertFalse(self.files.is_dir())
|
||||
assert not self.files.is_file()
|
||||
assert not self.files.is_dir()
|
||||
|
||||
def test_child_path_is(self):
|
||||
self.assertTrue((self.files / 'a').is_file())
|
||||
self.assertFalse((self.files / 'a').is_dir())
|
||||
assert (self.files / 'a').is_file()
|
||||
assert not (self.files / 'a').is_dir()
|
||||
|
||||
def test_orphan_path_is(self):
|
||||
self.assertFalse((self.files / 'a' / 'a').is_file())
|
||||
self.assertFalse((self.files / 'a' / 'a').is_dir())
|
||||
self.assertFalse((self.files / 'a' / 'a' / 'a').is_file())
|
||||
self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir())
|
||||
assert not (self.files / 'a' / 'a').is_file()
|
||||
assert not (self.files / 'a' / 'a').is_dir()
|
||||
assert not (self.files / 'a' / 'a' / 'a').is_file()
|
||||
assert not (self.files / 'a' / 'a' / 'a').is_dir()
|
||||
|
||||
def test_spec_path_name(self):
|
||||
self.assertEqual(self.files.name, 'testingpackage')
|
||||
assert self.files.name == 'testingpackage'
|
||||
|
||||
def test_child_path_name(self):
|
||||
self.assertEqual((self.files / 'a').name, 'a')
|
||||
assert (self.files / 'a').name == 'a'
|
||||
|
||||
def test_orphan_path_name(self):
|
||||
self.assertEqual((self.files / 'a' / 'b').name, 'b')
|
||||
self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c')
|
||||
assert (self.files / 'a' / 'b').name == 'b'
|
||||
assert (self.files / 'a' / 'b' / 'c').name == 'c'
|
||||
|
||||
def test_spec_path_open(self):
|
||||
self.assertEqual(self.files.read_bytes(), b'Hello, world!')
|
||||
self.assertEqual(self.files.read_text(encoding='utf-8'), 'Hello, world!')
|
||||
assert self.files.read_bytes() == b'Hello, world!'
|
||||
assert self.files.read_text(encoding='utf-8') == 'Hello, world!'
|
||||
|
||||
def test_child_path_open(self):
|
||||
self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!')
|
||||
self.assertEqual(
|
||||
(self.files / 'a').read_text(encoding='utf-8'), 'Hello, world!'
|
||||
)
|
||||
assert (self.files / 'a').read_bytes() == b'Hello, world!'
|
||||
assert (self.files / 'a').read_text(encoding='utf-8') == 'Hello, world!'
|
||||
|
||||
def test_orphan_path_open(self):
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
@@ -88,7 +81,7 @@ class CompatibilityFilesTests(unittest.TestCase):
|
||||
|
||||
def test_wrap_spec(self):
|
||||
spec = wrap_spec(self.package)
|
||||
self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles)
|
||||
assert isinstance(spec.loader.get_resource_reader(None), CompatibilityFiles)
|
||||
|
||||
|
||||
class CompatibilityFilesNoReaderTests(unittest.TestCase):
|
||||
@@ -101,4 +94,4 @@ class CompatibilityFilesNoReaderTests(unittest.TestCase):
|
||||
return resources.files(self.package)
|
||||
|
||||
def test_spec_path_joinpath(self):
|
||||
self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath)
|
||||
assert isinstance(self.files / 'a', CompatibilityFiles.OrphanPath)
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import importlib.resources as resources
|
||||
import unittest
|
||||
from importlib import resources
|
||||
|
||||
from . import util
|
||||
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
import unittest
|
||||
import contextlib
|
||||
import importlib.resources as resources
|
||||
import pathlib
|
||||
import unittest
|
||||
from importlib.resources import abc
|
||||
from importlib.resources.abc import ResourceReader, TraversableResources
|
||||
|
||||
from test.support import os_helper
|
||||
|
||||
from importlib import resources
|
||||
from importlib.resources import abc
|
||||
from importlib.resources.abc import TraversableResources, ResourceReader
|
||||
from . import util
|
||||
|
||||
|
||||
|
||||
+11
-16
@@ -1,15 +1,16 @@
|
||||
import contextlib
|
||||
import importlib
|
||||
import importlib.resources as resources
|
||||
import pathlib
|
||||
import py_compile
|
||||
import textwrap
|
||||
import unittest
|
||||
import warnings
|
||||
import importlib
|
||||
import contextlib
|
||||
|
||||
from importlib import resources
|
||||
from importlib.resources.abc import Traversable
|
||||
|
||||
from test.support import import_helper, os_helper
|
||||
|
||||
from . import util
|
||||
from test.support import os_helper, import_helper
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -36,15 +37,7 @@ class FilesTests:
|
||||
def test_joinpath_with_multiple_args(self):
|
||||
files = resources.files(self.data)
|
||||
binfile = files.joinpath('subdirectory', 'binary.file')
|
||||
self.assertTrue(binfile.is_file())
|
||||
|
||||
def test_old_parameter(self):
|
||||
"""
|
||||
Files used to take a 'package' parameter. Make sure anyone
|
||||
passing by name is still supported.
|
||||
"""
|
||||
with suppress_known_deprecation():
|
||||
resources.files(package=self.data)
|
||||
assert binfile.is_file()
|
||||
|
||||
|
||||
class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase):
|
||||
@@ -70,7 +63,7 @@ class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase):
|
||||
to cause the ``PathEntryFinder`` to be called when searching
|
||||
for packages. In that case, resources should still be loadable.
|
||||
"""
|
||||
import namespacedata01
|
||||
import namespacedata01 # type: ignore[import-not-found]
|
||||
|
||||
namespacedata01.__path__.append(
|
||||
'__editable__.sample_namespace-1.0.finder.__path_hook__'
|
||||
@@ -161,7 +154,9 @@ class ImplicitContextFiles:
|
||||
sources = pathlib.Path(resources.__file__).parent
|
||||
|
||||
for source_path in sources.glob('**/*.py'):
|
||||
c_path = c_resources.joinpath(source_path.relative_to(sources)).with_suffix('.pyc')
|
||||
c_path = c_resources.joinpath(source_path.relative_to(sources)).with_suffix(
|
||||
'.pyc'
|
||||
)
|
||||
py_compile.compile(source_path, c_path)
|
||||
self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site))
|
||||
|
||||
|
||||
+81
-69
@@ -1,17 +1,12 @@
|
||||
import unittest
|
||||
import os
|
||||
import importlib
|
||||
import importlib.resources as resources
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from test.support import warnings_helper
|
||||
|
||||
from importlib import resources
|
||||
|
||||
from . import util
|
||||
|
||||
# Since the functional API forwards to Traversable, we only test
|
||||
# filesystem resources here -- not zip files, namespace packages etc.
|
||||
# We do test for two kinds of Anchor, though.
|
||||
|
||||
|
||||
class StringAnchorMixin:
|
||||
anchor01 = 'data01'
|
||||
@@ -28,7 +23,7 @@ class ModuleAnchorMixin:
|
||||
return importlib.import_module('data02')
|
||||
|
||||
|
||||
class FunctionalAPIBase(util.DiskSetup):
|
||||
class FunctionalAPIBase:
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.load_fixture('data02')
|
||||
@@ -43,83 +38,90 @@ class FunctionalAPIBase(util.DiskSetup):
|
||||
with self.subTest(path_parts=path_parts):
|
||||
yield path_parts
|
||||
|
||||
@staticmethod
|
||||
def remove_utf16_bom(string):
|
||||
"""Remove an architecture-specific UTF-16 BOM prefix when present.
|
||||
|
||||
Some platforms surface UTF-16 BOM bytes as escaped text when the
|
||||
fixture is intentionally decoded as UTF-8 with ``errors='backslashreplace'``.
|
||||
Strip that prefix so assertions validate content consistently."""
|
||||
for bom in ('\\xff\\xfe', '\\xfe\\xff', '\ufeff'):
|
||||
if string.startswith(bom):
|
||||
return string[len(bom) :]
|
||||
return string
|
||||
|
||||
def test_read_text(self):
|
||||
self.assertEqual(
|
||||
resources.read_text(self.anchor01, 'utf-8.file'),
|
||||
'Hello, UTF-8 world!\n',
|
||||
assert (
|
||||
resources.read_text(self.anchor01, 'utf-8.file') == 'Hello, UTF-8 world!\n'
|
||||
)
|
||||
self.assertEqual(
|
||||
assert (
|
||||
resources.read_text(
|
||||
self.anchor02,
|
||||
'subdirectory',
|
||||
'subsubdir',
|
||||
'resource.txt',
|
||||
encoding='utf-8',
|
||||
),
|
||||
'a resource',
|
||||
)
|
||||
== 'a resource'
|
||||
)
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
self.assertEqual(
|
||||
assert (
|
||||
resources.read_text(
|
||||
self.anchor02,
|
||||
*path_parts,
|
||||
encoding='utf-8',
|
||||
),
|
||||
'a resource',
|
||||
)
|
||||
== 'a resource'
|
||||
)
|
||||
# Use generic OSError, since e.g. attempting to read a directory can
|
||||
# fail with PermissionError rather than IsADirectoryError
|
||||
with self.assertRaises(OSError):
|
||||
resources.read_text(self.anchor01)
|
||||
with self.assertRaises(OSError):
|
||||
with self.assertRaises((OSError, resources.abc.TraversalError)):
|
||||
resources.read_text(self.anchor01, 'no-such-file')
|
||||
with self.assertRaises(UnicodeDecodeError):
|
||||
resources.read_text(self.anchor01, 'utf-16.file')
|
||||
self.assertEqual(
|
||||
assert (
|
||||
resources.read_text(
|
||||
self.anchor01,
|
||||
'binary.file',
|
||||
encoding='latin1',
|
||||
),
|
||||
'\x00\x01\x02\x03',
|
||||
)
|
||||
== '\x00\x01\x02\x03'
|
||||
)
|
||||
self.assertEndsWith( # ignore the BOM
|
||||
assert self.remove_utf16_bom(
|
||||
resources.read_text(
|
||||
self.anchor01,
|
||||
'utf-16.file',
|
||||
errors='backslashreplace',
|
||||
),
|
||||
'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
|
||||
errors='backslashreplace',
|
||||
),
|
||||
) == 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
|
||||
errors='backslashreplace',
|
||||
)
|
||||
|
||||
def test_read_binary(self):
|
||||
self.assertEqual(
|
||||
resources.read_binary(self.anchor01, 'utf-8.file'),
|
||||
b'Hello, UTF-8 world!\n',
|
||||
assert (
|
||||
resources.read_binary(self.anchor01, 'utf-8.file')
|
||||
== b'Hello, UTF-8 world!\n'
|
||||
)
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
self.assertEqual(
|
||||
resources.read_binary(self.anchor02, *path_parts),
|
||||
b'a resource',
|
||||
)
|
||||
assert resources.read_binary(self.anchor02, *path_parts) == b'a resource'
|
||||
|
||||
def test_open_text(self):
|
||||
with resources.open_text(self.anchor01, 'utf-8.file') as f:
|
||||
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
|
||||
assert f.read() == 'Hello, UTF-8 world!\n'
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
with resources.open_text(
|
||||
self.anchor02,
|
||||
*path_parts,
|
||||
encoding='utf-8',
|
||||
) as f:
|
||||
self.assertEqual(f.read(), 'a resource')
|
||||
assert f.read() == 'a resource'
|
||||
# Use generic OSError, since e.g. attempting to read a directory can
|
||||
# fail with PermissionError rather than IsADirectoryError
|
||||
with self.assertRaises(OSError):
|
||||
resources.open_text(self.anchor01)
|
||||
with self.assertRaises(OSError):
|
||||
with self.assertRaises((OSError, resources.abc.TraversalError)):
|
||||
resources.open_text(self.anchor01, 'no-such-file')
|
||||
with resources.open_text(self.anchor01, 'utf-16.file') as f:
|
||||
with self.assertRaises(UnicodeDecodeError):
|
||||
@@ -129,71 +131,70 @@ class FunctionalAPIBase(util.DiskSetup):
|
||||
'binary.file',
|
||||
encoding='latin1',
|
||||
) as f:
|
||||
self.assertEqual(f.read(), '\x00\x01\x02\x03')
|
||||
assert f.read() == '\x00\x01\x02\x03'
|
||||
with resources.open_text(
|
||||
self.anchor01,
|
||||
'utf-16.file',
|
||||
errors='backslashreplace',
|
||||
) as f:
|
||||
self.assertEndsWith( # ignore the BOM
|
||||
f.read(),
|
||||
'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
|
||||
errors='backslashreplace',
|
||||
),
|
||||
assert self.remove_utf16_bom(f.read()) == 'Hello, UTF-16 world!\n'.encode(
|
||||
'utf-16-le'
|
||||
).decode(
|
||||
errors='backslashreplace',
|
||||
)
|
||||
|
||||
def test_open_binary(self):
|
||||
with resources.open_binary(self.anchor01, 'utf-8.file') as f:
|
||||
self.assertEqual(f.read(), b'Hello, UTF-8 world!\n')
|
||||
assert f.read() == b'Hello, UTF-8 world!\n'
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
with resources.open_binary(
|
||||
self.anchor02,
|
||||
*path_parts,
|
||||
) as f:
|
||||
self.assertEqual(f.read(), b'a resource')
|
||||
assert f.read() == b'a resource'
|
||||
|
||||
def test_path(self):
|
||||
with resources.path(self.anchor01, 'utf-8.file') as path:
|
||||
with open(str(path), encoding='utf-8') as f:
|
||||
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
|
||||
assert f.read() == 'Hello, UTF-8 world!\n'
|
||||
with resources.path(self.anchor01) as path:
|
||||
with open(os.path.join(path, 'utf-8.file'), encoding='utf-8') as f:
|
||||
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
|
||||
assert f.read() == 'Hello, UTF-8 world!\n'
|
||||
|
||||
def test_is_resource(self):
|
||||
is_resource = resources.is_resource
|
||||
self.assertTrue(is_resource(self.anchor01, 'utf-8.file'))
|
||||
self.assertFalse(is_resource(self.anchor01, 'no_such_file'))
|
||||
self.assertFalse(is_resource(self.anchor01))
|
||||
self.assertFalse(is_resource(self.anchor01, 'subdirectory'))
|
||||
assert is_resource(self.anchor01, 'utf-8.file')
|
||||
assert not is_resource(self.anchor01, 'no_such_file')
|
||||
assert not is_resource(self.anchor01)
|
||||
assert not is_resource(self.anchor01, 'subdirectory')
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
self.assertTrue(is_resource(self.anchor02, *path_parts))
|
||||
assert is_resource(self.anchor02, *path_parts)
|
||||
|
||||
def test_contents(self):
|
||||
with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)):
|
||||
c = resources.contents(self.anchor01)
|
||||
self.assertGreaterEqual(
|
||||
set(c),
|
||||
{'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'},
|
||||
)
|
||||
with self.assertRaises(OSError), warnings_helper.check_warnings((
|
||||
".*contents.*",
|
||||
DeprecationWarning,
|
||||
)):
|
||||
assert set(c) >= {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}
|
||||
with (
|
||||
self.assertRaises(OSError),
|
||||
warnings_helper.check_warnings((
|
||||
".*contents.*",
|
||||
DeprecationWarning,
|
||||
)),
|
||||
):
|
||||
list(resources.contents(self.anchor01, 'utf-8.file'))
|
||||
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
with self.assertRaises(OSError), warnings_helper.check_warnings((
|
||||
".*contents.*",
|
||||
DeprecationWarning,
|
||||
)):
|
||||
with (
|
||||
self.assertRaises((OSError, resources.abc.TraversalError)),
|
||||
warnings_helper.check_warnings((
|
||||
".*contents.*",
|
||||
DeprecationWarning,
|
||||
)),
|
||||
):
|
||||
list(resources.contents(self.anchor01, *path_parts))
|
||||
with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)):
|
||||
c = resources.contents(self.anchor01, 'subdirectory')
|
||||
self.assertGreaterEqual(
|
||||
set(c),
|
||||
{'binary.file'},
|
||||
)
|
||||
assert set(c) >= {'binary.file'}
|
||||
|
||||
@warnings_helper.ignore_warnings(category=DeprecationWarning)
|
||||
def test_common_errors(self):
|
||||
@@ -233,17 +234,28 @@ class FunctionalAPIBase(util.DiskSetup):
|
||||
)
|
||||
|
||||
|
||||
class FunctionalAPITest_StringAnchor(
|
||||
class FunctionalAPITest_StringAnchor_Disk(
|
||||
StringAnchorMixin,
|
||||
FunctionalAPIBase,
|
||||
util.DiskSetup,
|
||||
unittest.TestCase,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class FunctionalAPITest_ModuleAnchor(
|
||||
class FunctionalAPITest_ModuleAnchor_Disk(
|
||||
ModuleAnchorMixin,
|
||||
FunctionalAPIBase,
|
||||
util.DiskSetup,
|
||||
unittest.TestCase,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class FunctionalAPITest_StringAnchor_Memory(
|
||||
StringAnchorMixin,
|
||||
FunctionalAPIBase,
|
||||
util.MemorySetup,
|
||||
unittest.TestCase,
|
||||
):
|
||||
pass
|
||||
+6
-7
@@ -1,6 +1,6 @@
|
||||
import importlib.resources as resources
|
||||
import unittest
|
||||
|
||||
from importlib import resources
|
||||
from . import util
|
||||
|
||||
|
||||
@@ -23,19 +23,19 @@ class OpenTests:
|
||||
target = resources.files(self.data) / 'binary.file'
|
||||
with target.open('rb') as fp:
|
||||
result = fp.read()
|
||||
self.assertEqual(result, bytes(range(4)))
|
||||
assert result == bytes(range(4))
|
||||
|
||||
def test_open_text_default_encoding(self):
|
||||
target = resources.files(self.data) / 'utf-8.file'
|
||||
with target.open(encoding='utf-8') as fp:
|
||||
result = fp.read()
|
||||
self.assertEqual(result, 'Hello, UTF-8 world!\n')
|
||||
assert result == 'Hello, UTF-8 world!\n'
|
||||
|
||||
def test_open_text_given_encoding(self):
|
||||
target = resources.files(self.data) / 'utf-16.file'
|
||||
with target.open(encoding='utf-16', errors='strict') as fp:
|
||||
result = fp.read()
|
||||
self.assertEqual(result, 'Hello, UTF-16 world!\n')
|
||||
assert result == 'Hello, UTF-16 world!\n'
|
||||
|
||||
def test_open_text_with_errors(self):
|
||||
"""
|
||||
@@ -46,11 +46,10 @@ class OpenTests:
|
||||
self.assertRaises(UnicodeError, fp.read)
|
||||
with target.open(encoding='utf-8', errors='ignore') as fp:
|
||||
result = fp.read()
|
||||
self.assertEqual(
|
||||
result,
|
||||
assert result == (
|
||||
'H\x00e\x00l\x00l\x00o\x00,\x00 '
|
||||
'\x00U\x00T\x00F\x00-\x001\x006\x00 '
|
||||
'\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00',
|
||||
'\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00'
|
||||
)
|
||||
|
||||
def test_open_binary_FileNotFoundError(self):
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import importlib.resources as resources
|
||||
import io
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
from importlib import resources
|
||||
from . import util
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ class PathTests:
|
||||
"""
|
||||
target = resources.files(self.data) / 'utf-8.file'
|
||||
with resources.as_file(target) as path:
|
||||
self.assertIsInstance(path, pathlib.Path)
|
||||
self.assertEndsWith(path.name, "utf-8.file")
|
||||
self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8'))
|
||||
assert isinstance(path, pathlib.Path)
|
||||
assert path.name.endswith("utf-8.file"), repr(path)
|
||||
assert 'Hello, UTF-8 world!\n' == path.read_text(encoding='utf-8')
|
||||
|
||||
|
||||
class PathDiskTests(PathTests, util.DiskSetup, unittest.TestCase):
|
||||
|
||||
+17
-15
@@ -1,6 +1,6 @@
|
||||
import importlib.resources as resources
|
||||
import unittest
|
||||
|
||||
from importlib import import_module, resources
|
||||
from importlib import import_module
|
||||
|
||||
from . import util
|
||||
|
||||
@@ -18,23 +18,25 @@ class CommonTextTests(util.CommonTests, unittest.TestCase):
|
||||
class ReadTests:
|
||||
def test_read_bytes(self):
|
||||
result = resources.files(self.data).joinpath('binary.file').read_bytes()
|
||||
self.assertEqual(result, bytes(range(4)))
|
||||
assert result == bytes(range(4))
|
||||
|
||||
def test_read_text_default_encoding(self):
|
||||
result = (
|
||||
resources.files(self.data)
|
||||
resources
|
||||
.files(self.data)
|
||||
.joinpath('utf-8.file')
|
||||
.read_text(encoding='utf-8')
|
||||
)
|
||||
self.assertEqual(result, 'Hello, UTF-8 world!\n')
|
||||
assert result == 'Hello, UTF-8 world!\n'
|
||||
|
||||
def test_read_text_given_encoding(self):
|
||||
result = (
|
||||
resources.files(self.data)
|
||||
resources
|
||||
.files(self.data)
|
||||
.joinpath('utf-16.file')
|
||||
.read_text(encoding='utf-16')
|
||||
)
|
||||
self.assertEqual(result, 'Hello, UTF-16 world!\n')
|
||||
assert result == 'Hello, UTF-16 world!\n'
|
||||
|
||||
def test_read_text_with_errors(self):
|
||||
"""
|
||||
@@ -43,11 +45,10 @@ class ReadTests:
|
||||
target = resources.files(self.data) / 'utf-16.file'
|
||||
self.assertRaises(UnicodeError, target.read_text, encoding='utf-8')
|
||||
result = target.read_text(encoding='utf-8', errors='ignore')
|
||||
self.assertEqual(
|
||||
result,
|
||||
assert result == (
|
||||
'H\x00e\x00l\x00l\x00o\x00,\x00 '
|
||||
'\x00U\x00T\x00F\x00-\x001\x006\x00 '
|
||||
'\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00',
|
||||
'\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00'
|
||||
)
|
||||
|
||||
|
||||
@@ -59,13 +60,13 @@ class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
|
||||
def test_read_submodule_resource(self):
|
||||
submodule = import_module('data01.subdirectory')
|
||||
result = resources.files(submodule).joinpath('binary.file').read_bytes()
|
||||
self.assertEqual(result, bytes(range(4, 8)))
|
||||
assert result == bytes(range(4, 8))
|
||||
|
||||
def test_read_submodule_resource_by_name(self):
|
||||
result = (
|
||||
resources.files('data01.subdirectory').joinpath('binary.file').read_bytes()
|
||||
)
|
||||
self.assertEqual(result, bytes(range(4, 8)))
|
||||
assert result == bytes(range(4, 8))
|
||||
|
||||
|
||||
class ReadNamespaceTests(ReadTests, util.DiskSetup, unittest.TestCase):
|
||||
@@ -78,15 +79,16 @@ class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
|
||||
def test_read_submodule_resource(self):
|
||||
submodule = import_module('namespacedata01.subdirectory')
|
||||
result = resources.files(submodule).joinpath('binary.file').read_bytes()
|
||||
self.assertEqual(result, bytes(range(12, 16)))
|
||||
assert result == bytes(range(12, 16))
|
||||
|
||||
def test_read_submodule_resource_by_name(self):
|
||||
result = (
|
||||
resources.files('namespacedata01.subdirectory')
|
||||
resources
|
||||
.files('namespacedata01.subdirectory')
|
||||
.joinpath('binary.file')
|
||||
.read_bytes()
|
||||
)
|
||||
self.assertEqual(result, bytes(range(12, 16)))
|
||||
assert result == bytes(range(12, 16))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+25
-38
@@ -1,9 +1,8 @@
|
||||
import os.path
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
from importlib import import_module
|
||||
from importlib.readers import MultiplexedPath, NamespaceReader
|
||||
from importlib.resources.readers import MultiplexedPath, NamespaceReader
|
||||
|
||||
from . import util
|
||||
|
||||
@@ -31,9 +30,7 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
|
||||
contents.remove('__pycache__')
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
self.assertEqual(
|
||||
contents, {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'}
|
||||
)
|
||||
assert contents == {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'}
|
||||
|
||||
def test_iterdir_duplicate(self):
|
||||
contents = {
|
||||
@@ -44,16 +41,19 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
|
||||
contents.remove(remove)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
self.assertEqual(
|
||||
contents,
|
||||
{'__init__.py', 'binary.file', 'subdirectory', 'utf-16.file', 'utf-8.file'},
|
||||
)
|
||||
assert contents == {
|
||||
'__init__.py',
|
||||
'binary.file',
|
||||
'subdirectory',
|
||||
'utf-16.file',
|
||||
'utf-8.file',
|
||||
}
|
||||
|
||||
def test_is_dir(self):
|
||||
self.assertEqual(MultiplexedPath(self.folder).is_dir(), True)
|
||||
assert MultiplexedPath(self.folder).is_dir()
|
||||
|
||||
def test_is_file(self):
|
||||
self.assertEqual(MultiplexedPath(self.folder).is_file(), False)
|
||||
assert not MultiplexedPath(self.folder).is_file()
|
||||
|
||||
def test_open_file(self):
|
||||
path = MultiplexedPath(self.folder)
|
||||
@@ -67,19 +67,17 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
|
||||
def test_join_path(self):
|
||||
prefix = str(self.folder.parent)
|
||||
path = MultiplexedPath(self.folder, self.data01)
|
||||
self.assertEqual(
|
||||
str(path.joinpath('binary.file'))[len(prefix) + 1 :],
|
||||
os.path.join('namespacedata01', 'binary.file'),
|
||||
assert str(path.joinpath('binary.file'))[len(prefix) + 1 :] == os.path.join(
|
||||
'namespacedata01', 'binary.file'
|
||||
)
|
||||
sub = path.joinpath('subdirectory')
|
||||
assert isinstance(sub, MultiplexedPath)
|
||||
assert 'namespacedata01' in str(sub)
|
||||
assert 'data01' in str(sub)
|
||||
self.assertEqual(
|
||||
str(path.joinpath('imaginary'))[len(prefix) + 1 :],
|
||||
os.path.join('namespacedata01', 'imaginary'),
|
||||
assert str(path.joinpath('imaginary'))[len(prefix) + 1 :] == os.path.join(
|
||||
'namespacedata01', 'imaginary'
|
||||
)
|
||||
self.assertEqual(path.joinpath(), path)
|
||||
assert path.joinpath() == path
|
||||
|
||||
def test_join_path_compound(self):
|
||||
path = MultiplexedPath(self.folder)
|
||||
@@ -88,23 +86,16 @@ class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):
|
||||
def test_join_path_common_subdir(self):
|
||||
prefix = str(self.data02.parent)
|
||||
path = MultiplexedPath(self.data01, self.data02)
|
||||
self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath)
|
||||
self.assertEqual(
|
||||
str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :],
|
||||
os.path.join('data02', 'subdirectory', 'subsubdir'),
|
||||
assert isinstance(path.joinpath('subdirectory'), MultiplexedPath)
|
||||
assert str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :] == (
|
||||
os.path.join('data02', 'subdirectory', 'subsubdir')
|
||||
)
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual(
|
||||
repr(MultiplexedPath(self.folder)),
|
||||
f"MultiplexedPath('{self.folder}')",
|
||||
)
|
||||
assert repr(MultiplexedPath(self.folder)) == f"MultiplexedPath('{self.folder}')"
|
||||
|
||||
def test_name(self):
|
||||
self.assertEqual(
|
||||
MultiplexedPath(self.folder).name,
|
||||
os.path.basename(self.folder),
|
||||
)
|
||||
assert MultiplexedPath(self.folder).name == os.path.basename(self.folder)
|
||||
|
||||
|
||||
class NamespaceReaderTest(util.DiskSetup, unittest.TestCase):
|
||||
@@ -119,18 +110,14 @@ class NamespaceReaderTest(util.DiskSetup, unittest.TestCase):
|
||||
reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
|
||||
|
||||
root = self.data.__path__[0]
|
||||
self.assertEqual(
|
||||
reader.resource_path('binary.file'), os.path.join(root, 'binary.file')
|
||||
)
|
||||
self.assertEqual(
|
||||
reader.resource_path('imaginary'), os.path.join(root, 'imaginary')
|
||||
)
|
||||
assert reader.resource_path('binary.file') == os.path.join(root, 'binary.file')
|
||||
assert reader.resource_path('imaginary') == os.path.join(root, 'imaginary')
|
||||
|
||||
def test_files(self):
|
||||
reader = NamespaceReader(self.data.__spec__.submodule_search_locations)
|
||||
root = self.data.__path__[0]
|
||||
self.assertIsInstance(reader.files(), MultiplexedPath)
|
||||
self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')")
|
||||
assert isinstance(reader.files(), MultiplexedPath)
|
||||
assert repr(reader.files()) == f"MultiplexedPath('{root}')"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+48
-42
@@ -1,7 +1,9 @@
|
||||
import importlib.resources as resources
|
||||
import types
|
||||
import unittest
|
||||
from importlib import import_module
|
||||
|
||||
from . import util
|
||||
from importlib import resources, import_module
|
||||
|
||||
|
||||
class ResourceTests:
|
||||
@@ -9,16 +11,16 @@ class ResourceTests:
|
||||
|
||||
def test_is_file_exists(self):
|
||||
target = resources.files(self.data) / 'binary.file'
|
||||
self.assertTrue(target.is_file())
|
||||
assert target.is_file()
|
||||
|
||||
def test_is_file_missing(self):
|
||||
target = resources.files(self.data) / 'not-a-file'
|
||||
self.assertFalse(target.is_file())
|
||||
assert not target.is_file()
|
||||
|
||||
def test_is_dir(self):
|
||||
target = resources.files(self.data) / 'subdirectory'
|
||||
self.assertFalse(target.is_file())
|
||||
self.assertTrue(target.is_dir())
|
||||
assert not target.is_file()
|
||||
assert target.is_dir()
|
||||
|
||||
|
||||
class ResourceDiskTests(ResourceTests, util.DiskSetup, unittest.TestCase):
|
||||
@@ -38,7 +40,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
|
||||
package = util.create_package(
|
||||
file=self.data, path=self.data.__file__, contents=['A', 'B', 'C']
|
||||
)
|
||||
self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'})
|
||||
assert names(resources.files(package)) == {'A', 'B', 'C'}
|
||||
|
||||
def test_is_file(self):
|
||||
package = util.create_package(
|
||||
@@ -46,7 +48,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
|
||||
path=self.data.__file__,
|
||||
contents=['A', 'B', 'C', 'D/E', 'D/F'],
|
||||
)
|
||||
self.assertTrue(resources.files(package).joinpath('B').is_file())
|
||||
assert resources.files(package).joinpath('B').is_file()
|
||||
|
||||
def test_is_dir(self):
|
||||
package = util.create_package(
|
||||
@@ -54,7 +56,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
|
||||
path=self.data.__file__,
|
||||
contents=['A', 'B', 'C', 'D/E', 'D/F'],
|
||||
)
|
||||
self.assertTrue(resources.files(package).joinpath('D').is_dir())
|
||||
assert resources.files(package).joinpath('D').is_dir()
|
||||
|
||||
def test_resource_missing(self):
|
||||
package = util.create_package(
|
||||
@@ -62,7 +64,7 @@ class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):
|
||||
path=self.data.__file__,
|
||||
contents=['A', 'B', 'C', 'D/E', 'D/F'],
|
||||
)
|
||||
self.assertFalse(resources.files(package).joinpath('Z').is_file())
|
||||
assert not resources.files(package).joinpath('Z').is_file()
|
||||
|
||||
|
||||
class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase):
|
||||
@@ -82,30 +84,26 @@ class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase):
|
||||
module.__file__ = '/path/which/shall/not/be/named'
|
||||
module.__spec__.loader = module.__loader__
|
||||
module.__spec__.origin = module.__file__
|
||||
self.assertFalse(resources.files(module).joinpath('A').is_file())
|
||||
assert not resources.files(module).joinpath('A').is_file()
|
||||
|
||||
|
||||
class ResourceFromZipsTest01(util.ZipSetup, unittest.TestCase):
|
||||
def test_is_submodule_resource(self):
|
||||
submodule = import_module('data01.subdirectory')
|
||||
self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file())
|
||||
assert resources.files(submodule).joinpath('binary.file').is_file()
|
||||
|
||||
def test_read_submodule_resource_by_name(self):
|
||||
self.assertTrue(
|
||||
resources.files('data01.subdirectory').joinpath('binary.file').is_file()
|
||||
)
|
||||
assert resources.files('data01.subdirectory').joinpath('binary.file').is_file()
|
||||
|
||||
def test_submodule_contents(self):
|
||||
submodule = import_module('data01.subdirectory')
|
||||
self.assertEqual(
|
||||
names(resources.files(submodule)), {'__init__.py', 'binary.file'}
|
||||
)
|
||||
assert names(resources.files(submodule)) == {'__init__.py', 'binary.file'}
|
||||
|
||||
def test_submodule_contents_by_name(self):
|
||||
self.assertEqual(
|
||||
names(resources.files('data01.subdirectory')),
|
||||
{'__init__.py', 'binary.file'},
|
||||
)
|
||||
assert names(resources.files('data01.subdirectory')) == {
|
||||
'__init__.py',
|
||||
'binary.file',
|
||||
}
|
||||
|
||||
def test_as_file_directory(self):
|
||||
with resources.as_file(resources.files('data01')) as data:
|
||||
@@ -124,14 +122,8 @@ class ResourceFromZipsTest02(util.ZipSetup, unittest.TestCase):
|
||||
Test thata zip with two unrelated subpackages return
|
||||
distinct resources. Ref python/importlib_resources#44.
|
||||
"""
|
||||
self.assertEqual(
|
||||
names(resources.files('data02.one')),
|
||||
{'__init__.py', 'resource1.txt'},
|
||||
)
|
||||
self.assertEqual(
|
||||
names(resources.files('data02.two')),
|
||||
{'__init__.py', 'resource2.txt'},
|
||||
)
|
||||
assert names(resources.files('data02.one')) == {'__init__.py', 'resource1.txt'}
|
||||
assert names(resources.files('data02.two')) == {'__init__.py', 'resource2.txt'}
|
||||
|
||||
|
||||
class DeletingZipsTest(util.ZipSetup, unittest.TestCase):
|
||||
@@ -168,16 +160,15 @@ class DeletingZipsTest(util.ZipSetup, unittest.TestCase):
|
||||
|
||||
class ResourceFromNamespaceTests:
|
||||
def test_is_submodule_resource(self):
|
||||
self.assertTrue(
|
||||
resources.files(import_module('namespacedata01'))
|
||||
assert (
|
||||
resources
|
||||
.files(import_module('namespacedata01'))
|
||||
.joinpath('binary.file')
|
||||
.is_file()
|
||||
)
|
||||
|
||||
def test_read_submodule_resource_by_name(self):
|
||||
self.assertTrue(
|
||||
resources.files('namespacedata01').joinpath('binary.file').is_file()
|
||||
)
|
||||
assert resources.files('namespacedata01').joinpath('binary.file').is_file()
|
||||
|
||||
def test_submodule_contents(self):
|
||||
contents = names(resources.files(import_module('namespacedata01')))
|
||||
@@ -185,9 +176,7 @@ class ResourceFromNamespaceTests:
|
||||
contents.remove('__pycache__')
|
||||
except KeyError:
|
||||
pass
|
||||
self.assertEqual(
|
||||
contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
|
||||
)
|
||||
assert contents == {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
|
||||
|
||||
def test_submodule_contents_by_name(self):
|
||||
contents = names(resources.files('namespacedata01'))
|
||||
@@ -195,9 +184,7 @@ class ResourceFromNamespaceTests:
|
||||
contents.remove('__pycache__')
|
||||
except KeyError:
|
||||
pass
|
||||
self.assertEqual(
|
||||
contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
|
||||
)
|
||||
assert contents == {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
|
||||
|
||||
def test_submodule_sub_contents(self):
|
||||
contents = names(resources.files(import_module('namespacedata01.subdirectory')))
|
||||
@@ -205,7 +192,7 @@ class ResourceFromNamespaceTests:
|
||||
contents.remove('__pycache__')
|
||||
except KeyError:
|
||||
pass
|
||||
self.assertEqual(contents, {'binary.file'})
|
||||
assert contents == {'binary.file'}
|
||||
|
||||
def test_submodule_sub_contents_by_name(self):
|
||||
contents = names(resources.files('namespacedata01.subdirectory'))
|
||||
@@ -213,7 +200,7 @@ class ResourceFromNamespaceTests:
|
||||
contents.remove('__pycache__')
|
||||
except KeyError:
|
||||
pass
|
||||
self.assertEqual(contents, {'binary.file'})
|
||||
assert contents == {'binary.file'}
|
||||
|
||||
|
||||
class ResourceFromNamespaceDiskTests(
|
||||
@@ -232,5 +219,24 @@ class ResourceFromNamespaceZipTests(
|
||||
MODULE = 'namespacedata01'
|
||||
|
||||
|
||||
class MainModuleTests(unittest.TestCase):
|
||||
def test_main_module_with_none_spec(self):
|
||||
"""
|
||||
__main__ module with no spec should raise TypeError (for clarity).
|
||||
|
||||
See python/cpython#138531 for details.
|
||||
"""
|
||||
# construct a __main__ module with no __spec__.
|
||||
mainmodule = types.ModuleType("__main__")
|
||||
|
||||
assert mainmodule.__spec__ is None
|
||||
|
||||
with self.assertRaises(
|
||||
TypeError,
|
||||
msg="Cannot access resources for '__main__' as it does not appear to correspond to an importable module (its __spec__ is None).",
|
||||
):
|
||||
resources.files(mainmodule)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import unittest
|
||||
|
||||
from .util import MemorySetup, Traversable
|
||||
|
||||
|
||||
class TestMemoryTraversableImplementation(unittest.TestCase):
|
||||
def test_concrete_methods_are_not_overridden(self):
|
||||
"""`MemoryTraversable` must not override `Traversable` concrete methods.
|
||||
|
||||
This test is not an attempt to enforce a particular `Traversable` protocol;
|
||||
it merely catches changes in the `Traversable` abstract/concrete methods
|
||||
that have not been mirrored in the `MemoryTraversable` subclass.
|
||||
"""
|
||||
|
||||
traversable_concrete_methods = {
|
||||
method
|
||||
for method, value in Traversable.__dict__.items()
|
||||
if callable(value) and method not in Traversable.__abstractmethods__
|
||||
}
|
||||
memory_traversable_concrete_methods = {
|
||||
method
|
||||
for method, value in MemorySetup.MemoryTraversable.__dict__.items()
|
||||
if callable(value) and not method.startswith("__")
|
||||
}
|
||||
overridden_methods = (
|
||||
memory_traversable_concrete_methods & traversable_concrete_methods
|
||||
)
|
||||
|
||||
assert not overridden_methods
|
||||
@@ -1,53 +0,0 @@
|
||||
"""
|
||||
Generate the zip test data files.
|
||||
|
||||
Run to build the tests/zipdataNN/ziptestdata.zip files from
|
||||
files in tests/dataNN.
|
||||
|
||||
Replaces the file with the working copy, but does commit anything
|
||||
to the source repo.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import pathlib
|
||||
import zipfile
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
>>> from unittest import mock
|
||||
>>> monkeypatch = getfixture('monkeypatch')
|
||||
>>> monkeypatch.setattr(zipfile, 'ZipFile', mock.MagicMock())
|
||||
>>> print(); main() # print workaround for bpo-32509
|
||||
<BLANKLINE>
|
||||
...data01... -> ziptestdata/...
|
||||
...
|
||||
...data02... -> ziptestdata/...
|
||||
...
|
||||
"""
|
||||
suffixes = '01', '02'
|
||||
tuple(map(generate, suffixes))
|
||||
|
||||
|
||||
def generate(suffix):
|
||||
root = pathlib.Path(__file__).parent.relative_to(os.getcwd())
|
||||
zfpath = root / f'zipdata{suffix}/ziptestdata.zip'
|
||||
with zipfile.ZipFile(zfpath, 'w') as zf:
|
||||
for src, rel in walk(root / f'data{suffix}'):
|
||||
dst = 'ziptestdata' / pathlib.PurePosixPath(rel.as_posix())
|
||||
print(src, '->', dst)
|
||||
zf.write(src, dst)
|
||||
|
||||
|
||||
def walk(datapath):
|
||||
for dirpath, dirnames, filenames in os.walk(datapath):
|
||||
with contextlib.suppress(ValueError):
|
||||
dirnames.remove('__pycache__')
|
||||
for filename in filenames:
|
||||
res = pathlib.Path(dirpath) / filename
|
||||
rel = res.relative_to(datapath)
|
||||
yield res, rel
|
||||
|
||||
|
||||
__name__ == '__main__' and main()
|
||||
+114
-11
@@ -1,18 +1,18 @@
|
||||
import abc
|
||||
import contextlib
|
||||
import functools
|
||||
import importlib
|
||||
import io
|
||||
import pathlib
|
||||
import sys
|
||||
import types
|
||||
import pathlib
|
||||
import contextlib
|
||||
|
||||
from importlib.resources.abc import ResourceReader
|
||||
from test.support import import_helper, os_helper
|
||||
from . import zip as zip_
|
||||
from . import _path
|
||||
|
||||
|
||||
from importlib.machinery import ModuleSpec
|
||||
from importlib.resources.abc import ResourceReader, Traversable, TraversableResources
|
||||
|
||||
from test.support import import_helper, os_helper
|
||||
|
||||
from . import _path
|
||||
from . import zip as zip_
|
||||
|
||||
|
||||
class Reader(ResourceReader):
|
||||
@@ -122,7 +122,7 @@ class CommonTestsBase(metaclass=abc.ABCMeta):
|
||||
bytes_data = io.BytesIO(b'Hello, world!')
|
||||
package = create_package(file=bytes_data, path=FileNotFoundError())
|
||||
self.execute(package, 'utf-8.file')
|
||||
self.assertEqual(package.__loader__._path, 'utf-8.file')
|
||||
assert package.__loader__._path == 'utf-8.file'
|
||||
|
||||
def test_extant_path(self):
|
||||
# Attempting to open or read or request the path when the
|
||||
@@ -133,7 +133,7 @@ class CommonTestsBase(metaclass=abc.ABCMeta):
|
||||
path = __file__
|
||||
package = create_package(file=bytes_data, path=path)
|
||||
self.execute(package, 'utf-8.file')
|
||||
self.assertEqual(package.__loader__._path, 'utf-8.file')
|
||||
assert package.__loader__._path == 'utf-8.file'
|
||||
|
||||
def test_useless_loader(self):
|
||||
package = create_package(file=FileNotFoundError(), path=FileNotFoundError())
|
||||
@@ -202,5 +202,108 @@ class DiskSetup(ModuleSetup):
|
||||
self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir))
|
||||
|
||||
|
||||
class MemorySetup(ModuleSetup):
|
||||
"""Support loading a module in memory."""
|
||||
|
||||
MODULE = 'data01'
|
||||
|
||||
def load_fixture(self, module):
|
||||
self.fixtures.enter_context(self.augment_sys_metapath(module))
|
||||
return importlib.import_module(module)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def augment_sys_metapath(self, module):
|
||||
finder_instance = self.MemoryFinder(module)
|
||||
sys.meta_path.append(finder_instance)
|
||||
yield
|
||||
sys.meta_path.remove(finder_instance)
|
||||
|
||||
class MemoryFinder(importlib.abc.MetaPathFinder):
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if fullname != self._module:
|
||||
return None
|
||||
|
||||
return importlib.machinery.ModuleSpec(
|
||||
name=fullname,
|
||||
loader=MemorySetup.MemoryLoader(self._module),
|
||||
is_package=True,
|
||||
)
|
||||
|
||||
class MemoryLoader(importlib.abc.Loader):
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
def get_resource_reader(self, fullname):
|
||||
return MemorySetup.MemoryTraversableResources(self._module, fullname)
|
||||
|
||||
class MemoryTraversableResources(TraversableResources):
|
||||
def __init__(self, module, fullname):
|
||||
self._module = module
|
||||
self._fullname = fullname
|
||||
|
||||
def files(self):
|
||||
return MemorySetup.MemoryTraversable(self._module, self._fullname)
|
||||
|
||||
class MemoryTraversable(Traversable):
|
||||
"""Implement only the abstract methods of `Traversable`.
|
||||
|
||||
Besides `.__init__()`, no other methods may be implemented or overridden.
|
||||
This is critical for validating the concrete `Traversable` implementations.
|
||||
"""
|
||||
|
||||
def __init__(self, module, fullname):
|
||||
self._module = module
|
||||
self._fullname = fullname
|
||||
|
||||
def _resolve(self):
|
||||
"""
|
||||
Fully traverse the `fixtures` dictionary.
|
||||
|
||||
This should be wrapped in a `try/except KeyError`
|
||||
but it is not currently needed and lowers the code coverage numbers.
|
||||
"""
|
||||
path = pathlib.PurePosixPath(self._fullname)
|
||||
return functools.reduce(lambda d, p: d[p], path.parts, fixtures)
|
||||
|
||||
def iterdir(self):
|
||||
directory = self._resolve()
|
||||
if not isinstance(directory, dict):
|
||||
# Filesystem openers raise OSError, and that exception is mirrored here.
|
||||
raise OSError(f"{self._fullname} is not a directory")
|
||||
for path in directory:
|
||||
yield MemorySetup.MemoryTraversable(
|
||||
self._module, f"{self._fullname}/{path}"
|
||||
)
|
||||
|
||||
def is_dir(self) -> bool:
|
||||
return isinstance(self._resolve(), dict)
|
||||
|
||||
def is_file(self) -> bool:
|
||||
return not self.is_dir()
|
||||
|
||||
def open(self, mode='r', encoding=None, errors=None, *_, **__):
|
||||
contents = self._resolve()
|
||||
if isinstance(contents, dict):
|
||||
# Filesystem openers raise OSError when attempting to open a directory,
|
||||
# and that exception is mirrored here.
|
||||
raise OSError(f"{self._fullname} is a directory")
|
||||
if isinstance(contents, str):
|
||||
contents = contents.encode("utf-8")
|
||||
result = io.BytesIO(contents)
|
||||
if "b" in mode:
|
||||
return result
|
||||
return io.TextIOWrapper(result, encoding=encoding, errors=errors)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return pathlib.PurePosixPath(self._fullname).name
|
||||
|
||||
|
||||
class CommonTests(DiskSetup, CommonTestsBase):
|
||||
pass
|
||||
Whitespace-only changes.
Binary file not shown.
Whitespace-only changes.
Binary file not shown.
+18
-219
@@ -5,24 +5,21 @@ importlib_abc = util.import_importlib('importlib.abc')
|
||||
machinery = util.import_importlib('importlib.machinery')
|
||||
importlib_util = util.import_importlib('importlib.util')
|
||||
|
||||
import errno
|
||||
import marshal
|
||||
import os
|
||||
import py_compile
|
||||
import shutil
|
||||
import stat
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
from test.support.import_helper import make_legacy_pyc, unload
|
||||
from test.support.import_helper import make_legacy_pyc
|
||||
|
||||
from test.test_py_compile import without_source_date_epoch
|
||||
from test.test_py_compile import SourceDateEpochTestMeta
|
||||
from test.support.os_helper import without_source_date_epoch
|
||||
from test.support.os_helper import SourceDateEpochTestMeta
|
||||
|
||||
|
||||
class SimpleTest(abc.LoaderTests):
|
||||
class SimpleTest:
|
||||
|
||||
"""Should have no issue importing a source module [basic]. And if there is
|
||||
a syntax error, it should raise a SyntaxError [syntax error].
|
||||
@@ -34,17 +31,6 @@ class SimpleTest(abc.LoaderTests):
|
||||
self.filepath = os.path.join('ham', self.name + '.py')
|
||||
self.loader = self.machinery.SourceFileLoader(self.name, self.filepath)
|
||||
|
||||
def test_load_module_API(self):
|
||||
class Tester(self.abc.FileLoader):
|
||||
def get_source(self, _): return 'attr = 42'
|
||||
def is_package(self, _): return False
|
||||
|
||||
loader = Tester('blah', 'blah.py')
|
||||
self.addCleanup(unload, 'blah')
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
module = loader.load_module() # Should not raise an exception.
|
||||
|
||||
def test_get_filename_API(self):
|
||||
# If fullname is not set then assume self.path is desired.
|
||||
class Tester(self.abc.FileLoader):
|
||||
@@ -69,173 +55,11 @@ class SimpleTest(abc.LoaderTests):
|
||||
other = self.machinery.SourceFileLoader('_' + self.name, self.filepath)
|
||||
self.assertNotEqual(self.loader, other)
|
||||
|
||||
# [basic]
|
||||
def test_module(self):
|
||||
with util.create_modules('_temp') as mapping:
|
||||
loader = self.machinery.SourceFileLoader('_temp', mapping['_temp'])
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
module = loader.load_module('_temp')
|
||||
self.assertIn('_temp', sys.modules)
|
||||
check = {'__name__': '_temp', '__file__': mapping['_temp'],
|
||||
'__package__': ''}
|
||||
for attr, value in check.items():
|
||||
self.assertEqual(getattr(module, attr), value)
|
||||
|
||||
def test_package(self):
|
||||
with util.create_modules('_pkg.__init__') as mapping:
|
||||
loader = self.machinery.SourceFileLoader('_pkg',
|
||||
mapping['_pkg.__init__'])
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
module = loader.load_module('_pkg')
|
||||
self.assertIn('_pkg', sys.modules)
|
||||
check = {'__name__': '_pkg', '__file__': mapping['_pkg.__init__'],
|
||||
'__path__': [os.path.dirname(mapping['_pkg.__init__'])],
|
||||
'__package__': '_pkg'}
|
||||
for attr, value in check.items():
|
||||
self.assertEqual(getattr(module, attr), value)
|
||||
|
||||
|
||||
def test_lacking_parent(self):
|
||||
with util.create_modules('_pkg.__init__', '_pkg.mod')as mapping:
|
||||
loader = self.machinery.SourceFileLoader('_pkg.mod',
|
||||
mapping['_pkg.mod'])
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
module = loader.load_module('_pkg.mod')
|
||||
self.assertIn('_pkg.mod', sys.modules)
|
||||
check = {'__name__': '_pkg.mod', '__file__': mapping['_pkg.mod'],
|
||||
'__package__': '_pkg'}
|
||||
for attr, value in check.items():
|
||||
self.assertEqual(getattr(module, attr), value)
|
||||
|
||||
def fake_mtime(self, fxn):
|
||||
"""Fake mtime to always be higher than expected."""
|
||||
return lambda name: fxn(name) + 1
|
||||
|
||||
def test_module_reuse(self):
|
||||
with util.create_modules('_temp') as mapping:
|
||||
loader = self.machinery.SourceFileLoader('_temp', mapping['_temp'])
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
module = loader.load_module('_temp')
|
||||
module_id = id(module)
|
||||
module_dict_id = id(module.__dict__)
|
||||
with open(mapping['_temp'], 'w', encoding='utf-8') as file:
|
||||
file.write("testing_var = 42\n")
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
module = loader.load_module('_temp')
|
||||
self.assertIn('testing_var', module.__dict__,
|
||||
"'testing_var' not in "
|
||||
"{0}".format(list(module.__dict__.keys())))
|
||||
self.assertEqual(module, sys.modules['_temp'])
|
||||
self.assertEqual(id(module), module_id)
|
||||
self.assertEqual(id(module.__dict__), module_dict_id)
|
||||
|
||||
def test_state_after_failure(self):
|
||||
# A failed reload should leave the original module intact.
|
||||
attributes = ('__file__', '__path__', '__package__')
|
||||
value = '<test>'
|
||||
name = '_temp'
|
||||
with util.create_modules(name) as mapping:
|
||||
orig_module = types.ModuleType(name)
|
||||
for attr in attributes:
|
||||
setattr(orig_module, attr, value)
|
||||
with open(mapping[name], 'w', encoding='utf-8') as file:
|
||||
file.write('+++ bad syntax +++')
|
||||
loader = self.machinery.SourceFileLoader('_temp', mapping['_temp'])
|
||||
with self.assertRaises(SyntaxError):
|
||||
loader.exec_module(orig_module)
|
||||
for attr in attributes:
|
||||
self.assertEqual(getattr(orig_module, attr), value)
|
||||
with self.assertRaises(SyntaxError):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
loader.load_module(name)
|
||||
for attr in attributes:
|
||||
self.assertEqual(getattr(orig_module, attr), value)
|
||||
|
||||
# [syntax error]
|
||||
def test_bad_syntax(self):
|
||||
with util.create_modules('_temp') as mapping:
|
||||
with open(mapping['_temp'], 'w', encoding='utf-8') as file:
|
||||
file.write('=')
|
||||
loader = self.machinery.SourceFileLoader('_temp', mapping['_temp'])
|
||||
with self.assertRaises(SyntaxError):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
loader.load_module('_temp')
|
||||
self.assertNotIn('_temp', sys.modules)
|
||||
|
||||
def test_file_from_empty_string_dir(self):
|
||||
# Loading a module found from an empty string entry on sys.path should
|
||||
# not only work, but keep all attributes relative.
|
||||
file_path = '_temp.py'
|
||||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
file.write("# test file for importlib")
|
||||
try:
|
||||
with util.uncache('_temp'):
|
||||
loader = self.machinery.SourceFileLoader('_temp', file_path)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
mod = loader.load_module('_temp')
|
||||
self.assertEqual(file_path, mod.__file__)
|
||||
self.assertEqual(self.util.cache_from_source(file_path),
|
||||
mod.__cached__)
|
||||
finally:
|
||||
os.unlink(file_path)
|
||||
pycache = os.path.dirname(self.util.cache_from_source(file_path))
|
||||
if os.path.exists(pycache):
|
||||
shutil.rmtree(pycache)
|
||||
|
||||
@util.writes_bytecode_files
|
||||
def test_timestamp_overflow(self):
|
||||
# When a modification timestamp is larger than 2**32, it should be
|
||||
# truncated rather than raise an OverflowError.
|
||||
with util.create_modules('_temp') as mapping:
|
||||
source = mapping['_temp']
|
||||
compiled = self.util.cache_from_source(source)
|
||||
with open(source, 'w', encoding='utf-8') as f:
|
||||
f.write("x = 5")
|
||||
try:
|
||||
os.utime(source, (2 ** 33 - 5, 2 ** 33 - 5))
|
||||
except OverflowError:
|
||||
self.skipTest("cannot set modification time to large integer")
|
||||
except OSError as e:
|
||||
if e.errno != getattr(errno, 'EOVERFLOW', None):
|
||||
raise
|
||||
self.skipTest("cannot set modification time to large integer ({})".format(e))
|
||||
loader = self.machinery.SourceFileLoader('_temp', mapping['_temp'])
|
||||
# PEP 451
|
||||
module = types.ModuleType('_temp')
|
||||
module.__spec__ = self.util.spec_from_loader('_temp', loader)
|
||||
loader.exec_module(module)
|
||||
self.assertEqual(module.x, 5)
|
||||
self.assertTrue(os.path.exists(compiled))
|
||||
os.unlink(compiled)
|
||||
# PEP 302
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
mod = loader.load_module('_temp')
|
||||
# Sanity checks.
|
||||
self.assertEqual(mod.__cached__, compiled)
|
||||
self.assertEqual(mod.x, 5)
|
||||
# The pyc file was created.
|
||||
self.assertTrue(os.path.exists(compiled))
|
||||
|
||||
def test_unloadable(self):
|
||||
loader = self.machinery.SourceFileLoader('good name', {})
|
||||
module = types.ModuleType('bad name')
|
||||
module.__spec__ = self.machinery.ModuleSpec('bad name', loader)
|
||||
with self.assertRaises(ImportError):
|
||||
loader.exec_module(module)
|
||||
with self.assertRaises(ImportError):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
loader.load_module('bad name')
|
||||
|
||||
@util.writes_bytecode_files
|
||||
def test_checked_hash_based_pyc(self):
|
||||
with util.create_modules('_temp') as mapping:
|
||||
@@ -386,12 +210,21 @@ class BadBytecodeTest:
|
||||
del sys.modules['_temp']
|
||||
except KeyError:
|
||||
pass
|
||||
py_compile.compile(mapping[name], invalidation_mode=invalidation_mode)
|
||||
if not del_source:
|
||||
bytecode_path = self.util.cache_from_source(mapping[name])
|
||||
if sys.implementation.cache_tag is None:
|
||||
if del_source:
|
||||
bytecode_path = mapping[name] + 'c'
|
||||
py_compile.compile(mapping[name], bytecode_path,
|
||||
invalidation_mode=invalidation_mode)
|
||||
os.unlink(mapping[name])
|
||||
else:
|
||||
raise unittest.SkipTest('requires sys.implementation.cache_tag')
|
||||
else:
|
||||
os.unlink(mapping[name])
|
||||
bytecode_path = make_legacy_pyc(mapping[name])
|
||||
py_compile.compile(mapping[name], invalidation_mode=invalidation_mode)
|
||||
if not del_source:
|
||||
bytecode_path = self.util.cache_from_source(mapping[name])
|
||||
else:
|
||||
os.unlink(mapping[name])
|
||||
bytecode_path = make_legacy_pyc(mapping[name])
|
||||
if manipulator:
|
||||
with open(bytecode_path, 'rb') as file:
|
||||
bc = file.read()
|
||||
@@ -511,16 +344,6 @@ class BadBytecodeTestPEP451(BadBytecodeTest):
|
||||
loader.exec_module(module)
|
||||
|
||||
|
||||
class BadBytecodeTestPEP302(BadBytecodeTest):
|
||||
|
||||
def import_(self, file, module_name):
|
||||
loader = self.loader(module_name, file)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
module = loader.load_module(module_name)
|
||||
self.assertIn(module_name, sys.modules)
|
||||
|
||||
|
||||
class SourceLoaderBadBytecodeTest:
|
||||
|
||||
@classmethod
|
||||
@@ -681,18 +504,6 @@ class SourceLoaderBadBytecodeTestPEP451(
|
||||
util=importlib_util)
|
||||
|
||||
|
||||
class SourceLoaderBadBytecodeTestPEP302(
|
||||
SourceLoaderBadBytecodeTest, BadBytecodeTestPEP302):
|
||||
pass
|
||||
|
||||
|
||||
(Frozen_SourceBadBytecodePEP302,
|
||||
Source_SourceBadBytecodePEP302
|
||||
) = util.test_both(SourceLoaderBadBytecodeTestPEP302, importlib=importlib,
|
||||
machinery=machinery, abc=importlib_abc,
|
||||
util=importlib_util)
|
||||
|
||||
|
||||
class SourcelessLoaderBadBytecodeTest:
|
||||
|
||||
@classmethod
|
||||
@@ -779,17 +590,5 @@ class SourcelessLoaderBadBytecodeTestPEP451(SourcelessLoaderBadBytecodeTest,
|
||||
util=importlib_util)
|
||||
|
||||
|
||||
class SourcelessLoaderBadBytecodeTestPEP302(SourcelessLoaderBadBytecodeTest,
|
||||
BadBytecodeTestPEP302):
|
||||
pass
|
||||
|
||||
|
||||
(Frozen_SourcelessBadBytecodePEP302,
|
||||
Source_SourcelessBadBytecodePEP302
|
||||
) = util.test_both(SourcelessLoaderBadBytecodeTestPEP302, importlib=importlib,
|
||||
machinery=machinery, abc=importlib_abc,
|
||||
util=importlib_util)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+6
-4
@@ -57,6 +57,8 @@ class FinderTests(abc.FinderTests):
|
||||
"""
|
||||
if create is None:
|
||||
create = {test}
|
||||
if (compile_ or unlink) and sys.implementation.cache_tag is None:
|
||||
raise unittest.SkipTest('requires sys.implementation.cache_tag')
|
||||
with util.create_modules(*create) as mapping:
|
||||
if compile_:
|
||||
for name in compile_:
|
||||
@@ -73,7 +75,7 @@ class FinderTests(abc.FinderTests):
|
||||
if error.errno != errno.ENOENT:
|
||||
raise
|
||||
loader = self.import_(mapping['.root'], test)
|
||||
self.assertHasAttr(loader, 'load_module')
|
||||
self.assertHasAttr(loader, 'exec_module')
|
||||
return loader
|
||||
|
||||
def test_module(self):
|
||||
@@ -100,7 +102,7 @@ class FinderTests(abc.FinderTests):
|
||||
with util.create_modules('pkg.__init__', 'pkg.sub') as mapping:
|
||||
pkg_dir = os.path.dirname(mapping['pkg.__init__'])
|
||||
loader = self.import_(pkg_dir, 'pkg.sub')
|
||||
self.assertHasAttr(loader, 'load_module')
|
||||
self.assertHasAttr(loader, 'exec_module')
|
||||
|
||||
# [sub package]
|
||||
def test_package_in_package(self):
|
||||
@@ -108,7 +110,7 @@ class FinderTests(abc.FinderTests):
|
||||
with context as mapping:
|
||||
pkg_dir = os.path.dirname(mapping['pkg.__init__'])
|
||||
loader = self.import_(pkg_dir, 'pkg.sub')
|
||||
self.assertHasAttr(loader, 'load_module')
|
||||
self.assertHasAttr(loader, 'exec_module')
|
||||
|
||||
# [package over modules]
|
||||
def test_package_over_module(self):
|
||||
@@ -129,7 +131,7 @@ class FinderTests(abc.FinderTests):
|
||||
file.write("# test file for importlib")
|
||||
try:
|
||||
loader = self._find(finder, 'mod', loader_only=True)
|
||||
self.assertHasAttr(loader, 'load_module')
|
||||
self.assertHasAttr(loader, 'exec_module')
|
||||
finally:
|
||||
os.unlink('mod.py')
|
||||
|
||||
|
||||
@@ -102,19 +102,6 @@ class EncodingTestPEP451(EncodingTest):
|
||||
) = util.test_both(EncodingTestPEP451, machinery=machinery)
|
||||
|
||||
|
||||
class EncodingTestPEP302(EncodingTest):
|
||||
|
||||
def load(self, loader):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
return loader.load_module(self.module_name)
|
||||
|
||||
|
||||
(Frozen_EncodingTestPEP302,
|
||||
Source_EncodingTestPEP302
|
||||
) = util.test_both(EncodingTestPEP302, machinery=machinery)
|
||||
|
||||
|
||||
class LineEndingTest:
|
||||
|
||||
r"""Source written with the three types of line endings (\n, \r\n, \r)
|
||||
@@ -158,18 +145,5 @@ class LineEndingTestPEP451(LineEndingTest):
|
||||
) = util.test_both(LineEndingTestPEP451, machinery=machinery)
|
||||
|
||||
|
||||
class LineEndingTestPEP302(LineEndingTest):
|
||||
|
||||
def load(self, loader, module_name):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
return loader.load_module(module_name)
|
||||
|
||||
|
||||
(Frozen_LineEndingTestPEP302,
|
||||
Source_LineEndingTestPEP302
|
||||
) = util.test_both(LineEndingTestPEP302, machinery=machinery)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
import unittest
|
||||
|
||||
|
||||
class fake_filesystem_unittest:
|
||||
"""
|
||||
Stubbed version of the pyfakefs module
|
||||
"""
|
||||
class TestCase(unittest.TestCase):
|
||||
def setUpPyfakefs(self):
|
||||
self.skipTest("pyfakefs not available")
|
||||
Vendored
+14
-127
@@ -194,10 +194,6 @@ class LoaderDefaultsTests(ABCTestHarness):
|
||||
spec = 'a spec'
|
||||
self.assertIsNone(self.ins.create_module(spec))
|
||||
|
||||
def test_load_module(self):
|
||||
with self.assertRaises(ImportError):
|
||||
self.ins.load_module('something')
|
||||
|
||||
def test_module_repr(self):
|
||||
mod = types.ModuleType('blah')
|
||||
with warnings.catch_warnings():
|
||||
@@ -358,38 +354,6 @@ class LoaderLoadModuleTests:
|
||||
|
||||
return SpecLoader()
|
||||
|
||||
def test_fresh(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
loader = self.loader()
|
||||
name = 'blah'
|
||||
with test_util.uncache(name):
|
||||
loader.load_module(name)
|
||||
module = loader.found
|
||||
self.assertIs(sys.modules[name], module)
|
||||
self.assertEqual(loader, module.__loader__)
|
||||
self.assertEqual(loader, module.__spec__.loader)
|
||||
self.assertEqual(name, module.__name__)
|
||||
self.assertEqual(name, module.__spec__.name)
|
||||
self.assertIsNotNone(module.__path__)
|
||||
self.assertIsNotNone(module.__path__,
|
||||
module.__spec__.submodule_search_locations)
|
||||
|
||||
def test_reload(self):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
name = 'blah'
|
||||
loader = self.loader()
|
||||
module = types.ModuleType(name)
|
||||
module.__spec__ = self.util.spec_from_loader(name, loader)
|
||||
module.__loader__ = loader
|
||||
with test_util.uncache(name):
|
||||
sys.modules[name] = module
|
||||
loader.load_module(name)
|
||||
found = loader.found
|
||||
self.assertIs(found, sys.modules[name])
|
||||
self.assertIs(module, sys.modules[name])
|
||||
|
||||
|
||||
(Frozen_LoaderLoadModuleTests,
|
||||
Source_LoaderLoadModuleTests
|
||||
@@ -478,59 +442,6 @@ class InspectLoaderGetCodeTests:
|
||||
InspectLoaderSubclass=SPLIT_IL)
|
||||
|
||||
|
||||
class InspectLoaderLoadModuleTests:
|
||||
|
||||
"""Test InspectLoader.load_module()."""
|
||||
|
||||
module_name = 'blah'
|
||||
|
||||
def setUp(self):
|
||||
import_helper.unload(self.module_name)
|
||||
self.addCleanup(import_helper.unload, self.module_name)
|
||||
|
||||
def load(self, loader):
|
||||
spec = self.util.spec_from_loader(self.module_name, loader)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
return self.init._bootstrap._load_unlocked(spec)
|
||||
|
||||
def mock_get_code(self):
|
||||
return mock.patch.object(self.InspectLoaderSubclass, 'get_code')
|
||||
|
||||
def test_get_code_ImportError(self):
|
||||
# If get_code() raises ImportError, it should propagate.
|
||||
with self.mock_get_code() as mocked_get_code:
|
||||
mocked_get_code.side_effect = ImportError
|
||||
with self.assertRaises(ImportError):
|
||||
loader = self.InspectLoaderSubclass()
|
||||
self.load(loader)
|
||||
|
||||
def test_get_code_None(self):
|
||||
# If get_code() returns None, raise ImportError.
|
||||
with self.mock_get_code() as mocked_get_code:
|
||||
mocked_get_code.return_value = None
|
||||
with self.assertRaises(ImportError):
|
||||
loader = self.InspectLoaderSubclass()
|
||||
self.load(loader)
|
||||
|
||||
def test_module_returned(self):
|
||||
# The loaded module should be returned.
|
||||
code = compile('attr = 42', '<string>', 'exec')
|
||||
with self.mock_get_code() as mocked_get_code:
|
||||
mocked_get_code.return_value = code
|
||||
loader = self.InspectLoaderSubclass()
|
||||
module = self.load(loader)
|
||||
self.assertEqual(module, sys.modules[self.module_name])
|
||||
|
||||
|
||||
(Frozen_ILLoadModuleTests,
|
||||
Source_ILLoadModuleTests
|
||||
) = test_util.test_both(InspectLoaderLoadModuleTests,
|
||||
InspectLoaderSubclass=SPLIT_IL,
|
||||
init=init,
|
||||
util=util)
|
||||
|
||||
|
||||
##### ExecutionLoader concrete methods #########################################
|
||||
class ExecutionLoaderGetCodeTests:
|
||||
|
||||
@@ -599,8 +510,7 @@ class ExecutionLoaderGetCodeTests:
|
||||
class SourceOnlyLoader:
|
||||
|
||||
# Globals that should be defined for all modules.
|
||||
source = (b"_ = '::'.join([__name__, __file__, __cached__, __package__, "
|
||||
b"repr(__loader__)])")
|
||||
source = (b"_ = '::'.join([__name__, __file__, __package__, repr(__loader__)])")
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
@@ -623,7 +533,10 @@ class SourceLoader(SourceOnlyLoader):
|
||||
|
||||
def __init__(self, path, magic=None):
|
||||
super().__init__(path)
|
||||
self.bytecode_path = self.util.cache_from_source(self.path)
|
||||
try:
|
||||
self.bytecode_path = self.util.cache_from_source(self.path)
|
||||
except NotImplementedError:
|
||||
self.bytecode_path = None
|
||||
self.source_size = len(self.source)
|
||||
if magic is None:
|
||||
magic = self.util.MAGIC_NUMBER
|
||||
@@ -669,26 +582,26 @@ class SourceLoaderTestHarness:
|
||||
module_name = 'mod'
|
||||
self.path = os.path.join(self.package, '.'.join(['mod', 'py']))
|
||||
self.name = '.'.join([self.package, module_name])
|
||||
self.cached = self.util.cache_from_source(self.path)
|
||||
try:
|
||||
self.cached = self.util.cache_from_source(self.path)
|
||||
except NotImplementedError:
|
||||
self.cached = None
|
||||
self.loader = self.loader_mock(self.path, **kwargs)
|
||||
|
||||
def verify_module(self, module):
|
||||
self.assertEqual(module.__name__, self.name)
|
||||
self.assertEqual(module.__file__, self.path)
|
||||
self.assertEqual(module.__cached__, self.cached)
|
||||
self.assertEqual(module.__package__, self.package)
|
||||
self.assertEqual(module.__loader__, self.loader)
|
||||
values = module._.split('::')
|
||||
self.assertEqual(values[0], self.name)
|
||||
self.assertEqual(values[1], self.path)
|
||||
self.assertEqual(values[2], self.cached)
|
||||
self.assertEqual(values[3], self.package)
|
||||
self.assertEqual(values[4], repr(self.loader))
|
||||
self.assertEqual(values[2], self.package)
|
||||
self.assertEqual(values[3], repr(self.loader))
|
||||
|
||||
def verify_code(self, code_object):
|
||||
module = types.ModuleType(self.name)
|
||||
module.__file__ = self.path
|
||||
module.__cached__ = self.cached
|
||||
module.__package__ = self.package
|
||||
module.__loader__ = self.loader
|
||||
module.__path__ = []
|
||||
@@ -729,34 +642,6 @@ class SourceOnlyLoaderTests(SourceLoaderTestHarness):
|
||||
code = self.loader.source_to_code(self.loader.source, self.path)
|
||||
self.verify_code(code)
|
||||
|
||||
def test_load_module(self):
|
||||
# Loading a module should set __name__, __loader__, __package__,
|
||||
# __path__ (for packages), __file__, and __cached__.
|
||||
# The module should also be put into sys.modules.
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", ImportWarning)
|
||||
with test_util.uncache(self.name):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
module = self.loader.load_module(self.name)
|
||||
self.verify_module(module)
|
||||
self.assertEqual(module.__path__, [os.path.dirname(self.path)])
|
||||
self.assertIn(self.name, sys.modules)
|
||||
|
||||
def test_package_settings(self):
|
||||
# __package__ needs to be set, while __path__ is set on if the module
|
||||
# is a package.
|
||||
# Testing the values for a package are covered by test_load_module.
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", ImportWarning)
|
||||
self.setUp(is_package=False)
|
||||
with test_util.uncache(self.name):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
module = self.loader.load_module(self.name)
|
||||
self.verify_module(module)
|
||||
self.assertNotHasAttr(module, '__path__')
|
||||
|
||||
def test_get_source_encoding(self):
|
||||
# Source is considered encoded in UTF-8 by default unless otherwise
|
||||
# specified by an encoding line.
|
||||
@@ -777,6 +662,8 @@ class SourceOnlyLoaderTests(SourceLoaderTestHarness):
|
||||
|
||||
|
||||
@unittest.skipIf(sys.dont_write_bytecode, "sys.dont_write_bytecode is true")
|
||||
@unittest.skipIf(sys.implementation.cache_tag is None,
|
||||
"sys.implementation.cache_tag is None")
|
||||
class SourceLoaderBytecodeTests(SourceLoaderTestHarness):
|
||||
|
||||
"""Test importlib.abc.SourceLoader's use of bytecode.
|
||||
@@ -904,7 +791,7 @@ class SourceLoaderGetSourceTests:
|
||||
mock = self.SourceOnlyLoaderMock('mod.file')
|
||||
source = "x = 42\r\ny = -13\r\n"
|
||||
mock.source = source.encode('utf-8')
|
||||
expect = io.IncrementalNewlineDecoder(None, True).decode(source)
|
||||
expect = io.StringIO(source, newline=None).getvalue()
|
||||
self.assertEqual(mock.get_source(name), expect)
|
||||
|
||||
|
||||
|
||||
Vendored
-7
@@ -231,11 +231,9 @@ class ReloadTests:
|
||||
# Start as a plain module.
|
||||
self.init.invalidate_caches()
|
||||
path = os.path.join(cwd, name + '.py')
|
||||
cached = self.util.cache_from_source(path)
|
||||
expected = {'__name__': name,
|
||||
'__package__': '',
|
||||
'__file__': path,
|
||||
'__cached__': cached,
|
||||
'__doc__': None,
|
||||
}
|
||||
os_helper.create_empty_file(path)
|
||||
@@ -252,11 +250,9 @@ class ReloadTests:
|
||||
# Change to a package.
|
||||
self.init.invalidate_caches()
|
||||
init_path = os.path.join(cwd, name, '__init__.py')
|
||||
cached = self.util.cache_from_source(init_path)
|
||||
expected = {'__name__': name,
|
||||
'__package__': name,
|
||||
'__file__': init_path,
|
||||
'__cached__': cached,
|
||||
'__path__': [os.path.dirname(init_path)],
|
||||
'__doc__': None,
|
||||
}
|
||||
@@ -283,7 +279,6 @@ class ReloadTests:
|
||||
# Start as a namespace package.
|
||||
self.init.invalidate_caches()
|
||||
bad_path = os.path.join(cwd, name, '__init.py')
|
||||
cached = self.util.cache_from_source(bad_path)
|
||||
expected = {'__name__': name,
|
||||
'__package__': name,
|
||||
'__doc__': None,
|
||||
@@ -312,11 +307,9 @@ class ReloadTests:
|
||||
# Change to a regular package.
|
||||
self.init.invalidate_caches()
|
||||
init_path = os.path.join(cwd, name, '__init__.py')
|
||||
cached = self.util.cache_from_source(init_path)
|
||||
expected = {'__name__': name,
|
||||
'__package__': name,
|
||||
'__file__': init_path,
|
||||
'__cached__': cached,
|
||||
'__path__': [os.path.dirname(init_path)],
|
||||
'__doc__': None,
|
||||
'eggs': None,
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
from test.test_importlib import util
|
||||
|
||||
importlib = util.import_importlib('importlib')
|
||||
machinery = util.import_importlib('importlib.machinery')
|
||||
|
||||
|
||||
class DiscoverableFinder:
|
||||
def __init__(self, discover=[]):
|
||||
self._discovered_values = discover
|
||||
|
||||
def find_spec(self, fullname, path=None, target=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def discover(self, parent=None):
|
||||
yield from self._discovered_values
|
||||
|
||||
|
||||
class TestPathFinder:
|
||||
"""PathFinder implements MetaPathFinder, which uses the PathEntryFinder(s)
|
||||
registered in sys.path_hooks (and sys.path_importer_cache) to search
|
||||
sys.path or the parent's __path__.
|
||||
|
||||
PathFinder.discover() should redirect to the .discover() method of the
|
||||
PathEntryFinder for each path entry.
|
||||
"""
|
||||
|
||||
def test_search_path_hooks_top_level(self):
|
||||
modules = [
|
||||
self.machinery.ModuleSpec(name='example1', loader=None),
|
||||
self.machinery.ModuleSpec(name='example2', loader=None),
|
||||
self.machinery.ModuleSpec(name='example3', loader=None),
|
||||
]
|
||||
|
||||
with util.import_state(
|
||||
path_importer_cache={
|
||||
'discoverable': DiscoverableFinder(discover=modules),
|
||||
},
|
||||
path=['discoverable'],
|
||||
):
|
||||
discovered = list(self.machinery.PathFinder.discover())
|
||||
|
||||
self.assertEqual(discovered, modules)
|
||||
|
||||
|
||||
def test_search_path_hooks_parent(self):
|
||||
parent = self.machinery.ModuleSpec(name='example', loader=None, is_package=True)
|
||||
parent.submodule_search_locations.append('discoverable')
|
||||
|
||||
children = [
|
||||
self.machinery.ModuleSpec(name='example.child1', loader=None),
|
||||
self.machinery.ModuleSpec(name='example.child2', loader=None),
|
||||
self.machinery.ModuleSpec(name='example.child3', loader=None),
|
||||
]
|
||||
|
||||
with util.import_state(
|
||||
path_importer_cache={
|
||||
'discoverable': DiscoverableFinder(discover=children)
|
||||
},
|
||||
path=[],
|
||||
):
|
||||
discovered = list(self.machinery.PathFinder.discover(parent))
|
||||
|
||||
self.assertEqual(discovered, children)
|
||||
|
||||
def test_invalid_parent(self):
|
||||
parent = self.machinery.ModuleSpec(name='example', loader=None)
|
||||
with self.assertRaises(ValueError):
|
||||
list(self.machinery.PathFinder.discover(parent))
|
||||
|
||||
|
||||
(
|
||||
Frozen_TestPathFinder,
|
||||
Source_TestPathFinder,
|
||||
) = util.test_both(TestPathFinder, importlib=importlib, machinery=machinery)
|
||||
|
||||
|
||||
class TestFileFinder:
|
||||
"""FileFinder implements PathEntryFinder and provides the base finder
|
||||
implementation to search the file system.
|
||||
"""
|
||||
|
||||
def get_finder(self, path):
|
||||
loader_details = [
|
||||
(self.machinery.SourceFileLoader, self.machinery.SOURCE_SUFFIXES),
|
||||
(self.machinery.SourcelessFileLoader, self.machinery.BYTECODE_SUFFIXES),
|
||||
]
|
||||
return self.machinery.FileFinder(path, *loader_details)
|
||||
|
||||
def test_discover_top_level(self):
|
||||
modules = {'example1', 'example2', 'example3'}
|
||||
with util.create_modules(*modules) as mapping:
|
||||
finder = self.get_finder(mapping['.root'])
|
||||
discovered = list(finder.discover())
|
||||
self.assertEqual({spec.name for spec in discovered}, modules)
|
||||
|
||||
def test_discover_parent(self):
|
||||
modules = {
|
||||
'example.child1',
|
||||
'example.child2',
|
||||
'example.child3',
|
||||
}
|
||||
with util.create_modules(*modules) as mapping:
|
||||
example = self.get_finder(mapping['.root']).find_spec('example')
|
||||
finder = self.get_finder(example.submodule_search_locations[0])
|
||||
discovered = list(finder.discover(example))
|
||||
self.assertEqual({spec.name for spec in discovered}, modules)
|
||||
|
||||
def test_invalid_parent(self):
|
||||
with util.create_modules('example') as mapping:
|
||||
finder = self.get_finder(mapping['.root'])
|
||||
example = finder.find_spec('example')
|
||||
with self.assertRaises(ValueError):
|
||||
list(finder.discover(example))
|
||||
|
||||
|
||||
(
|
||||
Frozen_TestFileFinder,
|
||||
Source_TestFileFinder,
|
||||
) = util.test_both(TestFileFinder, importlib=importlib, machinery=machinery)
|
||||
Vendored
-427
@@ -1,427 +0,0 @@
|
||||
import re
|
||||
import pickle
|
||||
import unittest
|
||||
import warnings
|
||||
import importlib.metadata
|
||||
import contextlib
|
||||
import itertools
|
||||
|
||||
try:
|
||||
import pyfakefs.fake_filesystem_unittest as ffs
|
||||
except ImportError:
|
||||
from .stubs import fake_filesystem_unittest as ffs
|
||||
|
||||
from . import fixtures
|
||||
from ._context import suppress
|
||||
from importlib.metadata import (
|
||||
Distribution,
|
||||
EntryPoint,
|
||||
PackageNotFoundError,
|
||||
_unique,
|
||||
distributions,
|
||||
entry_points,
|
||||
metadata,
|
||||
packages_distributions,
|
||||
version,
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_known_deprecation():
|
||||
with warnings.catch_warnings(record=True) as ctx:
|
||||
warnings.simplefilter('default', category=DeprecationWarning)
|
||||
yield ctx
|
||||
|
||||
|
||||
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
|
||||
version_pattern = r'\d+\.\d+(\.\d)?'
|
||||
|
||||
def test_retrieves_version_of_self(self):
|
||||
dist = Distribution.from_name('distinfo-pkg')
|
||||
assert isinstance(dist.version, str)
|
||||
assert re.match(self.version_pattern, dist.version)
|
||||
|
||||
def test_for_name_does_not_exist(self):
|
||||
with self.assertRaises(PackageNotFoundError):
|
||||
Distribution.from_name('does-not-exist')
|
||||
|
||||
def test_package_not_found_mentions_metadata(self):
|
||||
"""
|
||||
When a package is not found, that could indicate that the
|
||||
package is not installed or that it is installed without
|
||||
metadata. Ensure the exception mentions metadata to help
|
||||
guide users toward the cause. See #124.
|
||||
"""
|
||||
with self.assertRaises(PackageNotFoundError) as ctx:
|
||||
Distribution.from_name('does-not-exist')
|
||||
|
||||
assert "metadata" in str(ctx.exception)
|
||||
|
||||
# expected to fail until ABC is enforced
|
||||
@suppress(AssertionError)
|
||||
@suppress_known_deprecation()
|
||||
def test_abc_enforced(self):
|
||||
with self.assertRaises(TypeError):
|
||||
type('DistributionSubclass', (Distribution,), {})()
|
||||
|
||||
@fixtures.parameterize(
|
||||
dict(name=None),
|
||||
dict(name=''),
|
||||
)
|
||||
def test_invalid_inputs_to_from_name(self, name):
|
||||
with self.assertRaises(ValueError):
|
||||
Distribution.from_name(name)
|
||||
|
||||
|
||||
class ImportTests(fixtures.DistInfoPkg, unittest.TestCase):
|
||||
def test_import_nonexistent_module(self):
|
||||
# Ensure that the MetadataPathFinder does not crash an import of a
|
||||
# non-existent module.
|
||||
with self.assertRaises(ImportError):
|
||||
importlib.import_module('does_not_exist')
|
||||
|
||||
def test_resolve(self):
|
||||
ep = entry_points(group='entries')['main']
|
||||
self.assertEqual(ep.load().__name__, "main")
|
||||
|
||||
def test_entrypoint_with_colon_in_name(self):
|
||||
ep = entry_points(group='entries')['ns:sub']
|
||||
self.assertEqual(ep.value, 'mod:main')
|
||||
|
||||
def test_resolve_without_attr(self):
|
||||
ep = EntryPoint(
|
||||
name='ep',
|
||||
value='importlib.metadata',
|
||||
group='grp',
|
||||
)
|
||||
assert ep.load() is importlib.metadata
|
||||
|
||||
|
||||
class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||
@staticmethod
|
||||
def make_pkg(name):
|
||||
"""
|
||||
Create minimal metadata for a dist-info package with
|
||||
the indicated name on the file system.
|
||||
"""
|
||||
return {
|
||||
f'{name}.dist-info': {
|
||||
'METADATA': 'VERSION: 1.0\n',
|
||||
},
|
||||
}
|
||||
|
||||
def test_dashes_in_dist_name_found_as_underscores(self):
|
||||
"""
|
||||
For a package with a dash in the name, the dist-info metadata
|
||||
uses underscores in the name. Ensure the metadata loads.
|
||||
"""
|
||||
fixtures.build_files(self.make_pkg('my_pkg'), self.site_dir)
|
||||
assert version('my-pkg') == '1.0'
|
||||
|
||||
def test_dist_name_found_as_any_case(self):
|
||||
"""
|
||||
Ensure the metadata loads when queried with any case.
|
||||
"""
|
||||
pkg_name = 'CherryPy'
|
||||
fixtures.build_files(self.make_pkg(pkg_name), self.site_dir)
|
||||
assert version(pkg_name) == '1.0'
|
||||
assert version(pkg_name.lower()) == '1.0'
|
||||
assert version(pkg_name.upper()) == '1.0'
|
||||
|
||||
def test_unique_distributions(self):
|
||||
"""
|
||||
Two distributions varying only by non-normalized name on
|
||||
the file system should resolve as the same.
|
||||
"""
|
||||
fixtures.build_files(self.make_pkg('abc'), self.site_dir)
|
||||
before = list(_unique(distributions()))
|
||||
|
||||
alt_site_dir = self.fixtures.enter_context(fixtures.tempdir())
|
||||
self.fixtures.enter_context(self.add_sys_path(alt_site_dir))
|
||||
fixtures.build_files(self.make_pkg('ABC'), alt_site_dir)
|
||||
after = list(_unique(distributions()))
|
||||
|
||||
assert len(after) == len(before)
|
||||
|
||||
|
||||
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||
@staticmethod
|
||||
def pkg_with_non_ascii_description(site_dir):
|
||||
"""
|
||||
Create minimal metadata for a package with non-ASCII in
|
||||
the description.
|
||||
"""
|
||||
contents = {
|
||||
'portend.dist-info': {
|
||||
'METADATA': 'Description: pôrˈtend',
|
||||
},
|
||||
}
|
||||
fixtures.build_files(contents, site_dir)
|
||||
return 'portend'
|
||||
|
||||
@staticmethod
|
||||
def pkg_with_non_ascii_description_egg_info(site_dir):
|
||||
"""
|
||||
Create minimal metadata for an egg-info package with
|
||||
non-ASCII in the description.
|
||||
"""
|
||||
contents = {
|
||||
'portend.dist-info': {
|
||||
'METADATA': """
|
||||
Name: portend
|
||||
|
||||
pôrˈtend""",
|
||||
},
|
||||
}
|
||||
fixtures.build_files(contents, site_dir)
|
||||
return 'portend'
|
||||
|
||||
def test_metadata_loads(self):
|
||||
pkg_name = self.pkg_with_non_ascii_description(self.site_dir)
|
||||
meta = metadata(pkg_name)
|
||||
assert meta['Description'] == 'pôrˈtend'
|
||||
|
||||
def test_metadata_loads_egg_info(self):
|
||||
pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir)
|
||||
meta = metadata(pkg_name)
|
||||
assert meta['Description'] == 'pôrˈtend'
|
||||
|
||||
|
||||
class DiscoveryTests(
|
||||
fixtures.EggInfoPkg,
|
||||
fixtures.EggInfoPkgPipInstalledNoToplevel,
|
||||
fixtures.EggInfoPkgPipInstalledNoModules,
|
||||
fixtures.EggInfoPkgSourcesFallback,
|
||||
fixtures.DistInfoPkg,
|
||||
unittest.TestCase,
|
||||
):
|
||||
def test_package_discovery(self):
|
||||
dists = list(distributions())
|
||||
assert all(isinstance(dist, Distribution) for dist in dists)
|
||||
assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists)
|
||||
assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists)
|
||||
assert any(dist.metadata['Name'] == 'egg_with_no_modules-pkg' for dist in dists)
|
||||
assert any(dist.metadata['Name'] == 'sources_fallback-pkg' for dist in dists)
|
||||
assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
|
||||
|
||||
def test_invalid_usage(self):
|
||||
with self.assertRaises(ValueError):
|
||||
list(distributions(context='something', name='else'))
|
||||
|
||||
|
||||
class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||
def test_egg_info(self):
|
||||
# make an `EGG-INFO` directory that's unrelated
|
||||
self.site_dir.joinpath('EGG-INFO').mkdir()
|
||||
# used to crash with `IsADirectoryError`
|
||||
with self.assertRaises(PackageNotFoundError):
|
||||
version('unknown-package')
|
||||
|
||||
def test_egg(self):
|
||||
egg = self.site_dir.joinpath('foo-3.6.egg')
|
||||
egg.mkdir()
|
||||
with self.add_sys_path(egg):
|
||||
with self.assertRaises(PackageNotFoundError):
|
||||
version('foo')
|
||||
|
||||
|
||||
class MissingSysPath(fixtures.OnSysPath, unittest.TestCase):
|
||||
site_dir = '/does-not-exist'
|
||||
|
||||
def test_discovery(self):
|
||||
"""
|
||||
Discovering distributions should succeed even if
|
||||
there is an invalid path on sys.path.
|
||||
"""
|
||||
importlib.metadata.distributions()
|
||||
|
||||
|
||||
class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase):
|
||||
site_dir = '/access-denied'
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.setUpPyfakefs()
|
||||
self.fs.create_dir(self.site_dir, perm_bits=000)
|
||||
|
||||
def test_discovery(self):
|
||||
"""
|
||||
Discovering distributions should succeed even if
|
||||
there is an invalid path on sys.path.
|
||||
"""
|
||||
list(importlib.metadata.distributions())
|
||||
|
||||
|
||||
class TestEntryPoints(unittest.TestCase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.ep = importlib.metadata.EntryPoint(
|
||||
name='name', value='value', group='group'
|
||||
)
|
||||
|
||||
def test_entry_point_pickleable(self):
|
||||
revived = pickle.loads(pickle.dumps(self.ep))
|
||||
assert revived == self.ep
|
||||
|
||||
def test_positional_args(self):
|
||||
"""
|
||||
Capture legacy (namedtuple) construction, discouraged.
|
||||
"""
|
||||
EntryPoint('name', 'value', 'group')
|
||||
|
||||
def test_immutable(self):
|
||||
"""EntryPoints should be immutable"""
|
||||
with self.assertRaises(AttributeError):
|
||||
self.ep.name = 'badactor'
|
||||
|
||||
def test_repr(self):
|
||||
assert 'EntryPoint' in repr(self.ep)
|
||||
assert 'name=' in repr(self.ep)
|
||||
assert "'name'" in repr(self.ep)
|
||||
|
||||
def test_hashable(self):
|
||||
"""EntryPoints should be hashable"""
|
||||
hash(self.ep)
|
||||
|
||||
def test_module(self):
|
||||
assert self.ep.module == 'value'
|
||||
|
||||
def test_attr(self):
|
||||
assert self.ep.attr is None
|
||||
|
||||
def test_sortable(self):
|
||||
"""
|
||||
EntryPoint objects are sortable, but result is undefined.
|
||||
"""
|
||||
sorted(
|
||||
[
|
||||
EntryPoint(name='b', value='val', group='group'),
|
||||
EntryPoint(name='a', value='val', group='group'),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class FileSystem(
|
||||
fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder, unittest.TestCase
|
||||
):
|
||||
def test_unicode_dir_on_sys_path(self):
|
||||
"""
|
||||
Ensure a Unicode subdirectory of a directory on sys.path
|
||||
does not crash.
|
||||
"""
|
||||
fixtures.build_files(
|
||||
{self.unicode_filename(): {}},
|
||||
prefix=self.site_dir,
|
||||
)
|
||||
list(distributions())
|
||||
|
||||
|
||||
class PackagesDistributionsPrebuiltTest(fixtures.ZipFixtures, unittest.TestCase):
|
||||
def test_packages_distributions_example(self):
|
||||
self._fixture_on_path('example-21.12-py3-none-any.whl')
|
||||
assert packages_distributions()['example'] == ['example']
|
||||
|
||||
def test_packages_distributions_example2(self):
|
||||
"""
|
||||
Test packages_distributions on a wheel built
|
||||
by trampolim.
|
||||
"""
|
||||
self._fixture_on_path('example2-1.0.0-py3-none-any.whl')
|
||||
assert packages_distributions()['example2'] == ['example2']
|
||||
|
||||
|
||||
class PackagesDistributionsTest(
|
||||
fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase
|
||||
):
|
||||
def test_packages_distributions_neither_toplevel_nor_files(self):
|
||||
"""
|
||||
Test a package built without 'top-level.txt' or a file list.
|
||||
"""
|
||||
fixtures.build_files(
|
||||
{
|
||||
'trim_example-1.0.0.dist-info': {
|
||||
'METADATA': """
|
||||
Name: trim_example
|
||||
Version: 1.0.0
|
||||
""",
|
||||
}
|
||||
},
|
||||
prefix=self.site_dir,
|
||||
)
|
||||
packages_distributions()
|
||||
|
||||
def test_packages_distributions_all_module_types(self):
|
||||
"""
|
||||
Test top-level modules detected on a package without 'top-level.txt'.
|
||||
"""
|
||||
suffixes = importlib.machinery.all_suffixes()
|
||||
metadata = dict(
|
||||
METADATA="""
|
||||
Name: all_distributions
|
||||
Version: 1.0.0
|
||||
""",
|
||||
)
|
||||
files = {
|
||||
'all_distributions-1.0.0.dist-info': metadata,
|
||||
}
|
||||
for i, suffix in enumerate(suffixes):
|
||||
files.update(
|
||||
{
|
||||
f'importable-name {i}{suffix}': '',
|
||||
f'in_namespace_{i}': {
|
||||
f'mod{suffix}': '',
|
||||
},
|
||||
f'in_package_{i}': {
|
||||
'__init__.py': '',
|
||||
f'mod{suffix}': '',
|
||||
},
|
||||
}
|
||||
)
|
||||
metadata.update(RECORD=fixtures.build_record(files))
|
||||
fixtures.build_files(files, prefix=self.site_dir)
|
||||
|
||||
distributions = packages_distributions()
|
||||
|
||||
for i in range(len(suffixes)):
|
||||
assert distributions[f'importable-name {i}'] == ['all_distributions']
|
||||
assert distributions[f'in_namespace_{i}'] == ['all_distributions']
|
||||
assert distributions[f'in_package_{i}'] == ['all_distributions']
|
||||
|
||||
assert not any(name.endswith('.dist-info') for name in distributions)
|
||||
|
||||
|
||||
class PackagesDistributionsEggTest(
|
||||
fixtures.EggInfoPkg,
|
||||
fixtures.EggInfoPkgPipInstalledNoToplevel,
|
||||
fixtures.EggInfoPkgPipInstalledNoModules,
|
||||
fixtures.EggInfoPkgSourcesFallback,
|
||||
unittest.TestCase,
|
||||
):
|
||||
def test_packages_distributions_on_eggs(self):
|
||||
"""
|
||||
Test old-style egg packages with a variation of 'top_level.txt',
|
||||
'SOURCES.txt', and 'installed-files.txt', available.
|
||||
"""
|
||||
distributions = packages_distributions()
|
||||
|
||||
def import_names_from_package(package_name):
|
||||
return {
|
||||
import_name
|
||||
for import_name, package_names in distributions.items()
|
||||
if package_name in package_names
|
||||
}
|
||||
|
||||
# egginfo-pkg declares one import ('mod') via top_level.txt
|
||||
assert import_names_from_package('egginfo-pkg') == {'mod'}
|
||||
|
||||
# egg_with_module-pkg has one import ('egg_with_module') inferred from
|
||||
# installed-files.txt (top_level.txt is missing)
|
||||
assert import_names_from_package('egg_with_module-pkg') == {'egg_with_module'}
|
||||
|
||||
# egg_with_no_modules-pkg should not be associated with any import names
|
||||
# (top_level.txt is empty, and installed-files.txt has no .py files)
|
||||
assert import_names_from_package('egg_with_no_modules-pkg') == set()
|
||||
|
||||
# sources_fallback-pkg has one import ('sources_fallback') inferred from
|
||||
# SOURCES.txt (top_level.txt and installed-files.txt is missing)
|
||||
assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'}
|
||||
-322
@@ -1,322 +0,0 @@
|
||||
import re
|
||||
import textwrap
|
||||
import unittest
|
||||
import warnings
|
||||
import importlib
|
||||
import contextlib
|
||||
|
||||
from . import fixtures
|
||||
from importlib.metadata import (
|
||||
Distribution,
|
||||
PackageNotFoundError,
|
||||
distribution,
|
||||
entry_points,
|
||||
files,
|
||||
metadata,
|
||||
requires,
|
||||
version,
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_known_deprecation():
|
||||
with warnings.catch_warnings(record=True) as ctx:
|
||||
warnings.simplefilter('default', category=DeprecationWarning)
|
||||
yield ctx
|
||||
|
||||
|
||||
class APITests(
|
||||
fixtures.EggInfoPkg,
|
||||
fixtures.EggInfoPkgPipInstalledNoToplevel,
|
||||
fixtures.EggInfoPkgPipInstalledNoModules,
|
||||
fixtures.EggInfoPkgSourcesFallback,
|
||||
fixtures.DistInfoPkg,
|
||||
fixtures.DistInfoPkgWithDot,
|
||||
fixtures.EggInfoFile,
|
||||
unittest.TestCase,
|
||||
):
|
||||
version_pattern = r'\d+\.\d+(\.\d)?'
|
||||
|
||||
def test_retrieves_version_of_self(self):
|
||||
pkg_version = version('egginfo-pkg')
|
||||
assert isinstance(pkg_version, str)
|
||||
assert re.match(self.version_pattern, pkg_version)
|
||||
|
||||
def test_retrieves_version_of_distinfo_pkg(self):
|
||||
pkg_version = version('distinfo-pkg')
|
||||
assert isinstance(pkg_version, str)
|
||||
assert re.match(self.version_pattern, pkg_version)
|
||||
|
||||
def test_for_name_does_not_exist(self):
|
||||
with self.assertRaises(PackageNotFoundError):
|
||||
distribution('does-not-exist')
|
||||
|
||||
def test_name_normalization(self):
|
||||
names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
|
||||
for name in names:
|
||||
with self.subTest(name):
|
||||
assert distribution(name).metadata['Name'] == 'pkg.dot'
|
||||
|
||||
def test_prefix_not_matched(self):
|
||||
prefixes = 'p', 'pkg', 'pkg.'
|
||||
for prefix in prefixes:
|
||||
with self.subTest(prefix):
|
||||
with self.assertRaises(PackageNotFoundError):
|
||||
distribution(prefix)
|
||||
|
||||
def test_for_top_level(self):
|
||||
tests = [
|
||||
('egginfo-pkg', 'mod'),
|
||||
('egg_with_no_modules-pkg', ''),
|
||||
]
|
||||
for pkg_name, expect_content in tests:
|
||||
with self.subTest(pkg_name):
|
||||
self.assertEqual(
|
||||
distribution(pkg_name).read_text('top_level.txt').strip(),
|
||||
expect_content,
|
||||
)
|
||||
|
||||
def test_read_text(self):
|
||||
tests = [
|
||||
('egginfo-pkg', 'mod\n'),
|
||||
('egg_with_no_modules-pkg', '\n'),
|
||||
]
|
||||
for pkg_name, expect_content in tests:
|
||||
with self.subTest(pkg_name):
|
||||
top_level = [
|
||||
path for path in files(pkg_name) if path.name == 'top_level.txt'
|
||||
][0]
|
||||
self.assertEqual(top_level.read_text(), expect_content)
|
||||
|
||||
def test_entry_points(self):
|
||||
eps = entry_points()
|
||||
assert 'entries' in eps.groups
|
||||
entries = eps.select(group='entries')
|
||||
assert 'main' in entries.names
|
||||
ep = entries['main']
|
||||
self.assertEqual(ep.value, 'mod:main')
|
||||
self.assertEqual(ep.extras, [])
|
||||
|
||||
def test_entry_points_distribution(self):
|
||||
entries = entry_points(group='entries')
|
||||
for entry in ("main", "ns:sub"):
|
||||
ep = entries[entry]
|
||||
self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg'))
|
||||
self.assertEqual(ep.dist.version, "1.0.0")
|
||||
|
||||
def test_entry_points_unique_packages_normalized(self):
|
||||
"""
|
||||
Entry points should only be exposed for the first package
|
||||
on sys.path with a given name (even when normalized).
|
||||
"""
|
||||
alt_site_dir = self.fixtures.enter_context(fixtures.tempdir())
|
||||
self.fixtures.enter_context(self.add_sys_path(alt_site_dir))
|
||||
alt_pkg = {
|
||||
"DistInfo_pkg-1.1.0.dist-info": {
|
||||
"METADATA": """
|
||||
Name: distinfo-pkg
|
||||
Version: 1.1.0
|
||||
""",
|
||||
"entry_points.txt": """
|
||||
[entries]
|
||||
main = mod:altmain
|
||||
""",
|
||||
},
|
||||
}
|
||||
fixtures.build_files(alt_pkg, alt_site_dir)
|
||||
entries = entry_points(group='entries')
|
||||
assert not any(
|
||||
ep.dist.name == 'distinfo-pkg' and ep.dist.version == '1.0.0'
|
||||
for ep in entries
|
||||
)
|
||||
# ns:sub doesn't exist in alt_pkg
|
||||
assert 'ns:sub' not in entries.names
|
||||
|
||||
def test_entry_points_missing_name(self):
|
||||
with self.assertRaises(KeyError):
|
||||
entry_points(group='entries')['missing']
|
||||
|
||||
def test_entry_points_missing_group(self):
|
||||
assert entry_points(group='missing') == ()
|
||||
|
||||
def test_entry_points_allows_no_attributes(self):
|
||||
ep = entry_points().select(group='entries', name='main')
|
||||
with self.assertRaises(AttributeError):
|
||||
ep.foo = 4
|
||||
|
||||
def test_metadata_for_this_package(self):
|
||||
md = metadata('egginfo-pkg')
|
||||
assert md['author'] == 'Steven Ma'
|
||||
assert md['LICENSE'] == 'Unknown'
|
||||
assert md['Name'] == 'egginfo-pkg'
|
||||
classifiers = md.get_all('Classifier')
|
||||
assert 'Topic :: Software Development :: Libraries' in classifiers
|
||||
|
||||
def test_missing_key_legacy(self):
|
||||
"""
|
||||
Requesting a missing key will still return None, but warn.
|
||||
"""
|
||||
md = metadata('distinfo-pkg')
|
||||
with suppress_known_deprecation():
|
||||
assert md['does-not-exist'] is None
|
||||
|
||||
def test_get_key(self):
|
||||
"""
|
||||
Getting a key gets the key.
|
||||
"""
|
||||
md = metadata('egginfo-pkg')
|
||||
assert md.get('Name') == 'egginfo-pkg'
|
||||
|
||||
def test_get_missing_key(self):
|
||||
"""
|
||||
Requesting a missing key will return None.
|
||||
"""
|
||||
md = metadata('distinfo-pkg')
|
||||
assert md.get('does-not-exist') is None
|
||||
|
||||
@staticmethod
|
||||
def _test_files(files):
|
||||
root = files[0].root
|
||||
for file in files:
|
||||
assert file.root == root
|
||||
assert not file.hash or file.hash.value
|
||||
assert not file.hash or file.hash.mode == 'sha256'
|
||||
assert not file.size or file.size >= 0
|
||||
assert file.locate().exists()
|
||||
assert isinstance(file.read_binary(), bytes)
|
||||
if file.name.endswith('.py'):
|
||||
file.read_text()
|
||||
|
||||
def test_file_hash_repr(self):
|
||||
util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0]
|
||||
self.assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
|
||||
|
||||
def test_files_dist_info(self):
|
||||
self._test_files(files('distinfo-pkg'))
|
||||
|
||||
def test_files_egg_info(self):
|
||||
self._test_files(files('egginfo-pkg'))
|
||||
self._test_files(files('egg_with_module-pkg'))
|
||||
self._test_files(files('egg_with_no_modules-pkg'))
|
||||
self._test_files(files('sources_fallback-pkg'))
|
||||
|
||||
def test_version_egg_info_file(self):
|
||||
self.assertEqual(version('egginfo-file'), '0.1')
|
||||
|
||||
def test_requires_egg_info_file(self):
|
||||
requirements = requires('egginfo-file')
|
||||
self.assertIsNone(requirements)
|
||||
|
||||
def test_requires_egg_info(self):
|
||||
deps = requires('egginfo-pkg')
|
||||
assert len(deps) == 2
|
||||
assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps)
|
||||
|
||||
def test_requires_egg_info_empty(self):
|
||||
fixtures.build_files(
|
||||
{
|
||||
'requires.txt': '',
|
||||
},
|
||||
self.site_dir.joinpath('egginfo_pkg.egg-info'),
|
||||
)
|
||||
deps = requires('egginfo-pkg')
|
||||
assert deps == []
|
||||
|
||||
def test_requires_dist_info(self):
|
||||
deps = requires('distinfo-pkg')
|
||||
assert len(deps) == 2
|
||||
assert all(deps)
|
||||
assert 'wheel >= 1.0' in deps
|
||||
assert "pytest; extra == 'test'" in deps
|
||||
|
||||
def test_more_complex_deps_requires_text(self):
|
||||
requires = textwrap.dedent(
|
||||
"""
|
||||
dep1
|
||||
dep2
|
||||
|
||||
[:python_version < "3"]
|
||||
dep3
|
||||
|
||||
[extra1]
|
||||
dep4
|
||||
dep6@ git+https://example.com/python/dep.git@v1.0.0
|
||||
|
||||
[extra2:python_version < "3"]
|
||||
dep5
|
||||
"""
|
||||
)
|
||||
deps = sorted(Distribution._deps_from_requires_text(requires))
|
||||
expected = [
|
||||
'dep1',
|
||||
'dep2',
|
||||
'dep3; python_version < "3"',
|
||||
'dep4; extra == "extra1"',
|
||||
'dep5; (python_version < "3") and extra == "extra2"',
|
||||
'dep6@ git+https://example.com/python/dep.git@v1.0.0 ; extra == "extra1"',
|
||||
]
|
||||
# It's important that the environment marker expression be
|
||||
# wrapped in parentheses to avoid the following 'and' binding more
|
||||
# tightly than some other part of the environment expression.
|
||||
|
||||
assert deps == expected
|
||||
|
||||
def test_as_json(self):
|
||||
md = metadata('distinfo-pkg').json
|
||||
assert 'name' in md
|
||||
assert md['keywords'] == ['sample', 'package']
|
||||
desc = md['description']
|
||||
assert desc.startswith('Once upon a time\nThere was')
|
||||
assert len(md['requires_dist']) == 2
|
||||
|
||||
def test_as_json_egg_info(self):
|
||||
md = metadata('egginfo-pkg').json
|
||||
assert 'name' in md
|
||||
assert md['keywords'] == ['sample', 'package']
|
||||
desc = md['description']
|
||||
assert desc.startswith('Once upon a time\nThere was')
|
||||
assert len(md['classifier']) == 2
|
||||
|
||||
def test_as_json_odd_case(self):
|
||||
self.make_uppercase()
|
||||
md = metadata('distinfo-pkg').json
|
||||
assert 'name' in md
|
||||
assert len(md['requires_dist']) == 2
|
||||
assert md['keywords'] == ['SAMPLE', 'PACKAGE']
|
||||
|
||||
|
||||
class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase):
|
||||
def test_name_normalization(self):
|
||||
names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
|
||||
for name in names:
|
||||
with self.subTest(name):
|
||||
assert distribution(name).metadata['Name'] == 'pkg.dot'
|
||||
|
||||
def test_name_normalization_versionless_egg_info(self):
|
||||
names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot'
|
||||
for name in names:
|
||||
with self.subTest(name):
|
||||
assert distribution(name).metadata['Name'] == 'pkg.lot'
|
||||
|
||||
|
||||
class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
|
||||
def test_find_distributions_specified_path(self):
|
||||
dists = Distribution.discover(path=[str(self.site_dir)])
|
||||
assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
|
||||
|
||||
def test_distribution_at_pathlib(self):
|
||||
"""Demonstrate how to load metadata direct from a directory."""
|
||||
dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
|
||||
dist = Distribution.at(dist_info_path)
|
||||
assert dist.version == '1.0.0'
|
||||
|
||||
def test_distribution_at_str(self):
|
||||
dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
|
||||
dist = Distribution.at(str(dist_info_path))
|
||||
assert dist.version == '1.0.0'
|
||||
|
||||
|
||||
class InvalidateCache(unittest.TestCase):
|
||||
def test_invalidate_cache(self):
|
||||
# No externally observable behavior, but ensures test coverage...
|
||||
importlib.invalidate_caches()
|
||||
+11
@@ -318,6 +318,17 @@ class ModuleAndNamespacePackageInSameDir(NamespacePackageTest):
|
||||
self.assertEqual(a_test.attr, 'in module')
|
||||
|
||||
|
||||
class NamespaceSubpackageSameName(NamespacePackageTest):
|
||||
paths = ['']
|
||||
|
||||
def test_namespace_subpackage_shares_name_with_directory(self):
|
||||
submodule_path = 'project4.foo'
|
||||
with self.assertRaises(ModuleNotFoundError) as cm:
|
||||
importlib.machinery.PathFinder.find_spec(submodule_path)
|
||||
|
||||
self.assertEqual(cm.exception.name, 'project4')
|
||||
|
||||
|
||||
class ReloadTests(NamespacePackageTest):
|
||||
paths = ['portion1']
|
||||
|
||||
|
||||
+6
-3
@@ -39,9 +39,12 @@ class TestImport(unittest.TestCase):
|
||||
self.remove_modules()
|
||||
|
||||
def rewrite_file(self, contents):
|
||||
compiled_path = cache_from_source(self.module_path)
|
||||
if os.path.exists(compiled_path):
|
||||
os.remove(compiled_path)
|
||||
try:
|
||||
compiled_path = cache_from_source(self.module_path)
|
||||
if os.path.exists(compiled_path):
|
||||
os.remove(compiled_path)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
with open(self.module_path, 'w', encoding='utf-8') as f:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user