Skip to content

Commit 1cd46d5

Browse files
committed
Fixes dpath-maintainers#30: Allow usage of lists as paths
1 parent 7fa15a2 commit 1cd46d5

9 files changed

Lines changed: 165 additions & 32 deletions

File tree

README.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,19 @@ setting a library-wide dpath option:
408408
Again, by default, this behavior is OFF, and empty string keys will
409409
result in ``dpath.exceptions.InvalidKeyName`` being thrown.
410410

411+
Separator got you down? Use lists as paths
412+
==========================================
413+
414+
The default behavior in dpath is to assume that the path given is a string, which must be tokenized by splitting at the separator to yield a distinct set of path components against which dictionary keys can be individually glob tested. However, this presents a problem when you want to use paths that have a separator in their name; the tokenizer cannot properly understand what you mean by '/a/b/c' if it is possible for '/' to exist as a valid character in a key name.
415+
416+
To get around this, you can sidestep the whole "filesystem path" style, and abandon the separator entirely, by using lists as paths. All of the methods in dpath.util.* support the use of a list instead of a string as a path. So for example:
417+
418+
.. code-block: python
419+
420+
>>> x = { 'a': {'b/c': 0}}
421+
>>> dpath.util.get(['a', 'b/c'])
422+
0
423+
411424
dpath.path : The Undocumented Backend
412425
=====================================
413426

dpath/path.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,22 @@ def paths_only(path):
4242
l.append(p[0])
4343
return l
4444

45-
def validate(path, separator="/", regex=None):
45+
def validate(path, regex=None):
4646
"""
4747
Validate that all the keys in the given list of path components are valid, given that they do not contain the separator, and match any optional regex given.
4848
"""
4949
validated = []
5050
for elem in path:
5151
key = elem[0]
5252
strkey = str(key)
53-
if (separator and (separator in strkey)):
54-
raise dpath.exceptions.InvalidKeyName("{0} at {1} contains the separator {2}"
55-
"".format(strkey,
56-
separator.join(validated),
57-
separator))
58-
elif (regex and (not regex.findall(strkey))):
53+
if (regex and (not regex.findall(strkey))):
5954
raise dpath.exceptions.InvalidKeyName("{} at {} does not match the expression {}"
6055
"".format(strkey,
61-
separator.join(validated),
56+
validated,
6257
regex.pattern))
6358
validated.append(strkey)
6459

65-
def paths(obj, dirs=True, leaves=True, path=[], skip=False, separator="/"):
60+
def paths(obj, dirs=True, leaves=True, path=[], skip=False):
6661
"""Yield all paths of the object.
6762
6863
Arguments:
@@ -94,17 +89,17 @@ def paths(obj, dirs=True, leaves=True, path=[], skip=False, separator="/"):
9489
elif (skip and k[0] == '+'):
9590
continue
9691
newpath = path + [[k, v.__class__]]
97-
validate(newpath, separator=separator)
92+
validate(newpath)
9893
if dirs:
9994
yield newpath
100-
for child in paths(v, dirs, leaves, newpath, skip, separator=separator):
95+
for child in paths(v, dirs, leaves, newpath, skip):
10196
yield child
10297
elif isinstance(obj, (list, tuple)):
10398
for (i, v) in enumerate(obj):
10499
newpath = path + [[i, v.__class__]]
105100
if dirs:
106101
yield newpath
107-
for child in paths(obj[i], dirs, leaves, newpath, skip, separator=separator):
102+
for child in paths(obj[i], dirs, leaves, newpath, skip):
108103
yield child
109104
elif leaves:
110105
yield path + [[obj, obj.__class__]]
@@ -150,7 +145,7 @@ def match(path, glob):
150145
def is_glob(string):
151146
return any([c in string for c in '*?[]!'])
152147

153-
def set(obj, path, value, create_missing=True, separator="/", afilter=None):
148+
def set(obj, path, value, create_missing=True, afilter=None):
154149
"""Set the value of the given path in the object. Path
155150
must be a list of specific path elements, not a glob.
156151
You can use dpath.util.set for globs, but the paths must
@@ -208,7 +203,7 @@ def _assigner_list(obj, elem, value):
208203
if not str(elem_value).isdigit():
209204
raise TypeError("Can only create integer indexes in lists, "
210205
"not {}, in {}".format(type(obj),
211-
separator.join(traversed)
206+
traversed
212207
)
213208
)
214209
tester = _presence_test_list
@@ -217,15 +212,15 @@ def _assigner_list(obj, elem, value):
217212
assigner = _assigner_list
218213
else:
219214
raise TypeError("Unable to path into elements of type {} "
220-
"at {}".format(obj, separator.join(traversed)))
215+
"at {}".format(obj, traversed))
221216

