Skip to content

Commit 447d061

Browse files
authored
gh-97930: Apply changes from importlib_resources 5.10. (GH-100598)
1 parent ba1342c commit 447d061

File tree

9 files changed

+268
-90
lines changed

9 files changed

+268
-90
lines changed

Doc/library/importlib.resources.rst

+42-19
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ This module leverages Python's import system to provide access to *resources*
1414
within *packages*.
1515

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

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

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

52-
.. data:: Package
53+
.. data:: Anchor
5354

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

59-
The ``Package`` type is defined as ``Union[str, ModuleType]``.
60-
61-
.. function:: files(package)
59+
.. function:: files(anchor: Optional[Anchor] = None)
6260

6361
Returns a :class:`~importlib.resources.abc.Traversable` object
64-
representing the resource container for the package (think directory)
65-
and its resources (think files). A Traversable may contain other
66-
containers (think subdirectories).
62+
representing the resource container (think directory) and its resources
63+
(think files). A Traversable may contain other containers (think
64+
subdirectories).
6765

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

7172
.. versionadded:: 3.9
7273

74+
.. versionchanged:: 3.12
75+
"package" parameter was renamed to "anchor". "anchor" can now
76+
be a non-package module and if omitted will default to the caller's
77+
module. "package" is still accepted for compatibility but will raise
78+
a DeprecationWarning. Consider passing the anchor positionally or
79+
using ``importlib_resources >= 5.10`` for a compatible interface
80+
on older Pythons.
81+
7382
.. function:: as_file(traversable)
7483

7584
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
8695

8796
.. versionadded:: 3.9
8897

98+
8999
Deprecated functions
90100
--------------------
91101

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

107+
.. data:: Package
108+
109+
Whenever a function accepts a ``Package`` argument, you can pass in
110+
either a :class:`module object <types.ModuleType>` or a module name
111+
as a string. You can only pass module objects whose
112+
``__spec__.submodule_search_locations`` is not ``None``.
113+
114+
The ``Package`` type is defined as ``Union[str, ModuleType]``.
115+
116+
.. deprecated:: 3.12
117+
118+
97119
.. data:: Resource
98120

99121
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*.
102124

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

127+
105128
.. function:: open_binary(package, resource)
106129

107130
Open for binary reading the *resource* within *package*.

Lib/importlib/resources/_common.py

+67-19
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,58 @@
55
import contextlib
66
import types
77
import importlib
8+
import inspect
9+
import warnings
10+
import itertools
811

9-
from typing import Union, Optional
12+
from typing import Union, Optional, cast
1013
from .abc import ResourceReader, Traversable
1114

1215
from ._adapters import wrap_spec
1316

1417
Package = Union[types.ModuleType, str]
18+
Anchor = Package
1519

1620

17-
def files(package):
18-
# type: (Package) -> Traversable
21+
def package_to_anchor(func):
1922
"""
20-
Get a Traversable resource from a package
23+
Replace 'package' parameter as 'anchor' and warn about the change.
24+
25+
Other errors should fall through.
26+
27+
>>> files('a', 'b')
28+
Traceback (most recent call last):
29+
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
30+
"""
31+
undefined = object()
32+
33+
@functools.wraps(func)
34+
def wrapper(anchor=undefined, package=undefined):
35+
if package is not undefined:
36+
if anchor is not undefined:
37+
return func(anchor, package)
38+
warnings.warn(
39+
"First parameter to files is renamed to 'anchor'",
40+
DeprecationWarning,
41+
stacklevel=2,
42+
)
43+
return func(package)
44+
elif anchor is undefined:
45+
return func()
46+
return func(anchor)
47+
48+
return wrapper
49+
50+
51+
@package_to_anchor
52+
def files(anchor: Optional[Anchor] = None) -> Traversable:
53+
"""
54+
Get a Traversable resource for an anchor.
2155
"""
22-
return from_package(get_package(package))
56+
return from_package(resolve(anchor))
2357

2458

25-
def get_resource_reader(package):
26-
# type: (types.ModuleType) -> Optional[ResourceReader]
59+
def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
2760
"""
2861
Return the package's loader if it's a ResourceReader.
2962
"""
@@ -39,24 +72,39 @@ def get_resource_reader(package):
3972
return reader(spec.name) # type: ignore
4073

4174

42-
def resolve(cand):
43-
# type: (Package) -> types.ModuleType
44-
return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand)
75+
@functools.singledispatch
76+
def resolve(cand: Optional[Anchor]) -> types.ModuleType:
77+
return cast(types.ModuleType, cand)
78+
79+
80+
@resolve.register
81+
def _(cand: str) -> types.ModuleType:
82+
return importlib.import_module(cand)
83+
4584

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

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

51-
Raise an exception if the resolved module is not a package.
90+
def _infer_caller():
5291
"""
53-
resolved = resolve(package)
54-
if wrap_spec(resolved).submodule_search_locations is None:
55-
raise TypeError(f'{package!r} is not a package')
56-
return resolved
92+
Walk the stack and find the frame of the first caller not in this module.
93+
"""
94+
95+
def is_this_file(frame_info):
96+
return frame_info.filename == __file__
97+
98+
def is_wrapper(frame_info):
99+
return frame_info.function == 'wrapper'
100+
101+
not_this_file = itertools.filterfalse(is_this_file, inspect.stack())
102+
# also exclude 'wrapper' due to singledispatch in the call stack
103+
callers = itertools.filterfalse(is_wrapper, not_this_file)
104+
return next(callers).frame
57105

