mirror of https://github.com/python/cpython
gh-116608: importlib.resources: Un-deprecate functional API & add subdirectory support (GH-116609)
This commit is contained in:
parent
757b62493b
commit
9ceaee74db
|
@ -97,3 +97,181 @@ for example, a package and its resources can be imported from a zip file using
|
|||
|
||||
.. versionchanged:: 3.12
|
||||
Added support for *traversable* representing a directory.
|
||||
|
||||
|
||||
.. _importlib_resources_functional:
|
||||
|
||||
Functional API
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
A set of simplified, backwards-compatible helpers is available.
|
||||
These allow common operations in a single function call.
|
||||
|
||||
For all the following functions:
|
||||
|
||||
- *anchor* is an :class:`~importlib.resources.Anchor`,
|
||||
as in :func:`~importlib.resources.files`.
|
||||
Unlike in ``files``, it may not be omitted.
|
||||
|
||||
- *path_names* are components of a resource's path name, relative to
|
||||
the anchor.
|
||||
For example, to get the text of resource named ``info.txt``, use::
|
||||
|
||||
importlib.resources.read_text(my_module, "info.txt")
|
||||
|
||||
Like :meth:`Traversable.joinpath <importlib.resources.abc.Traversable>`,
|
||||
The individual components should use forward slashes (``/``)
|
||||
as path separators.
|
||||
For example, the following are equivalent::
|
||||
|
||||
importlib.resources.read_binary(my_module, "pics/painting.png")
|
||||
importlib.resources.read_binary(my_module, "pics", "painting.png")
|
||||
|
||||
For backward compatibility reasons, functions that read text require
|
||||
an explicit *encoding* argument if multiple *path_names* are given.
|
||||
For example, to get the text of ``info/chapter1.txt``, use::
|
||||
|
||||
importlib.resources.read_text(my_module, "info", "chapter1.txt",
|
||||
encoding='utf-8')
|
||||
|
||||
.. function:: open_binary(anchor, *path_names)
|
||||
|
||||
Open the named resource for binary reading.
|
||||
|
||||
See :ref:`the introduction <importlib_resources_functional>` for
|
||||
details on *anchor* and *path_names*.
|
||||
|
||||
This function returns a :class:`~typing.BinaryIO` object,
|
||||
that is, a binary stream open for reading.
|
||||
|
||||
This function is roughly equivalent to::
|
||||
|
||||
files(anchor).joinpath(*path_names).open('rb')
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
Multiple *path_names* are accepted.
|
||||
|
||||
|
||||
.. function:: open_text(anchor, *path_names, encoding='utf-8', errors='strict')
|
||||
|
||||
Open the named resource for text reading.
|
||||
By default, the contents are read as strict UTF-8.
|
||||
|
||||
See :ref:`the introduction <importlib_resources_functional>` for
|
||||
details on *anchor* and *path_names*.
|
||||
*encoding* and *errors* have the same meaning as in built-in :func:`open`.
|
||||
|
||||
For backward compatibility reasons, the *encoding* argument must be given
|
||||
explicitly if there are multiple *path_names*.
|
||||
This limitation is scheduled to be removed in Python 3.15.
|
||||
|
||||
This function returns a :class:`~typing.TextIO` object,
|
||||
that is, a text stream open for reading.
|
||||
|
||||
This function is roughly equivalent to::
|
||||
|
||||
files(anchor).joinpath(*path_names).open('r', encoding=encoding)
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
Multiple *path_names* are accepted.
|
||||
*encoding* and *errors* must be given as keyword arguments.
|
||||
|
||||
|
||||
.. function:: read_binary(anchor, *path_names)
|
||||
|
||||
Read and return the contents of the named resource as :class:`bytes`.
|
||||
|
||||
See :ref:`the introduction <importlib_resources_functional>` for
|
||||
details on *anchor* and *path_names*.
|
||||
|
||||
This function is roughly equivalent to::
|
||||
|
||||
files(anchor).joinpath(*path_names).read_bytes()
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
Multiple *path_names* are accepted.
|
||||
|
||||
|
||||
.. function:: read_text(anchor, *path_names, encoding='utf-8', errors='strict')
|
||||
|
||||
Read and return the contents of the named resource as :class:`str`.
|
||||
By default, the contents are read as strict UTF-8.
|
||||
|
||||
See :ref:`the introduction <importlib_resources_functional>` for
|
||||
details on *anchor* and *path_names*.
|
||||
*encoding* and *errors* have the same meaning as in built-in :func:`open`.
|
||||
|
||||
For backward compatibility reasons, the *encoding* argument must be given
|
||||
explicitly if there are multiple *path_names*.
|
||||
This limitation is scheduled to be removed in Python 3.15.
|
||||
|
||||
This function is roughly equivalent to::
|
||||
|
||||
files(anchor).joinpath(*path_names).read_text(encoding=encoding)
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
Multiple *path_names* are accepted.
|
||||
*encoding* and *errors* must be given as keyword arguments.
|
||||
|
||||
|
||||
.. function:: path(anchor, *path_names)
|
||||
|
||||
Provides the path to the *resource* as an actual file system path. This
|
||||
function returns a context manager for use in a :keyword:`with` statement.
|
||||
The context manager provides a :class:`pathlib.Path` object.
|
||||
|
||||
Exiting the context manager cleans up any temporary files created, e.g.
|
||||
when the resource needs to be extracted from a zip file.
|
||||
|
||||
For example, the :meth:`~pathlib.Path.stat` method requires
|
||||
an actual file system path; it can be used like this::
|
||||
|
||||
with importlib.resources.path(anchor, "resource.txt") as fspath:
|
||||
result = fspath.stat()
|
||||
|
||||
See :ref:`the introduction <importlib_resources_functional>` for
|
||||
details on *anchor* and *path_names*.
|
||||
|
||||
This function is roughly equivalent to::
|
||||
|
||||
as_file(files(anchor).joinpath(*path_names))
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
Multiple *path_names* are accepted.
|
||||
*encoding* and *errors* must be given as keyword arguments.
|
||||
|
||||
|
||||
.. function:: is_resource(anchor, *path_names)
|
||||
|
||||
Return ``True`` if the named resource exists, otherwise ``False``.
|
||||
This function does not consider directories to be resources.
|
||||
|
||||
See :ref:`the introduction <importlib_resources_functional>` for
|
||||
details on *anchor* and *path_names*.
|
||||
|
||||
This function is roughly equivalent to::
|
||||
|
||||
files(anchor).joinpath(*path_names).is_file()
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
Multiple *path_names* are accepted.
|
||||
|
||||
|
||||
.. function:: contents(anchor, *path_names)
|
||||
|
||||
Return an iterable over the named items within the package or path.
|
||||
The iterable returns names of resources (e.g. files) and non-resources
|
||||
(e.g. directories) as :class:`str`.
|
||||
The iterable does not recurse into subdirectories.
|
||||
|
||||
See :ref:`the introduction <importlib_resources_functional>` for
|
||||
details on *anchor* and *path_names*.
|
||||
|
||||
This function is roughly equivalent to::
|
||||
|
||||
for resource in files(anchor).joinpath(*path_names).iterdir():
|
||||
yield resource.name
|
||||
|
||||
.. deprecated:: 3.11
|
||||
Prefer ``iterdir()`` as above, which offers more control over the
|
||||
results and richer functionality.
|
||||
|
|
|
@ -409,6 +409,30 @@ and only logged in :ref:`Python Development Mode <devmode>` or on :ref:`Python
|
|||
built on debug mode <debug-build>`.
|
||||
(Contributed by Victor Stinner in :gh:`62948`.)
|
||||
|
||||
importlib
|
||||
---------
|
||||
|
||||
Previously deprecated :mod:`importlib.resources` functions are un-deprecated:
|
||||
|
||||
* :func:`~importlib.resources.is_resource()`
|
||||
* :func:`~importlib.resources.open_binary()`
|
||||
* :func:`~importlib.resources.open_text()`
|
||||
* :func:`~importlib.resources.path()`
|
||||
* :func:`~importlib.resources.read_binary()`
|
||||
* :func:`~importlib.resources.read_text()`
|
||||
|
||||
All now allow for a directory (or tree) of resources, using multiple positional
|
||||
arguments.
|
||||
|
||||
For text-reading functions, the *encoding* and *errors* must now be given as
|
||||
keyword arguments.
|
||||
|
||||
The :func:`~importlib.resources.contents()` remains deprecated in favor of
|
||||
the full-featured :class:`~importlib.resources.abc.Traversable` API.
|
||||
However, there is now no plan to remove it.
|
||||
|
||||
(Contributed by Petr Viktorin in :gh:`106532`.)
|
||||
|
||||
ipaddress
|
||||
---------
|
||||
|
||||
|
@ -1357,21 +1381,6 @@ configparser
|
|||
importlib
|
||||
---------
|
||||
|
||||
* Remove :mod:`importlib.resources` deprecated methods:
|
||||
|
||||
* ``contents()``
|
||||
* ``is_resource()``
|
||||
* ``open_binary()``
|
||||
* ``open_text()``
|
||||
* ``path()``
|
||||
* ``read_binary()``
|
||||
* ``read_text()``
|
||||
|
||||
Use :func:`importlib.resources.files()` instead. Refer to `importlib-resources: Migrating from Legacy
|
||||
<https://importlib-resources.readthedocs.io/en/latest/using.html#migrating-from-legacy>`_
|
||||
for migration advice.
|
||||
(Contributed by Jason R. Coombs in :gh:`106532`.)
|
||||
|
||||
* Remove deprecated :meth:`~object.__getitem__` access for
|
||||
:class:`!importlib.metadata.EntryPoint` objects.
|
||||
(Contributed by Jason R. Coombs in :gh:`113175`.)
|
||||
|
|
|
@ -7,6 +7,16 @@ from ._common import (
|
|||
Anchor,
|
||||
)
|
||||
|
||||
from ._functional import (
|
||||
contents,
|
||||
is_resource,
|
||||
open_binary,
|
||||
open_text,
|
||||
path,
|
||||
read_binary,
|
||||
read_text,
|
||||
)
|
||||
|
||||
from .abc import ResourceReader
|
||||
|
||||
|
||||
|
@ -16,4 +26,11 @@ __all__ = [
|
|||
'ResourceReader',
|
||||
'as_file',
|
||||
'files',
|
||||
'contents',
|
||||
'is_resource',
|
||||
'open_binary',
|
||||
'open_text',
|
||||
'path',
|
||||
'read_binary',
|
||||
'read_text',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
"""Simplified function-based API for importlib.resources"""
|
||||
|
||||
import warnings
|
||||
|
||||
from ._common import files, as_file
|
||||
|
||||
|
||||
_MISSING = object()
|
||||
|
||||
|
||||
def open_binary(anchor, *path_names):
|
||||
"""Open for binary reading the *resource* within *package*."""
|
||||
return _get_resource(anchor, path_names).open('rb')
|
||||
|
||||
|
||||
def open_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
|
||||
"""Open for text reading the *resource* within *package*."""
|
||||
encoding = _get_encoding_arg(path_names, encoding)
|
||||
resource = _get_resource(anchor, path_names)
|
||||
return resource.open('r', encoding=encoding, errors=errors)
|
||||
|
||||
|
||||
def read_binary(anchor, *path_names):
|
||||
"""Read and return contents of *resource* within *package* as bytes."""
|
||||
return _get_resource(anchor, path_names).read_bytes()
|
||||
|
||||
|
||||
def read_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
|
||||
"""Read and return contents of *resource* within *package* as str."""
|
||||
encoding = _get_encoding_arg(path_names, encoding)
|
||||
resource = _get_resource(anchor, path_names)
|
||||
return resource.read_text(encoding=encoding, errors=errors)
|
||||
|
||||
|
||||
def path(anchor, *path_names):
|
||||
"""Return the path to the *resource* as an actual file system path."""
|
||||
return as_file(_get_resource(anchor, path_names))
|
||||
|
||||
|
||||
def is_resource(anchor, *path_names):
|
||||
"""Return ``True`` if there is a resource named *name* in the package,
|
||||
|
||||
Otherwise returns ``False``.
|
||||
"""
|
||||
return _get_resource(anchor, path_names).is_file()
|
||||
|
||||
|
||||
def contents(anchor, *path_names):
|
||||
"""Return an iterable over the named resources within the package.
|
||||
|
||||
The iterable returns :class:`str` resources (e.g. files).
|
||||
The iterable does not recurse into subdirectories.
|
||||
"""
|
||||
warnings.warn(
|
||||
"importlib.resources.contents is deprecated. "
|
||||
"Use files(anchor).iterdir() instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=1,
|
||||
)
|
||||
return (
|
||||
resource.name
|
||||
for resource
|
||||
in _get_resource(anchor, path_names).iterdir()
|
||||
)
|
||||
|
||||
|
||||
def _get_encoding_arg(path_names, encoding):
|
||||
# For compatibility with versions where *encoding* was a positional
|
||||
# argument, it needs to be given explicitly when there are multiple
|
||||
# *path_names*.
|
||||
# This limitation can be removed in Python 3.15.
|
||||
if encoding is _MISSING:
|
||||
if len(path_names) > 1:
|
||||
raise TypeError(
|
||||
"'encoding' argument required with multiple path names",
|
||||
)
|
||||
else:
|
||||
return 'utf-8'
|
||||
return encoding
|
||||
|
||||
|
||||
def _get_resource(anchor, path_names):
|
||||
if anchor is None:
|
||||
raise TypeError("anchor must be module or string, got None")
|
||||
return files(anchor).joinpath(*path_names)
|
|
@ -0,0 +1,219 @@
|
|||
import unittest
|
||||
import os
|
||||
|
||||
from test.support.warnings_helper import ignore_warnings, check_warnings
|
||||
|
||||
import importlib.resources as resources
|
||||
|
||||
# Since the functional API forwards to Traversable, we only test
|
||||
# filesystem resources here -- not zip files, namespace packages etc.
|
||||
# We do test for two kinds of Anchor, though.
|
||||
|
||||
|
||||
class StringAnchorMixin:
|
||||
anchor01 = 'test.test_importlib.resources.data01'
|
||||
anchor02 = 'test.test_importlib.resources.data02'
|
||||
|
||||
|
||||
class ModuleAnchorMixin:
|
||||
from . import data01 as anchor01
|
||||
from . import data02 as anchor02
|
||||
|
||||
|
||||
class FunctionalAPIBase:
|
||||
def _gen_resourcetxt_path_parts(self):
|
||||
"""Yield various names of a text file in anchor02, each in a subTest
|
||||
"""
|
||||
for path_parts in (
|
||||
('subdirectory', 'subsubdir', 'resource.txt'),
|
||||
('subdirectory/subsubdir/resource.txt',),
|
||||
('subdirectory/subsubdir', 'resource.txt'),
|
||||
):
|
||||
with self.subTest(path_parts=path_parts):
|
||||
yield path_parts
|
||||
|
||||
def test_read_text(self):
|
||||
self.assertEqual(
|
||||
resources.read_text(self.anchor01, 'utf-8.file'),
|
||||
'Hello, UTF-8 world!\n',
|
||||
)
|
||||
self.assertEqual(
|
||||
resources.read_text(
|
||||
self.anchor02, 'subdirectory', 'subsubdir', 'resource.txt',
|
||||
encoding='utf-8',
|
||||
),
|
||||
'a resource',
|
||||
)
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
self.assertEqual(
|
||||
resources.read_text(
|
||||
self.anchor02, *path_parts, encoding='utf-8',
|
||||
),
|
||||
'a resource',
|
||||
)
|
||||
# Use generic OSError, since e.g. attempting to read a directory can
|
||||
# fail with PermissionError rather than IsADirectoryError
|
||||
with self.assertRaises(OSError):
|
||||
resources.read_text(self.anchor01)
|
||||
with self.assertRaises(OSError):
|
||||
resources.read_text(self.anchor01, 'no-such-file')
|
||||
with self.assertRaises(UnicodeDecodeError):
|
||||
resources.read_text(self.anchor01, 'utf-16.file')
|
||||
self.assertEqual(
|
||||
resources.read_text(
|
||||
self.anchor01, 'binary.file', encoding='latin1',
|
||||
),
|
||||
'\x00\x01\x02\x03',
|
||||
)
|
||||
self.assertEqual(
|
||||
resources.read_text(
|
||||
self.anchor01, 'utf-16.file',
|
||||
errors='backslashreplace',
|
||||
),
|
||||
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
|
||||
errors='backslashreplace',
|
||||
),
|
||||
)
|
||||
|
||||
def test_read_binary(self):
|
||||
self.assertEqual(
|
||||
resources.read_binary(self.anchor01, 'utf-8.file'),
|
||||
b'Hello, UTF-8 world!\n',
|
||||
)
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
self.assertEqual(
|
||||
resources.read_binary(self.anchor02, *path_parts),
|
||||
b'a resource',
|
||||
)
|
||||
|
||||
def test_open_text(self):
|
||||
with resources.open_text(self.anchor01, 'utf-8.file') as f:
|
||||
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
with resources.open_text(
|
||||
self.anchor02, *path_parts,
|
||||
encoding='utf-8',
|
||||
) as f:
|
||||
self.assertEqual(f.read(), 'a resource')
|
||||
# Use generic OSError, since e.g. attempting to read a directory can
|
||||
# fail with PermissionError rather than IsADirectoryError
|
||||
with self.assertRaises(OSError):
|
||||
resources.open_text(self.anchor01)
|
||||
with self.assertRaises(OSError):
|
||||
resources.open_text(self.anchor01, 'no-such-file')
|
||||
with resources.open_text(self.anchor01, 'utf-16.file') as f:
|
||||
with self.assertRaises(UnicodeDecodeError):
|
||||
f.read()
|
||||
with resources.open_text(
|
||||
self.anchor01, 'binary.file', encoding='latin1',
|
||||
) as f:
|
||||
self.assertEqual(f.read(), '\x00\x01\x02\x03')
|
||||
with resources.open_text(
|
||||
self.anchor01, 'utf-16.file',
|
||||
errors='backslashreplace',
|
||||
) as f:
|
||||
self.assertEqual(
|
||||
f.read(),
|
||||
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
|
||||
errors='backslashreplace',
|
||||
),
|
||||
)
|
||||
|
||||
def test_open_binary(self):
|
||||
with resources.open_binary(self.anchor01, 'utf-8.file') as f:
|
||||
self.assertEqual(f.read(), b'Hello, UTF-8 world!\n')
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
with resources.open_binary(
|
||||
self.anchor02, *path_parts,
|
||||
) as f:
|
||||
self.assertEqual(f.read(), b'a resource')
|
||||
|
||||
def test_path(self):
|
||||
with resources.path(self.anchor01, 'utf-8.file') as path:
|
||||
with open(str(path)) as f:
|
||||
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
|
||||
with resources.path(self.anchor01) as path:
|
||||
with open(os.path.join(path, 'utf-8.file')) as f:
|
||||
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
|
||||
|
||||
def test_is_resource(self):
|
||||
is_resource = resources.is_resource
|
||||
self.assertTrue(is_resource(self.anchor01, 'utf-8.file'))
|
||||
self.assertFalse(is_resource(self.anchor01, 'no_such_file'))
|
||||
self.assertFalse(is_resource(self.anchor01))
|
||||
self.assertFalse(is_resource(self.anchor01, 'subdirectory'))
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
self.assertTrue(is_resource(self.anchor02, *path_parts))
|
||||
|
||||
def test_contents(self):
|
||||
is_resource = resources.is_resource
|
||||
with check_warnings((".*contents.*", DeprecationWarning)):
|
||||
c = resources.contents(self.anchor01)
|
||||
self.assertGreaterEqual(
|
||||
set(c),
|
||||
{'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'},
|
||||
)
|
||||
with (
|
||||
self.assertRaises(OSError),
|
||||
check_warnings((".*contents.*", DeprecationWarning)),
|
||||
):
|
||||
list(resources.contents(self.anchor01, 'utf-8.file'))
|
||||
for path_parts in self._gen_resourcetxt_path_parts():
|
||||
with (
|
||||
self.assertRaises(OSError),
|
||||
check_warnings((".*contents.*", DeprecationWarning)),
|
||||
):
|
||||
list(resources.contents(self.anchor01, *path_parts))
|
||||
with check_warnings((".*contents.*", DeprecationWarning)):
|
||||
c = resources.contents(self.anchor01, 'subdirectory')
|
||||
self.assertGreaterEqual(
|
||||
set(c),
|
||||
{'binary.file'},
|
||||
)
|
||||
|
||||
@ignore_warnings(category=DeprecationWarning)
|
||||
def test_common_errors(self):
|
||||
for func in (
|
||||
resources.read_text,
|
||||
resources.read_binary,
|
||||
resources.open_text,
|
||||
resources.open_binary,
|
||||
resources.path,
|
||||
resources.is_resource,
|
||||
resources.contents,
|
||||
):
|
||||
with self.subTest(func=func):
|
||||
# Rejecting None anchor
|
||||
with self.assertRaises(TypeError):
|
||||
func(None)
|
||||
# Rejecting invalid anchor type
|
||||
with self.assertRaises((TypeError, AttributeError)):
|
||||
func(1234)
|
||||
# Unknown module
|
||||
with self.assertRaises(ModuleNotFoundError):
|
||||
func('$missing module$')
|
||||
|
||||
def test_text_errors(self):
|
||||
for func in (
|
||||
resources.read_text,
|
||||
resources.open_text,
|
||||
):
|
||||
with self.subTest(func=func):
|
||||
# Multiple path arguments need explicit encoding argument.
|
||||
with self.assertRaises(TypeError):
|
||||
func(
|
||||
self.anchor02, 'subdirectory',
|
||||
'subsubdir', 'resource.txt',
|
||||
)
|
||||
|
||||
|
||||
class FunctionalAPITest_StringAnchor(
|
||||
unittest.TestCase, FunctionalAPIBase, StringAnchorMixin,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class FunctionalAPITest_ModuleAnchor(
|
||||
unittest.TestCase, FunctionalAPIBase, ModuleAnchorMixin,
|
||||
):
|
||||
pass
|
|
@ -0,0 +1,10 @@
|
|||
The :mod:`importlib.resources` functions
|
||||
:func:`~importlib.resources.is_resource()`,
|
||||
:func:`~importlib.resources.open_binary()`,
|
||||
:func:`~importlib.resources.open_text()`,
|
||||
:func:`~importlib.resources.path()`,
|
||||
:func:`~importlib.resources.read_binary()`, and
|
||||
:func:`~importlib.resources.read_text()` are un-deprecated, and support
|
||||
subdirectories via multiple positional arguments.
|
||||
The :func:`~importlib.resources.contents()` function also allows subdirectories,
|
||||
but remains deprecated.
|
Loading…
Reference in New Issue