bpo-42129: Add support for resources in namespaces by jaraco · Pull Request #24670 · 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
82 changes: 82 additions & 0 deletions Lib/importlib/_adapters.py
4 changes: 4 additions & 0 deletions Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,10 @@ def load_module(self, fullname):
# 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)


# Finders #####################################################################

Expand Down
15 changes: 7 additions & 8 deletions Lib/importlib/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from typing import Union, Any, Optional
from .abc import ResourceReader

from ._adapters import wrap_spec

Package = Union[types.ModuleType, str]


Expand Down Expand Up @@ -43,18 +45,15 @@ def get_resource_reader(package):
# 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)
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore
if reader is None:
return None
return reader(spec.name)
return reader(spec.name) # type: ignore


def resolve(cand):
# type: (Package) -> types.ModuleType
return (
cand if isinstance(cand, types.ModuleType)
else importlib.import_module(cand)
)
return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand)


def get_package(package):
Expand All @@ -64,7 +63,7 @@ def get_package(package):
Raise an exception if the resolved module is not a package.
"""
resolved = resolve(package)
if resolved.__spec__.submodule_search_locations is None:
if wrap_spec(resolved).submodule_search_locations is None:
raise TypeError('{!r} is not a package'.format(package))
return resolved

Expand All @@ -74,7 +73,7 @@ def from_package(package):
Return a Traversable object for the given package.

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

Expand Down
65 changes: 34 additions & 31 deletions Lib/importlib/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ._abc import Loader
import abc
import warnings
from typing import BinaryIO, Iterable, Text
from typing import Protocol, runtime_checkable


Expand Down Expand Up @@ -297,49 +298,45 @@ def set_data(self, path, data):


class ResourceReader(metaclass=abc.ABCMeta):

"""Abstract base class to provide resource-reading support.

Loaders that support resource reading are expected to implement
the ``get_resource_reader(fullname)`` method and have it either return None
or an object compatible with this ABC.
"""
"""Abstract base class for loaders to provide resource reading support."""

@abc.abstractmethod
def open_resource(self, resource):
def open_resource(self, resource: Text) -> BinaryIO:
"""Return an opened, file-like object for binary reading.

The 'resource' argument is expected to represent only a file name
and thus not contain any subdirectory components.

The 'resource' argument is expected to represent only a file name.
If the resource cannot be found, FileNotFoundError is raised.
"""
# This deliberately raises FileNotFoundError instead of
# NotImplementedError so that if this method is accidentally called,
# it'll still do the right thing.
raise FileNotFoundError

@abc.abstractmethod
def resource_path(self, resource):
def resource_path(self, resource: Text) -> Text:
"""Return the file system path to the specified resource.

The 'resource' argument is expected to represent only a file name
and thus not contain any subdirectory components.

The 'resource' argument is expected to represent only a file name.
If the resource does not exist on the file system, raise
FileNotFoundError.
"""
# This deliberately raises FileNotFoundError instead of
# NotImplementedError so that if this method is accidentally called,
# it'll still do the right thing.
raise FileNotFoundError

@abc.abstractmethod
def is_resource(self, name):
"""Return True if the named 'name' is consider a resource."""
def is_resource(self, path: Text) -> bool:
"""Return True if the named 'path' is a resource.

Files are resources, directories are not.
"""
raise FileNotFoundError

@abc.abstractmethod
def contents(self):
"""Return an iterable of strings over the contents of the package."""
return []


_register(ResourceReader, machinery.SourceFileLoader)
def contents(self) -> Iterable[str]:
"""Return an iterable of entries in `package`."""
raise FileNotFoundError


@runtime_checkable
Expand All @@ -355,26 +352,28 @@ def iterdir(self):
Yield Traversable objects in self
"""

@abc.abstractmethod
def read_bytes(self):
"""
Read contents of self as bytes
"""
with self.open('rb') as strm:
return strm.read()

@abc.abstractmethod
def read_text(self, encoding=None):
"""
Read contents of self as bytes
Read contents of self as text
"""
with self.open(encoding=encoding) as strm:
return strm.read()

@abc.abstractmethod
def is_dir(self):
def is_dir(self) -> bool:
"""
Return True if self is a dir
"""

@abc.abstractmethod
def is_file(self):
def is_file(self) -> bool:
"""
Return True if self is a file
"""
Expand All @@ -385,11 +384,11 @@ def joinpath(self, child):
Return Traversable child in self
"""

@abc.abstractmethod
def __truediv__(self, child):
"""
Return Traversable child in self
"""
return self.joinpath(child)

@abc.abstractmethod
def open(self, mode='r', *args, **kwargs):
Expand All @@ -402,14 +401,18 @@ def open(self, mode='r', *args, **kwargs):
"""

@abc.abstractproperty
def name(self):
# type: () -> str
def name(self) -> str:
"""
The base name of this object without any parent references.
"""


class TraversableResources(ResourceReader):
"""
The required interface for providing traversable
resources.
"""

@abc.abstractmethod
def files(self):
"""Return a Traversable object for the loaded package."""
Expand Down
82 changes: 82 additions & 0 deletions Lib/importlib/readers.py
Loading