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 ====== diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 5e38ba61..efa86ac6 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 @@ -50,6 +52,10 @@ def contents(self) -> Iterable[str]: raise FileNotFoundError +class TraversalError(Exception): + pass + + @runtime_checkable class Traversable(Protocol): """ @@ -92,7 +98,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 +106,22 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": and each may contain multiple levels separated by ``posixpath.sep`` (``/``). """ + if not descendants: + return self + names = itertools.chain.from_iterable( + path.parts for path in map(pathlib.PurePosixPath, descendants) + ) + target = next(names) + matches = ( + traversable for traversable in self.iterdir() if traversable.name == target + ) + try: + 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": """ diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index f1190ca4..19450f46 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: # pragma: nocover + 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') diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index d0fbf237..b85e4694 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -99,20 +99,6 @@ 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) - target = next(names) - return next( - traversable for traversable in self.iterdir() if traversable.name == target - ).joinpath(*names) - class TraversableReader(TraversableResources, SimpleReader): """ 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(