Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-97930: Apply changes from importlib_resources 5.10. #100598

Merged
merged 4 commits into from Jan 1, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 42 additions & 19 deletions Doc/library/importlib.resources.rst
Expand Up @@ -14,12 +14,13 @@ This module leverages Python's import system to provide access to *resources*
within *packages*.

"Resources" are file-like resources associated with a module or package in
Python. The resources may be contained directly in a package or within a
subdirectory contained in that package. Resources may be text or binary. As a
result, Python module sources (.py) of a package and compilation artifacts
(pycache) are technically de-facto resources of that package. In practice,
however, resources are primarily those non-Python artifacts exposed
specifically by the package author.
Python. The resources may be contained directly in a package, within a
subdirectory contained in that package, or adjacent to modules outside a
package. Resources may be text or binary. As a result, Python module sources
(.py) of a package and compilation artifacts (pycache) are technically
de-facto resources of that package. In practice, however, resources are
primarily those non-Python artifacts exposed specifically by the package
author.

Resources can be opened or read in either binary or text mode.

Expand Down Expand Up @@ -49,27 +50,35 @@ for example, a package and its resources can be imported from a zip file using
``get_resource_reader(fullname)`` method as specified by
:class:`importlib.resources.abc.ResourceReader`.

.. data:: Package
.. data:: Anchor

Whenever a function accepts a ``Package`` argument, you can pass in
either a :class:`module object <types.ModuleType>` or a module name
as a string. You can only pass module objects whose
``__spec__.submodule_search_locations`` is not ``None``.
Represents an anchor for resources, either a :class:`module object
<types.ModuleType>` or a module name as a string. Defined as
``Union[str, ModuleType]``.

The ``Package`` type is defined as ``Union[str, ModuleType]``.

.. function:: files(package)
.. function:: files(anchor: Optional[Anchor] = None)

Returns a :class:`~importlib.resources.abc.Traversable` object
representing the resource container for the package (think directory)
and its resources (think files). A Traversable may contain other
containers (think subdirectories).
representing the resource container (think directory) and its resources
(think files). A Traversable may contain other containers (think
subdirectories).

*package* is either a name or a module object which conforms to the
:data:`Package` requirements.
*anchor* is an optional :data:`Anchor`. If the anchor is a
package, resources are resolved from that package. If a module,
resources are resolved adjacent to that module (in the same package
or the package root). If the anchor is omitted, the caller's module
is used.

.. versionadded:: 3.9

.. versionchanged:: 3.12
"package" parameter was renamed to "anchor". "anchor" can now
be a non-package module and if omitted will default to the caller's
module. "package" is still accepted for compatibility but will raise
a DeprecationWarning. Consider passing the anchor positionally or
using ``importlib_resources >= 5.10`` for a compatible interface
on older Pythons.

.. function:: as_file(traversable)

Given a :class:`~importlib.resources.abc.Traversable` object representing
Expand All @@ -86,6 +95,7 @@ for example, a package and its resources can be imported from a zip file using

.. versionadded:: 3.9


Deprecated functions
--------------------

Expand All @@ -94,6 +104,18 @@ scheduled for removal in a future version of Python.
The main drawback of these functions is that they do not support
directories: they assume all resources are located directly within a *package*.

.. data:: Package

Whenever a function accepts a ``Package`` argument, you can pass in
either a :class:`module object <types.ModuleType>` or a module name
as a string. You can only pass module objects whose
``__spec__.submodule_search_locations`` is not ``None``.

The ``Package`` type is defined as ``Union[str, ModuleType]``.

.. deprecated:: 3.12


.. data:: Resource

For *resource* arguments of the functions below, you can pass in
Expand All @@ -102,6 +124,7 @@ directories: they assume all resources are located directly within a *package*.

The ``Resource`` type is defined as ``Union[str, os.PathLike]``.


.. function:: open_binary(package, resource)

Open for binary reading the *resource* within *package*.
Expand Down
86 changes: 67 additions & 19 deletions Lib/importlib/resources/_common.py
Expand Up @@ -5,25 +5,58 @@
import contextlib
import types
import importlib
import inspect
import warnings
import itertools

from typing import Union, Optional
from typing import Union, Optional, cast
from .abc import ResourceReader, Traversable

from ._adapters import wrap_spec

Package = Union[types.ModuleType, str]
Anchor = Package


def files(package):
# type: (Package) -> Traversable
def package_to_anchor(func):
"""
Get a Traversable resource from a package
Replace 'package' parameter as 'anchor' and warn about the change.

Other errors should fall through.

>>> files('a', 'b')
Traceback (most recent call last):
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
"""
undefined = object()

@functools.wraps(func)
def wrapper(anchor=undefined, package=undefined):
if package is not undefined:
if anchor is not undefined:
return func(anchor, package)
warnings.warn(
"First parameter to files is renamed to 'anchor'",
DeprecationWarning,
stacklevel=2,
)
return func(package)
elif anchor is undefined:
return func()
return func(anchor)

