[3.9] bpo-40924: Revert "bpo-39791 native hooks for importlib.resources.files (GH-20576)" (#20760)
This reverts commit 9cf1be46e3692d565461afd3afa326d124d743dd due to
https://bugs.python.org/issue40924.
diff --git a/Lib/importlib/resources.py b/Lib/importlib/resources.py
index 4535619..b803a01 100644
--- a/Lib/importlib/resources.py
+++ b/Lib/importlib/resources.py
@@ -1,13 +1,15 @@
import os
+from . import abc as resources_abc
from . import _common
-from ._common import as_file, files
+from ._common import as_file
from contextlib import contextmanager, suppress
+from importlib import import_module
from importlib.abc import ResourceLoader
from io import BytesIO, TextIOWrapper
from pathlib import Path
from types import ModuleType
-from typing import ContextManager, Iterable, Union
+from typing import ContextManager, Iterable, Optional, Union
from typing import cast
from typing.io import BinaryIO, TextIO
@@ -31,11 +33,60 @@
Resource = Union[str, os.PathLike]
+def _resolve(name) -> ModuleType:
+ """If name is a string, resolve to a module."""
+ if hasattr(name, '__spec__'):
+ return name
+ return import_module(name)
+
+
+def _get_package(package) -> ModuleType:
+ """Take a package name or module object and return the module.
+
+ If a name, the module is imported. If the resolved module
+ object is not a package, raise an exception.
+ """
+ module = _resolve(package)
+ if module.__spec__.submodule_search_locations is None:
+ raise TypeError('{!r} is not a package'.format(package))
+ return module
+
+
+def _normalize_path(path) -> str:
+ """Normalize a path by ensuring it is a string.
+
+ If the resulting string contains path separators, an exception is raised.
+ """
+ parent, file_name = os.path.split(path)
+ if parent:
+ raise ValueError('{!r} must be only a file name'.format(path))
+ return file_name
+
+
+def _get_resource_reader(
+ package: ModuleType) -> Optional[resources_abc.ResourceReader]:
+ # Return the package's loader if it's a ResourceReader. We can't use
+ # a issubclass() check here because apparently abc.'s __subclasscheck__()
+ # hook wants to create a weak reference to the object, but
+ # zipimport.zipimporter does not support weak references, resulting in a
+ # TypeError. That seems terrible.
+ spec = package.__spec__
+ if hasattr(spec.loader, 'get_resource_reader'):
+ return cast(resources_abc.ResourceReader,
+ spec.loader.get_resource_reader(spec.name))
+ return None
+
+
+def _check_location(package):
+ if package.__spec__.origin is None or not package.__spec__.has_location:
+ raise FileNotFoundError(f'Package has no location {package!r}')
+
+
def open_binary(package: Package, resource: Resource) -> BinaryIO:
"""Return a file-like object opened for binary reading of the resource."""
- resource = _common.normalize_path(resource)
- package = _common.get_package(package)
- reader = _common.get_resource_reader(package)
+ resource = _normalize_path(resource)
+ package = _get_package(package)
+ reader = _get_resource_reader(package)
if reader is not None:
return reader.open_resource(resource)
absolute_package_path = os.path.abspath(
@@ -89,6 +140,13 @@
return fp.read()
+def files(package: Package) -> resources_abc.Traversable:
+ """
+ Get a Traversable resource from a package
+ """
+ return _common.from_package(_get_package(package))
+
+
def path(
package: Package, resource: Resource,
) -> 'ContextManager[Path]':
@@ -100,18 +158,17 @@
raised if the file was deleted prior to the context manager
exiting).
"""
- reader = _common.get_resource_reader(_common.get_package(package))
+ reader = _get_resource_reader(_get_package(package))
return (
_path_from_reader(reader, resource)
if reader else
- _common.as_file(
- _common.files(package).joinpath(_common.normalize_path(resource)))
+ _common.as_file(files(package).joinpath(_normalize_path(resource)))
)
@contextmanager
def _path_from_reader(reader, resource):
- norm_resource = _common.normalize_path(resource)
+ norm_resource = _normalize_path(resource)
with suppress(FileNotFoundError):
yield Path(reader.resource_path(norm_resource))
return
@@ -125,9 +182,9 @@
Directories are *not* resources.
"""
- package = _common.get_package(package)
- _common.normalize_path(name)
- reader = _common.get_resource_reader(package)
+ package = _get_package(package)
+ _normalize_path(name)
+ reader = _get_resource_reader(package)
if reader is not None:
return reader.is_resource(name)
package_contents = set(contents(package))
@@ -143,8 +200,8 @@
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)
+ package = _get_package(package)
+ reader = _get_resource_reader(package)
if reader is not None:
return reader.contents()
# Is the package a namespace package? By definition, namespace packages