diff --git a/jsonpath_ng/jsonpath.py b/jsonpath_ng/jsonpath.py index f4f9d4b..1617330 100644 --- a/jsonpath_ng/jsonpath.py +++ b/jsonpath_ng/jsonpath.py @@ -606,8 +606,8 @@ class Index(JSONPath): NOTE: For the concrete syntax of `[*]`, the abstract syntax is a Slice() with no parameters (equiv to `[:]` """ - def __init__(self, index): - self.index = index + def __init__(self, *indices): + self.indices = indices def find(self, datum): return self._find_base(datum, create=False) @@ -621,10 +621,12 @@ def _find_base(self, datum, create): if datum.value == {}: datum.value = _create_list_key(datum.value) self._pad_value(datum.value) - if datum.value and len(datum.value) > self.index: - return [DatumInContext(datum.value[self.index], path=self, context=datum)] - else: - return [] + rv = [] + for index in self.indices: + # invalid indices do not crash, return [] instead + if datum.value and len(datum.value) > index: + rv += [DatumInContext(datum.value[index], path=Index(index), context=datum)] + return rv def update(self, data, val): return self._update_base(data, val, create=False) @@ -638,31 +640,39 @@ def _update_base(self, data, val, create): data = _create_list_key(data) self._pad_value(data) if hasattr(val, '__call__'): - val.__call__(data[self.index], data, self.index) - elif len(data) > self.index: - data[self.index] = val + for index in self.indices: + val.__call__(data[index], data, index) + else: + if not isinstance(val, list): + val = [val] + # allows somelist[5,1,2] = [some_value, another_value, third_value] + # skip the indices that are too high but the value will be applied to the next index + for index in self.indices: + if len(data) > index: + data[index] = val.pop(0) return data def filter(self, fn, data): - if fn(data[self.index]): - data.pop(self.index) # relies on mutation :( + for index in self.indices: + if fn(data[index]): + data.pop(index) # relies on mutation :( return data def __eq__(self, other): - return isinstance(other, Index) and self.index == other.index + return isinstance(other, Index) and sorted(self.indices) == sorted(other.indices) def __str__(self): - return '[%i]' % self.index + return str(list(self.indices)) def __repr__(self): - return '%s(index=%r)' % (self.__class__.__name__, self.index) + return '%s(indices=%r)' % (self.__class__.__name__, self.indices) def _pad_value(self, value): - if len(value) <= self.index: - pad = self.index - len(value) + 1 + _max = max(self.indices) + if len(value) <= _max: + pad = _max - len(value) + 1 value += [{} for __ in range(pad)] - class Slice(JSONPath): """ JSONPath matching a slice of an array. @@ -709,7 +719,7 @@ def find(self, datum): return [DatumInContext(datum.value[i], path=Index(i), context=datum) for i in xrange(0, len(datum.value))] else: return [DatumInContext(datum.value[i], path=Index(i), context=datum) for i in range(0, len(datum.value))[self.start:self.end:self.step]] - + def update(self, data, val): for datum in self.find(data): datum.path.update(data, val) diff --git a/jsonpath_ng/parser.py b/jsonpath_ng/parser.py index 72333c2..7e9693a 100644 --- a/jsonpath_ng/parser.py +++ b/jsonpath_ng/parser.py @@ -120,7 +120,7 @@ def p_jsonpath_root(self, p): def p_jsonpath_idx(self, p): "jsonpath : '[' idx ']'" - p[0] = p[2] + p[0] = Index(*p[2]) def p_jsonpath_slice(self, p): "jsonpath : '[' slice ']'" @@ -136,7 +136,7 @@ def p_jsonpath_child_fieldbrackets(self, p): def p_jsonpath_child_idxbrackets(self, p): "jsonpath : jsonpath '[' idx ']'" - p[0] = Child(p[1], p[3]) + p[0] = Child(p[1], Index(*p[3])) def p_jsonpath_child_slicebrackets(self, p): "jsonpath : jsonpath '[' slice ']'" @@ -165,15 +165,20 @@ def p_fields_comma(self, p): def p_idx(self, p): "idx : NUMBER" - p[0] = Index(p[1]) + p[0] = [p[1]] + def p_idx_comma(self, p): + "idx : idx ',' idx " + p[0] = p[1] + p[3] + def p_slice_any(self, p): "slice : '*'" p[0] = Slice() def p_slice(self, p): # Currently does not support `step` - "slice : maybe_int ':' maybe_int" - p[0] = Slice(start=p[1], end=p[3]) + """slice : maybe_int ':' maybe_int + | maybe_int ':' maybe_int ':' maybe_int """ + p[0] = Slice(*p[1::2]) def p_maybe_int(self, p): """maybe_int : NUMBER diff --git a/tests/test_create.py b/tests/test_create.py index 579fcd2..cb91289 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -31,6 +31,11 @@ insert_val=42, target={'foo': [{}, 42]}), + Params(string='$.foo[1,3]', + initial_data={}, + insert_val=[42, 51], + target={'foo': [{}, 42, {}, 51]}), + Params(string='$.foo[0].bar', initial_data={}, insert_val=42, @@ -41,6 +46,12 @@ insert_val=42, target={'foo': [{}, {'bar': 42}]}), + # Note that each field will received the full + Params(string='$.foo[1,3].bar', + initial_data={}, + insert_val=[42, 51], + target={'foo': [{}, {'bar': [42, 51]}, {}, {'bar': [42, 51]}]}), + Params(string='$.foo[0][0]', initial_data={}, insert_val=42, @@ -51,6 +62,22 @@ insert_val=42, target={'foo': [{}, [{}, 42]]}), + # But here Note that each index will received one value of + Params(string='$.foo[1,3][1]', + initial_data={}, + insert_val=[42, 51], + target={'foo': [{}, [{}, 42], {}, [{}, 51]]}), + + Params(string='$.foo[1,3][1]', + initial_data={}, + insert_val=[[42, 51], [42, 51]], + target={'foo': [{}, [{}, [42, 51]], {}, [{}, [42, 51]]]}), + + Params(string='$.foo[1,3][0,2]', + initial_data={}, + insert_val=[42, 51, 42, 51], + target={'foo': [{}, [42, {}, 51], {}, [42, {}, 51]]}), + Params(string='foo[0]', initial_data={}, insert_val=42, @@ -61,6 +88,11 @@ insert_val=42, target={'foo': [{}, 42]}), + Params(string='foo[1,3]', + initial_data={}, + insert_val=[42, 51], + target={'foo': [{}, 42, {}, 51]}), + Params(string='foo', initial_data={}, insert_val=42, @@ -77,6 +109,11 @@ insert_val=42, target=[{}, 42]), + Params(string='[1,3]', + initial_data=[], + insert_val=[42, 51], + target=[{}, 42, {}, 51]), + # Converts initial data to a list if necessary Params(string='[0]', initial_data={}, @@ -88,6 +125,11 @@ insert_val=42, target=[{}, 42]), + Params(string='[1,3]', + initial_data={}, + insert_val=[42, 51], + target=[{}, 42, {}, 51]), + Params(string='foo[?bar="baz"].qux', initial_data={'foo': [ {'bar': 'baz'}, @@ -127,6 +169,14 @@ def test_update_or_create(string, initial_data, insert_val, target): target={'foo': 42}), # raises TypeError + # more indices than values to insert + Params(string='$.foo[1,2,3]', + initial_data={}, + insert_val=[42, 51], + target={'foo': [{}, 42, 51, {}]}), + # raises IndexError + + ]) @pytest.mark.xfail def test_unsupported_classes(string, initial_data, insert_val, target): diff --git a/tests/test_examples.py b/tests/test_examples.py index 1b08123..b9e3ad6 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -32,8 +32,9 @@ Child(Descendants(Root(), Fields('book')), Slice(start=-1))), # The first two books - # ("$..book[0,1]", # Not implemented - # Child(Descendants(Root(), Fields('book')), Slice(end=2))), + ("$..book[0,1]", + Child(Descendants(Root(), Fields('book')), Index(0,1))), + ("$..book[:2]", Child(Descendants(Root(), Fields('book')), Slice(end=2))), diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index 60670b1..07c6d9b 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -86,10 +86,13 @@ def setup_class(cls): # # Check that the data value returned is good # - def check_cases(self, test_cases): + def check_cases(self, test_cases, auto_id_field=None): # Note that just manually building an AST would avoid this dep and isolate the tests, but that would suck a bit # Also, we coerce iterables, etc, into the desired target type + # This is a global parameter, it should be updated with each call of this function automatically + # Not manually outside this function, or someone might forget to switch it back to None + jsonpath.auto_id_field = auto_id_field for string, data, target in test_cases: print('parse("%s").find(%s) =?= %s' % (string, data, target)) result = parse(string).find(data) @@ -101,17 +104,16 @@ def check_cases(self, test_cases): assert result.value == target def test_fields_value(self): - jsonpath.auto_id_field = None self.check_cases([ ('foo', {'foo': 'baz'}, ['baz']), ('foo,baz', {'foo': 1, 'baz': 2}, [1, 2]), ('@foo', {'@foo': 1}, [1]), - ('*', {'foo': 1, 'baz': 2}, set([1, 2])) ]) + ('*', {'foo': 1, 'baz': 2}, set([1, 2])) + ]) - jsonpath.auto_id_field = 'id' - self.check_cases([ ('*', {'foo': 1, 'baz': 2}, set([1, 2, '`this`'])) ]) + self.check_cases([ ('*', {'foo': 1, 'baz': 2}, set([1, 2, '`this`'])) + ], 'id') def test_root_value(self): - jsonpath.auto_id_field = None self.check_cases([ ('$', {'foo': 'baz'}, [{'foo':'baz'}]), ('foo.$', {'foo': 'baz'}, [{'foo':'baz'}]), @@ -119,7 +121,6 @@ def test_root_value(self): ]) def test_this_value(self): - jsonpath.auto_id_field = None self.check_cases([ ('`this`', {'foo': 'baz'}, [{'foo':'baz'}]), ('foo.`this`', {'foo': 'baz'}, ['baz']), @@ -131,6 +132,7 @@ def test_index_value(self): ('[0]', [42], [42]), ('[5]', [42], []), ('[2]', [34, 65, 29, 59], [29]), + ('[0,2,5]', [34, 65, 29, 59, 17, 3], [34, 29, 3]), ('[0]', None, []) ]) @@ -138,7 +140,12 @@ def test_slice_value(self): self.check_cases([('[*]', [1, 2, 3], [1, 2, 3]), ('[*]', xrange(1, 4), [1, 2, 3]), ('[1:]', [1, 2, 3, 4], [2, 3, 4]), - ('[:2]', [1, 2, 3, 4], [1, 2])]) + ('[:2]', [1, 2, 3, 4], [1, 2]), + ('[:3:2]', [1, 2, 3, 4], [1, 3]), + ('[1::2]', [1, 2, 3, 4], [2, 4]), + ('[1:5:3]', [1, 2, 3, 4, 5], [2, 5]), + ('[::-2]', [1, 2, 3, 4, 5], [5, 3, 1]), + ]) # Funky slice hacks self.check_cases([ @@ -151,7 +158,8 @@ def test_slice_value(self): def test_child_value(self): self.check_cases([('foo.baz', {'foo': {'baz': 3}}, [3]), ('foo.baz', {'foo': {'baz': [3]}}, [[3]]), - ('foo.baz.bizzle', {'foo': {'baz': {'bizzle': 5}}}, [5])]) + ('foo.baz.bizzle', {'foo': {'baz': {'bizzle': 5}}}, [5]) + ]) def test_descendants_value(self): self.check_cases([ @@ -161,28 +169,28 @@ def test_descendants_value(self): def test_parent_value(self): self.check_cases([('foo.baz.`parent`', {'foo': {'baz': 3}}, [{'baz': 3}]), - ('foo.`parent`.foo.baz.`parent`.baz.bizzle', {'foo': {'baz': {'bizzle': 5}}}, [5])]) + ('foo.`parent`.foo.baz.`parent`.baz.bizzle', {'foo': {'baz': {'bizzle': 5}}}, [5]) + ]) def test_hyphen_key(self): self.check_cases([('foo.bar-baz', {'foo': {'bar-baz': 3}}, [3]), - ('foo.[bar-baz,blah-blah]', {'foo': {'bar-baz': 3, 'blah-blah':5}}, - [3,5])]) + ('foo.[bar-baz,blah-blah]', {'foo': {'bar-baz': 3, 'blah-blah':5}}, [3,5]) + ]) self.assertRaises(JsonPathLexerError, self.check_cases, [('foo.-baz', {'foo': {'-baz': 8}}, [8])]) - - # # Check that the paths for the data are correct. # FIXME: merge these tests with the above, since the inputs are the same anyhow # - def check_paths(self, test_cases): + def check_paths(self, test_cases, auto_id_field=None): # Note that just manually building an AST would avoid this dep and isolate the tests, but that would suck a bit # Also, we coerce iterables, etc, into the desired target type + jsonpath.auto_id_field = auto_id_field for string, data, target in test_cases: - print('parse("%s").find(%s).paths =?= %s' % (string, data, target)) + print('parse("%s").find(%s).path =?= %s' % (string, data, target)) result = parse(string).find(data) if isinstance(target, list): assert [str(r.full_path) for r in result] == target @@ -192,16 +200,15 @@ def check_paths(self, test_cases): assert str(result.path) == target def test_fields_paths(self): - jsonpath.auto_id_field = None self.check_paths([ ('foo', {'foo': 'baz'}, ['foo']), ('foo,baz', {'foo': 1, 'baz': 2}, ['foo', 'baz']), - ('*', {'foo': 1, 'baz': 2}, set(['foo', 'baz'])) ]) + ('*', {'foo': 1, 'baz': 2}, set(['foo', 'baz'])) + ], None) - jsonpath.auto_id_field = 'id' - self.check_paths([ ('*', {'foo': 1, 'baz': 2}, set(['foo', 'baz', 'id'])) ]) + self.check_paths([ ('*', {'foo': 1, 'baz': 2}, set(['foo', 'baz', 'id'])) + ], 'id') def test_root_paths(self): - jsonpath.auto_id_field = None self.check_paths([ ('$', {'foo': 'baz'}, ['$']), ('foo.$', {'foo': 'baz'}, ['$']), @@ -209,7 +216,6 @@ def test_root_paths(self): ]) def test_this_paths(self): - jsonpath.auto_id_field = None self.check_paths([ ('`this`', {'foo': 'baz'}, ['`this`']), ('foo.`this`', {'foo': 'baz'}, ['foo']), @@ -218,70 +224,76 @@ def test_this_paths(self): def test_index_paths(self): self.check_paths([('[0]', [42], ['[0]']), - ('[2]', [34, 65, 29, 59], ['[2]'])]) + ('[2]', [34, 65, 29, 59], ['[2]']), + ('[1,2,3]', [34, 65, 29, 59], ['[1]', '[2]', '[3]']), + ]) def test_slice_paths(self): self.check_paths([ ('[*]', [1, 2, 3], ['[0]', '[1]', '[2]']), - ('[1:]', [1, 2, 3, 4], ['[1]', '[2]', '[3]']) ]) + ('[1:]', [1, 2, 3, 4], ['[1]', '[2]', '[3]']), + ('[1:3]', [1, 2, 3, 4], ['[1]', '[2]']), + ('[1::2]', [1, 2, 3, 4], ['[1]', '[3]']), + ('[::-1]', [1, 2, 3], ['[2]', '[1]', '[0]']), + ('[1:6:3]', xrange(10), ['[1]', '[4]']), + ]) def test_child_paths(self): self.check_paths([('foo.baz', {'foo': {'baz': 3}}, ['foo.baz']), ('foo.baz', {'foo': {'baz': [3]}}, ['foo.baz']), - ('foo.baz.bizzle', {'foo': {'baz': {'bizzle': 5}}}, ['foo.baz.bizzle'])]) + ('foo.baz.bizzle', {'foo': {'baz': {'bizzle': 5}}}, ['foo.baz.bizzle']) + ]) def test_descendants_paths(self): - self.check_paths([('foo..baz', {'foo': {'baz': 1, 'bing': {'baz': 2}}}, ['foo.baz', 'foo.bing.baz'] )]) + self.check_paths([('foo..baz', {'foo': {'baz': 1, 'bing': {'baz': 2}}}, ['foo.baz', 'foo.bing.baz'] ) + ]) # # Check the "auto_id_field" feature # def test_fields_auto_id(self): - jsonpath.auto_id_field = "id" self.check_cases([ ('foo.id', {'foo': 'baz'}, ['foo']), ('foo.id', {'foo': {'id': 'baz'}}, ['baz']), ('foo,baz.id', {'foo': 1, 'baz': 2}, ['foo', 'baz']), ('*.id', {'foo':{'id': 1}, 'baz': 2}, - set(['1', 'baz'])) ]) + set(['1', 'baz'])) + ], 'id') def test_root_auto_id(self): - jsonpath.auto_id_field = 'id' self.check_cases([ ('$.id', {'foo': 'baz'}, ['$']), # This is a wonky case that is not that interesting ('foo.$.id', {'foo': 'baz', 'id': 'bizzle'}, ['bizzle']), ('foo.$.baz.id', {'foo': 4, 'baz': 3}, ['baz']), - ]) + ], 'id') def test_this_auto_id(self): - jsonpath.auto_id_field = 'id' self.check_cases([ ('id', {'foo': 'baz'}, ['`this`']), # This is, again, a wonky case that is not that interesting ('foo.`this`.id', {'foo': 'baz'}, ['foo']), ('foo.`this`.baz.id', {'foo': {'baz': 3}}, ['foo.baz']), - ]) + ], 'id') def test_index_auto_id(self): - jsonpath.auto_id_field = "id" self.check_cases([('[0].id', [42], ['[0]']), - ('[2].id', [34, 65, 29, 59], ['[2]'])]) + ('[2].id', [34, 65, 29, 59], ['[2]']) + ], 'id') def test_slice_auto_id(self): - jsonpath.auto_id_field = "id" self.check_cases([ ('[*].id', [1, 2, 3], ['[0]', '[1]', '[2]']), - ('[1:].id', [1, 2, 3, 4], ['[1]', '[2]', '[3]']) ]) + ('[1:].id', [1, 2, 3, 4], ['[1]', '[2]', '[3]']) + ], 'id') def test_child_auto_id(self): - jsonpath.auto_id_field = "id" self.check_cases([('foo.baz.id', {'foo': {'baz': 3}}, ['foo.baz']), ('foo.baz.id', {'foo': {'baz': [3]}}, ['foo.baz']), ('foo.baz.id', {'foo': {'id': 'bizzle', 'baz': 3}}, ['bizzle.baz']), ('foo.baz.id', {'foo': {'baz': {'id': 'hi'}}}, ['foo.hi']), - ('foo.baz.bizzle.id', {'foo': {'baz': {'bizzle': 5}}}, ['foo.baz.bizzle'])]) + ('foo.baz.bizzle.id', {'foo': {'baz': {'bizzle': 5}}}, ['foo.baz.bizzle']) + ], 'id') def test_descendants_auto_id(self): - jsonpath.auto_id_field = "id" self.check_cases([('foo..baz.id', {'foo': { 'baz': 1, @@ -290,9 +302,11 @@ def test_descendants_auto_id(self): } } }, ['foo.baz', - 'foo.bing.baz'] )]) + 'foo.bing.baz'] ) + ], 'id') - def check_update_cases(self, test_cases): + def check_update_cases(self, test_cases, auto_id_field=None): + jsonpath.auto_id_field = auto_id_field for original, expr_str, value, expected in test_cases: print('parse(%r).update(%r, %r) =?= %r' % (expr_str, original, value, expected)) diff --git a/tests/test_parser.py b/tests/test_parser.py index b728590..820bb16 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -24,6 +24,7 @@ def test_atomic(self): ('*', Fields('*')), ('baz,bizzle', Fields('baz','bizzle')), ('[1]', Index(1)), + ('[0,2,3]', Index(0,2,3)), ('[1:]', Slice(start=1)), ('[:]', Slice()), ('[*]', Slice()),