Skip to content

Commit 8207434

Browse files
committed
Add _adapters module, making SpecLoaderAdapter a first-class feature and not a compatibility feature. Add DegenerateFiles for readers that don't implement the files API such that the rest of the API can rely on it being implemented. Fixes #214.
1 parent 06da7de commit 8207434

File tree

3 files changed

+109
-28
lines changed

3 files changed

+109
-28
lines changed

importlib_resources/_adapters.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from contextlib import suppress
2+
3+
from . import abc
4+
5+
6+
class SpecLoaderAdapter:
7+
"""
8+
Adapt a package spec to adapt the underlying loader.
9+
"""
10+
11+
def __init__(self, spec, adapter=lambda spec: spec.loader):
12+
self.spec = spec
13+
self.loader = adapter(spec)
14+
15+
def __getattr__(self, name):
16+
return getattr(self.spec, name)
17+
18+
19+
class TraversableResourcesLoader:
20+
"""
21+
Adapt a loader to provide TraversableResources.
22+
"""
23+
24+
def __init__(self, spec):
25+
self.spec = spec
26+
27+
def get_resource_reader(self, name):
28+
return DegenerateFiles(self.spec)._native()
29+
30+
31+
class DegenerateFiles:
32+
"""
33+
Adapter for an existing or non-existant resource reader
34+
to provide a degenerate .files().
35+
"""
36+
37+
class Path(abc.Traversable):
38+
def iterdir(self):
39+
return iter(())
40+
41+
def read_bytes(self):
42+
raise ValueError()
43+
44+
def read_text(self):
45+
raise ValueError()
46+
47+
def is_dir(self):
48+
return False
49+
50+
is_file = exists = is_dir
51+
52+
def joinpath(self, other):
53+
return DegenerateFiles.Path()
54+
55+
__truediv__ = joinpath
56+
57+
def name(self):
58+
return ''
59+
60+
def open(self):
61+
raise ValueError()
62+
63+
def __init__(self, spec):
64+
self.spec = spec
65+
66+
@property
67+
def _reader(self):
68+
with suppress(AttributeError):
69+
return self.spec.loader.get_resource_reader(self.spec.name)
70+
71+
def _native(self):
72+
"""
73+
Return the native reader if it supports files().
74+
"""
75+
reader = self._reader
76+
return reader if hasattr(reader, 'files') else self
77+
78+
def __getattr__(self, attr):
79+
return getattr(self._reader, attr)
80+
81+
def files(self):
82+
return DegenerateFiles.Path()
83+
84+
85+
def wrap_spec(package):
86+
"""
87+
Construct a package spec with traversable compatibility
88+
on the spec/loader/reader.
89+
"""
90+
return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)

importlib_resources/_common.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typing import Union, Any, Optional
1010
from .abc import ResourceReader
1111

12-
from ._compat import package_spec
12+
from ._compat import wrap_spec
1313

1414
Package = Union[types.ModuleType, str]
1515

@@ -63,7 +63,7 @@ def get_package(package):
6363
Raise an exception if the resolved module is not a package.
6464
"""
6565
resolved = resolve(package)
66-
if package_spec(resolved).submodule_search_locations is None:
66+
if wrap_spec(resolved).submodule_search_locations is None:
6767
raise TypeError('{!r} is not a package'.format(package))
6868
return resolved
6969

@@ -73,7 +73,7 @@ def from_package(package):
7373
Return a Traversable object for the given package.
7474
7575
"""
76-
spec = package_spec(package)
76+
spec = wrap_spec(package)
7777
reader = spec.loader.get_resource_reader(spec.name)
7878
return reader.files()
7979

importlib_resources/_compat.py

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
# flake8: noqa
2+
13
import abc
24
import sys
5+
import pathlib
36
from contextlib import suppress
47

5-
# flake8: noqa
6-
78
try:
89
from zipfile import Path as ZipPath # type: ignore
910
except ImportError:
@@ -24,19 +25,6 @@ def runtime_checkable(cls): # type: ignore
2425
Protocol = abc.ABC # type: ignore
2526

2627

27-
class SpecLoaderAdapter:
28-
"""
29-
Adapt a package spec to adapt the underlying loader.
30-
"""
31-
32-
def __init__(self, spec, adapter=lambda spec: spec.loader):
33-
self.spec = spec
34-
self.loader = adapter(spec)
35-
36-
def __getattr__(self, name):
37-
return getattr(self.spec, name)
38-
39-
4028
class TraversableResourcesLoader:
4129
"""
4230
Adapt loaders to provide TraversableResources and other
@@ -52,7 +40,7 @@ def path(self):
5240

5341
def get_resource_reader(self, name):
5442
# Python < 3.9
55-
from . import readers
43+
from . import readers, _adapters
5644

5745
def _zip_reader(spec):
5846
with suppress(AttributeError):
@@ -70,6 +58,10 @@ def _native_reader(spec):
7058
reader = _available_reader(spec)
7159
return reader if hasattr(reader, 'files') else None
7260

61+
def _file_reader(spec):
62+
if pathlib.Path(self.path).exists():
63+
return readers.FileReader(self)
64+
7365
return (
7466
# native reader if it supplies 'files'
7567
_native_reader(self.spec)
@@ -81,17 +73,16 @@ def _native_reader(spec):
8173
_namespace_reader(self.spec)
8274
or
8375
# local FileReader
84-
readers.FileReader(self)
76+
_file_reader(self.spec)
77+
or _adapters.DegenerateFiles(self.spec)
8578
)
8679

8780

88-
def package_spec(package):
81+
def wrap_spec(package):
8982
"""
90-
Construct a minimal package spec suitable for
91-
matching the interfaces this library relies upon
92-
in later Python versions.
83+
Construct a package spec with traversable compatibility
84+
on the spec/loader/reader.
9385
"""
94-
spec = package.__spec__
95-
# avoid adapting the spec in test_package_has_no_reader_fallback
96-
adapt = spec.loader.__class__ is not object
97-
return SpecLoaderAdapter(spec, TraversableResourcesLoader) if adapt else spec
86+
from . import _adapters
87+
88+
return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)

0 commit comments

Comments
 (0)