bpo-32248: Implement importlib.abc.ResourceReader (GH-4892)
This commit is contained in:
parent
d2b02310ac
commit
4ac5150e06
|
@ -233,6 +233,7 @@ ABC hierarchy::
|
||||||
| +-- MetaPathFinder
|
| +-- MetaPathFinder
|
||||||
| +-- PathEntryFinder
|
| +-- PathEntryFinder
|
||||||
+-- Loader
|
+-- Loader
|
||||||
|
+-- ResourceReader
|
||||||
+-- ResourceLoader --------+
|
+-- ResourceLoader --------+
|
||||||
+-- InspectLoader |
|
+-- InspectLoader |
|
||||||
+-- ExecutionLoader --+
|
+-- ExecutionLoader --+
|
||||||
|
@ -468,6 +469,71 @@ ABC hierarchy::
|
||||||
The import machinery now takes care of this automatically.
|
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
|
.. class:: ResourceLoader
|
||||||
|
|
||||||
An abstract base class for a :term:`loader` which implements the optional
|
An abstract base class for a :term:`loader` which implements the optional
|
||||||
|
|
|
@ -340,3 +340,41 @@ class SourceLoader(_bootstrap_external.SourceLoader, ResourceLoader, ExecutionLo
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_register(SourceLoader, machinery.SourceFileLoader)
|
_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([])
|
||||||
|
|
|
@ -305,6 +305,45 @@ class ExecutionLoaderDefaultsTests(ABCTestHarness):
|
||||||
) = test_util.test_both(InspectLoaderDefaultsTests)
|
) = 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 ##########################################
|
##### MetaPathFinder concrete methods ##########################################
|
||||||
class MetaPathFinderFindModuleTests:
|
class MetaPathFinderFindModuleTests:
|
||||||
|
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue