Skip to content

Re-use joinpath logic in Traversable and MultiplexedPath #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 16, 2022
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -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
======

Expand Down
23 changes: 22 additions & 1 deletion importlib_resources/abc.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -50,6 +52,10 @@ def contents(self) -> Iterable[str]:
raise FileNotFoundError


class TraversalError(Exception):
pass


@runtime_checkable
class Traversable(Protocol):
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only way for this to happen is no arguments being passed, no? Wouldn't checking for that directly make the code easier to read?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's correct. A StopIteration would occur if none of the traversable.name == target in the matches above (including if self.iterdir() yields no results). It's trapping StopIteration on next(matches), not *names. *names can be empty. Maybe only the exception should be trapped for next(matches).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, that makes sense.

"Target not found during traversal.", target, list(names)
)
return match.joinpath(*names)

def __truediv__(self, child: StrPath) -> "Traversable":
"""
Expand Down
19 changes: 10 additions & 9 deletions importlib_resources/readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
14 changes: 0 additions & 14 deletions importlib_resources/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
1 change: 1 addition & 0 deletions importlib_resources/tests/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down