Visitar URL original
Merge pull request #196 from FFY00/namespace-resources · jaraco/cpython@eff1584 · GitHub
Skip to content

Commit eff1584

Browse files
authored
Merge pull request python#196 from FFY00/namespace-resources
Add support for resources in namespace packages
2 parents f92adfb + 8e1a218 commit eff1584

12 files changed

Lines changed: 164 additions & 61 deletions

File tree

‎importlib_resources/_compat.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ def _zip_reader(spec):
9393
with suppress(AttributeError):
9494
return readers.ZipReader(spec.loader, spec.name)
9595

96+
def _namespace_reader(spec):
97+
with suppress(AttributeError, ValueError):
98+
return readers.NamespaceReader(spec.submodule_search_locations)
99+
96100
def _available_reader(spec):
97101
with suppress(AttributeError):
98102
return spec.loader.get_resource_reader(spec.name)
@@ -106,6 +110,8 @@ def _native_reader(spec):
106110
_native_reader(self.spec) or
107111
# local ZipReader if a zip module
108112
_zip_reader(self.spec) or
113+
# local NamespaceReader if a namespace module
114+
_namespace_reader(self.spec) or
109115
# local FileReader
110116
readers.FileReader(self)
111117
)

‎importlib_resources/_py3.py‎

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,30 @@ def open_binary(package: Package, resource: Resource) -> BinaryIO:
3131
return reader.open_resource(resource)
3232
# Using pathlib doesn't work well here due to the lack of 'strict'
3333
# argument for pathlib.Path.resolve() prior to Python 3.6.
34-
absolute_package_path = os.path.abspath(
35-
package.__spec__.origin or 'non-existent file')
36-
package_path = os.path.dirname(absolute_package_path)
37-
full_path = os.path.join(package_path, resource)
38-
try:
39-
return open(full_path, mode='rb')
40-
except OSError:
41-
# Just assume the loader is a resource loader; all the relevant
42-
# importlib.machinery loaders are and an AttributeError for
43-
# get_data() will make it clear what is needed from the loader.
44-
loader = cast(ResourceLoader, package.__spec__.loader)
45-
data = None
46-
if hasattr(package.__spec__.loader, 'get_data'):
47-
with suppress(OSError):
48-
data = loader.get_data(full_path)
49-
if data is None:
50-
package_name = package.__spec__.name
51-
message = '{!r} resource not found in {!r}'.format(
52-
resource, package_name)
53-
raise FileNotFoundError(message)
54-
return BytesIO(data)
34+
spec = package.__spec__
35+
if package.__spec__.submodule_search_locations is not None:
36+
paths = package.__spec__.submodule_search_locations
37+
elif package.__spec__.origin is not None:
38+
paths = [os.path.dirname(os.path.abspath(package.__spec__.origin))]
39+
40+
for package_path in paths:
41+
full_path = os.path.join(package_path, resource)
42+
try:
43+
return open(full_path, mode='rb')
44+
except OSError:
45+
# Just assume the loader is a resource loader; all the relevant
46+
# importlib.machinery loaders are and an AttributeError for
47+
# get_data() will make it clear what is needed from the loader.
48+
loader = cast(ResourceLoader, package.__spec__.loader)
49+
data = None
50+
if hasattr(package.__spec__.loader, 'get_data'):
51+
with suppress(OSError):
52+
data = loader.get_data(full_path)
53+
if data is not None:
54+
return BytesIO(data)
55+
56+
raise FileNotFoundError('{!r} resource not found in {!r}'.format(
57+
resource, package.__spec__.name))
5558

5659

5760
def open_text(package: Package,
@@ -144,12 +147,7 @@ def contents(package: Package) -> Iterable[str]:
144147
reader = _common.get_resource_reader(package)
145148
if reader is not None:
146149
return reader.contents()
147-
# Is the package a namespace package? By definition, namespace packages
148-
# cannot have resources.
149-
namespace = (
150-
package.__spec__.origin is None or
151-
package.__spec__.origin == 'namespace'
152-
)
153-
if namespace or not package.__spec__.has_location:
154-
return ()
155-
return list(item.name for item in _common.from_package(package).iterdir())
150+
transversable = _common.from_package(package)
151+
if transversable.is_dir():
152+
return list(item.name for item in transversable.iterdir())
153+
return []

‎importlib_resources/readers.py‎

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import itertools
2+
import os.path
3+
14
from . import abc
25

3-
from ._compat import Path, ZipPath
6+
from ._compat import suppress, Path, ZipPath
47

58

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

4043
def files(self):
4144
return ZipPath(self.archive, self.prefix)
45+
46+
47+
class MultiplexedPath(abc.Traversable):
48+
"""
49+
Given a series of Traversable objects, implement a merged
50+
version of the interface across all objects. Useful for
51+
namespace packages which may be multihomed at a single
52+
name.
53+
"""
54+
def __init__(self, *paths):
55+
self._paths = list(map(Path, set(paths)))
56+
if not self._paths:
57+
message = 'MultiplexedPath must contain at least one path'
58+
raise FileNotFoundError(message)
59+
if any(not path.is_dir() for path in self._paths):
60+
raise NotADirectoryError(
61+
'MultiplexedPath only supports directories')
62+
63+
def iterdir(self):
64+
visited = []
65+
for path in self._paths:
66+
for file in path.iterdir():
67+
if file.name in visited:
68+
continue
69+
visited.append(file.name)
70+
yield file
71+
72+
def read_bytes(self):
73+
return self.open(mode='rb').read()
74+
75+
def read_text(self, *args, **kwargs):
76+
return self.open(mode='r', *args, **kwargs).read()
77+
78+
def is_dir(self):
79+
return True
80+
81+
def is_file(self):
82+
return False
83+
84+
def joinpath(self, child):
85+
# first try to find child in current paths
86+
for file in self.iterdir():
87+
if file.name == child:
88+
return file
89+
# if it does not exist, construct it with the first path
90+
return Path(os.path.join(self._paths[0], child))
91+
92+
__truediv__ = joinpath
93+
94+
def open(self, *args, **kwargs):
95+
for path in self._paths[:-1]:
96+
with suppress(Exception):
97+
return path.open(*args, **kwargs)
98+
return self._paths[-1].open(*args, **kwargs)
99+
100+
def name(self):
101+
return os.path.basename(self._paths[0])
102+
103+
def __repr__(self):
104+
return 'MultiplexedPath({})'.format(
105+
', '.join("'{}'".format(path) for path in self._paths))
106+
107+
108+
class NamespaceReader(abc.TraversableResources):
109+
def __init__(self, namespace_path):
110+
if 'NamespacePath' not in str(namespace_path):
111+
raise ValueError('Invalid path')
112+
self.path = MultiplexedPath(*list(namespace_path))
113+
114+
def resource_path(self, resource):
115+
"""
116+
Return the file system path to prevent
117+
`resources.path()` from creating a temporary
118+
copy.
119+
"""
120+
return str(self.path.joinpath(resource))
121+
122+
def files(self):
123+
return self.path

‎importlib_resources/tests/data03/__init__.py‎

Whitespace-only changes.

‎importlib_resources/tests/data03/namespace/portion1/__init__.py‎

Whitespace-only changes.

‎importlib_resources/tests/data03/namespace/portion2/__init__.py‎

Whitespace-only changes.

‎importlib_resources/tests/data03/namespace/resource1.txt‎

Whitespace-only changes.
4 Bytes
Binary file not shown.
44 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello, UTF-8 world!

0 commit comments

Comments
 (0)