Skip to content

Commit 9ceaee7

Browse files
authored
pythongh-116608: importlib.resources: Un-deprecate functional API & add subdirectory support (pythonGH-116609)
1 parent 757b624 commit 9ceaee7

File tree

6 files changed

+533
-15
lines changed

6 files changed

+533
-15
lines changed

Doc/library/importlib.resources.rst

+178
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,181 @@ for example, a package and its resources can be imported from a zip file using
9797

9898
.. versionchanged:: 3.12
9999
Added support for *traversable* representing a directory.
100+
101+
102+
.. _importlib_resources_functional:
103+
104+
Functional API
105+
^^^^^^^^^^^^^^
106+
107+
A set of simplified, backwards-compatible helpers is available.
108+
These allow common operations in a single function call.
109+
110+
For all the following functions:
111+
112+
- *anchor* is an :class:`~importlib.resources.Anchor`,
113+
as in :func:`~importlib.resources.files`.
114+
Unlike in ``files``, it may not be omitted.
115+
116+
- *path_names* are components of a resource's path name, relative to
117+
the anchor.
118+
For example, to get the text of resource named ``info.txt``, use::
119+
120+
importlib.resources.read_text(my_module, "info.txt")
121+
122+
Like :meth:`Traversable.joinpath <importlib.resources.abc.Traversable>`,
123+
The individual components should use forward slashes (``/``)
124+
as path separators.
125+
For example, the following are equivalent::
126+
127+
importlib.resources.read_binary(my_module, "pics/painting.png")
128+
importlib.resources.read_binary(my_module, "pics", "painting.png")
129+
130+
For backward compatibility reasons, functions that read text require
131+
an explicit *encoding* argument if multiple *path_names* are given.
132+
For example, to get the text of ``info/chapter1.txt``, use::
133+
134+
importlib.resources.read_text(my_module, "info", "chapter1.txt",
135+
encoding='utf-8')
136+
137+
.. function:: open_binary(anchor, *path_names)
138+
139+
Open the named resource for binary reading.
140+
141+
See :ref:`the introduction <importlib_resources_functional>` for
142+
details on *anchor* and *path_names*.
143+
144+
This function returns a :class:`~typing.BinaryIO` object,
145+
that is, a binary stream open for reading.
146+
147+
This function is roughly equivalent to::
148+
149+
files(anchor).joinpath(*path_names).open('rb')
150+
151+
.. versionchanged:: 3.13
152+
Multiple *path_names* are accepted.
153+
154+
155+
.. function:: open_text(anchor, *path_names, encoding='utf-8', errors='strict')
156+
157+
Open the named resource for text reading.
158+
By default, the contents are read as strict UTF-8.
159+
160+
See :ref:`the introduction <importlib_resources_functional>` for
161+
details on *anchor* and *path_names*.
162+
*encoding* and *errors* have the same meaning as in built-in :func:`open`.
163+
164+
For backward compatibility reasons, the *encoding* argument must be given
165+
explicitly if there are multiple *path_names*.
166+
This limitation is scheduled to be removed in Python 3.15.
167+
168+
This function returns a :class:`~typing.TextIO` object,
169+
that is, a text stream open for reading.
170+
171+
This function is roughly equivalent to::
172+
173+
files(anchor).joinpath(*path_names).open('r', encoding=encoding)
174+
175+
.. versionchanged:: 3.13
176+
Multiple *path_names* are accepted.
177+
*encoding* and *errors* must be given as keyword arguments.
178+
179+
180+
.. function:: read_binary(anchor, *path_names)
181+
182+
Read and return the contents of the named resource as :class:`bytes`.
183+
184+
See :ref:`the introduction <importlib_resources_functional>` for
185+
details on *anchor* and *path_names*.
186+
187+
This function is roughly equivalent to::
188+
189+
files(anchor).joinpath(*path_names).read_bytes()
190+
191+
.. versionchanged:: 3.13
192+
Multiple *path_names* are accepted.
193+
194+
195+
.. function:: read_text(anchor, *path_names, encoding='utf-8', errors='strict')
196+
197+
Read and return the contents of the named resource as :class:`str`.
198+
By default, the contents are read as strict UTF-8.
199+
200+
See :ref:`the introduction <importlib_resources_functional>` for
201+
details on *anchor* and *path_names*.
202+
*encoding* and *errors* have the same meaning as in built-in :func:`open`.
203+
204+
For backward compatibility reasons, the *encoding* argument must be given
205+
explicitly if there are multiple *path_names*.
206+
This limitation is scheduled to be removed in Python 3.15.
207+
208+
This function is roughly equivalent to::
209+
210+
files(anchor).joinpath(*path_names).read_text(encoding=encoding)
211+
212+
.. versionchanged:: 3.13
213+
Multiple *path_names* are accepted.
214+
*encoding* and *errors* must be given as keyword arguments.
215+
216+
217+
.. function:: path(anchor, *path_names)
218+
219+
Provides the path to the *resource* as an actual file system path. This
220+
function returns a context manager for use in a :keyword:`with` statement.
221+
The context manager provides a :class:`pathlib.Path` object.
222+
223+
Exiting the context manager cleans up any temporary files created, e.g.
224+
when the resource needs to be extracted from a zip file.
225+
226+
For example, the :meth:`~pathlib.Path.stat` method requires
227+
an actual file system path; it can be used like this::
228+
229+
with importlib.resources.path(anchor, "resource.txt") as fspath:
230+
result = fspath.stat()
231+
232+
See :ref:`the introduction <importlib_resources_functional>` for
233+
details on *anchor* and *path_names*.
234+
235+
This function is roughly equivalent to::
236+
237+
as_file(files(anchor).joinpath(*path_names))
238+
239+
.. versionchanged:: 3.13
240+
Multiple *path_names* are accepted.
241+
*encoding* and *errors* must be given as keyword arguments.
242+
243+
244+
.. function:: is_resource(anchor, *path_names)
245+
246+
Return ``True`` if the named resource exists, otherwise ``False``.
247+
This function does not consider directories to be resources.
248+
249+
See :ref:`the introduction <importlib_resources_functional>` for
250+
details on *anchor* and *path_names*.
251+
252+
This function is roughly equivalent to::
253+
254+
files(anchor).joinpath(*path_names).is_file()
255+
256+
.. versionchanged:: 3.13
257+
Multiple *path_names* are accepted.
258+
259+
260+
.. function:: contents(anchor, *path_names)
261+
262+
Return an iterable over the named items within the package or path.
263+
The iterable returns names of resources (e.g. files) and non-resources
264+
(e.g. directories) as :class:`str`.
265+
The iterable does not recurse into subdirectories.
266+
267+
See :ref:`the introduction <importlib_resources_functional>` for
268+
details on *anchor* and *path_names*.
269+
270+
This function is roughly equivalent to::
271+
272+
for resource in files(anchor).joinpath(*path_names).iterdir():
273+
yield resource.name
274+
275+
.. deprecated:: 3.11
276+
Prefer ``iterdir()`` as above, which offers more control over the
277+
results and richer functionality.

