From 4ac5150e068a3a795ef00465f6dff51747b62b91 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 15 Dec 2017 16:29:35 -0800 Subject: 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 3cafb41..eeccc9d 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 ` 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 d7cadf2..b772db3 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 4ba28c6..f1e1db3 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 0000000..f77cdb0 --- /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. -- cgit v0.12