Add support for resources in namespace packages by FFY00 · Pull Request #196 · python/importlib_resources · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions importlib_resources/_compat.py
58 changes: 28 additions & 30 deletions importlib_resources/_py3.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,30 @@ def open_binary(package: Package, resource: Resource) -> BinaryIO:
return reader.open_resource(resource)
# 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 or 'non-existent file')
package_path = os.path.dirname(absolute_package_path)
full_path = os.path.join(package_path, resource)
try:
return open(full_path, mode='rb')
except OSError:
# 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 = cast(ResourceLoader, package.__spec__.loader)
data = None
if hasattr(package.__spec__.loader, 'get_data'):
with suppress(OSError):
data = loader.get_data(full_path)
if data is None:
package_name = package.__spec__.name
message = '{!r} resource not found in {!r}'.format(
resource, package_name)
raise FileNotFoundError(message)
return BytesIO(data)
spec = package.__spec__
if package.__spec__.submodule_search_locations is not None:
paths = package.__spec__.submodule_search_locations
elif package.__spec__.origin is not None:
paths = [os.path.dirname(os.path.abspath(package.__spec__.origin))]

for package_path in paths:
full_path = os.path.join(package_path, resource)
try:
return open(full_path, mode='rb')
except OSError:
# 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 = cast(ResourceLoader, package.__spec__.loader)
data = None
if hasattr(package.__spec__.loader, 'get_data'):
with suppress(OSError):
data = loader.get_data(full_path)
if data is not None:
return BytesIO(data)

raise FileNotFoundError('{!r} resource not found in {!r}'.format(
resource, package.__spec__.name))


def open_text(package: Package,
Expand Down Expand Up @@ -144,12 +147,7 @@ def contents(package: Package) -> Iterable[str]:
reader = _common.get_resource_reader(package)
if reader is not None:
return reader.contents()
# Is the package a namespace package? By definition, namespace packages
# cannot have resources.
namespace = (
package.__spec__.origin is None or
package.__spec__.origin == 'namespace'
)
if namespace or not package.__spec__.has_location:
return ()
return list(item.name for item in _common.from_package(package).iterdir())
transversable = _common.from_package(package)
if transversable.is_dir():
return list(item.name for item in transversable.iterdir())
return []
84 changes: 83 additions & 1 deletion importlib_resources/readers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import itertools
import os.path

from . import abc

from ._compat import Path, ZipPath
from ._compat import suppress, Path, ZipPath


class FileReader(abc.TraversableResources):
Expand Down Expand Up @@ -39,3 +42,82 @@ def is_resource(self, path):

def files(self):
return ZipPath(self.archive, self.prefix)


class MultiplexedPath(abc.Traversable):
"""
Given a series of Traversable objects, implement a merged
version of the interface across all objects. Useful for
namespace packages which may be multihomed at a single
name.
"""
def __init__(self, *paths):
self._paths = list(map(Path, set(paths)))
if not self._paths:
message = 'MultiplexedPath must contain at least one path'
raise FileNotFoundError(message)
if any(not path.is_dir() for path in self._paths):
raise NotADirectoryError(
'MultiplexedPath only supports directories')

def iterdir(self):
visited = []
for path in self._paths:
for file in path.iterdir():
if file.name in visited:
continue
visited.append(file.name)
yield file

def read_bytes(self):
return self.open(mode='rb').read()

def read_text(self, *args, **kwargs):
return self.open(mode='r', *args, **kwargs).read()

def is_dir(self):
return True

def is_file(self):
return False

def joinpath(self, child):
# first try to find child in current paths
for file in self.iterdir():
if file.name == child:
return file
# if it does not exist, construct it with the first path
return Path(os.path.join(self._paths[0], child))

__truediv__ = joinpath

def open(self, *args, **kwargs):
for path in self._paths[:-1]:
with suppress(Exception):
return path.open(*args, **kwargs)
return self._paths[-1].open(*args, **kwargs)

def name(self):
return os.path.basename(self._paths[0])

def __repr__(self):
return 'MultiplexedPath({})'.format(
', '.join("'{}'".format(path) for path in self._paths))


class NamespaceReader(abc.TraversableResources):
def __init__(self, namespace_path):
if 'NamespacePath' not in str(namespace_path):
raise ValueError('Invalid path')
self.path = MultiplexedPath(*list(namespace_path))

def resource_path(self, resource):
"""
Return the file system path to prevent
`resources.path()` from creating a temporary
copy.
"""
return str(self.path.joinpath(resource))

def files(self):
return self.path
Empty file.
Empty file.
Empty file.
Empty file.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions importlib_resources/tests/namespacedata01/utf-8.file
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello, UTF-8 world!
11 changes: 11 additions & 0 deletions importlib_resources/tests/test_open.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
import unittest

import importlib_resources as resources
Expand Down Expand Up @@ -65,6 +66,16 @@ def setUp(self):
self.data = data01


@unittest.skipUnless(
sys.version_info[0] >= 3,
'namespace packages not available on Python 2'
)
class OpenDiskNamespaceTests(OpenTests, unittest.TestCase):
def setUp(self):
from . import namespacedata01
self.data = namespacedata01


class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
pass

Expand Down
65 changes: 35 additions & 30 deletions importlib_resources/tests/test_resource.py