From e39642364b5fff461b719e435d69374568ebcd3b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 18:41:10 -0400 Subject: [PATCH 1/9] Inline the flattening of descendants. --- importlib_resources/simple.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index d0fbf237..8c5d88cf 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -99,15 +99,14 @@ def iterdir(self): def open(self, *args, **kwargs): raise IsADirectoryError() - @staticmethod - def _flatten(compound_names): - for name in compound_names: - yield from name.split('/') - def joinpath(self, *descendants): if not descendants: return self - names = self._flatten(descendants) + names = ( + name + for compound in descendants + for name in compound.split('/') + ) target = next(names) return next( traversable for traversable in self.iterdir() if traversable.name == target From df36958ed2ce587ad583f8d8d65b4670665e6050 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 18:46:38 -0400 Subject: [PATCH 2/9] Move ResourceContainer.joinpath to Traversable.joinpath, providing a concrete implementation. --- importlib_resources/abc.py | 8 +++++++- importlib_resources/simple.py | 13 ------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 5e38ba61..ac31fb96 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -92,7 +92,6 @@ def is_file(self) -> bool: Return True if self is a file """ - @abc.abstractmethod def joinpath(self, *descendants: StrPath) -> "Traversable": """ Return Traversable resolved with any descendants applied. @@ -101,6 +100,13 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": and each may contain multiple levels separated by ``posixpath.sep`` (``/``). """ + if not descendants: + return self + names = (name for compound in descendants for name in compound.split('/')) + target = next(names) + return next( + traversable for traversable in self.iterdir() if traversable.name == target + ).joinpath(*names) def __truediv__(self, child: StrPath) -> "Traversable": """ diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index 8c5d88cf..b85e4694 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -99,19 +99,6 @@ def iterdir(self): def open(self, *args, **kwargs): raise IsADirectoryError() - def joinpath(self, *descendants): - if not descendants: - return self - names = ( - name - for compound in descendants - for name in compound.split('/') - ) - target = next(names) - return next( - traversable for traversable in self.iterdir() if traversable.name == target - ).joinpath(*names) - class TraversableReader(TraversableResources, SimpleReader): """ From b923ce204247f6f0fd277243e08faf1238593bf9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 18:55:14 -0400 Subject: [PATCH 3/9] Refactor to converge descendants to a single type and rely on Path.parts for getting the parts. --- importlib_resources/abc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index ac31fb96..fc316a82 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,5 +1,7 @@ import abc import io +import itertools +import pathlib from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional from ._compat import runtime_checkable, Protocol, StrPath @@ -102,7 +104,9 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": """ if not descendants: return self - names = (name for compound in descendants for name in compound.split('/')) + names = itertools.chain.from_iterable( + path.parts for path in map(pathlib.PurePosixPath, descendants) + ) target = next(names) return next( traversable for traversable in self.iterdir() if traversable.name == target From c3cefa6100eeef0e40ca570d74585fe9d02d1e53 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 19:19:25 -0400 Subject: [PATCH 4/9] Replace StopIteration with a TraversalError for use in capturing a failed traversal. --- importlib_resources/abc.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index fc316a82..7ab38b66 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -52,6 +52,10 @@ def contents(self) -> Iterable[str]: raise FileNotFoundError +class TraversalError(Exception): + pass + + @runtime_checkable class Traversable(Protocol): """ @@ -108,9 +112,15 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": path.parts for path in map(pathlib.PurePosixPath, descendants) ) target = next(names) - return next( + matches = ( traversable for traversable in self.iterdir() if traversable.name == target - ).joinpath(*names) + ) + try: + return next(matches).joinpath(*names) + except StopIteration: + raise TraversalError( + "Target not found during traversal.", target, list(names) + ) def __truediv__(self, child: StrPath) -> "Traversable": """ From 64403234fdbc3b9df60a57ff1ee76778a03a83b4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 19:22:25 -0400 Subject: [PATCH 5/9] In readers.MultiplexedPath, re-use Traversable.joinpath. --- importlib_resources/readers.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index f1190ca4..c59e11ba 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -82,15 +82,16 @@ def is_dir(self): def is_file(self): return False - def joinpath(self, child): - # first try to find child in current paths - for file in self.iterdir(): - if file.name == child: - return file - # if it does not exist, construct it with the first path - return self._paths[0] / child - - __truediv__ = joinpath + def joinpath(self, *descendants): + try: + return super().joinpath(*descendants) + except abc.TraversalError as exc: + # One of the paths didn't resolve. + msg, target, names = exc.args + if names: + raise + # It was the last; construct result with the first path. + return self._paths[0].joinpath(target) def open(self, *args, **kwargs): raise FileNotFoundError(f'{self} is not a file') From 075b6ceab4b926e78721b614b34e4f1c8bb72758 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 20:12:53 -0400 Subject: [PATCH 6/9] Add test for empty joinpath. --- importlib_resources/tests/test_reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 16841a50..37550cad 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -75,6 +75,7 @@ def test_join_path(self): str(path.joinpath('imaginary'))[len(prefix) + 1 :], os.path.join('namespacedata01', 'imaginary'), ) + self.assertEqual(path.joinpath(), path) def test_repr(self): self.assertEqual( From 0140430e80c8adbdfb148510e219051fefa86730 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 20:17:01 -0400 Subject: [PATCH 7/9] Leave this behavior uncovered. --- importlib_resources/readers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index c59e11ba..19450f46 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -88,7 +88,7 @@ def joinpath(self, *descendants): except abc.TraversalError as exc: # One of the paths didn't resolve. msg, target, names = exc.args - if names: + if names: # pragma: nocover raise # It was the last; construct result with the first path. return self._paths[0].joinpath(target) From 643caba9f150fd1a9175560338f63d91dedfd454 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 20:36:10 -0400 Subject: [PATCH 8/9] Separate match resolution from recursive join. --- importlib_resources/abc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 7ab38b66..efa86ac6 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -116,11 +116,12 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": traversable for traversable in self.iterdir() if traversable.name == target ) try: - return next(matches).joinpath(*names) + match = next(matches) except StopIteration: raise TraversalError( "Target not found during traversal.", target, list(names) ) + return match.joinpath(*names) def __truediv__(self, child: StrPath) -> "Traversable": """ From 1def9a2cba956d00fb20b3dd2feb4a8878047a21 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 21:01:28 -0400 Subject: [PATCH 9/9] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1055a2f0..5321b177 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.8.0 +====== + +* #250: Now ``Traversable.joinpath`` provides a concrete + implementation, replacing the implementation in ``.simple`` + and converging with the behavior in ``MultiplexedPath``. + v5.7.1 ======