222217
if (not tester(obj, elem)) and (create_missing):
223218
creator(obj, elem)
224219
elif (not tester(obj, elem)):
225220
raise dpath.exceptions.PathNotFound(
226221
"{} does not exist in {}".format(
227222
elem,
228-
separator.join(traversed)
223+
traversed
229224
)
230225
)
231226
traversed.append(elem_value)

dpath/util.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
11
import dpath.path
22
import dpath.exceptions
3+
import traceback
34

45
MERGE_REPLACE=(1 << 1)
56
MERGE_ADDITIVE=(1 << 2)
67
MERGE_TYPESAFE=(1 << 3)
78

9+
def __safe_path__(path, separator):
10+
"""
11+
Given a path and separator, return a list of path components. If path
12+
is already a list, return it.
13+
14+
Note that a string path with the separator at index[0] will have the
15+
separator stripped off. If you pass a list path, the separator is
16+
ignored, and is assumed to be part of each key glob. It will not be
17+
stripped.
18+
"""
19+
if issubclass(path.__class__, (list)):
20+
return path
21+
path = path.lstrip(separator).split(separator)
22+
validated = []
23+
for elem in path:
24+
key = elem[0]
25+
strkey = str(key)
26+
if (separator and (separator in strkey)):
27+
raise dpath.exceptions.InvalidKeyName("{0} at {1} contains the separator {2}"
28+
"".format(strkey,
29+
separator.join(validated),
30+
separator))
31+
validated.append(strkey)
32+
return path
33+
834
def new(obj, path, value, separator="/"):
935
"""
1036
Set the element at the terminus of path to value, and create
@@ -15,7 +41,8 @@ def new(obj, path, value, separator="/"):
1541
characters in it, they will become part of the resulting
1642
keys
1743
"""
18-
pathobj = dpath.path.path_types(obj, path.lstrip(separator).split(separator))
44+
pathlist = __safe_path__(path, separator)
45+
pathobj = dpath.path.path_types(obj, pathlist)
1946
return dpath.path.set(obj, pathobj, value, create_missing=True)
2047

2148
def delete(obj, glob, separator="/", afilter=None):
@@ -27,7 +54,8 @@ def delete(obj, glob, separator="/", afilter=None):
2754
"""
2855
deleted = 0
2956
paths = []
30-
for path in _inner_search(obj, glob.lstrip(separator).split(separator), separator):
57+
globlist = __safe_path__(glob, separator)
58+
for path in _inner_search(obj, globlist, separator):
3159
# These are yielded back, don't mess up the dict.
3260
paths.append(path)
3361

@@ -55,7 +83,8 @@ def set(obj, glob, value, separator="/", afilter=None):
5583
to the given value. Returns the number of elements changed.
5684
"""
5785
changed = 0
58-
for path in _inner_search(obj, glob.lstrip(separator).split(separator), separator):
86+
globlist = __safe_path__(glob, separator)
87+
for path in _inner_search(obj, globlist, separator):
5988
changed += 1
6089
dpath.path.set(obj, path, value, create_missing=False, afilter=afilter)
6190
return changed
@@ -98,7 +127,8 @@ def search(obj, glob, yielded=False, separator="/", afilter=None, dirs = True):
98127

99128
def _search_view(obj, glob, separator, afilter, dirs):
100129
view = {}
101-
for path in _inner_search(obj, glob.lstrip(separator).split(separator), separator, dirs=dirs):
130+
globlist = __safe_path__(glob, separator)
131+
for path in _inner_search(obj, globlist, separator, dirs=dirs):
102132
try:
103133
val = dpath.path.get(obj, path, afilter=afilter, view=True)
104134
merge(view, val)
@@ -107,7 +137,8 @@ def _search_view(obj, glob, separator, afilter, dirs):
107137
return view
108138

109139
def _search_yielded(obj, glob, separator, afilter, dirs):
110-
for path in _inner_search(obj, glob.lstrip(separator).split(separator), separator, dirs=dirs):
140+
globlist = __safe_path__(glob, separator)
141+
for path in _inner_search(obj, globlist, separator, dirs=dirs):
111142
try:
112143
val = dpath.path.get(obj, path, view=False, afilter=afilter)
113144
yield (separator.join(map(str, dpath.path.paths_only(path))), val)
@@ -122,7 +153,7 @@ def _search_yielded(obj, glob, separator, afilter, dirs):
122153

123154
def _inner_search(obj, glob, separator, dirs=True, leaves=False):
124155
"""Search the object paths that match the glob."""
125-
for path in dpath.path.paths(obj, dirs, leaves, skip=True, separator = separator):
156+
for path in dpath.path.paths(obj, dirs, leaves, skip=True):
126157
if dpath.path.match(path, glob):
127158
yield path
128159