Doc/whatsnew/3.13.rst

+24-15
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,30 @@ and only logged in :ref:`Python Development Mode <devmode>` or on :ref:`Python
409409
built on debug mode <debug-build>`.
410410
(Contributed by Victor Stinner in :gh:`62948`.)
411411

412+
importlib
413+
---------
414+
415+
Previously deprecated :mod:`importlib.resources` functions are un-deprecated:
416+
417+
* :func:`~importlib.resources.is_resource()`
418+
* :func:`~importlib.resources.open_binary()`
419+
* :func:`~importlib.resources.open_text()`
420+
* :func:`~importlib.resources.path()`
421+
* :func:`~importlib.resources.read_binary()`
422+
* :func:`~importlib.resources.read_text()`
423+
424+
All now allow for a directory (or tree) of resources, using multiple positional
425+
arguments.
426+
427+
For text-reading functions, the *encoding* and *errors* must now be given as
428+
keyword arguments.
429+
430+
The :func:`~importlib.resources.contents()` remains deprecated in favor of
431+
the full-featured :class:`~importlib.resources.abc.Traversable` API.
432+
However, there is now no plan to remove it.
433+
434+
(Contributed by Petr Viktorin in :gh:`106532`.)
435+
412436
ipaddress
413437
---------
414438

@@ -1357,21 +1381,6 @@ configparser
13571381
importlib
13581382
---------
13591383

