diff options
author | Jason R. Coombs <jaraco@jaraco.com> | 2021-03-04 18:43:00 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-04 18:43:00 (GMT) |
commit | 67148254146948041a77d8a2989f41b88cdb2f99 (patch) | |
tree | 036bcb818e80090b34f0c59f57f8b6946b52b21d /Lib/importlib/resources.py | |
parent | fbf75b9997e280b1220755d0a17dbed71240d42e (diff) | |
download | cpython-67148254146948041a77d8a2989f41b88cdb2f99.zip cpython-67148254146948041a77d8a2989f41b88cdb2f99.tar.gz cpython-67148254146948041a77d8a2989f41b88cdb2f99.tar.bz2 |
bpo-42129: Add support for resources in namespaces (GH-24670)
* Unify behavior in ResourceReaderDefaultsTests and align with the behavior found in importlib_resources.
* Equip NamespaceLoader with a NamespaceReader.
* Apply changes from importlib_resources 5.0.4
Diffstat (limited to 'Lib/importlib/resources.py')
-rw-r--r-- | Lib/importlib/resources.py | 110 |
1 files changed, 61 insertions, 49 deletions
diff --git a/Lib/importlib/resources.py b/Lib/importlib/resources.py index 4169171..db0e0c0 100644 --- a/Lib/importlib/resources.py +++ b/Lib/importlib/resources.py @@ -3,8 +3,10 @@ import io 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 @@ -18,6 +20,7 @@ from functools import singledispatch __all__ = [ 'Package', 'Resource', + 'ResourceReader', 'as_file', 'contents', 'files', @@ -27,7 +30,7 @@ __all__ = [ 'path', 'read_binary', 'read_text', - ] +] Package = Union[str, ModuleType] @@ -41,36 +44,45 @@ def open_binary(package: Package, resource: Resource) -> BinaryIO: reader = _common.get_resource_reader(package) if reader is not None: return reader.open_resource(resource) - absolute_package_path = os.path.abspath( - package.__spec__.origin or 'non-existent file') - package_path = os.path.dirname(absolute_package_path) - full_path = os.path.join(package_path, resource) - try: - return open(full_path, mode='rb') - except OSError: - # Just assume the loader is a resource loader; all the relevant - # importlib.machinery loaders are and an AttributeError for - # get_data() will make it clear what is needed from the loader. - loader = cast(ResourceLoader, package.__spec__.loader) - data = None - if hasattr(package.__spec__.loader, 'get_data'): - with suppress(OSError): - data = loader.get_data(full_path) - if data is None: - package_name = package.__spec__.name - message = '{!r} resource not found in {!r}'.format( - resource, package_name) - raise FileNotFoundError(message) - return BytesIO(data) - - -def open_text(package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict') -> TextIO: + 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( + '{!r} resource not found in {!r}'.format(resource, spec.name) + ) + + +def open_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> TextIO: """Return a file-like object opened for text reading of the resource.""" return TextIOWrapper( - open_binary(package, resource), encoding=encoding, errors=errors) + open_binary(package, resource), encoding=encoding, errors=errors + ) def read_binary(package: Package, resource: Resource) -> bytes: @@ -79,10 +91,12 @@ def read_binary(package: Package, resource: Resource) -> bytes: return fp.read() -def read_text(package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict') -> str: +def read_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> str: """Return the decoded string of the resource. The decoding-related arguments have the same semantics as those of @@ -93,8 +107,9 @@ def read_text(package: Package, def path( - package: Package, resource: Resource, - ) -> 'ContextManager[Path]': + package: Package, + resource: Resource, +) -> 'ContextManager[Path]': """A context manager providing a file path object to the resource. If the resource does not already exist on its own on the file system, @@ -106,15 +121,17 @@ def path( 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))) + 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) + return _path_from_resource_path(reader, resource) or _path_from_open_resource( + reader, resource + ) def _path_from_resource_path(reader, resource): @@ -154,15 +171,10 @@ def contents(package: Package) -> Iterable[str]: reader = _common.get_resource_reader(package) if reader is not None: return _ensure_sequence(reader.contents()) - # Is the package a namespace package? By definition, namespace packages - # cannot have resources. - namespace = ( - package.__spec__.origin is None or - package.__spec__.origin == 'namespace' - ) - if namespace or not package.__spec__.has_location: - return () - return list(item.name for item in _common.from_package(package).iterdir()) + transversable = _common.from_package(package) + if transversable.is_dir(): + return list(item.name for item in transversable.iterdir()) + return [] @singledispatch |