mirror of https://github.com/python/cpython
gh-97930: Apply changes from importlib_resources 5.10. (GH-100598)
This commit is contained in:
parent
ba1342ce99
commit
447d061bc7
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -86,6 +95,7 @@ for example, a package and its resources can be imported from a zip file using
|
|||
|
||||
.. versionadded:: 3.9
|
||||
|
||||
|
||||
Deprecated functions
|
||||
--------------------
|
||||
|
||||
|
@ -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
|
||||
|
@ -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*.
|
||||
|
|
|
@ -5,25 +5,58 @@ import functools
|
|||
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
|
||||
"""
|
||||
return from_package(get_package(package))
|
||||
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
|
||||
|
||||
|
||||
def get_resource_reader(package):
|
||||
# type: (types.ModuleType) -> Optional[ResourceReader]
|
||||
@package_to_anchor
|
||||
def files(anchor: Optional[Anchor] = None) -> Traversable:
|
||||
"""
|
||||
Get a Traversable resource for an anchor.
|
||||
"""
|
||||
return from_package(resolve(anchor))
|
||||
|
||||
|
||||
def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
|
||||
"""
|
||||
Return the package's loader if it's a ResourceReader.
|
||||
"""
|
||||
|
@ -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)
|
||||
|
||||
|
||||
def get_package(package):
|
||||
# type: (Package) -> types.ModuleType
|
||||
"""Take a package name or module object and return the module.
|
||||
@resolve.register
|
||||
def _(cand: str) -> types.ModuleType:
|
||||
return importlib.import_module(cand)
|
||||
|
||||
Raise an exception if the resolved module is not a package.
|
||||
|
||||
@resolve.register
|
||||
def _(cand: None) -> types.ModuleType:
|
||||
return resolve(_infer_caller().f_globals['__name__'])
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -27,8 +27,7 @@ def deprecated(func):
|
|||
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.
|
||||
|
|
|
@ -142,7 +142,8 @@ class Traversable(Protocol):
|
|||
accepted by io.TextIOWrapper.
|
||||
"""
|
||||
|
||||
@abc.abstractproperty
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def name(self) -> str:
|
||||
"""
|
||||
The base name of this object without any parent references.
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
@ -50,13 +47,35 @@ class SimpleReader(abc.ABC):
|
|||
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
|
||||
|
||||
|
@ -76,30 +95,6 @@ class ResourceHandle(Traversable):
|
|||
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
|
||||
|
|
|
@ -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
|
||||
####
|
|
@ -1,10 +1,24 @@
|
|||
import typing
|
||||
import textwrap
|
||||
import unittest
|
||||
import warnings
|
||||
import importlib
|
||||
import contextlib
|
||||
|
||||
from importlib import resources
|
||||
from importlib.resources.abc import Traversable
|
||||
from . import data01
|
||||
from . import util
|
||||
from . import _path
|
||||
from test.support import os_helper
|
||||
from test.support import import_helper
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_known_deprecation():
|
||||
with warnings.catch_warnings(record=True) as ctx:
|
||||
warnings.simplefilter('default', category=DeprecationWarning)
|
||||
yield ctx
|
||||
|
||||
|
||||
class FilesTests:
|
||||
|
@ -25,6 +39,14 @@ class FilesTests:
|
|||
def test_traversable(self):
|
||||
assert isinstance(resources.files(self.data), Traversable)
|
||||
|
||||
def test_old_parameter(self):
|
||||
"""
|
||||
Files used to take a 'package' parameter. Make sure anyone
|
||||
passing by name is still supported.
|
||||
"""
|
||||
with suppress_known_deprecation():
|
||||
resources.files(package=self.data)
|
||||
|
||||
|
||||
class OpenDiskTests(FilesTests, unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
@ -42,5 +64,50 @@ class OpenNamespaceTests(FilesTests, unittest.TestCase):
|
|||
self.data = namespacedata01
|
||||
|
||||
|
||||
class SiteDir:
|
||||
def setUp(self):
|
||||
self.fixtures = contextlib.ExitStack()
|
||||
self.addCleanup(self.fixtures.close)
|
||||
self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())
|
||||
self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir))
|
||||
self.fixtures.enter_context(import_helper.CleanImport())
|
||||
|
||||
|
||||
class ModulesFilesTests(SiteDir, unittest.TestCase):
|
||||
def test_module_resources(self):
|
||||
"""
|
||||
A module can have resources found adjacent to the module.
|
||||
"""
|
||||
spec = {
|
||||
'mod.py': '',
|
||||
'res.txt': 'resources are the best',
|
||||
}
|
||||
_path.build(spec, self.site_dir)
|
||||
import mod
|
||||
|
||||
actual = resources.files(mod).joinpath('res.txt').read_text()
|
||||
assert actual == spec['res.txt']
|
||||
|
||||
|
||||
class ImplicitContextFilesTests(SiteDir, unittest.TestCase):
|
||||
def test_implicit_files(self):
|
||||
"""
|
||||
Without any parameter, files() will infer the location as the caller.
|
||||
"""
|
||||
spec = {
|
||||
'somepkg': {
|
||||
'__init__.py': textwrap.dedent(
|
||||
"""
|
||||
import importlib.resources as res
|
||||
val = res.files().joinpath('res.txt').read_text()
|
||||
"""
|
||||
),
|
||||
'res.txt': 'resources are the best',
|
||||
},
|
||||
}
|
||||
_path.build(spec, self.site_dir)
|
||||
assert importlib.import_module('somepkg').val == 'resources are the best'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -3,7 +3,7 @@ import importlib
|
|||
import io
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path, PurePath
|
||||
import pathlib
|
||||
|
||||
from . import data01
|
||||
from . import zipdata01
|
||||
|
@ -94,7 +94,7 @@ class CommonTests(metaclass=abc.ABCMeta):
|
|||
|
||||
def test_pathlib_path(self):
|
||||
# Passing in a pathlib.PurePath object for the path should succeed.
|
||||
path = PurePath('utf-8.file')
|
||||
path = pathlib.PurePath('utf-8.file')
|
||||
self.execute(data01, path)
|
||||
|
||||
def test_importing_module_as_side_effect(self):
|
||||
|
@ -102,17 +102,6 @@ class CommonTests(metaclass=abc.ABCMeta):
|
|||
del sys.modules[data01.__name__]
|
||||
self.execute(data01.__name__, 'utf-8.file')
|
||||
|
||||
def test_non_package_by_name(self):
|
||||
# The anchor package cannot be a module.
|
||||
with self.assertRaises(TypeError):
|
||||
self.execute(__name__, 'utf-8.file')
|
||||
|
||||
def test_non_package_by_package(self):
|
||||
# The anchor package cannot be a module.
|
||||
with self.assertRaises(TypeError):
|
||||
module = sys.modules['test.test_importlib.resources.util']
|
||||
self.execute(module, 'utf-8.file')
|
||||
|
||||
def test_missing_path(self):
|
||||
# Attempting to open or read or request the path for a
|
||||
# non-existent path should succeed if open_resource
|
||||
|
@ -144,7 +133,7 @@ class ZipSetupBase:
|
|||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
data_path = Path(cls.ZIP_MODULE.__file__)
|
||||
data_path = pathlib.Path(cls.ZIP_MODULE.__file__)
|
||||
data_dir = data_path.parent
|
||||
cls._zip_path = str(data_dir / 'ziptestdata.zip')
|
||||
sys.path.append(cls._zip_path)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
``importlib.resources.files`` now accepts a module as an anchor instead of
|
||||
only accepting packages. If a module is passed, resources are resolved
|
||||
adjacent to that module (in the same package or at the package root). The
|
||||
parameter was renamed from ``package`` to ``anchor`` with a compatibility
|
||||
shim for those passing by keyword. Additionally, the new ``anchor``
|
||||
parameter is now optional and will default to the caller's module.
|
Loading…
Reference in New Issue