1360-
* Remove :mod:`importlib.resources` deprecated methods:
1361-
1362-
* ``contents()``
1363-
* ``is_resource()``
1364-
* ``open_binary()``
1365-
* ``open_text()``
1366-
* ``path()``
1367-
* ``read_binary()``
1368-
* ``read_text()``
1369-
1370-
Use :func:`importlib.resources.files()` instead. Refer to `importlib-resources: Migrating from Legacy
1371-
<https://importlib-resources.readthedocs.io/en/latest/using.html#migrating-from-legacy>`_
1372-
for migration advice.
1373-
(Contributed by Jason R. Coombs in :gh:`106532`.)
1374-
13751384
* Remove deprecated :meth:`~object.__getitem__` access for
13761385
:class:`!importlib.metadata.EntryPoint` objects.
13771386
(Contributed by Jason R. Coombs in :gh:`113175`.)

Lib/importlib/resources/__init__.py

+17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
Anchor,
88
)
99

10+
from ._functional import (
11+
contents,
12+
is_resource,
13+
open_binary,
14+
open_text,
15+
path,
16+
read_binary,
17+
read_text,
18+
)
19+
1020
from .abc import ResourceReader
1121

1222

@@ -16,4 +26,11 @@
1626
'ResourceReader',
1727
'as_file',
1828
'files',
29+
'contents',
30+
'is_resource',
31+
'open_binary',
32+
'open_text',
33+
'path',
34+
'read_binary',
35+
'read_text',
1936
]
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Simplified function-based API for importlib.resources"""
2+
3+
import warnings
4+
5+
from ._common import files, as_file
6+
7+
8+
_MISSING = object()
9+
10+
11+
def open_binary(anchor, *path_names):
12+
"""Open for binary reading the *resource* within *package*."""
13+
return _get_resource(anchor, path_names).open('rb')
14+
15+
16+
def open_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
17+
"""Open for text reading the *resource* within *package*."""
18+
encoding = _get_encoding_arg(path_names, encoding)
19+
resource = _get_resource(anchor, path_names)
20+
return resource.open('r', encoding=encoding, errors=errors)
21+
22+
23+
def read_binary(anchor, *path_names):
24+
"""Read and return contents of *resource* within *package* as bytes."""
25+
return _get_resource(anchor, path_names).read_bytes()
26+
27+
28+
def read_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
29+
"""Read and return contents of *resource* within *package* as str."""
30+
encoding = _get_encoding_arg(path_names, encoding)
31+
resource = _get_resource(anchor, path_names)
32+
return resource.read_text(encoding=encoding, errors=errors)
33+
34+
35+
def path(anchor, *path_names):
36+
"""Return the path to the *resource* as an actual file system path."""
37+
return as_file(_get_resource(anchor, path_names))
38+
39+
40+
def is_resource(anchor, *path_names):
41+
"""Return ``True`` if there is a resource named *name* in the package,
42+
43+
Otherwise returns ``False``.
44+
"""
45+
return _get_resource(anchor, path_names).is_file()
46+
47+
48+
def contents(anchor, *path_names):
49+
"""Return an iterable over the named resources within the package.
50+
51+
The iterable returns :class:`str` resources (e.g. files).
52+
The iterable does not recurse into subdirectories.
53+
"""
54+
warnings.warn(
55+
"importlib.resources.contents is deprecated. "
56+
"Use files(anchor).iterdir() instead.",
57+
DeprecationWarning,
58+
stacklevel=1,
59+
)
60+
return (
61+
resource.name
62+
for resource
63+
in _get_resource(anchor, path_names).iterdir()
64+
)
65+
66+
67+
def _get_encoding_arg(path_names, encoding):
68+
# For compatibility with versions where *encoding* was a positional
69+
# argument, it needs to be given explicitly when there are multiple
70+
# *path_names*.
71+
# This limitation can be removed in Python 3.15.
72+
if encoding is _MISSING:
73+
if len(path_names) > 1:
74+
raise TypeError(
75+
"'encoding' argument required with multiple path names",
76+
)
77+
else:
78+
return 'utf-8'
79+
return encoding
80+
81+
82+
def _get_resource(anchor, path_names):
83+
if anchor is None:
84+
raise TypeError("anchor must be module or string, got None")
85+
return files(anchor).joinpath(*path_names)

0 commit comments

Comments
 (0)