Skip to content

Commit 0655c46

Browse files
committed
add delete to index/slice/root/this/where
1 parent 71f9ae5 commit 0655c46

File tree

4 files changed

+127
-27
lines changed

4 files changed

+127
-27
lines changed

jsonpath_rw/jsonpath.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ def find(self, data):
184184
def update(self, data, val):
185185
return val
186186

187+
def delete(self, data):
188+
return None
189+
187190
def __str__(self):
188191
return '$'
189192

@@ -204,6 +207,9 @@ def find(self, datum):
204207
def update(self, data, val):
205208
return val
206209

210+
def delete(self, data):
211+
return None
212+
207213
def __str__(self):
208214
return '`this`'
209215

@@ -239,6 +245,11 @@ def update(self, data, val):
239245
self.right.update(datum.value, val)
240246
return data
241247

248+
def delete(self, data):
249+
for datum in self.left.find(data):
250+
self.right.delete(datum.value)
251+
return data
252+
242253
def __eq__(self, other):
243254
return isinstance(other, Child) and self.left == other.left and self.right == other.right
244255

@@ -291,6 +302,12 @@ def update(self, data, val):
291302
datum.path.update(data, val)
292303
return data
293304

305+
def delete(self, data):
306+
for path in reversed([datum.path for datum in self.find(data)]):
307+
path.delete(data)
308+
309+
return data
310+
294311
def __str__(self):
295312
return '%s where %s' % (self.left, self.right)
296313

@@ -343,36 +360,45 @@ def match_recursively(datum):
343360
for left_match in left_matches
344361
for submatch in match_recursively(left_match)]
345362

346-
def is_singular():
363+
def is_singular(self):
347364
return False
348365

349-
def update(self, data, val):
366+
def _modify(self, data, val = None, delete = False):
350367
# Get all left matches into a list
351368
left_matches = self.left.find(data)
352369
if not isinstance(left_matches, list):
353370
left_matches = [left_matches]
354371

355-
def update_recursively(data):
372+
def modify_recursively(data):
356373
# Update only mutable values corresponding to JSON types
357374
if not (isinstance(data, list) or isinstance(data, dict)):
358375
return
359376

360-
self.right.update(data, val)
377+
if delete:
378+
self.right.delete(data)
379+
else:
380+
self.right.update(data, val)
361381

362382
# Manually do the * or [*] to avoid coercion and recurse just the right-hand pattern
363383
if isinstance(data, list):
364-
for i in range(0, len(data)):
365-
update_recursively(data[i])
384+
for i in reversed(range(0, len(data))):
385+
modify_recursively(data[i])
366386

367387
elif isinstance(data, dict):
368388
for field in data.keys():
369-
update_recursively(data[field])
389+
modify_recursively(data[field])
370390

371391
for submatch in left_matches:
372-
update_recursively(submatch.value)
392+
modify_recursively(submatch.value)
373393

374394
return data
375395

396+
def update(self, data, val):
397+
return self._modify(data, val, delete = False)
398+
399+
def delete(self, data):
400+
return self._modify(data, None, delete = True)
401+
376402
def __str__(self):
377403
return '%s..%s' % (self.left, self.right)
378404

@@ -566,6 +592,12 @@ def update(self, data, val):
566592
datum.path.update(data, val)
567593
return data
568594

595+
def delete(self, data):
596+
for path in reversed([datum.path for datum in self.find(data)]):
597+
path.delete(data)
598+
599+
return data
600+
569601
def __str__(self):
570602
if self.start == None and self.end == None and self.step == None:
571603
return '[*]'

tests/test_jsonpath.py

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,10 @@ def test_update_root(self):
310310

311311
def test_update_this(self):
312312
self.check_update_cases([
313-
('foo', '`this`', 'bar', 'bar')
313+
('foo', '`this`', 'bar', 'bar'),
314+
# TODO: fixme
315+
# ({'foo': 'bar'}, 'foo.`this`', 'baz', {'foo': 'baz'}),
316+
({'foo': {'bar': 'baz'}}, 'foo.`this`.bar', 'foo', {'foo': {'bar': 'foo'}})
314317
])
315318