return wrapper


@package_to_anchor
def files(anchor: Optional[Anchor] = None) -> Traversable:
"""
Get a Traversable resource for an anchor.
"""
return from_package(get_package(package))
return from_package(resolve(anchor))


def get_resource_reader(package):
# type: (types.ModuleType) -> Optional[ResourceReader]
def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
"""
Return the package's loader if it's a ResourceReader.
"""
Expand All @@ -39,24 +72,39 @@ def get_resource_reader(package):
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)
@functools.singledispatch
def resolve(cand: Optional[Anchor]) -> types.ModuleType:
return cast(types.ModuleType, cand)


@resolve.register
def _(cand: str) -> types.ModuleType:
return importlib.import_module(cand)


@resolve.register
def _(cand: None) -> types.ModuleType:
return resolve(_infer_caller().f_globals['__name__'])

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.
def _infer_caller():
"""
resolved = resolve(package)
if wrap_spec(resolved).submodule_search_locations is None:
raise TypeError(f'{package!r} is not a package')
return resolved
Walk the stack and find the frame of the first caller not in this module.
"""

def is_this_file(frame_info):
return frame_info.filename == __file__

def is_wrapper(frame_info):
return frame_info.function == 'wrapper'

not_this_file = itertools.filterfalse(is_this_file, inspect.stack())
# also exclude 'wrapper' due to singledispatch in the call stack
callers = itertools.filterfalse(is_wrapper, not_this_file)
return next(callers).frame


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

Expand Down
3 changes: 1 addition & 2 deletions Lib/importlib/resources/_legacy.py
Expand Up @@ -27,8 +27,7 @@ def wrapper(*args, **kwargs):
return wrapper


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

If the resulting string contains path separators, an exception is raised.
Expand Down
3 changes: 2 additions & 1 deletion Lib/importlib/resources/abc.py
Expand Up @@ -142,7 +142,8 @@ def open(self, mode='r', *args, **kwargs):
accepted by io.TextIOWrapper.
"""

@abc.abstractproperty
@property
@abc.abstractmethod
def name(self) -> str:
"""
The base name of this object without any parent references.
Expand Down
65 changes: 30 additions & 35 deletions Lib/importlib/resources/simple.py
Expand Up @@ -16,31 +16,28 @@ class SimpleReader(abc.ABC):
provider.
"""

@abc.abstractproperty
def package(self):
# type: () -> str
@property
@abc.abstractmethod
def package(self) -> str:
"""
The name of the package for which this reader loads resources.
"""

@abc.abstractmethod
def children(self):
# type: () -> List['SimpleReader']
def children(self) -> List['SimpleReader']:
"""
Obtain an iterable of SimpleReader for available
child containers (e.g. directories).
"""

@abc.abstractmethod
def resources(self):
# type: () -> List[str]
def resources(self) -> List[str]:
"""
Obtain available named resources for this virtual package.
"""

@abc.abstractmethod
def open_binary(self, resource):
# type: (str) -> BinaryIO
def open_binary(self, resource: str) -> BinaryIO:
"""
Obtain a File-like for a named resource.
"""
Expand All @@ -50,13 +47,35 @@ def name(self):
return self.package.split('.')[-1]


class ResourceContainer(Traversable):
"""
Traversable container for a package's resources via its reader.
"""

def __init__(self, reader: SimpleReader):
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()


class ResourceHandle(Traversable):
"""
Handle to a named resource in a ResourceReader.
"""

def __init__(self, parent, name):
# type: (ResourceContainer, str) -> None
def __init__(self, parent: ResourceContainer, name: str):
self.parent = parent
self.name = name # type: ignore

Expand All @@ -76,30 +95,6 @@ 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()


class TraversableReader(TraversableResources, SimpleReader):
"""
A TraversableResources based on SimpleReader. Resource providers
Expand Down
50 changes: 50 additions & 0 deletions Lib/test/test_importlib/resources/_path.py
@@ -0,0 +1,50 @@
import pathlib
import functools


####
# from jaraco.path 3.4


def build(spec, prefix=pathlib.Path()):
"""
Build a set of files/directories, as described by the spec.

Each key represents a pathname, and the value represents
the content. Content may be a nested directory.

>>> spec = {
... 'README.txt': "A README file",
... "foo": {
... "__init__.py": "",
... "bar": {
... "__init__.py": "",
... },
... "baz.py": "# Some code",
... }
... }
>>> tmpdir = getfixture('tmpdir')
>>> build(spec, tmpdir)
"""
for name, contents in spec.items():
create(contents, pathlib.Path(prefix) / name)


@functools.singledispatch
def create(content, path):
path.mkdir(exist_ok=True)
build(content, prefix=path) # type: ignore


@create.register
def _(content: bytes, path):
path.write_bytes(content)


@create.register
def _(content: str, path):
path.write_text(content)


# end from jaraco.path
####