diff options
author | Jason R. Coombs <jaraco@jaraco.com> | 2021-07-30 01:05:05 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-30 01:05:05 (GMT) |
commit | aaa83cdfab6817446285e631232f64b394ac6791 (patch) | |
tree | d41195f1412a533b1c7333b99adcc77f171329a1 /Lib/importlib | |
parent | 851cca8c22795a2f143ad5ebc10adab3c7784ad0 (diff) | |
download | cpython-aaa83cdfab6817446285e631232f64b394ac6791.zip cpython-aaa83cdfab6817446285e631232f64b394ac6791.tar.gz cpython-aaa83cdfab6817446285e631232f64b394ac6791.tar.bz2 |
bpo-44771: Apply changes from importlib_resources 5.2.1 (GH-27436)
* bpo-44771: Apply changes from importlib_resources@3b24bd6307
* Add blurb
* Exclude namespacedata01 from eol conversion.
Diffstat (limited to 'Lib/importlib')
-rw-r--r-- | Lib/importlib/_adapters.py | 108 | ||||
-rw-r--r-- | Lib/importlib/_common.py | 3 | ||||
-rw-r--r-- | Lib/importlib/_itertools.py | 19 | ||||
-rw-r--r-- | Lib/importlib/_legacy.py | 84 | ||||
-rw-r--r-- | Lib/importlib/readers.py | 15 | ||||
-rw-r--r-- | Lib/importlib/resources.py | 186 | ||||
-rw-r--r-- | Lib/importlib/simple.py | 116 |
7 files changed, 345 insertions, 186 deletions
diff --git a/Lib/importlib/_adapters.py b/Lib/importlib/_adapters.py index eedde49..9907b14 100644 --- a/Lib/importlib/_adapters.py +++ b/Lib/importlib/_adapters.py @@ -1,4 +1,5 @@ from contextlib import suppress +from io import TextIOWrapper from . import abc @@ -25,32 +26,119 @@ class TraversableResourcesLoader: self.spec = spec def get_resource_reader(self, name): - return DegenerateFiles(self.spec)._native() + return CompatibilityFiles(self.spec)._native() -class DegenerateFiles: +def _io_wrapper(file, mode='r', *args, **kwargs): + if mode == 'r': + return TextIOWrapper(file, *args, **kwargs) + elif mode == 'rb': + return file + raise ValueError( + "Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode) + ) + + +class CompatibilityFiles: """ Adapter for an existing or non-existant resource reader - to provide a degenerate .files(). + to provide a compability .files(). """ - class Path(abc.Traversable): + class SpecPath(abc.Traversable): + """ + Path tied to a module spec. + Can be read and exposes the resource reader children. + """ + + def __init__(self, spec, reader): + self._spec = spec + self._reader = reader + + def iterdir(self): + if not self._reader: + return iter(()) + return iter( + CompatibilityFiles.ChildPath(self._reader, path) + for path in self._reader.contents() + ) + + def is_file(self): + return False + + is_dir = is_file + + def joinpath(self, other): + if not self._reader: + return CompatibilityFiles.OrphanPath(other) + return CompatibilityFiles.ChildPath(self._reader, other) + + @property + def name(self): + return self._spec.name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs) + + class ChildPath(abc.Traversable): + """ + Path tied to a resource reader child. + Can be read but doesn't expose any meaningfull children. + """ + + def __init__(self, reader, name): + self._reader = reader + self._name = name + def iterdir(self): return iter(()) + def is_file(self): + return self._reader.is_resource(self.name) + def is_dir(self): + return not self.is_file() + + def joinpath(self, other): + return CompatibilityFiles.OrphanPath(self.name, other) + + @property + def name(self): + return self._name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper( + self._reader.open_resource(self.name), mode, *args, **kwargs + ) + + class OrphanPath(abc.Traversable): + """ + Orphan path, not tied to a module spec or resource reader. + Can't be read and doesn't expose any meaningful children. + """ + + def __init__(self, *path_parts): + if len(path_parts) < 1: + raise ValueError('Need at least one path part to construct a path') + self._path = path_parts + + def iterdir(self): + return iter(()) + + def is_file(self): return False - is_file = exists = is_dir # type: ignore + is_dir = is_file def joinpath(self, other): - return DegenerateFiles.Path() + return CompatibilityFiles.OrphanPath(*self._path, other) + @property def name(self): - return '' + return self._path[-1] - def open(self): - raise ValueError() + def open(self, mode='r', *args, **kwargs): + raise FileNotFoundError("Can't open orphan path") def __init__(self, spec): self.spec = spec @@ -71,7 +159,7 @@ class DegenerateFiles: return getattr(self._reader, attr) def files(self): - return DegenerateFiles.Path() + return CompatibilityFiles.SpecPath(self.spec, self._reader) def wrap_spec(package): diff --git a/Lib/importlib/_common.py b/Lib/importlib/_common.py index 549fee3..74654b3 100644 --- a/Lib/importlib/_common.py +++ b/Lib/importlib/_common.py @@ -12,6 +12,7 @@ from .abc import ResourceReader, Traversable from ._adapters import wrap_spec Package = Union[types.ModuleType, str] +Resource = Union[str, os.PathLike] def files(package): @@ -93,7 +94,7 @@ def _tempfile(reader, suffix=''): finally: try: os.remove(raw_path) - except FileNotFoundError: + except (FileNotFoundError, PermissionError): pass diff --git a/Lib/importlib/_itertools.py b/Lib/importlib/_itertools.py new file mode 100644 index 0000000..dd45f2f --- /dev/null +++ b/Lib/importlib/_itertools.py @@ -0,0 +1,19 @@ +from itertools import filterfalse + + +def unique_everseen(iterable, key=None): + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen = set() + seen_add = seen.add + if key is None: + for element in filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element + else: + for element in iterable: + k = key(element) + if k not in seen: + seen_add(k) + yield element diff --git a/Lib/importlib/_legacy.py b/Lib/importlib/_legacy.py new file mode 100644 index 0000000..2ddec5f --- /dev/null +++ b/Lib/importlib/_legacy.py @@ -0,0 +1,84 @@ +import os +import pathlib +import types + +from typing import Union, Iterable, ContextManager, BinaryIO, TextIO + +from . import _common + +Package = Union[types.ModuleType, str] +Resource = Union[str, os.PathLike] + + +def open_binary(package: Package, resource: Resource) -> BinaryIO: + """Return a file-like object opened for binary reading of the resource.""" + return (_common.files(package) / _common.normalize_path(resource)).open('rb') + + +def read_binary(package: Package, resource: Resource) -> bytes: + """Return the binary contents of the resource.""" + return (_common.files(package) / _common.normalize_path(resource)).read_bytes() + + +def open_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> TextIO: + """Return a file-like object opened for text reading of the resource.""" + return (_common.files(package) / _common.normalize_path(resource)).open( + 'r', encoding=encoding, errors=errors + ) + + +def read_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> str: + """Return the decoded string of the resource. + + The decoding-related arguments have the same semantics as those of + bytes.decode(). + """ + with open_text(package, resource, encoding, errors) as fp: + return fp.read() + + +def contents(package: Package) -> Iterable[str]: + """Return an iterable of entries in `package`. + + Note that not all entries are resources. Specifically, directories are + not considered resources. Use `is_resource()` on each entry returned here + to check if it is a resource or not. + """ + return [path.name for path in _common.files(package).iterdir()] + + +def is_resource(package: Package, name: str) -> bool: + """True if `name` is a resource inside `package`. + + Directories are *not* resources. + """ + resource = _common.normalize_path(name) + return any( + traversable.name == resource and traversable.is_file() + for traversable in _common.files(package).iterdir() + ) + + +def path( + package: Package, + resource: Resource, +) -> ContextManager[pathlib.Path]: + """A context manager providing a file path object to the resource. + + If the resource does not already exist on its own on the file system, + a temporary file will be created. If the file was created, the file + will be deleted upon exiting the context manager (no exception is + raised if the file was deleted prior to the context manager + exiting). + """ + return _common.as_file(_common.files(package) / _common.normalize_path(resource)) diff --git a/Lib/importlib/readers.py b/Lib/importlib/readers.py index 41089c0..b470a20 100644 --- a/Lib/importlib/readers.py +++ b/Lib/importlib/readers.py @@ -1,8 +1,12 @@ import collections -import zipfile +import operator import pathlib +import zipfile + from . import abc +from ._itertools import unique_everseen + def remove_duplicates(items): return iter(collections.OrderedDict.fromkeys(items)) @@ -63,13 +67,8 @@ class MultiplexedPath(abc.Traversable): raise NotADirectoryError('MultiplexedPath only supports directories') def iterdir(self): - visited = [] - for path in self._paths: - for file in path.iterdir(): - if file.name in visited: - continue - visited.append(file.name) - yield file + files = (file for path in self._paths for file in path.iterdir()) + return unique_everseen(files, key=operator.attrgetter('name')) def read_bytes(self): raise FileNotFoundError(f'{self} is not a file') diff --git a/Lib/importlib/resources.py b/Lib/importlib/resources.py index bb5c354..6cc4628 100644 --- a/Lib/importlib/resources.py +++ b/Lib/importlib/resources.py @@ -1,19 +1,23 @@ -import os -import io +"""Read resources contained within a package.""" -from . import _common -from ._common import as_file, files -from .abc import ResourceReader -from contextlib import suppress -from importlib.abc import ResourceLoader -from importlib.machinery import ModuleSpec -from io import BytesIO, TextIOWrapper -from pathlib import Path -from types import ModuleType -from typing import ContextManager, Iterable, Union -from typing import cast, BinaryIO, TextIO -from collections.abc import Sequence -from functools import singledispatch +from ._common import ( + as_file, + files, + Package, + Resource, +) + +from ._legacy import ( + contents, + open_binary, + read_binary, + open_text, + read_text, + is_resource, + path, +) + +from importlib.abc import ResourceReader __all__ = [ @@ -30,155 +34,3 @@ __all__ = [ 'read_binary', 'read_text', ] - - -Package = Union[str, ModuleType] -Resource = Union[str, os.PathLike] - - -def open_binary(package: Package, resource: Resource) -> BinaryIO: - """Return a file-like object opened for binary reading of the resource.""" - resource = _common.normalize_path(resource) - package = _common.get_package(package) - reader = _common.get_resource_reader(package) - if reader is not None: - return reader.open_resource(resource) - spec = cast(ModuleSpec, package.__spec__) - # Using pathlib doesn't work well here due to the lack of 'strict' - # argument for pathlib.Path.resolve() prior to Python 3.6. - if spec.submodule_search_locations is not None: - paths = spec.submodule_search_locations - elif spec.origin is not None: - paths = [os.path.dirname(os.path.abspath(spec.origin))] - - for package_path in paths: - full_path = os.path.join(package_path, resource) - try: - return open(full_path, mode='rb') - except OSError: - # Just assume the loader is a resource loader; all the relevant - # importlib.machinery loaders are and an AttributeError for - # get_data() will make it clear what is needed from the loader. - loader = cast(ResourceLoader, spec.loader) - data = None - if hasattr(spec.loader, 'get_data'): - with suppress(OSError): - data = loader.get_data(full_path) - if data is not None: - return BytesIO(data) - - raise FileNotFoundError(f'{resource!r} resource not found in {spec.name!r}') - - -def open_text( - package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict', -) -> TextIO: - """Return a file-like object opened for text reading of the resource.""" - return TextIOWrapper( - open_binary(package, resource), encoding=encoding, errors=errors - ) - - -def read_binary(package: Package, resource: Resource) -> bytes: - """Return the binary contents of the resource.""" - with open_binary(package, resource) as fp: - return fp.read() - - -def read_text( - package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict', -) -> str: - """Return the decoded string of the resource. - - The decoding-related arguments have the same semantics as those of - bytes.decode(). - """ - with open_text(package, resource, encoding, errors) as fp: - return fp.read() - - -def path( - package: Package, - resource: Resource, -) -> 'ContextManager[Path]': - """A context manager providing a file path object to the resource. - - If the resource does not already exist on its own on the file system, - a temporary file will be created. If the file was created, the file - will be deleted upon exiting the context manager (no exception is - raised if the file was deleted prior to the context manager - exiting). - """ - reader = _common.get_resource_reader(_common.get_package(package)) - return ( - _path_from_reader(reader, _common.normalize_path(resource)) - if reader - else _common.as_file( - _common.files(package).joinpath(_common.normalize_path(resource)) - ) - ) - - -def _path_from_reader(reader, resource): - return _path_from_resource_path(reader, resource) or _path_from_open_resource( - reader, resource - ) - - -def _path_from_resource_path(reader, resource): - with suppress(FileNotFoundError): - return Path(reader.resource_path(resource)) - - -def _path_from_open_resource(reader, resource): - saved = io.BytesIO(reader.open_resource(resource).read()) - return _common._tempfile(saved.read, suffix=resource) - - -def is_resource(package: Package, name: str) -> bool: - """True if 'name' is a resource inside 'package'. - - Directories are *not* resources. - """ - package = _common.get_package(package) - _common.normalize_path(name) - reader = _common.get_resource_reader(package) - if reader is not None: - return reader.is_resource(name) - package_contents = set(contents(package)) - if name not in package_contents: - return False - return (_common.from_package(package) / name).is_file() - - -def contents(package: Package) -> Iterable[str]: - """Return an iterable of entries in 'package'. - - Note that not all entries are resources. Specifically, directories are - not considered resources. Use `is_resource()` on each entry returned here - to check if it is a resource or not. - """ - package = _common.get_package(package) - reader = _common.get_resource_reader(package) - if reader is not None: - return _ensure_sequence(reader.contents()) - transversable = _common.from_package(package) - if transversable.is_dir(): - return list(item.name for item in transversable.iterdir()) - return [] - - -@singledispatch -def _ensure_sequence(iterable): - return list(iterable) - - -@_ensure_sequence.register(Sequence) -def _(iterable): - return iterable diff --git a/Lib/importlib/simple.py b/Lib/importlib/simple.py new file mode 100644 index 0000000..da073cb --- /dev/null +++ b/Lib/importlib/simple.py @@ -0,0 +1,116 @@ +""" +Interface adapters for low-level readers. +""" + +import abc +import io +import itertools +from typing import BinaryIO, List + +from .abc import Traversable, TraversableResources + + +class SimpleReader(abc.ABC): + """ + The minimum, low-level interface required from a resource + provider. + """ + + @abc.abstractproperty + def package(self): + # type: () -> str + """ + The name of the package for which this reader loads resources. + """ + + @abc.abstractmethod + def children(self): + # type: () -> List['SimpleReader'] + """ + Obtain an iterable of SimpleReader for available + child containers (e.g. directories). + """ + + @abc.abstractmethod + def resources(self): + # type: () -> List[str] + """ + Obtain available named resources for this virtual package. + """ + + @abc.abstractmethod + def open_binary(self, resource): + # type: (str) -> BinaryIO + """ + Obtain a File-like for a named resource. + """ + + @property + def name(self): + return self.package.split('.')[-1] + + +class ResourceHandle(Traversable): + """ + Handle to a named resource in a ResourceReader. + """ + + def __init__(self, parent, name): + # type: (ResourceContainer, str) -> None + self.parent = parent + self.name = name # type: ignore + + def is_file(self): + return True + + def is_dir(self): + return False + + def open(self, mode='r', *args, **kwargs): + stream = self.parent.reader.open_binary(self.name) + if 'b' not in mode: + stream = io.TextIOWrapper(*args, **kwargs) + return stream + + def joinpath(self, name): + raise RuntimeError("Cannot traverse into a resource") + + +class ResourceContainer(Traversable): + """ + Traversable container for a package's resources via its reader. + """ + + def __init__(self, reader): + # type: (SimpleReader) -> None + self.reader = reader + + def is_dir(self): + return True + + def is_file(self): + return False + + def iterdir(self): + files = (ResourceHandle(self, name) for name in self.reader.resources) + dirs = map(ResourceContainer, self.reader.children()) + return itertools.chain(files, dirs) + + def open(self, *args, **kwargs): + raise IsADirectoryError() + + def joinpath(self, name): + return next( + traversable for traversable in self.iterdir() if traversable.name == name + ) + + +class TraversableReader(TraversableResources, SimpleReader): + """ + A TraversableResources based on SimpleReader. Resource providers + may derive from this class to provide the TraversableResources + interface by supplying the SimpleReader interface. + """ + + def files(self): + return ResourceContainer(self) |