316319
def test_update_fields(self):
@@ -358,28 +361,86 @@ def test_update_slice(self):
358361
])
359362

360363
def check_delete_cases(self, test_cases):
361-
for string, original, expected in test_cases:
364+
for original, string, expected in test_cases:
362365
logging.debug('parse("%s").delete(%s) =?= %s' % (string, original, expected))
363366
actual = parse(string).delete(original)
364367
assert actual == expected
365368

366369
def test_delete_fields(self):
367370
jsonpath.auto_id_field = None
368371
self.check_delete_cases([
369-
('foo', {'foo': 'baz'}, {}),
370-
('foo', {'foo': 1, 'baz': 2}, {'baz': 2}),
371-
('foo,baz', {'foo': 1, 'baz': 2}, {}),
372-
('@foo', {'@foo': 1}, {}),
373-
('@foo', {'@foo': 1, 'baz': 2}, {'baz': 2}),
374-
('*', {'foo': 1, 'baz': 2}, {})
372+
({'foo': 'baz'}, 'foo', {}),
373+
({'foo': 1, 'baz': 2}, 'foo', {'baz': 2}),
374+
({'foo': 1, 'baz': 2}, 'foo,baz', {}),
375+
({'@foo': 1}, '@foo', {}),
376+
({'@foo': 1, 'baz': 2}, '@foo', {'baz': 2}),
377+
({'foo': 1, 'baz': 2}, '*', {})
378+
])
379+
380+
def test_delete_root(self):
381+
self.check_delete_cases([
382+
('foo', '$', None),
383+
])
384+
385+
def test_delete_this(self):
386+
self.check_delete_cases([
387+
('foo', '`this`', None),
388+
({}, '`this`', None),
389+
({'foo': 1}, '`this`', None),
390+
# TODO: fixme
391+
#({'foo': 1}, 'foo.`this`', {}),
392+
({'foo': {'bar': 1}}, 'foo.`this`.bar', {'foo': {}}),
393+
({'foo': {'bar': 1, 'baz': 2}}, 'foo.`this`.bar', {'foo': {'baz': 2}})
394+
])
395+
396+
def test_delete_child(self):
397+
self.check_delete_cases([
398+
({'foo': 'bar'}, '$.foo', {}),
399+
({'foo': 'bar'}, 'foo', {}),
400+
({'foo': {'bar': 1}}, 'foo.bar', {'foo': {}}),
401+
({'foo': {'bar': 1}}, 'foo.$.foo.bar', {'foo': {}})
402+
])
403+
404+
def test_delete_where(self):
405+
self.check_delete_cases([
406+
({'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}},
407+
'*.bar where none', {'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}),
408+
409+
({'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}},
410+
'*.bar where baz', {'foo': {}, 'bar': {'baz': 2}})
411+
])
412+
413+
def test_delete_descendants(self):
414+
self.check_delete_cases([
415+
({'somefield': 1}, '$..somefield', {}),
416+
({'outer': {'nestedfield': 1}}, '$..nestedfield', {'outer': {}}),
417+
({'outs': {'bar': 1, 'ins': {'bar': 9}}, 'outs2': {'bar': 2}},
418+
'$..bar',
419+
{'outs': {'ins': {}}, 'outs2': {}})
420+
])
421+
422+
def test_delete_descendants_where(self):
423+
self.check_delete_cases([
424+
({'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 2}},
425+
'(* where flag) .. bar',
426+
{'foo': {'flag': 1}, 'baz': {'bar': 2}})
375427
])
376428

377429
def test_delete_index(self):
378430
self.check_delete_cases([
379-
('[0]', [42], []),
380-
('[5]', [42], [42]),
381-
('[2]', [34, 65, 29, 59], [34, 65, 59]),
382-
('[0]', None, None),
383-
('[0]', [], []),
384-
('[0]', ['foo', 'bar', 'baz'], ['bar', 'baz']),
385-
])
431+
([42], '[0]', []),
432+
([42], '[5]', [42]),
433+
([34, 65, 29, 59], '[2]', [34, 65, 59]),
434+
(None, '[0]', None),
435+
([], '[0]', []),
436+
(['foo', 'bar', 'baz'], '[0]', ['bar', 'baz']),
437+
])
438+
439+
def test_delete_slice(self):
440+
self.check_delete_cases([
441+
(['foo', 'bar', 'baz'], '[0:2]', ['baz']),
442+
(['foo', 'bar', 'baz'], '[0:1]', ['bar', 'baz']),
443+
(['foo', 'bar', 'baz'], '[0:]', []),
444+
(['foo', 'bar', 'baz'], '[:2]', ['baz']),
445+
(['foo', 'bar', 'baz'], '[:3]', [])
446+
])

tests/test_lexer.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ def assert_lex_equiv(self, s, stream2):
2323
stream2 = list(stream2)
2424
assert len(stream1) == len(stream2)
2525
for token1, token2 in zip(stream1, stream2):
26-
print(token1, token2)
26+
logging.debug(token1, token2)
2727
assert token1.type == token2.type
2828
assert token1.value == token2.value
2929

3030
@classmethod
3131
def setup_class(cls):
32-
logging.basicConfig()
32+
logging.basicConfig(format = '%(levelname)s:%(funcName)s:%(message)s',
33+
level = logging.DEBUG)
3334

3435
def test_simple_inputs(self):
3536
self.assert_lex_equiv('$', [self.token('$', '$')])
@@ -51,6 +52,9 @@ def test_simple_inputs(self):
5152
self.assert_lex_equiv('&', [self.token('&', '&')])
5253
self.assert_lex_equiv('@', [self.token('@', 'ID')])
5354
self.assert_lex_equiv('`this`', [self.token('this', 'NAMED_OPERATOR')])
55+
self.assert_lex_equiv('fuzz.`this`', [self.token('fuzz', 'ID'),
56+
self.token('.', '.'),
57+
self.token('this', 'NAMED_OPERATOR')])
5458
self.assert_lex_equiv('|', [self.token('|', '|')])
5559
self.assert_lex_equiv('where', [self.token('where', 'WHERE')])
5660

tests/test_parser.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ class TestParser(unittest.TestCase):
1010

1111
@classmethod
1212
def setup_class(cls):
13-
logging.basicConfig()
13+
logging.basicConfig(format = '%(levelname)s:%(funcName)s:%(message)s',
14+
level = logging.DEBUG)
1415

1516
def check_parse_cases(self, test_cases):
1617
parser = JsonPathParser(debug=True, lexer_class=lambda:JsonPathLexer(debug=False)) # Note that just manually passing token streams avoids this dep, but that sucks
1718

1819
for string, parsed in test_cases:
19-
print(string, '=?=', parsed) # pytest captures this and we see it only on a failure, for debugging
20+
logging.debug(string, '=?=', parsed) # pytest captures this and we see it only on a failure, for debugging
2021
assert parser.parse(string) == parsed
2122

2223
def test_atomic(self):
@@ -36,5 +37,7 @@ def test_nested(self):
3637
self.check_parse_cases([('foo.baz', Child(Fields('foo'), Fields('baz'))),
3738
('foo.baz,bizzle', Child(Fields('foo'), Fields('baz', 'bizzle'))),
3839
('foo where baz', Where(Fields('foo'), Fields('baz'))),
40+
('`this`', This()),
41+
('foo.`this`', Child(Fields('foo'), This())),
3942
('foo..baz', Descendants(Fields('foo'), Fields('baz'))),
4043
('foo..baz.bing', Descendants(Fields('foo'), Child(Fields('baz'), Fields('bing'))))])

0 commit comments

Comments
 (0)