Skip to content

Commit 5d9fb42

Browse files
authored
Merge pull request #278 from python/bugfix/multiplexed-descendants
Support multiplexed descendants in MultiplexedPath.
2 parents ff16bd3 + 289aadb commit 5d9fb42

File tree

5 files changed

+79
-36
lines changed

5 files changed

+79
-36
lines changed

CHANGES.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
v5.11.0
2+
=======
3+
4+
* #265: ``MultiplexedPath`` now honors multiple subdirectories
5+
in ``iterdir`` and ``joinpath``.
6+
17
v5.10.3
28
=======
39

importlib_resources/_itertools.py

+36-33
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,38 @@
1-
from itertools import filterfalse
1+
# from more_itertools 9.0
2+
def only(iterable, default=None, too_long=None):
3+
"""If *iterable* has only one item, return it.
4+
If it has zero items, return *default*.
5+
If it has more than one item, raise the exception given by *too_long*,
6+
which is ``ValueError`` by default.
7+
>>> only([], default='missing')
8+
'missing'
9+
>>> only([1])
10+
1
11+
>>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL
12+
Traceback (most recent call last):
13+
...
14+
ValueError: Expected exactly one item in iterable, but got 1, 2,
15+
and perhaps more.'
16+
>>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL
17+
Traceback (most recent call last):
18+
...
19+
TypeError
20+
Note that :func:`only` attempts to advance *iterable* twice to ensure there
21+
is only one item. See :func:`spy` or :func:`peekable` to check
22+
iterable contents less destructively.
23+
"""
24+
it = iter(iterable)
25+
first_value = next(it, default)
226

3-
from typing import (
4-
Callable,
5-
Iterable,
6-
Iterator,
7-
Optional,
8-
Set,
9-
TypeVar,
10-
Union,
11-
)
12-
13-
# Type and type variable definitions
14-
_T = TypeVar('_T')
15-
_U = TypeVar('_U')
16-
17-
18-
def unique_everseen(
19-
iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = None
20-
) -> Iterator[_T]:
21-
"List unique elements, preserving order. Remember all elements ever seen."
22-
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
23-
# unique_everseen('ABBCcAD', str.lower) --> A B C D
24-
seen: Set[Union[_T, _U]] = set()
25-
seen_add = seen.add
26-
if key is None:
27-
for element in filterfalse(seen.__contains__, iterable):
28-
seen_add(element)
29-
yield element
27+
try:
28+
second_value = next(it)
29+
except StopIteration:
30+
pass
3031
else:
31-
for element in iterable:
32-
k = key(element)
33-
if k not in seen:
34-
seen_add(k)
35-
yield element
32+
msg = (
33+
'Expected exactly one item in iterable, but got {!r}, {!r}, '
34+
'and perhaps more.'.format(first_value, second_value)
35+
)
36+
raise too_long or ValueError(msg)
37+
38+
return first_value

importlib_resources/readers.py

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import collections
2+
import itertools
23
import pathlib
34
import operator
45

56
from . import abc
67

7-
from ._itertools import unique_everseen
8+
from ._itertools import only
89
from ._compat import ZipPath
910

1011

@@ -69,8 +70,10 @@ def __init__(self, *paths):
6970
raise NotADirectoryError('MultiplexedPath only supports directories')
7071

7172
def iterdir(self):
72-
files = (file for path in self._paths for file in path.iterdir())
73-
return unique_everseen(files, key=operator.attrgetter('name'))
73+
children = (child for path in self._paths for child in path.iterdir())
74+
by_name = operator.attrgetter('name')
75+
groups = itertools.groupby(sorted(children, key=by_name), key=by_name)
76+
return map(self._follow, (locs for name, locs in groups))
7477

7578
def read_bytes(self):
7679
raise FileNotFoundError(f'{self} is not a file')
@@ -92,6 +95,25 @@ def joinpath(self, *descendants):
9295
# Just return something that will not exist.
9396
return self._paths[0].joinpath(*descendants)
9497

98+
@classmethod
99+
def _follow(cls, children):
100+
"""
101+
Construct a MultiplexedPath if needed.
102+
103+
If children contains a sole element, return it.
104+
Otherwise, return a MultiplexedPath of the items.
105+
Unless one of the items is not a Directory, then return the first.
106+
"""
107+
subdirs, one_dir, one_file = itertools.tee(children, 3)
108+
109+
try:
110+
return only(one_dir)
111+
except ValueError:
112+
try:
113+
return cls(*subdirs)
114+
except NotADirectoryError:
115+
return next(one_file)
116+
95117
def open(self, *args, **kwargs):
96118
raise FileNotFoundError(f'{self} is not a file')
97119

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a resource

importlib_resources/tests/test_reader.py

+11
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ def test_join_path_compound(self):
8181
path = MultiplexedPath(self.folder)
8282
assert not path.joinpath('imaginary/foo.py').exists()
8383

84+
def test_join_path_common_subdir(self):
85+
prefix = os.path.abspath(os.path.join(__file__, '..'))
86+
data01 = os.path.join(prefix, 'data01')
87+
data02 = os.path.join(prefix, 'data02')
88+
path = MultiplexedPath(data01, data02)
89+
self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath)
90+
self.assertEqual(
91+
str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :],
92+
os.path.join('data02', 'subdirectory', 'subsubdir'),
93+
)
94+
8495
def test_repr(self):
8596
self.assertEqual(
8697
repr(MultiplexedPath(self.folder)),

0 commit comments

Comments
 (0)