[3.9] bpo-39791 native hooks for importlib.resources.files (GH-20576) by miss-islington · Pull Request #20703 · python/cpython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 2 additions & 24 deletions Lib/importlib/_bootstrap_external.py
82 changes: 63 additions & 19 deletions Lib/importlib/_common.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,82 @@
import os
import pathlib
import zipfile
import tempfile
import functools
import contextlib
import types
import importlib

from typing import Union, Any, Optional
from .abc import ResourceReader

def from_package(package):
Package = Union[types.ModuleType, str]


def files(package):
"""
Return a Traversable object for the given package.
Get a Traversable resource from a package
"""
return from_package(get_package(package))


def normalize_path(path):
# type: (Any) -> str
"""Normalize a path by ensuring it is a string.

If the resulting string contains path separators, an exception is raised.
"""
spec = package.__spec__
return from_traversable_resources(spec) or fallback_resources(spec)
str_path = str(path)
parent, file_name = os.path.split(str_path)
if parent:
raise ValueError('{!r} must be only a file name'.format(path))
return file_name


def from_traversable_resources(spec):
def get_resource_reader(package):
# type: (types.ModuleType) -> Optional[ResourceReader]
"""
If the spec.loader implements TraversableResources,
directly or implicitly, it will have a ``files()`` method.
Return the package's loader if it's a ResourceReader.
"""
with contextlib.suppress(AttributeError):
return spec.loader.files()
# We can't use
# a issubclass() check here because apparently abc.'s __subclasscheck__()
# hook wants to create a weak reference to the object, but
# zipimport.zipimporter does not support weak references, resulting in a
# TypeError. That seems terrible.
spec = package.__spec__
reader = getattr(spec.loader, 'get_resource_reader', None)
if reader is None:
return None
return reader(spec.name)


def fallback_resources(spec):
package_directory = pathlib.Path(spec.origin).parent
try:
archive_path = spec.loader.archive
rel_path = package_directory.relative_to(archive_path)
return zipfile.Path(archive_path, str(rel_path) + '/')
except Exception:
pass
return package_directory
def resolve(cand):
# type: (Package) -> types.ModuleType
return (
cand if isinstance(cand, types.ModuleType)
else importlib.import_module(cand)
)


def get_package(package):
# type: (Package) -> types.ModuleType
"""Take a package name or module object and return the module.

Raise an exception if the resolved module is not a package.
"""
resolved = resolve(package)
if resolved.__spec__.submodule_search_locations is None:
raise TypeError('{!r} is not a package'.format(package))
return resolved


def from_package(package):
"""
Return a Traversable object for the given package.

"""
spec = package.__spec__
reader = spec.loader.get_resource_reader(spec.name)
return reader.files()


@contextlib.contextmanager
Expand Down
2 changes: 1 addition & 1 deletion Lib/importlib/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ def resource_path(self, resource):
raise FileNotFoundError(resource)

def is_resource(self, path):
return self.files().joinpath(path).isfile()
return self.files().joinpath(path).is_file()

def contents(self):
return (item.name for item in self.files().iterdir())
30 changes: 30 additions & 0 deletions Lib/importlib/readers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import zipfile
import pathlib
from . import abc


class FileReader(abc.TraversableResources):
def __init__(self, loader):
self.path = pathlib.Path(loader.path).parent

def files(self):
return self.path


class ZipReader(FileReader):
def __init__(self, loader, module):
_, _, name = module.rpartition('.')
prefix = loader.prefix.replace('\\', '/') + name + '/'
self.path = zipfile.Path(loader.archive, prefix)

def open_resource(self, resource):
try:
return super().open_resource(resource)
except KeyError as exc:
raise FileNotFoundError(exc.args[0])

def is_resource(self, path):
# workaround for `zipfile.Path.is_file` returning true
# for non-existent paths.
target = self.files().joinpath(path)
return target.is_file() and target.exists()
85 changes: 14 additions & 71 deletions Lib/importlib/resources.py
Loading