Skip to content

Commit 4ac5150

Browse files
authored
bpo-32248: Implement importlib.abc.ResourceReader (GH-4892)
1 parent d2b0231 commit 4ac5150

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

Doc/library/importlib.rst

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ ABC hierarchy::
233233
| +-- MetaPathFinder
234234
| +-- PathEntryFinder
235235
+-- Loader
236+
+-- ResourceReader
236237
+-- ResourceLoader --------+
237238
+-- InspectLoader |
238239
+-- ExecutionLoader --+
@@ -468,6 +469,71 @@ ABC hierarchy::
468469
The import machinery now takes care of this automatically.
469470

470471

472+
.. class:: ResourceReader
473+
474+
An :term:`abstract base class` for :term:`package`
475+
:term:`loaders <loader>` to provide the ability to read
476+
*resources*.
477+
478+
From the perspective of this ABC, a *resource* is a binary
479+
artifact that is shipped within a package. Typically this is
480+
something like a data file that lives next to the ``__init__.py``
481+
file of the package. The purpose of this class is to help abstract
482+
out the accessing of such data files so that it does not matter if
483+
the package and its data file(s) are stored in a e.g. zip file
484+
versus on the file system.
485+
486+
For any of methods of this class, a *resource* argument is
487+
expected to be a :term:`file-like object` which represents
488+
conceptually just a file name. This means that no subdirectory
489+
paths should be included in the *resource* argument. This is
490+
because the location of the package that the loader is for acts
491+
as the "directory". Hence the metaphor for directories and file
492+
names is packages and resources, respectively. This is also why
493+
instances of this class are expected to directly correlate to
494+
a specific package (instead of potentially representing multiple
495+
packages or a module).
496+
497+
.. versionadded:: 3.7
498+
499+
.. abstractmethod:: open_resource(resource)
500+
501+
Returns an opened, :term:`file-like object` for binary reading
502+
of the *resource*.
503+
504+
If the resource cannot be found, :exc:`FileNotFoundError` is
505+
raised.
506+
507+
.. abstractmethod:: resource_path(resource)
508+
509+
Returns the file system path to the *resource*.
510+
511+
If the resource does not concretely exist on the file system,
512+
raise :exc:`FileNotFoundError`.
513+
514+
.. abstractmethod:: is_resource(name)
515+
516+
Returns ``True`` if the named *name* is considered a resource.
517+
:exc:`FileNotFoundError` is raised if *name* does not exist.
518+
519+
.. abstractmethod:: contents()
520+
521+
Returns an :term:`iterator` of strings over the contents of
522+
the package. Do note that it is not required that all names
523+
returned by the iterator be actual resources, e.g. it is
524+
acceptable to return names for which :meth:`is_resource` would
525+
be false.
526+
527+
Allowing non-resource names to be returned is to allow for
528+
situations where how a package and its resources are stored
529+
are known a priori and the non-resource names would be useful.
530+
For instance, returning subdirectory names is allowed so that
531+
when it is known that the package and resources are stored on
532+
the file system then those subdirectory names can be used.
533+
534+
The abstract method returns an empty iterator.
535+
536+
471537
.. class:: ResourceLoader
472538

473539
An abstract base class for a :term:`loader` which implements the optional

Lib/importlib/abc.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,41 @@ def set_data(self, path, data):
340340
"""
341341

342342
_register(SourceLoader, machinery.SourceFileLoader)
343+
344+
345+
class ResourceReader(Loader):
346+
347+
"""Abstract base class for loaders to provide resource reading support."""
348+
349+
@abc.abstractmethod
350+
def open_resource(self, resource):
351+
"""Return an opened, file-like object for binary reading.
352+
353+
The 'resource' argument is expected to represent only a file name
354+
and thus not contain any subdirectory components.
355+
356+
If the resource cannot be found, FileNotFoundError is raised.
357+
"""
358+
raise FileNotFoundError
359+
360+
@abc.abstractmethod
361+
def resource_path(self, resource):
362+
"""Return the file system path to the specified resource.
363+
364+
The 'resource' argument is expected to represent only a file name
365+
and thus not contain any subdirectory components.
366+
367+
If the resource does not exist on the file system, raise
368+
FileNotFoundError.
369+
"""
370+
raise FileNotFoundError
371+
372+
@abc.abstractmethod
373+
def is_resource(self, name):
374+
"""Return True if the named 'name' is consider a resource."""
375+
raise FileNotFoundError
376+
377+
@abc.abstractmethod
378+
def contents(self):
379+
"""Return an iterator of strings over the contents of the package."""
380+
return iter([])

Lib/test/test_importlib/test_abc.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,45 @@ def test_get_filename(self):
305305
) = test_util.test_both(InspectLoaderDefaultsTests)
306306

307307

308+
class ResourceReader:
309+
310+
def open_resource(self, *args, **kwargs):
311+
return super().open_resource(*args, **kwargs)
312+
313+
def resource_path(self, *args, **kwargs):
314+
return super().resource_path(*args, **kwargs)
315+
316+
def is_resource(self, *args, **kwargs):
317+
return super().is_resource(*args, **kwargs)
318+
319+
def contents(self, *args, **kwargs):
320+
return super().contents(*args, **kwargs)
321+
322+
323+
class ResourceReaderDefaultsTests(ABCTestHarness):
324+
325+
SPLIT = make_abc_subclasses(ResourceReader)
326+
327+
def test_open_resource(self):
328+
with self.assertRaises(FileNotFoundError):
329+
self.ins.open_resource('dummy_file')
330+
331+
def test_resource_path(self):
332+
with self.assertRaises(FileNotFoundError):
333+
self.ins.resource_path('dummy_file')
334+
335+
def test_is_resource(self):
336+
with self.assertRaises(FileNotFoundError):
337+
self.ins.is_resource('dummy_file')
338+
339+
def test_contents(self):
340+
self.assertEqual([], list(self.ins.contents()))
341+
342+
(Frozen_RRDefaultTests,
343+
Source_RRDefaultsTests
344+
) = test_util.test_both(ResourceReaderDefaultsTests)
345+
346+
308347
##### MetaPathFinder concrete methods ##########################################
309348
class MetaPathFinderFindModuleTests:
310349

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :class:`importlib.abc.ResourceReader` as an ABC for loaders to provide a
2+
unified API for reading resources contained within packages.

0 commit comments

Comments
 (0)