Skip to content

Commit

Permalink
bpo-39791: Add files() to importlib.resources (GH-19722)
Browse files Browse the repository at this point in the history
* bpo-39791: Update importlib.resources to support files() API (importlib_resources 1.5).

* πŸ“œπŸ€– Added by blurb_it.

* Add some documentation about the new objects added.

Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
  • Loading branch information
jaraco and blurb-it[bot] authored May 8, 2020
1 parent d10091a commit 7f7e706
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 102 deletions.
37 changes: 37 additions & 0 deletions Doc/library/importlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,8 @@ ABC hierarchy::

.. class:: ResourceReader

*Superseded by TraversableReader*

An :term:`abstract base class` to provide the ability to read
*resources*.

Expand Down Expand Up @@ -795,6 +797,28 @@ ABC hierarchy::
itself does not end in ``__init__``.


.. class:: Traversable

An object with a subset of pathlib.Path methods suitable for
traversing directories and opening files.

.. versionadded:: 3.9


.. class:: TraversableReader

An abstract base class for resource readers capable of serving
the ``files`` interface. Subclasses ResourceReader and provides
concrete implementations of the ResourceReader's abstract
methods. Therefore, any loader supplying TraversableReader
also supplies ResourceReader.

Loaders that wish to support resource reading are expected to
implement this interface.

.. versionadded:: 3.9


:mod:`importlib.resources` -- Resources
---------------------------------------

Expand Down Expand Up @@ -853,6 +877,19 @@ The following types are defined.

The following functions are available.


.. function:: files(package)

Returns an :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).

*package* is either a name or a module object which conforms to the
``Package`` requirements.

.. versionadded:: 3.9

.. function:: open_binary(package, resource)

Open for binary reading the *resource* within *package*.
Expand Down
72 changes: 72 additions & 0 deletions Lib/importlib/_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import os
import pathlib
import zipfile
import tempfile
import functools
import contextlib


def from_package(package):
"""
Return a Traversable object for the given package.
"""
spec = package.__spec__
return from_traversable_resources(spec) or fallback_resources(spec)


def from_traversable_resources(spec):
"""
If the spec.loader implements TraversableResources,
directly or implicitly, it will have a ``files()`` method.
"""
with contextlib.suppress(AttributeError):
return spec.loader.files()


def fallback_resources(spec):
package_directory = pathlib.Path(spec.origin).parent
try:
archive_path = spec.loader.archive
rel_path = package_directory.relative_to(archive_path)
return zipfile.Path(archive_path, str(rel_path) + '/')
except Exception:
pass
return package_directory


@contextlib.contextmanager
def _tempfile(reader, suffix=''):
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
# blocks due to the need to close the temporary file to work on Windows
# properly.
fd, raw_path = tempfile.mkstemp(suffix=suffix)
try:
os.write(fd, reader())
os.close(fd)
yield pathlib.Path(raw_path)
finally:
try:
os.remove(raw_path)
except FileNotFoundError:
pass


@functools.singledispatch
@contextlib.contextmanager
def as_file(path):
"""
Given a Traversable object, return that object as a
path on the local file system in a context manager.
"""
with _tempfile(path.read_bytes, suffix=path.name) as local:
yield local


@as_file.register(pathlib.Path)
@contextlib.contextmanager
def _(path):
"""
Degenerate behavior for pathlib.Path objects.
"""
yield path
86 changes: 86 additions & 0 deletions Lib/importlib/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
_frozen_importlib_external = _bootstrap_external
import abc
import warnings
from typing import Protocol, runtime_checkable


def _register(abstract_cls, *classes):
Expand Down Expand Up @@ -386,3 +387,88 @@ def contents(self):


_register(ResourceReader, machinery.SourceFileLoader)


@runtime_checkable
class Traversable(Protocol):
"""
An object with a subset of pathlib.Path methods suitable for
traversing directories and opening files.
"""

@abc.abstractmethod
def iterdir(self):
"""
Yield Traversable objects in self
"""

@abc.abstractmethod
def read_bytes(self):
"""
Read contents of self as bytes
"""

@abc.abstractmethod
def read_text(self, encoding=None):
"""
Read contents of self as bytes
"""

@abc.abstractmethod
def is_dir(self):
"""
Return True if self is a dir
"""

@abc.abstractmethod
def is_file(self):
"""
Return True if self is a file
"""

@abc.abstractmethod
def joinpath(self, child):
"""
Return Traversable child in self
"""

@abc.abstractmethod
def __truediv__(self, child):
"""
Return Traversable child in self
"""

@abc.abstractmethod
def open(self, mode='r', *args, **kwargs):
"""
mode may be 'r' or 'rb' to open as text or binary. Return a handle
suitable for reading (same as pathlib.Path.open).
When opening as text, accepts encoding parameters such as those
accepted by io.TextIOWrapper.
"""

@abc.abstractproperty
def name(self):
# type: () -> str
"""
The base name of this object without any parent references.
"""


class TraversableResources(ResourceReader):
@abc.abstractmethod
def files(self):
"""Return a Traversable object for the loaded package."""

def open_resource(self, resource):
return self.files().joinpath(resource).open('rb')

def resource_path(self, resource):
raise FileNotFoundError(resource)

def is_resource(self, path):
return self.files().joinpath(path).isfile()

def contents(self):
return (item.name for item in self.files().iterdir())
Loading

0 comments on commit 7f7e706

Please sign in to comment.