Skip to content

Commit 225f5b7

Browse files
authored
Merge pull request python#215 from python/bugfix/bpo-42129-no-reader-fallback
Add a reader fallback even for forward port.
2 parents e197b1f + 33d2a22 commit 225f5b7

4 files changed

Lines changed: 120 additions & 22 deletions

File tree

CHANGES.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
v5.0.2
2+
======
3+
4+
* #214: Added ``_adapters`` module to ensure that degenerate
5+
``files`` behavior can be made available for legacy loaders
6+
whose resource readers don't implement it. Fixes issue where
7+
backport compatibility module was masking this fallback
8+
behavior only to discover the defect when applying changes to
9+
CPython.
10+
111
v5.0.1
212
======
313

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 # type: ignore
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: 17 additions & 19 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,16 +25,7 @@ def runtime_checkable(cls): # type: ignore
2425
Protocol = abc.ABC # type: ignore
2526

2627

27-
class TraversableResourcesAdapter:
28-
def __init__(self, spec):
29-
self.spec = spec
30-
self.loader = LoaderAdapter(spec)
31-
32-
def __getattr__(self, name):
33-
return getattr(self.spec, name)
34-
35-
36-
class LoaderAdapter:
28+
class TraversableResourcesLoader:
3729
"""
3830
Adapt loaders to provide TraversableResources and other
3931
compatibility.
@@ -47,7 +39,7 @@ def path(self):
4739
return self.spec.origin
4840

4941
def get_resource_reader(self, name):
50-
from . import readers
42+
from . import readers, _adapters
5143

5244
def _zip_reader(spec):
5345
with suppress(AttributeError):
@@ -65,6 +57,10 @@ def _native_reader(spec):
6557
reader = _available_reader(spec)
6658
return reader if hasattr(reader, 'files') else None
6759

60+
def _file_reader(spec):
61+
if pathlib.Path(self.path).exists():
62+
return readers.FileReader(self)
63+
6864
return (
6965
# native reader if it supplies 'files'
7066
_native_reader(self.spec)
@@ -76,14 +72,16 @@ def _native_reader(spec):
7672
_namespace_reader(self.spec)
7773
or
7874
# local FileReader
79-
readers.FileReader(self)
75+
_file_reader(self.spec)
76+
or _adapters.DegenerateFiles(self.spec)
8077
)
8178

8279

83-
def package_spec(package):
80+
def wrap_spec(package):
8481
"""
85-
Construct a minimal package spec suitable for
86-
matching the interfaces this library relies upon
87-
in later Python versions.
82+
Construct a package spec with traversable compatibility
83+
on the spec/loader/reader.
8884
"""
89-
return TraversableResourcesAdapter(package.__spec__)
85+
from . import _adapters
86+
87+
return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)

0 commit comments

Comments
 (0)