Skip to content

Commit

Permalink
pythongh-116608: importlib.resources: Un-deprecate functional API & a…
Browse files Browse the repository at this point in the history
…dd subdirectory support (pythonGH-116609)
  • Loading branch information
encukou authored and diegorusso committed Apr 17, 2024
1 parent 9e454d1 commit 53963b5
Show file tree
Hide file tree
Showing 6 changed files with 533 additions and 15 deletions.
178 changes: 178 additions & 0 deletions Doc/library/importlib.resources.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
39 changes: 24 additions & 15 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------

Expand Down Expand Up @@ -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`.)
Expand Down
17 changes: 17 additions & 0 deletions Lib/importlib/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
Anchor,
)

from ._functional import (
contents,
is_resource,
open_binary,
open_text,
path,
read_binary,
read_text,
)

from .abc import ResourceReader


Expand All @@ -16,4 +26,11 @@
'ResourceReader',
'as_file',
'files',
'contents',
'is_resource',
'open_binary',
'open_text',
'path',
'read_binary',
'read_text',
]
85 changes: 85 additions & 0 deletions Lib/importlib/resources/_functional.py
Original file line number Diff line number Diff line change
@@ -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 comments on commit 53963b5

Please sign in to comment.