Skip to content

Commit

Permalink
Support the ResourceReader API
Browse files Browse the repository at this point in the history
  • Loading branch information
brettcannon authored Nov 2, 2017
2 parents 4d25546 + 71a5fe3 commit 83bfaa5
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.file binary
*.zip binary
60 changes: 39 additions & 21 deletions importlib_resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from typing import Iterator, Union
from typing.io import BinaryIO

from . import abc as resources_abc


Package = Union[types.ModuleType, str]
if sys.version_info >= (3, 6):
Expand Down Expand Up @@ -47,27 +49,33 @@ def open(package: Package, file_name: FileName) -> BinaryIO:
"""Return a file-like object opened for binary-reading of the resource."""
file_name = _normalize_path(file_name)
package = _get_package(package)
# Using pathlib doesn't work well here due to the lack of 'strict' argument
# for pathlib.Path.resolve() prior to Python 3.6.
absolute_package_path = os.path.abspath(package.__spec__.origin)
package_path = os.path.dirname(absolute_package_path)
full_path = os.path.join(package_path, file_name)
try:
return builtins.open(full_path, 'rb')
except IOError:
# Just assume the loader is a resource loader; all the relevant
# importlib.machinery loaders are and an AttributeError for get_data()
# will make it clear what is needed from the loader.
loader = typing.cast(importlib.abc.ResourceLoader,
if hasattr(package.__spec__.loader, 'open_resource'):
reader = typing.cast(resources_abc.ResourceReader,
package.__spec__.loader)
return reader.open_resource(file_name)
else:
# Using pathlib doesn't work well here due to the lack of 'strict'
# argument for pathlib.Path.resolve() prior to Python 3.6.
absolute_package_path = os.path.abspath(package.__spec__.origin)
package_path = os.path.dirname(absolute_package_path)
full_path = os.path.join(package_path, file_name)
try:
data = loader.get_data(full_path)
return builtins.open(full_path, 'rb')
except IOError:
package_name = package.__spec__.name
message = '{!r} resource not found in {!r}'.format(file_name, package_name)
raise FileNotFoundError(message)
else:
return io.BytesIO(data)
# Just assume the loader is a resource loader; all the relevant
# importlib.machinery loaders are and an AttributeError for
# get_data() will make it clear what is needed from the loader.
loader = typing.cast(importlib.abc.ResourceLoader,
package.__spec__.loader)
try:
data = loader.get_data(full_path)
except IOError:
package_name = package.__spec__.name
message = '{!r} resource not found in {!r}'.format(file_name,
package_name)
raise FileNotFoundError(message)
else:
return io.BytesIO(data)


def read(package: Package, file_name: FileName, encoding: str = 'utf-8',
Expand Down Expand Up @@ -98,14 +106,24 @@ def path(package: Package, file_name: FileName) -> Iterator[pathlib.Path]:
raised if the file was deleted prior to the context manager
exiting).
"""
normalized_path = _normalize_path(file_name)
file_name = _normalize_path(file_name)
package = _get_package(package)
if hasattr(package.__spec__.loader, 'resource_path'):
reader = typing.cast(resources_abc.ResourceReader,
package.__spec__.loader)
try:
yield pathlib.Path(reader.resource_path(file_name))
return
except FileNotFoundError:
pass
# Fall-through for both the lack of resource_path() *and* if resource_path()
# raises FileNotFoundError.
package_directory = pathlib.Path(package.__spec__.origin).parent
file_path = package_directory / normalized_path
file_path = package_directory / file_name
if file_path.exists():
yield file_path
else:
with open(package, normalized_path) as file:
with open(package, file_name) as file:
data = file.read()
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
# blocks due to the need to close the temporary file to work on
Expand Down
25 changes: 25 additions & 0 deletions importlib_resources/abc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import abc
from typing.io import BinaryIO


class ResourceReader(abc.ABC):

"""Abstract base class for loaders to provide resource reading support."""

@abc.abstractmethod
def open_resource(self, path: str) -> BinaryIO:
"""Return an opened, file-like object for binary reading of the resource.
The 'path' argument is expected to represent only a file name.
If the resource cannot be found, FileNotFoundError is raised.
"""
raise FileNotFoundError

def resource_path(self, path: str) -> str:
"""Return the file system path to the specified resource.
The 'path' argument is expected to represent only a file name.
If the resource does not exist on the file system, raise
FileNotFoundError.
"""
raise FileNotFoundError
50 changes: 50 additions & 0 deletions importlib_resources/tests/util.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
import abc
import importlib
import importlib.machinery
import io
import pathlib
import sys
import types
import unittest

from .. import abc as resources_abc
from . import data


def create_package(*, file, path):
class Reader(resources_abc.ResourceReader):
def open_resource(self, path):
self._path = path
if isinstance(file, Exception):
raise file
else:
return file

def resource_path(self, path_):
self._path = path_
if isinstance(path, Exception):
raise path
else:
return path

name = 'testingpackage'
spec = importlib.machinery.ModuleSpec(name, Reader(),
origin='does-not-exist',
is_package=True)
# Unforunately importlib.util.module_from_spec() was not introduced until
# Python 3.5.
module = types.ModuleType(name)
module.__spec__ = spec
return module


class CommonTests(abc.ABC):

@abc.abstractmethod
Expand Down Expand Up @@ -54,6 +85,25 @@ def test_non_package(self):
with self.assertRaises(TypeError):
self.execute(__spec__.name, 'utf-8.file')

def test_resource_opener(self):
data = io.BytesIO(b'Hello, world!')
package = create_package(file=data, path=FileNotFoundError())
self.execute(package, 'utf-8.file')
self.assertEqual(package.__spec__.loader._path, 'utf-8.file')

def test_resource_path(self):
data = io.BytesIO(b'Hello, world!')
path = __file__
package = create_package(file=data, path=path)
self.execute(package, 'utf-8.file')
self.assertEqual(package.__spec__.loader._path, 'utf-8.file')

def test_useless_loader(self):
package = create_package(file=FileNotFoundError(),
path=FileNotFoundError())
with self.assertRaises(FileNotFoundError):
self.execute(package, 'utf-8.file')


class ZipSetup:

Expand Down

0 comments on commit 83bfaa5

Please sign in to comment.