tests/test_path_get.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,3 @@ def test_path_get_list_of_dicts():
1616
assert(isinstance(res['a']['b'], list))
1717
assert(len(res['a']['b']) == 1)
1818
assert(res['a']['b'][0][0] == 0)
19-

tests/test_path_paths.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,6 @@
44
import dpath.exceptions
55
import dpath.options
66

7-
@raises(dpath.exceptions.InvalidKeyName)
8-
def test_path_paths_invalid_keyname():
9-
tdict = {
10-
"I/contain/the/separator": 0
11-
}
12-
for x in dpath.path.paths(tdict):
13-
pass
14-
157
@raises(dpath.exceptions.InvalidKeyName)
168
def test_path_paths_empty_key_disallowed():
179
tdict = {

tests/test_util_get_values.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def test_get_explicit_single():
1616
}
1717
}
1818
assert(dpath.util.get(ehash, '/a/b/c/f') == 2)
19+
assert(dpath.util.get(ehash, ['a', 'b', 'c', 'f']) == 2)
1920

2021
def test_get_glob_single():
2122
ehash = {
@@ -30,6 +31,7 @@ def test_get_glob_single():
3031
}
3132
}
3233
assert(dpath.util.get(ehash, '/a/b/*/f') == 2)
34+
assert(dpath.util.get(ehash, ['a', 'b', '*', 'f']) == 2)
3335

3436
def test_get_glob_multiple():
3537
ehash = {
@@ -45,10 +47,12 @@ def test_get_glob_multiple():
4547
}
4648
}
4749
assert_raises(ValueError, dpath.util.get, ehash, '/a/b/*/d')
50+
assert_raises(ValueError, dpath.util.get, ehash, ['a', 'b', '*', 'd'])
4851

4952
def test_get_absent():
5053
ehash = {}
5154
assert_raises(KeyError, dpath.util.get, ehash, '/a/b/c/d/f')
55+
assert_raises(KeyError, dpath.util.get, ehash, ['a', 'b', 'c', 'd', 'f'])
5256

5357
def test_values():
5458
ehash = {
@@ -68,10 +72,18 @@ def test_values():
6872
assert(1 in ret)
6973
assert(2 in ret)
7074

75+
ret = dpath.util.values(ehash, ['a', 'b', 'c', '*'])
76+
assert(isinstance(ret, list))
77+
assert(0 in ret)
78+
assert(1 in ret)
79+
assert(2 in ret)
80+
7181
@mock.patch('dpath.util.search')
7282
def test_values_passes_through(searchfunc):
7383
searchfunc.return_value = []
7484
def y():
7585
pass
7686
dpath.util.values({}, '/a/b', ':', y, False)
7787
searchfunc.assert_called_with({}, '/a/b', dirs=False, yielded=True, separator=':', afilter=y)
88+
dpath.util.values({}, ['a', 'b'], ':', y, False)
89+
searchfunc.assert_called_with({}, ['a', 'b'], dirs=False, yielded=True, separator=':', afilter=y)

tests/test_util_new.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ def test_set_new_separator():
88
}
99
dpath.util.new(dict, ';a;b', 1, separator=";")
1010
assert(dict['a']['b'] == 1)
11+
dpath.util.new(dict, ['a', 'b'], 1, separator=";")
12+
assert(dict['a']['b'] == 1)
1113

1214
def test_set_new_dict():
1315
dict = {
@@ -16,6 +18,8 @@ def test_set_new_dict():
1618
}
1719
dpath.util.new(dict, '/a/b', 1)
1820
assert(dict['a']['b'] == 1)
21+
dpath.util.new(dict, ['a', 'b'], 1)
22+
assert(dict['a']['b'] == 1)
1923

2024
def test_set_new_list():
2125
dict = {
@@ -25,3 +29,17 @@ def test_set_new_list():
2529
dpath.util.new(dict, '/a/1', 1)
2630
assert(dict['a'][1] == 1)
2731
assert(dict['a'][0] == None)
32+
dpath.util.new(dict, ['a', '1'], 1)
33+
assert(dict['a'][1] == 1)
34+
assert(dict['a'][0] == None)
35+
36+
def test_set_new_list_path_with_separator():
37+
# This test kills many birds with one stone, forgive me
38+
dict = {
39+
"a": {}
40+
}
41+
dpath.util.new(dict, ['a', 'b/c/d', 0], 1)
42+
assert(len(dict['a']) == 1)
43+
assert(len(dict['a']['b/c/d']) == 1)
44+
assert(dict['a']['b/c/d'][0] == 1)
45+

0 commit comments

Comments
 (0)