Skip to content

Commit 7f7e706

Browse files
bpo-39791: Add files() to importlib.resources (GH-19722)
* 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>
1 parent d10091a commit 7f7e706

File tree

7 files changed

+295
-102
lines changed

7 files changed

+295
-102
lines changed

β€ŽDoc/library/importlib.rst

+37
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,8 @@ ABC hierarchy::
480480

481481
.. class:: ResourceReader
482482

483+
*Superseded by TraversableReader*
484+
483485
An :term:`abstract base class` to provide the ability to read
484486
*resources*.
485487

@@ -795,6 +797,28 @@ ABC hierarchy::
795797
itself does not end in ``__init__``.
796798

797799

800+
.. class:: Traversable
801+
802+
An object with a subset of pathlib.Path methods suitable for
803+
traversing directories and opening files.
804+
805+
.. versionadded:: 3.9
806+
807+
808+
.. class:: TraversableReader
809+
810+
An abstract base class for resource readers capable of serving
811+
the ``files`` interface. Subclasses ResourceReader and provides
812+
concrete implementations of the ResourceReader's abstract
813+
methods. Therefore, any loader supplying TraversableReader
814+
also supplies ResourceReader.
815+
816+
Loaders that wish to support resource reading are expected to
817+
implement this interface.
818+
819+
.. versionadded:: 3.9
820+
821+
798822
:mod:`importlib.resources` -- Resources
799823
---------------------------------------
800824

@@ -853,6 +877,19 @@ The following types are defined.
853877

854878
The following functions are available.
855879

880+
881+
.. function:: files(package)
882+
883+
Returns an :class:`importlib.resources.abc.Traversable` object
884+
representing the resource container for the package (think directory)
885+
and its resources (think files). A Traversable may contain other
886+
containers (think subdirectories).
887+
888+
*package* is either a name or a module object which conforms to the
889+
``Package`` requirements.
890+
891+
.. versionadded:: 3.9
892+
856893
.. function:: open_binary(package, resource)
857894

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

β€ŽLib/importlib/_common.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import os
2+
import pathlib
3+
import zipfile
4+
import tempfile
5+
import functools
6+
import contextlib
7+
8+
9+
def from_package(package):
10+
"""
11+
Return a Traversable object for the given package.
12+
13+
"""
14+
spec = package.__spec__
15+
return from_traversable_resources(spec) or fallback_resources(spec)
16+
17+
18+
def from_traversable_resources(spec):
19+
"""
20+
If the spec.loader implements TraversableResources,
21+
directly or implicitly, it will have a ``files()`` method.
22+
"""
23+
with contextlib.suppress(AttributeError):
24+
return spec.loader.files()
25+
26+
27+
def fallback_resources(spec):
28+
package_directory = pathlib.Path(spec.origin).parent
29+
try:
30+
archive_path = spec.loader.archive
31+
rel_path = package_directory.relative_to(archive_path)
32+
return zipfile.Path(archive_path, str(rel_path) + '/')
33+
except Exception:
34+
pass
35+
return package_directory
36+
37+
38+
@contextlib.contextmanager
39+
def _tempfile(reader, suffix=''):
40+
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
41+
# blocks due to the need to close the temporary file to work on Windows
42+
# properly.
43+
fd, raw_path = tempfile.mkstemp(suffix=suffix)
44+
try:
45+
os.write(fd, reader())
46+
os.close(fd)
47+
yield pathlib.Path(raw_path)
48+
finally:
49+
try:
50+
os.remove(raw_path)
51+
except FileNotFoundError:
52+
pass
53+
54+
55+
@functools.singledispatch
56+
@contextlib.contextmanager
57+
def as_file(path):
58+
"""
59+
Given a Traversable object, return that object as a
60+
path on the local file system in a context manager.
61+
"""
62+
with _tempfile(path.read_bytes, suffix=path.name) as local:
63+
yield local
64+
65+
66+
@as_file.register(pathlib.Path)
67+
@contextlib.contextmanager
68+
def _(path):
69+
"""
70+
Degenerate behavior for pathlib.Path objects.
71+
"""
72+
yield path

β€ŽLib/importlib/abc.py

+86
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
_frozen_importlib_external = _bootstrap_external
1515
import abc
1616
import warnings
17+
from typing import Protocol, runtime_checkable
1718

1819

1920
def _register(abstract_cls, *classes):
@@ -386,3 +387,88 @@ def contents(self):
386387

387388

388389
_register(ResourceReader, machinery.SourceFileLoader)
390+
391+
392+
@runtime_checkable
393+
class Traversable(Protocol):
394+
"""
395+
An object with a subset of pathlib.Path methods suitable for
396+
traversing directories and opening files.
397+
"""
398+
399+
@abc.abstractmethod
400+
def iterdir(self):
401+
"""
402+
Yield Traversable objects in self
403+
"""
404+
405+
@abc.abstractmethod
406+
def read_bytes(self):
407+
"""
408+
Read contents of self as bytes
409+
"""
410+
411+
@abc.abstractmethod
412+
def read_text(self, encoding=None):
413+
"""
414+
Read contents of self as bytes
415+
"""
416+
417+
@abc.abstractmethod
418+
def is_dir(self):
419+
"""
420+
Return True if self is a dir
421+
"""
422+
423+
@abc.abstractmethod
424+
def is_file(self):
425+
"""
426+
Return True if self is a file
427+
"""
428+
429+
@abc.abstractmethod
430+
def joinpath(self, child):
431+
"""
432+
Return Traversable child in self
433+
"""
434+
435+
@abc.abstractmethod
436+
def __truediv__(self, child):
437+
"""
438+
Return Traversable child in self
439+
"""
440+
441+
@abc.abstractmethod
442+
def open(self, mode='r', *args, **kwargs):
443+
"""
444+
mode may be 'r' or 'rb' to open as text or binary. Return a handle
445+
suitable for reading (same as pathlib.Path.open).
446+
447+
When opening as text, accepts encoding parameters such as those
448+
accepted by io.TextIOWrapper.
449+
"""
450+
451+
@abc.abstractproperty
452+
def name(self):
453+
# type: () -> str
454+
"""
455+
The base name of this object without any parent references.
456+
"""
457+
458+
459+
class TraversableResources(ResourceReader):
460+
@abc.abstractmethod
461+
def files(self):
462+
"""Return a Traversable object for the loaded package."""
463+
464+
def open_resource(self, resource):
465+
return self.files().joinpath(resource).open('rb')
466+
467+
def resource_path(self, resource):
468+
raise FileNotFoundError(resource)
469+
470+
def is_resource(self, path):
471+
return self.files().joinpath(path).isfile()
472+
473+
def contents(self):
474+
return (item.name for item in self.files().iterdir())

0 commit comments

Comments
Β (0)