Skip to content

Commit 4a28d52

Browse files
committed
Support negative indexes
1 parent ea64635 commit 4a28d52

3 files changed

Lines changed: 56 additions & 7 deletions

File tree

dpath/segments.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from dpath import options
66
from dpath.exceptions import InvalidGlob, InvalidKeyName, PathNotFound
7-
from dpath.types import PathSegment, Creator, Hints
7+
from dpath.types import PathSegment, Creator, Hints, Glob, Path, CyclicInt
88

99

1010
def make_walkable(node) -> Iterator[Tuple[PathSegment, Any]]:
@@ -21,7 +21,10 @@ def make_walkable(node) -> Iterator[Tuple[PathSegment, Any]]:
2121
return iter(node.items())
2222
except AttributeError:
2323
try:
24-
return zip(range(len(node)), node)
24+
indices = range(len(node))
25+
# Make all list indices cyclic so negative (wraparound) indexes are supported
26+
indices = map(lambda i: CyclicInt(i, len(node)), indices)
27+
return zip(indices, node)
2528
except TypeError:
2629
# This can happen in cases where the node isn't leaf(node) == True,
2730
# but also isn't actually iterable. Instead of this being an error
@@ -163,7 +166,7 @@ class Star(object):
163166
STAR = Star()
164167

165168

166-
def match(segments: Sequence[PathSegment], glob: Sequence[str]):
169+
def match(segments: Path, glob: Glob):
167170
"""
168171
Return True if the segments match the given glob, otherwise False.
169172
@@ -214,7 +217,9 @@ def match(segments: Sequence[PathSegment], glob: Sequence[str]):
214217
# If we were successful in matching up the lengths, then we can
215218
# compare them using fnmatch.
216219
if path_len == len(ss_glob):
217-
for s, g in zip(map(int_str, segments), map(int_str, ss_glob)):
220+
# TODO: Delete if not needed (previous code) - i = zip(map(int_str, segments), map(int_str, ss_glob))
221+
i = zip(segments, ss_glob)
222+
for s, g in i:
218223
# Match the stars we added to the glob to the type of the
219224
# segment itself.
220225
if g is STAR:
@@ -223,10 +228,15 @@ def match(segments: Sequence[PathSegment], glob: Sequence[str]):
223228
else:
224229
g = '*'
225230

226-
# Let's see if the glob matches. We will turn any kind of
227-
# exception while attempting to match into a False for the
228-
# match.
229231
try:
232+
# If search path segment (s) is an int and the current evaluated index (g) is int-like,
233+
# then g is surely a sequence index. Convert it to int and compare.
234+
if isinstance(s, int) and isinstance(g, str) and (g.count("-") == 0 or g.lstrip("-").isdigit()):
235+
return s == int(g)
236+
237+
# Let's see if the glob matches. We will turn any kind of
238+
# exception while attempting to match into a False for the
239+
# match.
230240
if not fnmatchcase(s, g):
231241
return False
232242
except:

dpath/types.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22
from typing import Union, Any, Callable, Sequence, Tuple, List, Optional, MutableMapping
33

44

5+
class CyclicInt(int):
6+
"""Same as a normal int but mimicks the behavior of list indexes (can be compared to a negative number)"""
7+
8+
def __new__(cls, value, max_value, *args, **kwargs):
9+
if value >= max_value:
10+
raise TypeError(
11+
f"Tried to initiate a CyclicInt with a value ({value}) "
12+
f"greater than the provided max value ({max_value})"
13+
)
14+
15+
obj = super().__new__(cls, value)
16+
obj.max_value = max_value
17+
18+
return obj
19+
20+
def __eq__(self, other):
21+
return int(self) == (self.max_value + other) % self.max_value
22+
23+
def __repr__(self):
24+
return f"<CyclicInt {int(self)}/{self.max_value}>"
25+
26+
527
class MergeType(IntFlag):
628
ADDITIVE = auto()
729
"""List objects are combined onto one long list (NOT a set). This is the default flag."""

tests/test_search.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,20 @@ def test_search_multiple_stars():
236236
assert res['a'][0]['b'][0]['c'] == 1
237237
assert res['a'][0]['b'][1]['c'] == 2
238238
assert res['a'][0]['b'][2]['c'] == 3
239+
240+
241+
def test_search_glob_list():
242+
d = {'a': {'b': []}}
243+
res = dpath.search(d, 'a/b/*')
244+
assert res == {'a': {'b': []}}
245+
246+
d = {'a': {'b': [1, 2, 3]}}
247+
dpath.search(d, 'a/b/*', afilter=lambda x: x > 3 if isinstance(x, int) else True)
248+
assert res == {'a': {'b': []}}
249+
250+
251+
def test_search_negative_index():
252+
d = {'a': {'b': [1, 2, 3]}}
253+
res = dpath.search(d, 'a/b/-1')
254+
255+
assert res == dpath.search(d, "a/b/2")

0 commit comments

Comments
 (0)