From 4ac5150e068a3a795ef00465f6dff51747b62b91 Mon Sep 17 00:00:00 2001
From: Brett Cannon <brettcannon@users.noreply.github.com>
Date: Fri, 15 Dec 2017 16:29:35 -0800
Subject: [PATCH] bpo-32248: Implement importlib.abc.ResourceReader (GH-4892)

---
 Doc/library/importlib.rst                     | 66 +++++++++++++++++++
 Lib/importlib/abc.py                          | 38 +++++++++++
 Lib/test/test_importlib/test_abc.py           | 39 +++++++++++
 .../2017-12-15-15-34-12.bpo-32248.zmO8G2.rst  |  2 +
 4 files changed, 145 insertions(+)
 create mode 100644 Misc/NEWS.d/next/Library/2017-12-15-15-34-12.bpo-32248.zmO8G2.rst

diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst
index 3cafb41d44..eeccc9d40e 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 d7cadf2ee7..b772db3758 100644
--- a/Lib/importlib/abc.py
+++ b/Lib/importlib/abc.py
@@ -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([])
diff --git a/Lib/test/test_importlib/test_abc.py b/Lib/test/test_importlib/test_abc.py
index 4ba28c6638..f1e1db3562 100644
--- a/Lib/test/test_importlib/test_abc.py
+++ b/Lib/test/test_importlib/test_abc.py
@@ -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:
 
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 0000000000..f77cdb03dd
--- /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.
-- 
2.40.0