Skip to content

Commit 45b3488

Browse files
authored
Merge pull request #172 from dpath-maintainers/feature/166-negative-list-indexing
Feature/166 negative list indexing
2 parents ea64635 + eb1b2e2 commit 45b3488

File tree

4 files changed

+61
-9
lines changed

4 files changed

+61
-9
lines changed

dpath/segments.py

Lines changed: 22 additions & 8 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, SymmetricInt
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+
# Convert all list indices to object so negative indexes are supported.
26+
indices = map(lambda i: SymmetricInt(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,8 @@ 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+
i = zip(segments, ss_glob)
221+
for s, g in i:
218222
# Match the stars we added to the glob to the type of the
219223
# segment itself.
220224
if g is STAR:
@@ -223,10 +227,20 @@ def match(segments: Sequence[PathSegment], glob: Sequence[str]):
223227
else:
224228
g = '*'
225229

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.
229230
try:
231+
# If search path segment (s) is an int then assume currently evaluated index (g) might be a sequence
232+
# index as well. Try converting it to an int.
233+
if isinstance(s, int) and s == int(g):
234+
continue
235+
except:
236+
# Will reach this point if g can't be converted to an int (e.g. when g is a RegEx pattern).
237+
# In this case convert s to a str so fnmatch can work on it.
238+
s = str(s)
239+
240+
try:
241+
# Let's see if the glob matches. We will turn any kind of
242+
# exception while attempting to match into a False for the
243+
# match.
230244
if not fnmatchcase(s, g):
231245
return False
232246
except:
@@ -391,7 +405,7 @@ def foldm(obj, f, acc):
391405
return acc
392406

393407

394-
def view(obj, glob):
408+
def view(obj: MutableMapping, glob: Glob):
395409
"""
396410
Return a view of the object where the glob matches. A view retains
397411
the same form as the obj, but is limited to only the paths that

dpath/types.py

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

44

5+
class SymmetricInt(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: int, max_value: int, *args, **kwargs):
9+
if value >= max_value:
10+
raise TypeError(
11+
f"Tried to initiate a {cls.__name__} 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+
if not isinstance(other, int):
22+
return False
23+
24+
if other >= self.max_value or other <= -self.max_value:
25+
return False
26+
27+
return int(self) == (self.max_value + other) % self.max_value
28+
29+
def __repr__(self):
30+
return f"<{self.__class__.__name__} {int(self)}%{self.max_value}>"
31+
32+
def __str__(self):
33+
return str(int(self))
34+
35+
536
class MergeType(IntFlag):
637
ADDITIVE = auto()
738
"""List objects are combined onto one long list (NOT a set). This is the default flag."""

dpath/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "2.1.1"
1+
VERSION = "2.1.2"

tests/test_search.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,10 @@ 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_negative_index():
242+
d = {'a': {'b': [1, 2, 3]}}
243+
res = dpath.search(d, 'a/b/-1')
244+
245+
assert res == dpath.search(d, "a/b/2")

0 commit comments

Comments
 (0)