Skip to content

Commit

Permalink
gh-97930: Apply changes from importlib_resources 5.10. (GH-100598)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaraco authored Jan 1, 2023
1 parent ba1342c commit 447d061
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 90 deletions.
61 changes: 42 additions & 19 deletions Doc/library/importlib.resources.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
####
Loading

0 comments on commit 447d061

Please sign in to comment.