Skip to content

Commit acfc5c9

Browse files
authoredJun 16, 2022
Merge pull request python#250 from python/bugfix/MultiplexedPath-descendants
Re-use joinpath logic in Traversable and MultiplexedPath
2 parents 467d4e5 + 1def9a2 commit acfc5c9

File tree

5 files changed

+40
-24
lines changed

5 files changed

+40
-24
lines changed
 

‎CHANGES.rst

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
v5.8.0
2+
======
3+
4+
* #250: Now ``Traversable.joinpath`` provides a concrete
5+
implementation, replacing the implementation in ``.simple``
6+
and converging with the behavior in ``MultiplexedPath``.
7+
18
v5.7.1
29
======
310

‎importlib_resources/abc.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import abc
22
import io
3+
import itertools
4+
import pathlib
35
from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional
46

57
from ._compat import runtime_checkable, Protocol, StrPath
@@ -50,6 +52,10 @@ def contents(self) -> Iterable[str]:
5052
raise FileNotFoundError
5153

5254

55+
class TraversalError(Exception):
56+
pass
57+
58+
5359
@runtime_checkable
5460
class Traversable(Protocol):
5561
"""
@@ -92,7 +98,6 @@ def is_file(self) -> bool:
9298
Return True if self is a file
9399
"""
94100

95-
@abc.abstractmethod
96101
def joinpath(self, *descendants: StrPath) -> "Traversable":
97102
"""
98103
Return Traversable resolved with any descendants applied.
@@ -101,6 +106,22 @@ def joinpath(self, *descendants: StrPath) -> "Traversable":
101106
and each may contain multiple levels separated by
102107
``posixpath.sep`` (``/``).
103108
"""
109+
if not descendants:
110+
return self
111+
names = itertools.chain.from_iterable(
112+
path.parts for path in map(pathlib.PurePosixPath, descendants)
113+
)
114+
target = next(names)
115+
matches = (
116+
traversable for traversable in self.iterdir() if traversable.name == target
117+
)
118+
try:
119+
match = next(matches)
120+
except StopIteration:
121+
raise TraversalError(
122+
"Target not found during traversal.", target, list(names)
123+
)
124+
return match.joinpath(*names)
104125

105126
def __truediv__(self, child: StrPath) -> "Traversable":
106127
"""

‎importlib_resources/readers.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,16 @@ def is_dir(self):
8282
def is_file(self):
8383
return False
8484

85-
def joinpath(self, child):
86-
# first try to find child in current paths
87-
for file in self.iterdir():
88-
if file.name == child:
89-
return file
90-
# if it does not exist, construct it with the first path
91-
return self._paths[0] / child
92-
93-
__truediv__ = joinpath
85+
def joinpath(self, *descendants):
86+
try:
87+
return super().joinpath(*descendants)
88+
except abc.TraversalError as exc:
89+
# One of the paths didn't resolve.
90+
msg, target, names = exc.args
91+
if names: # pragma: nocover
92+
raise
93+
# It was the last; construct result with the first path.
94+
return self._paths[0].joinpath(target)
9495

9596
def open(self, *args, **kwargs):
9697
raise FileNotFoundError(f'{self} is not a file')

‎importlib_resources/simple.py

-14
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,6 @@ def iterdir(self):
9999
def open(self, *args, **kwargs):
100100
raise IsADirectoryError()
101101

102-
@staticmethod
103-
def _flatten(compound_names):
104-
for name in compound_names:
105-
yield from name.split('/')
106-
107-
def joinpath(self, *descendants):
108-
if not descendants:
109-
return self
110-
names = self._flatten(descendants)
111-
target = next(names)
112-
return next(
113-
traversable for traversable in self.iterdir() if traversable.name == target
114-
).joinpath(*names)
115-
116102

117103
class TraversableReader(TraversableResources, SimpleReader):
118104
"""

‎importlib_resources/tests/test_reader.py

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def test_join_path(self):
7575
str(path.joinpath('imaginary'))[len(prefix) + 1 :],
7676
os.path.join('namespacedata01', 'imaginary'),
7777
)
78+
self.assertEqual(path.joinpath(), path)
7879

7980
def test_repr(self):
8081
self.assertEqual(

0 commit comments

Comments
 (0)