58106

59-
def from_package(package):
107+
def from_package(package: types.ModuleType):
60108
"""
61109
Return a Traversable object for the given package.
62110

Lib/importlib/resources/_legacy.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ def wrapper(*args, **kwargs):
2727
return wrapper
2828

2929

30-
def normalize_path(path):
31-
# type: (Any) -> str
30+
def normalize_path(path: Any) -> str:
3231
"""Normalize a path by ensuring it is a string.
3332
3433
If the resulting string contains path separators, an exception is raised.

Lib/importlib/resources/abc.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ def open(self, mode='r', *args, **kwargs):
142142
accepted by io.TextIOWrapper.
143143
"""
144144

145-
@abc.abstractproperty
145+
@property
146+
@abc.abstractmethod
146147
def name(self) -> str:
147148
"""
148149
The base name of this object without any parent references.

Lib/importlib/resources/simple.py

+30-35
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,28 @@ class SimpleReader(abc.ABC):
1616
provider.
1717
"""
1818

19-
@abc.abstractproperty
20-
def package(self):
21-
# type: () -> str
19+
@property
20+
@abc.abstractmethod
21+
def package(self) -> str:
2222
"""
2323
The name of the package for which this reader loads resources.
2424
"""
2525

2626
@abc.abstractmethod
27-
def children(self):
28-
# type: () -> List['SimpleReader']
27+
def children(self) -> List['SimpleReader']:
2928
"""
3029
Obtain an iterable of SimpleReader for available
3130
child containers (e.g. directories).
3231
"""
3332

3433
@abc.abstractmethod
35-
def resources(self):
36-
# type: () -> List[str]
34+
def resources(self) -> List[str]:
3735
"""
3836
Obtain available named resources for this virtual package.
3937
"""
4038

4139
@abc.abstractmethod
42-
def open_binary(self, resource):
43-
# type: (str) -> BinaryIO
40+
def open_binary(self, resource: str) -> BinaryIO:
4441
"""
4542
Obtain a File-like for a named resource.
4643
"""
@@ -50,13 +47,35 @@ def name(self):
5047
return self.package.split('.')[-1]
5148

5249

50+
class ResourceContainer(Traversable):
51+
"""
52+
Traversable container for a package's resources via its reader.
53+
"""
54+
55+
def __init__(self, reader: SimpleReader):
56+
self.reader = reader
57+
58+
def is_dir(self):
59+
return True
60+
61+
def is_file(self):
62+
return False
63+
64+
def iterdir(self):
65+
files = (ResourceHandle(self, name) for name in self.reader.resources)
66+
dirs = map(ResourceContainer, self.reader.children())
67+
return itertools.chain(files, dirs)
68+
69+
def open(self, *args, **kwargs):
70+
raise IsADirectoryError()
71+
72+
5373
class ResourceHandle(Traversable):
5474
"""
5575
Handle to a named resource in a ResourceReader.
5676
"""
5777

58-
def __init__(self, parent, name):
59-
# type: (ResourceContainer, str) -> None
78+
def __init__(self, parent: ResourceContainer, name: str):
6079
self.parent = parent
6180
self.name = name # type: ignore
6281

@@ -76,30 +95,6 @@ def joinpath(self, name):
7695
raise RuntimeError("Cannot traverse into a resource")
7796

7897

79-
class ResourceContainer(Traversable):
80-
"""
81-
Traversable container for a package's resources via its reader.
82-
"""
83-
84-
def __init__(self, reader):
85-
# type: (SimpleReader) -> None
86-
self.reader = reader
87-
88-
def is_dir(self):
89-
return True
90-
91-
def is_file(self):
92-
return False
93-
94-
def iterdir(self):
95-
files = (ResourceHandle(self, name) for name in self.reader.resources)
96-
dirs = map(ResourceContainer, self.reader.children())
97-
return itertools.chain(files, dirs)
98-
99-
def open(self, *args, **kwargs):
100-
raise IsADirectoryError()
101-
102-
10398
class TraversableReader(TraversableResources, SimpleReader):
10499
"""
105100
A TraversableResources based on SimpleReader. Resource providers
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pathlib
2+
import functools
3+
4+
5+
####
6+
# from jaraco.path 3.4
7+
8+
9+
def build(spec, prefix=pathlib.Path()):
10+
"""
11+
Build a set of files/directories, as described by the spec.
12+
13+
Each key represents a pathname, and the value represents
14+
the content. Content may be a nested directory.
15+
16+
>>> spec = {
17+
... 'README.txt': "A README file",
18+
... "foo": {
19+
... "__init__.py": "",
20+
... "bar": {
21+
... "__init__.py": "",
22+
... },
23+
... "baz.py": "# Some code",
24+
... }
25+
... }
26+
>>> tmpdir = getfixture('tmpdir')
27+
>>> build(spec, tmpdir)
28+
"""
29+
for name, contents in spec.items():
30+
create(contents, pathlib.Path(prefix) / name)
31+
32+
33+
@functools.singledispatch
34+
def create(content, path):
35+
path.mkdir(exist_ok=True)
36+
build(content, prefix=path) # type: ignore
37+
38+
39+
@create.register
40+
def _(content: bytes, path):
41+
path.write_bytes(content)
42+
43+
44+
@create.register
45+
def _(content: str, path):
46+
path.write_text(content)
47+
48+
49+
# end from jaraco.path
50+
####

0 commit comments

Comments
 (0)