bpo-32248: Implement importlib.abc.ResourceReader (GH-4892)

This commit is contained in:
Brett Cannon 2017-12-15 16:29:35 -08:00 committed by GitHub
parent d2b02310ac
commit 4ac5150e06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 145 additions and 0 deletions

View File

@ -233,6 +233,7 @@ ABC hierarchy::
| +-- MetaPathFinder
| +-- PathEntryFinder
+-- Loader
+-- ResourceReader
+-- ResourceLoader --------+
+-- InspectLoader |
+-- ExecutionLoader --+
@ -468,6 +469,71 @@ ABC hierarchy::
The import machinery now takes care of this automatically.
.. class:: ResourceReader
An :term:`abstract base class` for :term:`package`
:term:`loaders <loader>` to provide the ability to read
*resources*.
From the perspective of this ABC, a *resource* is a binary
artifact that is shipped within a package. Typically this is
something like a data file that lives next to the ``__init__.py``
file of the package. The purpose of this class is to help abstract
out the accessing of such data files so that it does not matter if
the package and its data file(s) are stored in a e.g. zip file
versus on the file system.
For any of methods of this class, a *resource* argument is
expected to be a :term:`file-like object` which represents
conceptually just a file name. This means that no subdirectory
paths should be included in the *resource* argument. This is
because the location of the package that the loader is for acts
as the "directory". Hence the metaphor for directories and file
names is packages and resources, respectively. This is also why
instances of this class are expected to directly correlate to
a specific package (instead of potentially representing multiple
packages or a module).
.. versionadded:: 3.7
.. abstractmethod:: open_resource(resource)
Returns an opened, :term:`file-like object` for binary reading
of the *resource*.
If the resource cannot be found, :exc:`FileNotFoundError` is
raised.
.. abstractmethod:: resource_path(resource)
Returns the file system path to the *resource*.
If the resource does not concretely exist on the file system,
raise :exc:`FileNotFoundError`.
.. abstractmethod:: is_resource(name)
Returns ``True`` if the named *name* is considered a resource.
:exc:`FileNotFoundError` is raised if *name* does not exist.
.. abstractmethod:: contents()
Returns an :term:`iterator` of strings over the contents of
the package. Do note that it is not required that all names
returned by the iterator be actual resources, e.g. it is
acceptable to return names for which :meth:`is_resource` would
be false.
Allowing non-resource names to be returned is to allow for
situations where how a package and its resources are stored
are known a priori and the non-resource names would be useful.
For instance, returning subdirectory names is allowed so that
when it is known that the package and resources are stored on
the file system then those subdirectory names can be used.
The abstract method returns an empty iterator.
.. class:: ResourceLoader
An abstract base class for a :term:`loader` which implements the optional

View File

@ -340,3 +340,41 @@ class SourceLoader(_bootstrap_external.SourceLoader, ResourceLoader, ExecutionLo
"""
_register(SourceLoader, machinery.SourceFileLoader)
class ResourceReader(Loader):
"""Abstract base class for loaders to provide resource reading support."""
@abc.abstractmethod
def open_resource(self, resource):
"""Return an opened, file-like object for binary reading.
The 'resource' argument is expected to represent only a file name
and thus not contain any subdirectory components.
If the resource cannot be found, FileNotFoundError is raised.
"""
raise FileNotFoundError
@abc.abstractmethod
def resource_path(self, resource):
"""Return the file system path to the specified resource.
The 'resource' argument is expected to represent only a file name
and thus not contain any subdirectory components.
If the resource does not exist on the file system, raise
FileNotFoundError.
"""
raise FileNotFoundError
@abc.abstractmethod
def is_resource(self, name):
"""Return True if the named 'name' is consider a resource."""
raise FileNotFoundError
@abc.abstractmethod
def contents(self):
"""Return an iterator of strings over the contents of the package."""
return iter([])

View File

@ -305,6 +305,45 @@ class ExecutionLoaderDefaultsTests(ABCTestHarness):
) = test_util.test_both(InspectLoaderDefaultsTests)
class ResourceReader:
def open_resource(self, *args, **kwargs):
return super().open_resource(*args, **kwargs)
def resource_path(self, *args, **kwargs):
return super().resource_path(*args, **kwargs)
def is_resource(self, *args, **kwargs):
return super().is_resource(*args, **kwargs)
def contents(self, *args, **kwargs):
return super().contents(*args, **kwargs)
class ResourceReaderDefaultsTests(ABCTestHarness):
SPLIT = make_abc_subclasses(ResourceReader)
def test_open_resource(self):
with self.assertRaises(FileNotFoundError):
self.ins.open_resource('dummy_file')
def test_resource_path(self):
with self.assertRaises(FileNotFoundError):
self.ins.resource_path('dummy_file')
def test_is_resource(self):
with self.assertRaises(FileNotFoundError):
self.ins.is_resource('dummy_file')
def test_contents(self):
self.assertEqual([], list(self.ins.contents()))
(Frozen_RRDefaultTests,
Source_RRDefaultsTests
) = test_util.test_both(ResourceReaderDefaultsTests)
##### MetaPathFinder concrete methods ##########################################
class MetaPathFinderFindModuleTests:

View File

@ -0,0 +1,2 @@
Add :class:`importlib.abc.ResourceReader` as an ABC for loaders to provide a
unified API for reading resources contained within packages.