From 71f9ae5004ae42819f8f6f8ffbe12f52ac31434c Mon Sep 17 00:00:00 2001 From: Wang Jing Date: Tue, 20 Dec 2016 16:50:08 +0800 Subject: [PATCH 1/7] ignore coverage files and add fields/index exclude function --- .gitignore | 5 +++++ jsonpath_rw/jsonpath.py | 14 +++++++++++++ tests/test_jsonpath.py | 46 ++++++++++++++++++++++++++++++++++------- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 12994bc..d629a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,8 @@ parser.out /jsonpath_rw/VERSION .idea + +venv +.eggs +coverage.xml +htmlcov diff --git a/jsonpath_rw/jsonpath.py b/jsonpath_rw/jsonpath.py index 146a960..de10c7f 100644 --- a/jsonpath_rw/jsonpath.py +++ b/jsonpath_rw/jsonpath.py @@ -46,6 +46,9 @@ def child(self, child): else: return Child(self, child) + def delete(self, data): + raise NotImplementedError() + def make_datum(self, value): if isinstance(value, DatumInContext): return value @@ -462,6 +465,12 @@ def update(self, data, val): data[field] = val return data + def delete(self, data): + for field in self.reified_fields(DatumInContext.wrap(data)): + if field in data: + del data[field] + return data + def __str__(self): return ','.join(map(str, self.fields)) @@ -497,6 +506,11 @@ def update(self, data, val): data[self.index] = val return data + def delete(self, data): + if data is not None and len(data) > self.index: + del data[self.index] + return data + def __eq__(self, other): return isinstance(other, Index) and self.index == other.index diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index 33c0ea6..c590c1e 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -81,7 +81,8 @@ class TestJsonPath(unittest.TestCase): @classmethod def setup_class(cls): - logging.basicConfig() + logging.basicConfig(format = '%(levelname)s:%(funcName)s:%(message)s', + level = logging.DEBUG) # # Check that the data value returned is good @@ -91,7 +92,7 @@ def check_cases(self, test_cases): # Also, we coerce iterables, etc, into the desired target type for string, data, target in test_cases: - print('parse("%s").find(%s) =?= %s' % (string, data, target)) + logging.debug('parse("%s").find(%s) =?= %s' % (string, data, target)) result = parse(string).find(data) if isinstance(target, list): assert [r.value for r in result] == target @@ -102,10 +103,12 @@ def check_cases(self, test_cases): 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])) ]) + 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])) + ]) jsonpath.auto_id_field = 'id' self.check_cases([ ('*', {'foo': 1, 'baz': 2}, set([1, 2, '`this`'])) ]) @@ -182,7 +185,7 @@ def check_paths(self, test_cases): # Also, we coerce iterables, etc, into the desired target type for string, data, target in test_cases: - print('parse("%s").find(%s).paths =?= %s' % (string, data, target)) + logging.debug('parse("%s").find(%s).paths =?= %s' % (string, data, target)) result = parse(string).find(data) if isinstance(target, list): assert [str(r.full_path) for r in result] == target @@ -294,7 +297,7 @@ def test_descendants_auto_id(self): def check_update_cases(self, test_cases): for original, expr_str, value, expected in test_cases: - print('parse(%r).update(%r, %r) =?= %r' + logger.debug('parse(%r).update(%r, %r) =?= %r' % (expr_str, original, value, expected)) expr = parse(expr_str) actual = expr.update(original, value) @@ -353,3 +356,30 @@ def test_update_slice(self): self.check_update_cases([ (['foo', 'bar', 'baz'], '[0:2]', 'test', ['test', 'test', 'baz']) ]) + + def check_delete_cases(self, test_cases): + for string, original, expected in test_cases: + logging.debug('parse("%s").delete(%s) =?= %s' % (string, original, expected)) + actual = parse(string).delete(original) + assert actual == expected + + def test_delete_fields(self): + jsonpath.auto_id_field = None + self.check_delete_cases([ + ('foo', {'foo': 'baz'}, {}), + ('foo', {'foo': 1, 'baz': 2}, {'baz': 2}), + ('foo,baz', {'foo': 1, 'baz': 2}, {}), + ('@foo', {'@foo': 1}, {}), + ('@foo', {'@foo': 1, 'baz': 2}, {'baz': 2}), + ('*', {'foo': 1, 'baz': 2}, {}) + ]) + + def test_delete_index(self): + self.check_delete_cases([ + ('[0]', [42], []), + ('[5]', [42], [42]), + ('[2]', [34, 65, 29, 59], [34, 65, 59]), + ('[0]', None, None), + ('[0]', [], []), + ('[0]', ['foo', 'bar', 'baz'], ['bar', 'baz']), + ]) \ No newline at end of file From 0655c461840ac1fae98ccb3be55c6f332c4bd999 Mon Sep 17 00:00:00 2001 From: Wang Jing Date: Tue, 20 Dec 2016 21:32:22 +0800 Subject: [PATCH 2/7] add delete to index/slice/root/this/where --- jsonpath_rw/jsonpath.py | 48 ++++++++++++++++++---- tests/test_jsonpath.py | 91 ++++++++++++++++++++++++++++++++++------- tests/test_lexer.py | 8 +++- tests/test_parser.py | 7 +++- 4 files changed, 127 insertions(+), 27 deletions(-) diff --git a/jsonpath_rw/jsonpath.py b/jsonpath_rw/jsonpath.py index de10c7f..619fe3b 100644 --- a/jsonpath_rw/jsonpath.py +++ b/jsonpath_rw/jsonpath.py @@ -184,6 +184,9 @@ def find(self, data): def update(self, data, val): return val + def delete(self, data): + return None + def __str__(self): return '$' @@ -204,6 +207,9 @@ def find(self, datum): def update(self, data, val): return val + def delete(self, data): + return None + def __str__(self): return '`this`' @@ -239,6 +245,11 @@ def update(self, data, val): self.right.update(datum.value, val) return data + def delete(self, data): + for datum in self.left.find(data): + self.right.delete(datum.value) + return data + def __eq__(self, other): return isinstance(other, Child) and self.left == other.left and self.right == other.right @@ -291,6 +302,12 @@ def update(self, data, val): datum.path.update(data, val) return data + def delete(self, data): + for path in reversed([datum.path for datum in self.find(data)]): + path.delete(data) + + return data + def __str__(self): return '%s where %s' % (self.left, self.right) @@ -343,36 +360,45 @@ def match_recursively(datum): for left_match in left_matches for submatch in match_recursively(left_match)] - def is_singular(): + def is_singular(self): return False - def update(self, data, val): + def _modify(self, data, val = None, delete = False): # Get all left matches into a list left_matches = self.left.find(data) if not isinstance(left_matches, list): left_matches = [left_matches] - def update_recursively(data): + def modify_recursively(data): # Update only mutable values corresponding to JSON types if not (isinstance(data, list) or isinstance(data, dict)): return - self.right.update(data, val) + if delete: + self.right.delete(data) + else: + self.right.update(data, val) # Manually do the * or [*] to avoid coercion and recurse just the right-hand pattern if isinstance(data, list): - for i in range(0, len(data)): - update_recursively(data[i]) + for i in reversed(range(0, len(data))): + modify_recursively(data[i]) elif isinstance(data, dict): for field in data.keys(): - update_recursively(data[field]) + modify_recursively(data[field]) for submatch in left_matches: - update_recursively(submatch.value) + modify_recursively(submatch.value) return data + def update(self, data, val): + return self._modify(data, val, delete = False) + + def delete(self, data): + return self._modify(data, None, delete = True) + def __str__(self): return '%s..%s' % (self.left, self.right) @@ -566,6 +592,12 @@ def update(self, data, val): datum.path.update(data, val) return data + def delete(self, data): + for path in reversed([datum.path for datum in self.find(data)]): + path.delete(data) + + return data + def __str__(self): if self.start == None and self.end == None and self.step == None: return '[*]' diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index c590c1e..e4098eb 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -310,7 +310,10 @@ def test_update_root(self): def test_update_this(self): self.check_update_cases([ - ('foo', '`this`', 'bar', 'bar') + ('foo', '`this`', 'bar', 'bar'), + # TODO: fixme + # ({'foo': 'bar'}, 'foo.`this`', 'baz', {'foo': 'baz'}), + ({'foo': {'bar': 'baz'}}, 'foo.`this`.bar', 'foo', {'foo': {'bar': 'foo'}}) ]) def test_update_fields(self): @@ -358,7 +361,7 @@ def test_update_slice(self): ]) def check_delete_cases(self, test_cases): - for string, original, expected in test_cases: + for original, string, expected in test_cases: logging.debug('parse("%s").delete(%s) =?= %s' % (string, original, expected)) actual = parse(string).delete(original) assert actual == expected @@ -366,20 +369,78 @@ def check_delete_cases(self, test_cases): def test_delete_fields(self): jsonpath.auto_id_field = None self.check_delete_cases([ - ('foo', {'foo': 'baz'}, {}), - ('foo', {'foo': 1, 'baz': 2}, {'baz': 2}), - ('foo,baz', {'foo': 1, 'baz': 2}, {}), - ('@foo', {'@foo': 1}, {}), - ('@foo', {'@foo': 1, 'baz': 2}, {'baz': 2}), - ('*', {'foo': 1, 'baz': 2}, {}) + ({'foo': 'baz'}, 'foo', {}), + ({'foo': 1, 'baz': 2}, 'foo', {'baz': 2}), + ({'foo': 1, 'baz': 2}, 'foo,baz', {}), + ({'@foo': 1}, '@foo', {}), + ({'@foo': 1, 'baz': 2}, '@foo', {'baz': 2}), + ({'foo': 1, 'baz': 2}, '*', {}) + ]) + + def test_delete_root(self): + self.check_delete_cases([ + ('foo', '$', None), + ]) + + def test_delete_this(self): + self.check_delete_cases([ + ('foo', '`this`', None), + ({}, '`this`', None), + ({'foo': 1}, '`this`', None), + # TODO: fixme + #({'foo': 1}, 'foo.`this`', {}), + ({'foo': {'bar': 1}}, 'foo.`this`.bar', {'foo': {}}), + ({'foo': {'bar': 1, 'baz': 2}}, 'foo.`this`.bar', {'foo': {'baz': 2}}) + ]) + + def test_delete_child(self): + self.check_delete_cases([ + ({'foo': 'bar'}, '$.foo', {}), + ({'foo': 'bar'}, 'foo', {}), + ({'foo': {'bar': 1}}, 'foo.bar', {'foo': {}}), + ({'foo': {'bar': 1}}, 'foo.$.foo.bar', {'foo': {}}) + ]) + + def test_delete_where(self): + self.check_delete_cases([ + ({'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}, + '*.bar where none', {'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}), + + ({'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}, + '*.bar where baz', {'foo': {}, 'bar': {'baz': 2}}) + ]) + + def test_delete_descendants(self): + self.check_delete_cases([ + ({'somefield': 1}, '$..somefield', {}), + ({'outer': {'nestedfield': 1}}, '$..nestedfield', {'outer': {}}), + ({'outs': {'bar': 1, 'ins': {'bar': 9}}, 'outs2': {'bar': 2}}, + '$..bar', + {'outs': {'ins': {}}, 'outs2': {}}) + ]) + + def test_delete_descendants_where(self): + self.check_delete_cases([ + ({'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 2}}, + '(* where flag) .. bar', + {'foo': {'flag': 1}, 'baz': {'bar': 2}}) ]) def test_delete_index(self): self.check_delete_cases([ - ('[0]', [42], []), - ('[5]', [42], [42]), - ('[2]', [34, 65, 29, 59], [34, 65, 59]), - ('[0]', None, None), - ('[0]', [], []), - ('[0]', ['foo', 'bar', 'baz'], ['bar', 'baz']), - ]) \ No newline at end of file + ([42], '[0]', []), + ([42], '[5]', [42]), + ([34, 65, 29, 59], '[2]', [34, 65, 59]), + (None, '[0]', None), + ([], '[0]', []), + (['foo', 'bar', 'baz'], '[0]', ['bar', 'baz']), + ]) + + def test_delete_slice(self): + self.check_delete_cases([ + (['foo', 'bar', 'baz'], '[0:2]', ['baz']), + (['foo', 'bar', 'baz'], '[0:1]', ['bar', 'baz']), + (['foo', 'bar', 'baz'], '[0:]', []), + (['foo', 'bar', 'baz'], '[:2]', ['baz']), + (['foo', 'bar', 'baz'], '[:3]', []) + ]) diff --git a/tests/test_lexer.py b/tests/test_lexer.py index 9d9fe38..e252add 100644 --- a/tests/test_lexer.py +++ b/tests/test_lexer.py @@ -23,13 +23,14 @@ def assert_lex_equiv(self, s, stream2): stream2 = list(stream2) assert len(stream1) == len(stream2) for token1, token2 in zip(stream1, stream2): - print(token1, token2) + logging.debug(token1, token2) assert token1.type == token2.type assert token1.value == token2.value @classmethod def setup_class(cls): - logging.basicConfig() + logging.basicConfig(format = '%(levelname)s:%(funcName)s:%(message)s', + level = logging.DEBUG) def test_simple_inputs(self): self.assert_lex_equiv('$', [self.token('$', '$')]) @@ -51,6 +52,9 @@ def test_simple_inputs(self): self.assert_lex_equiv('&', [self.token('&', '&')]) self.assert_lex_equiv('@', [self.token('@', 'ID')]) self.assert_lex_equiv('`this`', [self.token('this', 'NAMED_OPERATOR')]) + self.assert_lex_equiv('fuzz.`this`', [self.token('fuzz', 'ID'), + self.token('.', '.'), + self.token('this', 'NAMED_OPERATOR')]) self.assert_lex_equiv('|', [self.token('|', '|')]) self.assert_lex_equiv('where', [self.token('where', 'WHERE')]) diff --git a/tests/test_parser.py b/tests/test_parser.py index fd1e121..b3c8ed4 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -10,13 +10,14 @@ class TestParser(unittest.TestCase): @classmethod def setup_class(cls): - logging.basicConfig() + logging.basicConfig(format = '%(levelname)s:%(funcName)s:%(message)s', + level = logging.DEBUG) def check_parse_cases(self, test_cases): parser = JsonPathParser(debug=True, lexer_class=lambda:JsonPathLexer(debug=False)) # Note that just manually passing token streams avoids this dep, but that sucks for string, parsed in test_cases: - print(string, '=?=', parsed) # pytest captures this and we see it only on a failure, for debugging + logging.debug(string, '=?=', parsed) # pytest captures this and we see it only on a failure, for debugging assert parser.parse(string) == parsed def test_atomic(self): @@ -36,5 +37,7 @@ def test_nested(self): self.check_parse_cases([('foo.baz', Child(Fields('foo'), Fields('baz'))), ('foo.baz,bizzle', Child(Fields('foo'), Fields('baz', 'bizzle'))), ('foo where baz', Where(Fields('foo'), Fields('baz'))), + ('`this`', This()), + ('foo.`this`', Child(Fields('foo'), This())), ('foo..baz', Descendants(Fields('foo'), Fields('baz'))), ('foo..baz.bing', Descendants(Fields('foo'), Child(Fields('baz'), Fields('bing'))))]) From 5d653b2d01535a79dd638ef7cd16ad12f5eb30dd Mon Sep 17 00:00:00 2001 From: Wang Jing Date: Wed, 21 Dec 2016 10:43:55 +0800 Subject: [PATCH 3/7] add union update and delete --- jsonpath_rw/jsonpath.py | 10 ++++++++++ tests/test_jsonpath.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/jsonpath_rw/jsonpath.py b/jsonpath_rw/jsonpath.py index 619fe3b..e3b7ce1 100644 --- a/jsonpath_rw/jsonpath.py +++ b/jsonpath_rw/jsonpath.py @@ -425,6 +425,16 @@ def is_singular(self): def find(self, data): return self.left.find(data) + self.right.find(data) + def update(self, data, val): + self.left.update(data, val) + self.right.update(data, val) + return data + + def delete(self, data): + self.left.delete(data) + self.right.delete(data) + return data + class Intersect(JSONPath): """ JSONPath for bits that match *both* patterns. diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index e4098eb..3269022 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -162,6 +162,11 @@ def test_descendants_value(self): ('foo..baz', {'foo': [{'baz': 1}, {'baz': 2}]}, [1, 2] ), ]) + def test_union_value(self): + self.check_cases([ + ('foo | bar', {'foo': 1, 'bar': 2}, [1, 2]) + ]) + 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])]) @@ -328,6 +333,11 @@ def test_update_child(self): ({'foo': {'bar': 1}}, 'foo.bar', 'baz', {'foo': {'bar': 'baz'}}) ]) + def test_update_union(self): + self.check_update_cases([ + ({'foo': 1, 'bar': 2}, 'foo | bar', 3, {'foo': 3, 'bar': 3}) + ]) + def test_update_where(self): self.check_update_cases([ ({'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}, @@ -426,6 +436,12 @@ def test_delete_descendants_where(self): {'foo': {'flag': 1}, 'baz': {'bar': 2}}) ]) + def test_delete_union(self): + self.check_delete_cases([ + ({'foo': 1, 'bar': 2}, 'foo | bar', {}), + ({'foo': 1, 'bar': 2, 'baz': 3}, 'foo | bar', {'baz': 3}), + ]) + def test_delete_index(self): self.check_delete_cases([ ([42], '[0]', []), From 4cea752d2e6f1f234adaf855731aef8c24c54302 Mon Sep 17 00:00:00 2001 From: Wang Jing Date: Wed, 21 Dec 2016 10:47:43 +0800 Subject: [PATCH 4/7] add update and delete doc --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index a6ef686..25d9307 100644 --- a/README.rst +++ b/README.rst @@ -38,6 +38,14 @@ Then: >>> [match.value for match in jsonpath_expr.find({'foo': [{'baz': 1}, {'baz': 2}]})] [1, 2] + # Update values + >>> jsonpath_expr.update({'foo': [{'baz': 1}, {'baz': 2}]}, 3) + {'foo': [{'baz': 3}, {'baz': 3}]} + + # Delete values + >>> jsonpath_expr.delete({'foo': [{'baz': 1}, {'baz': 2}]}) + {'foo': []} + # Matches remember where they came from >>> [str(match.full_path) for match in jsonpath_expr.find({'foo': [{'baz': 1}, {'baz': 2}]})] ['foo.[0].baz', 'foo.[1].baz'] From cd6e5e0806f70b33ae70796356746410fcd74508 Mon Sep 17 00:00:00 2001 From: Wang Jing Date: Wed, 21 Dec 2016 14:27:20 +0800 Subject: [PATCH 5/7] rename delete to exclude --- README.rst | 4 +-- jsonpath_rw/jsonpath.py | 57 ++++++++++++++++++++++++++--------------- tests/test_jsonpath.py | 53 +++++++++++++++++++++----------------- 3 files changed, 69 insertions(+), 45 deletions(-) diff --git a/README.rst b/README.rst index 25d9307..71fb7c3 100644 --- a/README.rst +++ b/README.rst @@ -42,8 +42,8 @@ Then: >>> jsonpath_expr.update({'foo': [{'baz': 1}, {'baz': 2}]}, 3) {'foo': [{'baz': 3}, {'baz': 3}]} - # Delete values - >>> jsonpath_expr.delete({'foo': [{'baz': 1}, {'baz': 2}]}) + # Exclude values + >>> jsonpath_expr.exclude({'foo': [{'baz': 1}, {'baz': 2}]}) {'foo': []} # Matches remember where they came from diff --git a/jsonpath_rw/jsonpath.py b/jsonpath_rw/jsonpath.py index e3b7ce1..201fc2c 100644 --- a/jsonpath_rw/jsonpath.py +++ b/jsonpath_rw/jsonpath.py @@ -46,7 +46,18 @@ def child(self, child): else: return Child(self, child) - def delete(self, data): + def exclude(self, data): + """ + Returns `data` without the specified path + """ + raise NotImplementedError() + + def include(self, data): + """ + Returns `data` with the specified path + :param data: + :return: + """ raise NotImplementedError() def make_datum(self, value): @@ -184,9 +195,12 @@ def find(self, data): def update(self, data, val): return val - def delete(self, data): + def exclude(self, data): return None + def include(self, data): + return data + def __str__(self): return '$' @@ -207,9 +221,12 @@ def find(self, datum): def update(self, data, val): return val - def delete(self, data): + def exclude(self, data): return None + def include(self, data): + return data + def __str__(self): return '`this`' @@ -245,9 +262,9 @@ def update(self, data, val): self.right.update(datum.value, val) return data - def delete(self, data): + def exclude(self, data): for datum in self.left.find(data): - self.right.delete(datum.value) + self.right.exclude(datum.value) return data def __eq__(self, other): @@ -302,9 +319,9 @@ def update(self, data, val): datum.path.update(data, val) return data - def delete(self, data): + def exclude(self, data): for path in reversed([datum.path for datum in self.find(data)]): - path.delete(data) + path.exclude(data) return data @@ -363,7 +380,7 @@ def match_recursively(datum): def is_singular(self): return False - def _modify(self, data, val = None, delete = False): + def _modify(self, data, val = None, exclude = False): # Get all left matches into a list left_matches = self.left.find(data) if not isinstance(left_matches, list): @@ -374,8 +391,8 @@ def modify_recursively(data): if not (isinstance(data, list) or isinstance(data, dict)): return - if delete: - self.right.delete(data) + if exclude: + self.right.exclude(data) else: self.right.update(data, val) @@ -394,10 +411,10 @@ def modify_recursively(data): return data def update(self, data, val): - return self._modify(data, val, delete = False) + return self._modify(data, val, exclude = False) - def delete(self, data): - return self._modify(data, None, delete = True) + def exclude(self, data): + return self._modify(data, None, exclude = True) def __str__(self): return '%s..%s' % (self.left, self.right) @@ -430,9 +447,9 @@ def update(self, data, val): self.right.update(data, val) return data - def delete(self, data): - self.left.delete(data) - self.right.delete(data) + def exclude(self, data): + self.left.exclude(data) + self.right.exclude(data) return data class Intersect(JSONPath): @@ -501,7 +518,7 @@ def update(self, data, val): data[field] = val return data - def delete(self, data): + def exclude(self, data): for field in self.reified_fields(DatumInContext.wrap(data)): if field in data: del data[field] @@ -542,7 +559,7 @@ def update(self, data, val): data[self.index] = val return data - def delete(self, data): + def exclude(self, data): if data is not None and len(data) > self.index: del data[self.index] return data @@ -602,9 +619,9 @@ def update(self, data, val): datum.path.update(data, val) return data - def delete(self, data): + def exclude(self, data): for path in reversed([datum.path for datum in self.find(data)]): - path.delete(data) + path.exclude(data) return data diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index 3269022..5110b60 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -370,15 +370,15 @@ def test_update_slice(self): (['foo', 'bar', 'baz'], '[0:2]', 'test', ['test', 'test', 'baz']) ]) - def check_delete_cases(self, test_cases): + def check_exclude_cases(self, test_cases): for original, string, expected in test_cases: - logging.debug('parse("%s").delete(%s) =?= %s' % (string, original, expected)) - actual = parse(string).delete(original) + logging.debug('parse("%s").exclude(%s) =?= %s' % (string, original, expected)) + actual = parse(string).exclude(original) assert actual == expected - def test_delete_fields(self): + def test_exclude_fields(self): jsonpath.auto_id_field = None - self.check_delete_cases([ + self.check_exclude_cases([ ({'foo': 'baz'}, 'foo', {}), ({'foo': 1, 'baz': 2}, 'foo', {'baz': 2}), ({'foo': 1, 'baz': 2}, 'foo,baz', {}), @@ -387,13 +387,13 @@ def test_delete_fields(self): ({'foo': 1, 'baz': 2}, '*', {}) ]) - def test_delete_root(self): - self.check_delete_cases([ + def test_exclude_root(self): + self.check_exclude_cases([ ('foo', '$', None), ]) - def test_delete_this(self): - self.check_delete_cases([ + def test_exclude_this(self): + self.check_exclude_cases([ ('foo', '`this`', None), ({}, '`this`', None), ({'foo': 1}, '`this`', None), @@ -403,16 +403,16 @@ def test_delete_this(self): ({'foo': {'bar': 1, 'baz': 2}}, 'foo.`this`.bar', {'foo': {'baz': 2}}) ]) - def test_delete_child(self): - self.check_delete_cases([ + def test_exclude_child(self): + self.check_exclude_cases([ ({'foo': 'bar'}, '$.foo', {}), ({'foo': 'bar'}, 'foo', {}), ({'foo': {'bar': 1}}, 'foo.bar', {'foo': {}}), ({'foo': {'bar': 1}}, 'foo.$.foo.bar', {'foo': {}}) ]) - def test_delete_where(self): - self.check_delete_cases([ + def test_exclude_where(self): + self.check_exclude_cases([ ({'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}, '*.bar where none', {'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}), @@ -420,8 +420,8 @@ def test_delete_where(self): '*.bar where baz', {'foo': {}, 'bar': {'baz': 2}}) ]) - def test_delete_descendants(self): - self.check_delete_cases([ + def test_exclude_descendants(self): + self.check_exclude_cases([ ({'somefield': 1}, '$..somefield', {}), ({'outer': {'nestedfield': 1}}, '$..nestedfield', {'outer': {}}), ({'outs': {'bar': 1, 'ins': {'bar': 9}}, 'outs2': {'bar': 2}}, @@ -429,21 +429,21 @@ def test_delete_descendants(self): {'outs': {'ins': {}}, 'outs2': {}}) ]) - def test_delete_descendants_where(self): - self.check_delete_cases([ + def test_exclude_descendants_where(self): + self.check_exclude_cases([ ({'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 2}}, '(* where flag) .. bar', {'foo': {'flag': 1}, 'baz': {'bar': 2}}) ]) - def test_delete_union(self): - self.check_delete_cases([ + def test_exclude_union(self): + self.check_exclude_cases([ ({'foo': 1, 'bar': 2}, 'foo | bar', {}), ({'foo': 1, 'bar': 2, 'baz': 3}, 'foo | bar', {'baz': 3}), ]) - def test_delete_index(self): - self.check_delete_cases([ + def test_exclude_index(self): + self.check_exclude_cases([ ([42], '[0]', []), ([42], '[5]', [42]), ([34, 65, 29, 59], '[2]', [34, 65, 59]), @@ -452,11 +452,18 @@ def test_delete_index(self): (['foo', 'bar', 'baz'], '[0]', ['bar', 'baz']), ]) - def test_delete_slice(self): - self.check_delete_cases([ + def test_exclude_slice(self): + self.check_exclude_cases([ (['foo', 'bar', 'baz'], '[0:2]', ['baz']), (['foo', 'bar', 'baz'], '[0:1]', ['bar', 'baz']), (['foo', 'bar', 'baz'], '[0:]', []), (['foo', 'bar', 'baz'], '[:2]', ['baz']), (['foo', 'bar', 'baz'], '[:3]', []) ]) + + def check_include_cases(self, test_cases): + for original, string, expected in test_cases: + logging.debug('parse("%s").exclude(%s) =?= %s' % (string, original, expected)) + actual = parse(string).include(original) + assert actual == expected + From 705bc3e829364233d4635d58e6638048a8315cb2 Mon Sep 17 00:00:00 2001 From: Wang Jing Date: Wed, 21 Dec 2016 16:52:47 +0800 Subject: [PATCH 6/7] add include function --- jsonpath_rw/jsonpath.py | 43 +++++++++++++++++++++ tests/test_jsonpath.py | 82 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/jsonpath_rw/jsonpath.py b/jsonpath_rw/jsonpath.py index 201fc2c..5429a80 100644 --- a/jsonpath_rw/jsonpath.py +++ b/jsonpath_rw/jsonpath.py @@ -267,6 +267,11 @@ def exclude(self, data): self.right.exclude(datum.value) return data + def include(self, data): + for datum in self.left.find(data): + self.right.include(datum.value) + return data + def __eq__(self, other): return isinstance(other, Child) and self.left == other.left and self.right == other.right @@ -524,6 +529,23 @@ def exclude(self, data): del data[field] return data + def include(self, data): + datum = DatumInContext.wrap(data) + + try: + all_fields = tuple(datum.value.keys()) + except AttributeError: + all_fields = () + + path_fields = self.reified_fields(datum) + remove_fields = set(all_fields) - set(path_fields) + + for field in remove_fields: + if field in data: + del data[field] + + return data + def __str__(self): return ','.join(map(str, self.fields)) @@ -564,6 +586,15 @@ def exclude(self, data): del data[self.index] return data + def include(self, data): + if data is None: + return None + + if len(data) > self.index: + return [data[self.index]] + + return [] + def __eq__(self, other): return isinstance(other, Index) and self.index == other.index @@ -625,6 +656,18 @@ def exclude(self, data): return data + def include(self, data): + + if not data: + return data + + ret = [] + for datum in self.find(data): + ret.append(datum.value) + + data = ret + return data + def __str__(self): if self.start == None and self.end == None and self.step == None: return '[*]' diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index 5110b60..7e535d1 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -463,7 +463,87 @@ def test_exclude_slice(self): def check_include_cases(self, test_cases): for original, string, expected in test_cases: - logging.debug('parse("%s").exclude(%s) =?= %s' % (string, original, expected)) + logging.debug('parse("%s").include(%s) =?= %s' % (string, original, expected)) actual = parse(string).include(original) assert actual == expected + def test_include_fields(self): + self.check_include_cases([ + ({'foo': 'baz'}, 'foo', {'foo': 'baz'}), + ({'foo': 1, 'baz': 2}, 'foo', {'foo': 1}), + ({'foo': 1, 'baz': 2}, 'foo,baz', {'foo': 1, 'baz': 2}), + ({'@foo': 1}, '@foo', {'@foo': 1}), + ({'@foo': 1, 'baz': 2}, '@foo', {'@foo': 1}), + ({'foo': 1, 'baz': 2}, '*', {'foo': 1, 'baz': 2}), + ]) + + def test_include_index(self): + self.check_include_cases([ + ([42], '[0]', [42]), + ([42], '[5]', []), + ([34, 65, 29, 59], '[2]', [29]), + (None, '[0]', None), + ([], '[0]', []), + (['foo', 'bar', 'baz'], '[0]', ['foo']), + ]) + + def test_include_slice(self): + self.check_include_cases([ + (['foo', 'bar', 'baz'], '[0:2]', ['foo', 'bar']), + (['foo', 'bar', 'baz'], '[0:1]', ['foo']), + (['foo', 'bar', 'baz'], '[0:]', ['foo', 'bar', 'baz']), + (['foo', 'bar', 'baz'], '[:2]', ['foo', 'bar']), + (['foo', 'bar', 'baz'], '[:3]', ['foo', 'bar', 'baz']), + (['foo', 'bar', 'baz'], '[0:0]', []), + ]) + + def test_include_root(self): + self.check_include_cases([ + ('foo', '$', 'foo'), + ({}, '$', {}), + ({'foo': 1}, '$', {'foo': 1}) + ]) + + def test_include_this(self): + self.check_include_cases([ + ('foo', '`this`', 'foo'), + ({}, '`this`', {}), + ({'foo': 1}, '`this`', {'foo': 1}), + # TODO: fixme + #({'foo': 1}, 'foo.`this`', {}), + ({'foo': {'bar': 1}}, 'foo.`this`.bar', {'foo': {'bar': 1}}), + ({'foo': {'bar': 1, 'baz': 2}}, 'foo.`this`.bar', {'foo': {'bar': 1}}) + ]) + + def test_include_child(self): + self.check_include_cases([ + ({'foo': 'bar'}, '$.foo', {'foo': 'bar'}), + ({'foo': 'bar'}, 'foo', {'foo': 'bar'}), + ({'foo': {'bar': 1}}, 'foo.bar', {'foo': {'bar': 1}}), + ({'foo': {'bar': 1}}, 'foo.$.foo.bar', {'foo': {'bar': 1}}), + ({'foo': {'bar': 1, 'baz': 2}}, 'foo.$.foo.bar', {'foo': {'bar': 1}}), + ({'foo': {'bar': 1, 'baz': 2}}, '*', {'foo': {'bar': 1, 'baz': 2}}), + ({'foo': {'bar': 1, 'baz': 2}}, 'non', {}), + ]) + + """ + def test_include_where(self): + self.check_include_cases([ + #({'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}, + # '*.bar where none', {}), + + ({'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}, + '*.bar where baz', {'foo': {'bar': {'baz': 1}}}) + ]) + """ + + """ + def test_include_descendants(self): + self.check_include_cases([ + ({'somefield': 1}, '$..somefield', {'somefield': 1}), + ({'outer': {'nestedfield': 1}}, '$..nestedfield', {'outer': {'nestedfield': 1}}), + ({'outs': {'bar': 1, 'ins': {'bar': 9}}, 'outs2': {'bar': 2}}, + '$..bar', + {'outs': {'bar': 1, 'ins': {'bar': 9}}, 'outs2': {'bar': 2}}) + ]) + """ From 5dadd0bea4717021d24874c89a06157a8f1b738b Mon Sep 17 00:00:00 2001 From: Wang Jing Date: Fri, 10 Mar 2017 11:33:25 +0800 Subject: [PATCH 7/7] fix non exists key cause exception --- jsonpath_rw/jsonpath.py | 2 +- tests/test_jsonpath.py | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/jsonpath_rw/jsonpath.py b/jsonpath_rw/jsonpath.py index 5429a80..1d10e69 100644 --- a/jsonpath_rw/jsonpath.py +++ b/jsonpath_rw/jsonpath.py @@ -525,7 +525,7 @@ def update(self, data, val): def exclude(self, data): for field in self.reified_fields(DatumInContext.wrap(data)): - if field in data: + if data and field in data: del data[field] return data diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index 7e535d1..e3014cb 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals, print_function, absolute_import, division, generators, nested_scopes import unittest +import json from jsonpath_rw import jsonpath # For setting the global auto_id_field flag @@ -71,8 +72,6 @@ def test_DatumInContext_in_context(self): # assert AutoIdForDatum(DatumInContext(value=3, path=Fields('foo')), # id_field='id', # context=DatumInContext(value={'id': 'bizzle'}, path=This())).pseudopath == Fields('bizzle').child(Fields('foo')) - - class TestJsonPath(unittest.TestCase): """ @@ -526,6 +525,25 @@ def test_include_child(self): ({'foo': {'bar': 1, 'baz': 2}}, 'non', {}), ]) + def test_exclude_not_exists(self): + self.check_exclude_cases([ + ( + { + 'foo': [ + {'bar': 'bar'}, + {'baz': None} + ] + }, + 'foo.[*].baz.not_exist_key', + { + 'foo': [ + {'bar': 'bar'}, + {'baz': None} + ] + }, + ), + ]) + """ def test_include_where(self): self.check_include_cases([