Skip to content

Commit 2779e80

Browse files
committed
gh-127337: Fix importlib.resources for legacy ResourceReader loaders
CompatibilityFiles.SpecPath, .ChildPath and .OrphanPath each defined joinpath() with a single required argument, but importlib.resources.abc.Traversable declares joinpath(*descendants) and documents that passing no descendants returns self. Since 3.13 the functional API is implemented on top of files(anchor).joinpath(*path_names), so any call that passes no path names -- contents(anchor), is_resource(anchor) -- raised TypeError for a package whose loader provides only a legacy ResourceReader. On 3.12, contents(anchor) returned the resource names. Passing several descendants at once now gives the same result as applying them one at a time, which is the equivalence the Traversable documentation relies on for implementations predating the multi-argument protocol.
1 parent f4b1d3e commit 2779e80

3 files changed

Lines changed: 68 additions & 7 deletions

File tree

Lib/importlib/resources/_adapters.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,14 @@ def is_file(self):
6666

6767
is_dir = is_file
6868

69-
def joinpath(self, other):
69+
def joinpath(self, *descendants):
70+
if not descendants:
71+
return self
7072
if not self._reader:
71-
return CompatibilityFiles.OrphanPath(other)
72-
return CompatibilityFiles.ChildPath(self._reader, other)
73+
return CompatibilityFiles.OrphanPath(*descendants)
74+
first, *rest = descendants
75+
child = CompatibilityFiles.ChildPath(self._reader, first)
76+
return child.joinpath(*rest)
7377

7478
@property
7579
def name(self):
@@ -97,8 +101,10 @@ def is_file(self):
97101
def is_dir(self):
98102
return not self.is_file()
99103

100-
def joinpath(self, other):
101-
return CompatibilityFiles.OrphanPath(self.name, other)
104+
def joinpath(self, *descendants):
105+
if not descendants:
106+
return self
107+
return CompatibilityFiles.OrphanPath(self.name, *descendants)
102108

103109
@property
104110
def name(self):
@@ -128,8 +134,10 @@ def is_file(self):
128134

129135
is_dir = is_file
130136

131-
def joinpath(self, other):
132-
return CompatibilityFiles.OrphanPath(*self._path, other)
137+
def joinpath(self, *descendants):
138+
if not descendants:
139+
return self
140+
return CompatibilityFiles.OrphanPath(*self._path, *descendants)
133141

134142
@property
135143
def name(self):

Lib/test/test_importlib/resources/test_compatibilty_files.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
wrap_spec,
77
)
88

9+
from test.support import warnings_helper
10+
911
from . import util
1012

1113

@@ -79,6 +81,37 @@ def test_orphan_path_invalid(self):
7981
with self.assertRaises(ValueError):
8082
CompatibilityFiles.OrphanPath()
8183

84+
def test_spec_path_joinpath_no_descendants(self):
85+
files = self.files
86+
assert files.joinpath() is files
87+
88+
def test_child_path_joinpath_no_descendants(self):
89+
child = self.files / 'a'
90+
assert child.joinpath() is child
91+
92+
def test_orphan_path_joinpath_no_descendants(self):
93+
orphan = self.files / 'a' / 'b'
94+
assert orphan.joinpath() is orphan
95+
96+
def test_joinpath_multiple_descendants(self):
97+
# Several descendants at once give the same result as applying them
98+
# one at a time, as documented for Traversable.joinpath().
99+
files = self.files
100+
for path, expected in (
101+
(files.joinpath('a', 'b'), files / 'a' / 'b'),
102+
((files / 'a').joinpath('b', 'c'), files / 'a' / 'b' / 'c'),
103+
((files / 'a' / 'b').joinpath('c', 'd'), files / 'a' / 'b' / 'c' / 'd'),
104+
):
105+
assert isinstance(path, CompatibilityFiles.OrphanPath)
106+
assert path._path == expected._path
107+
108+
def test_functional_api_without_path_names(self):
109+
# gh-127337: these call joinpath() with no descendants at all.
110+
package = self.package
111+
with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)):
112+
assert sorted(resources.contents(package)) == ['a', 'b', 'c']
113+
assert not resources.is_resource(package)
114+
82115
def test_wrap_spec(self):
83116
spec = wrap_spec(self.package)
84117
assert isinstance(spec.loader.get_resource_reader(None), CompatibilityFiles)
@@ -95,3 +128,14 @@ def files(self):
95128

96129
def test_spec_path_joinpath(self):
97130
assert isinstance(self.files / 'a', CompatibilityFiles.OrphanPath)
131+
132+
def test_spec_path_joinpath_no_descendants(self):
133+
# Returns self rather than an OrphanPath with no parts, which would
134+
# be rejected by OrphanPath.__init__().
135+
files = self.files
136+
assert files.joinpath() is files
137+
138+
def test_spec_path_joinpath_multiple_descendants(self):
139+
path = self.files.joinpath('a', 'b')
140+
assert isinstance(path, CompatibilityFiles.OrphanPath)
141+
assert path._path == (self.files / 'a' / 'b')._path
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Fix a regression in :mod:`importlib.resources` where functions such as
2+
:func:`~importlib.resources.is_resource` and
3+
:func:`~importlib.resources.contents` raised :exc:`TypeError` for a package
4+
whose loader provides only a legacy
5+
:class:`~importlib.resources.abc.ResourceReader`. The internal
6+
``CompatibilityFiles`` traversables wrapping such a reader now accept the
7+
zero-or-more argument ``joinpath()`` signature specified by
8+
:meth:`importlib.resources.abc.Traversable.joinpath`.
9+
Patch by pikammmmm.

0 commit comments

Comments
 (0)