[Python-checkins] bpo-32248: Implement importlib.abc.ResourceReader (GH-4892)

Brett Cannon webhook-mailer at python.org
Fri Dec 15 19:29:42 EST 2017


https://github.com/python/cpython/commit/4ac5150e068a3a795ef00465f6dff51747b62b91
commit: 4ac5150e068a3a795ef00465f6dff51747b62b91
branch: master
author: Brett Cannon <brettcannon at users.noreply.github.com>
committer: GitHub <noreply at github.com>
date: 2017-12-15T16:29:35-08:00
summary:

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

files:
A Misc/NEWS.d/next/Library/2017-12-15-15-34-12.bpo-32248.zmO8G2.rst
M Doc/library/importlib.rst
M Lib/importlib/abc.py
M Lib/test/test_importlib/test_abc.py

diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst
index 3cafb41d447..eeccc9d40e6 100644
--- a/Doc/library/importlib.rst
+++ b/Doc/library/importlib.rst
@@ -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
diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py
index d7cadf2ee74..b772db3758c 100644
--- a/Lib/importlib/abc.py
+++ b/Lib/importlib/abc.py
@@ -340,3 +340,41 @@ def set_data(self, path, data):
         """
 
 _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([])
diff --git a/Lib/test/test_importlib/test_abc.py b/Lib/test/test_importlib/test_abc.py
index 4ba28c6638b..f1e1db35623 100644
--- a/Lib/test/test_importlib/test_abc.py
+++ b/Lib/test/test_importlib/test_abc.py
@@ -305,6 +305,45 @@ def test_get_filename(self):
  ) = 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:
 
diff --git a/Misc/NEWS.d/next/Library/2017-12-15-15-34-12.bpo-32248.zmO8G2.rst b/Misc/NEWS.d/next/Library/2017-12-15-15-34-12.bpo-32248.zmO8G2.rst
new file mode 100644
index 00000000000..f77cdb03dde
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2017-12-15-15-34-12.bpo-32248.zmO8G2.rst
@@ -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.



More information about the Python-checkins mailing list