diff --git a/requirements-dev.txt b/requirements-dev.txt index 9d3c3ac..99d817a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ pytest flake8 coverage pytest-cov +pytest-randomly diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fb136cc --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest + + +@pytest.fixture(autouse=True) +def disable_auto_id_field(monkeypatch): + monkeypatch.setattr("jsonpath_ng.jsonpath.auto_id_field", None) + + +@pytest.fixture() +def auto_id_field(monkeypatch, disable_auto_id_field): + """Enable `jsonpath_ng.jsonpath.auto_id_field`.""" + + field_name = "id" + monkeypatch.setattr("jsonpath_ng.jsonpath.auto_id_field", field_name) + return field_name diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..90c5acb --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,35 @@ +def assert_value_equality(results, expected_values): + """Assert equality between two objects. + + *results* must be a list of results as returned by `.find()` methods. + + If *expected_values* is a list, then value equality and ordering will be checked. + If *expected_values* is a set, value equality and container length will be checked. + Otherwise, the value of the results will be compared to the expected values. + """ + + left_values = [result.value for result in results] + if isinstance(expected_values, list): + assert left_values == expected_values + elif isinstance(expected_values, set): + assert len(left_values) == len(expected_values) + assert set(left_values) == expected_values + else: + assert results[0].value == expected_values + + +def assert_full_path_equality(results, expected_full_paths): + """Assert equality between two objects. + + *results* must be a list or set of results as returned by `.find()` methods. + + If *expected_full_paths* is a list, then path equality and ordering will be checked. + If *expected_full_paths* is a set, then path equality and length will be checked. + """ + + full_paths = [str(result.full_path) for result in results] + if isinstance(expected_full_paths, list): + assert full_paths == expected_full_paths, full_paths + else: # isinstance(expected_full_paths, set): + assert len(full_paths) == len(expected_full_paths) + assert set(full_paths) == expected_full_paths diff --git a/tests/test_examples.py b/tests/test_examples.py index 1b08123..ca2ec17 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,55 +1,67 @@ import pytest -from jsonpath_ng.ext.filter import Filter, Expression from jsonpath_ng.ext import parse -from jsonpath_ng.jsonpath import * - - -@pytest.mark.parametrize('string, parsed', [ - # The authors of all books in the store - ("$.store.book[*].author", - Child(Child(Child(Child(Root(), Fields('store')), Fields('book')), - Slice()), Fields('author'))), - - # All authors - ("$..author", Descendants(Root(), Fields('author'))), - - # All things in the store - ("$.store.*", Child(Child(Root(), Fields('store')), Fields('*'))), - - # The price of everything in the store - ("$.store..price", - Descendants(Child(Root(), Fields('store')), Fields('price'))), - - # The third book - ("$..book[2]", - Child(Descendants(Root(), Fields('book')),Index(2))), - - # The last book in order - # ("$..book[(@.length-1)]", # Not implemented - # Child(Descendants(Root(), Fields('book')), Slice(start=-1))), - ("$..book[-1:]", - 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[:2]", - Child(Descendants(Root(), Fields('book')), Slice(end=2))), - - # Filter all books with ISBN number - ("$..book[?(@.isbn)]", - Child(Descendants(Root(), Fields('book')), - Filter([Expression(Child(This(), Fields('isbn')), None, None)]))), - - # Filter all books cheaper than 10 - ("$..book[?(@.price<10)]", - Child(Descendants(Root(), Fields('book')), - Filter([Expression(Child(This(), Fields('price')), '<', 10)]))), - - # All members of JSON structure - ("$..*", Descendants(Root(), Fields('*'))), -]) +from jsonpath_ng.ext.filter import Expression, Filter +from jsonpath_ng.jsonpath import Child, Descendants, Fields, Index, Root, Slice, This + + +@pytest.mark.parametrize( + "string, parsed", + [ + # The authors of all books in the store + ( + "$.store.book[*].author", + Child( + Child(Child(Child(Root(), Fields("store")), Fields("book")), Slice()), + Fields("author"), + ), + ), + # + # All authors + ("$..author", Descendants(Root(), Fields("author"))), + # + # All things in the store + ("$.store.*", Child(Child(Root(), Fields("store")), Fields("*"))), + # + # The price of everything in the store + ( + "$.store..price", + Descendants(Child(Root(), Fields("store")), Fields("price")), + ), + # + # The third book + ("$..book[2]", Child(Descendants(Root(), Fields("book")), Index(2))), + # + # The last book in order + # "$..book[(@.length-1)]" # Not implemented + ("$..book[-1:]", Child(Descendants(Root(), Fields("book")), Slice(start=-1))), + # + # The first two books + # "$..book[0,1]" # Not implemented + ("$..book[:2]", Child(Descendants(Root(), Fields("book")), Slice(end=2))), + # + # Filter all books with an ISBN + ( + "$..book[?(@.isbn)]", + Child( + Descendants(Root(), Fields("book")), + Filter([Expression(Child(This(), Fields("isbn")), None, None)]), + ), + ), + # + # Filter all books cheaper than 10 + ( + "$..book[?(@.price<10)]", + Child( + Descendants(Root(), Fields("book")), + Filter([Expression(Child(This(), Fields("price")), "<", 10)]), + ), + ), + # + # All members of JSON structure + ("$..*", Descendants(Root(), Fields("*"))), + ], +) def test_goessner_examples(string, parsed): """ Test Stefan Goessner's `examples`_ @@ -59,16 +71,7 @@ def test_goessner_examples(string, parsed): assert parse(string, debug=True) == parsed -@pytest.mark.parametrize('string, parsed', [ - # Navigate objects - ("$.store.book[0].title", - Child(Child(Child(Child(Root(), Fields('store')), Fields('book')), - Index(0)), Fields('title'))), +def test_attribute_and_dict_syntax(): + """Verify that attribute and dict syntax result in identical parse trees.""" - # Navigate dictionaries - ("$['store']['book'][0]['title']", - Child(Child(Child(Child(Root(), Fields('store')), Fields('book')), - Index(0)), Fields('title'))), -]) -def test_obj_v_dict(string, parsed): - assert parse(string, debug=True) == parsed + assert parse("$.store.book[0].title") == parse("$['store']['book'][0]['title']") diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 5190e1c..f7ad121 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,39 +1,32 @@ import pytest -from jsonpath_ng import parse as rw_parse -from jsonpath_ng.exceptions import JSONPathError, JsonPathParserError +from jsonpath_ng import parse as base_parse +from jsonpath_ng.exceptions import JsonPathParserError from jsonpath_ng.ext import parse as ext_parse -def test_rw_exception_class(): - with pytest.raises(JSONPathError): - rw_parse('foo.bar.`grandparent`.baz') - - @pytest.mark.parametrize( "path", ( - 'foo[*.bar.baz', - 'foo.bar.`grandparent`.baz', - # error at the end of string - 'foo[*', + "foo[*.bar.baz", + "foo.bar.`grandparent`.baz", + "foo[*", # `len` extension not available in the base parser - 'foo.bar.`len`', - ) + "foo.bar.`len`", + ), ) def test_rw_exception_subclass(path): with pytest.raises(JsonPathParserError): - rw_parse(path) + base_parse(path) @pytest.mark.parametrize( "path", ( - 'foo[*.bar.baz', - 'foo.bar.`grandparent`.baz', - # error at the end of string - 'foo[*', - ) + "foo[*.bar.baz", + "foo.bar.`grandparent`.baz", + "foo[*", + ), ) def test_ext_exception_subclass(path): with pytest.raises(JsonPathParserError): diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index 1a155e1..d9c4995 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -1,377 +1,356 @@ -import unittest +import pytest -from jsonpath_ng import jsonpath # For setting the global auto_id_field flag - -from jsonpath_ng.parser import parse -from jsonpath_ng.jsonpath import * +from jsonpath_ng.ext.parser import parse as ext_parse +from jsonpath_ng.jsonpath import DatumInContext, Fields, Root, This from jsonpath_ng.lexer import JsonPathLexerError - -class TestDatumInContext(unittest.TestCase): - """ - Tests of properties of the DatumInContext and AutoIdForDatum objects - """ - - @classmethod - def setup_class(cls): - logging.basicConfig() - - def test_DatumInContext_init(self): - - test_datum1 = DatumInContext(3) - self.assertEqual(test_datum1.path, This()) - self.assertEqual(test_datum1.full_path, This()) - - test_datum2 = DatumInContext(3, path=Root()) - self.assertEqual(test_datum2.path, Root()) - self.assertEqual(test_datum2.full_path, Root()) - - test_datum3 = DatumInContext(3, path=Fields('foo'), context='does not matter') - self.assertEqual(test_datum3.path, Fields('foo')) - self.assertEqual(test_datum3.full_path, Fields('foo')) - - test_datum3 = DatumInContext(3, path=Fields('foo'), context=DatumInContext('does not matter', path=Fields('baz'), context='does not matter')) - self.assertEqual(test_datum3.path, Fields('foo')) - self.assertEqual(test_datum3.full_path, Fields('baz').child(Fields('foo'))) - - def test_DatumInContext_in_context(self): - - self.assertEqual(DatumInContext(3).in_context(path=Fields('foo'), context=DatumInContext('whatever')), - DatumInContext(3, path=Fields('foo'), context=DatumInContext('whatever'))) - - self.assertEqual(DatumInContext(3).in_context(path=Fields('foo'), context='whatever').in_context(path=Fields('baz'), context='whatever'), - DatumInContext(3).in_context(path=Fields('foo'), context=DatumInContext('whatever').in_context(path=Fields('baz'), context='whatever'))) - - # def test_AutoIdForDatum_pseudopath(self): - # assert AutoIdForDatum(DatumInContext(value=3, path=Fields('foo')), id_field='id').pseudopath == Fields('foo') - # assert AutoIdForDatum(DatumInContext(value={'id': 'bizzle'}, path=Fields('foo')), id_field='id').pseudopath == Fields('bizzle') - - # assert AutoIdForDatum(DatumInContext(value={'id': 'bizzle'}, path=Fields('foo')), - # id_field='id', - # context=DatumInContext(value=3, path=This())).pseudopath == Fields('bizzle') - - # assert (AutoIdForDatum(DatumInContext(value=3, path=Fields('foo')), - # id_field='id').in_context(DatumInContext(value={'id': 'bizzle'}, path=This())) - # == - # AutoIdForDatum(DatumInContext(value=3, path=Fields('foo')), - # id_field='id', - # context=DatumInContext(value={'id': 'bizzle'}, path=This()))) - - # assert (AutoIdForDatum(DatumInContext(value=3, path=Fields('foo')), - # id_field='id', - # context=DatumInContext(value={"id": 'bizzle'}, - # path=Fields('maggle'))).in_context(DatumInContext(value='whatever', path=Fields('miggle'))) - # == - # AutoIdForDatum(DatumInContext(value=3, path=Fields('foo')), - # id_field='id', - # context=DatumInContext(value={'id': 'bizzle'}, path=Fields('miggle').child(Fields('maggle'))))) - - # 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): - """ - Tests of the actual jsonpath functionality - """ - - @classmethod - def setup_class(cls): - logging.basicConfig() - +from jsonpath_ng.parser import parse as base_parse + +from .helpers import assert_full_path_equality, assert_value_equality + + +@pytest.mark.parametrize( + "path_arg, context_arg, expected_path, expected_full_path", + ( + (None, None, This(), This()), + (Root(), None, Root(), Root()), + (Fields("foo"), "unimportant", Fields("foo"), Fields("foo")), + ( + Fields("foo"), + DatumInContext("unimportant", path=Fields("baz"), context="unimportant"), + Fields("foo"), + Fields("baz").child(Fields("foo")), + ), + ), +) +def test_datumincontext_init(path_arg, context_arg, expected_path, expected_full_path): + datum = DatumInContext(3, path=path_arg, context=context_arg) + assert datum.path == expected_path + assert datum.full_path == expected_full_path + + +def test_datumincontext_in_context(): + d1 = DatumInContext(3, path=Fields("foo"), context=DatumInContext("bar")) + d2 = DatumInContext(3).in_context(path=Fields("foo"), context=DatumInContext("bar")) + assert d1 == d2 + + +def test_datumincontext_in_context_nested(): + sequential_calls = ( + DatumInContext(3) + .in_context(path=Fields("foo"), context="whatever") + .in_context(path=Fields("baz"), context="whatever") + ) + nested_calls = DatumInContext(3).in_context( + path=Fields("foo"), + context=DatumInContext("whatever").in_context( + path=Fields("baz"), context="whatever" + ), + ) + assert sequential_calls == nested_calls + + +parsers = pytest.mark.parametrize( + "parse", + ( + pytest.param(base_parse, id="parse=jsonpath_ng.parser.parse"), + pytest.param(ext_parse, id="parse=jsonpath_ng.ext.parser.parse"), + ), +) + + +update_test_cases = ( # - # Check that the data value returned is good + # Fields + # ------ # - def check_cases(self, test_cases): - # 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 - - for string, data, target in test_cases: - print('parse("%s").find(%s) =?= %s' % (string, data, target)) - result = parse(string).find(data) - if isinstance(target, list): - self.assertEqual([r.value for r in result], target) - elif isinstance(target, set): - self.assertEqual(set([r.value for r in result]), target) - else: - self.assertEqual(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])) ]) - - jsonpath.auto_id_field = 'id' - self.check_cases([ ('*', {'foo': 1, 'baz': 2}, set([1, 2, '`this`'])) ]) - - def test_root_value(self): - jsonpath.auto_id_field = None - self.check_cases([ - ('$', {'foo': 'baz'}, [{'foo':'baz'}]), - ('foo.$', {'foo': 'baz'}, [{'foo':'baz'}]), - ('foo.$.foo', {'foo': 'baz'}, ['baz']), - ]) - - def test_this_value(self): - jsonpath.auto_id_field = None - self.check_cases([ - ('`this`', {'foo': 'baz'}, [{'foo':'baz'}]), - ('foo.`this`', {'foo': 'baz'}, ['baz']), - ('foo.`this`.baz', {'foo': {'baz': 3}}, [3]), - ]) - - def test_index_value(self): - self.check_cases([ - ('[0]', [42], [42]), - ('[5]', [42], []), - ('[2]', [34, 65, 29, 59], [29]), - ('[0]', None, []) - ]) - - def test_slice_value(self): - self.check_cases([('[*]', [1, 2, 3], [1, 2, 3]), - ('[*]', range(1, 4), [1, 2, 3]), - ('[1:]', [1, 2, 3, 4], [2, 3, 4]), - ('[: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([ - ('[*]', 1, [1]), # This is a funky hack - ('[0:]', 1, [1]), # This is a funky hack - ('[*]', {'foo':1}, [{'foo': 1}]), # This is a funky hack - ('[*].foo', {'foo':1}, [1]), # This is a funky hack - ]) - - 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])]) - - def test_descendants_value(self): - self.check_cases([ - ('foo..baz', {'foo': {'baz': 1, 'bing': {'baz': 2}}}, [1, 2] ), - ('foo..baz', {'foo': [{'baz': 1}, {'baz': 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])]) - - 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])]) - self.assertRaises(JsonPathLexerError, self.check_cases, - [('foo.-baz', {'foo': {'-baz': 8}}, [8])]) - - - - + ("foo", {"foo": 1}, 5, {"foo": 5}), + ("$.*", {"foo": 1, "bar": 2}, 3, {"foo": 3, "bar": 3}), # - # Check that the paths for the data are correct. - # FIXME: merge these tests with the above, since the inputs are the same anyhow + # Indexes + # ------- # - def check_paths(self, test_cases): - # 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 - - for string, data, target in test_cases: - print('parse("%s").find(%s).paths =?= %s' % (string, data, target)) - self.assertEqual(hash(parse(string)), hash(parse(string))) - result = parse(string).find(data) - if isinstance(target, list): - self.assertEqual([str(r.full_path) for r in result], target) - elif isinstance(target, set): - self.assertEqual(set([str(r.full_path) for r in result]), target) - else: - assert self.assertEqual(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'])) ]) - - jsonpath.auto_id_field = 'id' - self.check_paths([ ('*', {'foo': 1, 'baz': 2}, set(['foo', 'baz', 'id'])) ]) - - def test_root_paths(self): - jsonpath.auto_id_field = None - self.check_paths([ - ('$', {'foo': 'baz'}, ['$']), - ('foo.$', {'foo': 'baz'}, ['$']), - ('foo.$.foo', {'foo': 'baz'}, ['foo']), - ]) - - def test_this_paths(self): - jsonpath.auto_id_field = None - self.check_paths([ - ('`this`', {'foo': 'baz'}, ['`this`']), - ('foo.`this`', {'foo': 'baz'}, ['foo']), - ('foo.`this`.baz', {'foo': {'baz': 3}}, ['foo.baz']), - ]) - - def test_index_paths(self): - self.check_paths([('[0]', [42], ['[0]']), - ('[2]', [34, 65, 29, 59], ['[2]'])]) - - def test_slice_paths(self): - self.check_paths([ ('[*]', [1, 2, 3], ['[0]', '[1]', '[2]']), - ('[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]', range(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'])]) - - def test_descendants_paths(self): - self.check_paths([('foo..baz', {'foo': {'baz': 1, 'bing': {'baz': 2}}}, ['foo.baz', 'foo.bing.baz'] )]) - - def test_literals_in_field_names(self): - self.check_paths([("A.'a.c'", {'A' : {'a.c': 'd'}}, ["A.'a.c'"])]) - + ("[0]", ["foo", "bar", "baz"], "test", ["test", "bar", "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'])) ]) - - 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']), - ]) - - 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']), - ]) - - 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]'])]) - - def test_nested_index_auto_id(self): - jsonpath.auto_id_field = "id" - data = { - "id": 1, - "b": {'id': 'bid', 'name': 'bob'}, - "m": [ - {'a': 'a1'}, {'a': 'a2', 'id': 'a2id'} - ] - } - self.check_cases([('m.[1].id', data, ['1.m.a2id']), - ('m.[1].$.b.id', data, ['1.bid']), - ('m.[0].id', data, ['1.m.[0]'])]) - - 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]']) ]) - - 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'])]) - - def test_descendants_auto_id(self): - jsonpath.auto_id_field = "id" - self.check_cases([('foo..baz.id', - {'foo': { - 'baz': 1, - 'bing': { - 'baz': 2 - } - } }, - ['foo.baz', - 'foo.bing.baz'] )]) - - def check_update_cases(self, test_cases): - for original, expr_str, value, expected in test_cases: - print('parse(%r).update(%r, %r) =?= %r' - % (expr_str, original, value, expected)) - expr = parse(expr_str) - actual = expr.update(original, value) - self.assertEqual(actual, expected) - - def test_update_root(self): - self.check_update_cases([ - ('foo', '$', 'bar', 'bar') - ]) - - def test_update_this(self): - self.check_update_cases([ - ('foo', '`this`', 'bar', 'bar') - ]) - - def test_update_fields(self): - self.check_update_cases([ - ({'foo': 1}, 'foo', 5, {'foo': 5}), - ({'foo': 1, 'bar': 2}, '$.*', 3, {'foo': 3, 'bar': 3}) - ]) - - def test_update_child(self): - self.check_update_cases([ - ({'foo': 'bar'}, '$.foo', 'baz', {'foo': 'baz'}), - ({'foo': {'bar': 1}}, 'foo.bar', 'baz', {'foo': {'bar': 'baz'}}) - ]) + # Slices + # ------ + # + ("[0:2]", ["foo", "bar", "baz"], "test", ["test", "test", "baz"]), + # + # Root + # ---- + # + ("$", "foo", "bar", "bar"), + # + # This + # ---- + # + ("`this`", "foo", "bar", "bar"), + # + # Children + # -------- + # + ("$.foo", {"foo": "bar"}, "baz", {"foo": "baz"}), + ("foo.bar", {"foo": {"bar": 1}}, "baz", {"foo": {"bar": "baz"}}), + # + # Descendants + # ----------- + # + ("$..somefield", {"somefield": 1}, 42, {"somefield": 42}), + ( + "$..nestedfield", + {"outer": {"nestedfield": 1}}, + 42, + {"outer": {"nestedfield": 42}}, + ), + ( + "$..bar", + {"outs": {"bar": 1, "ins": {"bar": 9}}, "outs2": {"bar": 2}}, + 42, + {"outs": {"bar": 42, "ins": {"bar": 42}}, "outs2": {"bar": 42}}, + ), + # + # Where + # ----- + # + ( + "*.bar where baz", + {"foo": {"bar": {"baz": 1}}, "bar": {"baz": 2}}, + 5, + {"foo": {"bar": 5}, "bar": {"baz": 2}}, + ), + ( + "(* where flag) .. bar", + {"foo": {"bar": 1, "flag": 1}, "baz": {"bar": 2}}, + 3, + {"foo": {"bar": 3, "flag": 1}, "baz": {"bar": 2}}, + ), +) + + +@pytest.mark.parametrize( + "expression, data, update_value, expected_value", + update_test_cases, +) +@parsers +def test_update(parse, expression, data, update_value, expected_value): + result = parse(expression).update(data, update_value) + assert result == expected_value + + +find_test_cases = ( + # + # * (star) + # -------- + # + ("*", {"foo": 1, "baz": 2}, {1, 2}, {"foo", "baz"}), + # + # Fields + # ------ + # + ("foo", {"foo": "baz"}, ["baz"], ["foo"]), + ("foo,baz", {"foo": 1, "baz": 2}, [1, 2], ["foo", "baz"]), + ("@foo", {"@foo": 1}, [1], ["@foo"]), + # + # Roots + # ----- + # + ("$", {"foo": "baz"}, [{"foo": "baz"}], ["$"]), + ("foo.$", {"foo": "baz"}, [{"foo": "baz"}], ["$"]), + ("foo.$.foo", {"foo": "baz"}, ["baz"], ["foo"]), + # + # This + # ---- + # + ("`this`", {"foo": "baz"}, [{"foo": "baz"}], ["`this`"]), + ("foo.`this`", {"foo": "baz"}, ["baz"], ["foo"]), + ("foo.`this`.baz", {"foo": {"baz": 3}}, [3], ["foo.baz"]), + # + # Indexes + # ------- + # + ("[0]", [42], [42], ["[0]"]), + ("[5]", [42], [], []), + ("[2]", [34, 65, 29, 59], [29], ["[2]"]), + ("[0]", None, [], []), + # + # Slices + # ------ + # + ("[*]", [1, 2, 3], [1, 2, 3], ["[0]", "[1]", "[2]"]), + ("[*]", range(1, 4), [1, 2, 3], ["[0]", "[1]", "[2]"]), + ("[1:]", [1, 2, 3, 4], [2, 3, 4], ["[1]", "[2]", "[3]"]), + ("[1:3]", [1, 2, 3, 4], [2, 3], ["[1]", "[2]"]), + ("[:2]", [1, 2, 3, 4], [1, 2], ["[0]", "[1]"]), + ("[:3:2]", [1, 2, 3, 4], [1, 3], ["[0]", "[2]"]), + ("[1::2]", [1, 2, 3, 4], [2, 4], ["[1]", "[3]"]), + ("[1:6:3]", range(1, 10), [2, 5], ["[1]", "[4]"]), + ("[::-2]", [1, 2, 3, 4, 5], [5, 3, 1], ["[4]", "[2]", "[0]"]), + # + # Slices (funky hacks) + # -------------------- + # + ("[*]", 1, [1], ["[0]"]), + ("[0:]", 1, [1], ["[0]"]), + ("[*]", {"foo": 1}, [{"foo": 1}], ["[0]"]), + ("[*].foo", {"foo": 1}, [1], ["[0].foo"]), + # + # Children + # -------- + # + ("foo.baz", {"foo": {"baz": 3}}, [3], ["foo.baz"]), + ("foo.baz", {"foo": {"baz": [3]}}, [[3]], ["foo.baz"]), + ("foo.baz.qux", {"foo": {"baz": {"qux": 5}}}, [5], ["foo.baz.qux"]), + # + # Descendants + # ----------- + # + ( + "foo..baz", + {"foo": {"baz": 1, "bing": {"baz": 2}}}, + [1, 2], + ["foo.baz", "foo.bing.baz"], + ), + ( + "foo..baz", + {"foo": [{"baz": 1}, {"baz": 2}]}, + [1, 2], + ["foo.[0].baz", "foo.[1].baz"], + ), + # + # Parents + # ------- + # + ("foo.baz.`parent`", {"foo": {"baz": 3}}, [{"baz": 3}], ["foo"]), + ( + "foo.`parent`.foo.baz.`parent`.baz.qux", + {"foo": {"baz": {"qux": 5}}}, + [5], + ["foo.baz.qux"], + ), + # + # Hyphens + # ------- + # + ("foo.bar-baz", {"foo": {"bar-baz": 3}}, [3], ["foo.bar-baz"]), + ( + "foo.[bar-baz,blah-blah]", + {"foo": {"bar-baz": 3, "blah-blah": 5}}, + [3, 5], + ["foo.bar-baz", "foo.blah-blah"], + ), + # + # Literals + # -------- + # + ("A.'a.c'", {"A": {"a.c": "d"}}, ["d"], ["A.'a.c'"]), +) - def test_update_where(self): - self.check_update_cases([ - ({'foo': {'bar': {'baz': 1}}, 'bar': {'baz': 2}}, - '*.bar where baz', 5, {'foo': {'bar': 5}, 'bar': {'baz': 2}}) - ]) - def test_update_descendants_where(self): - self.check_update_cases([ - ({'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 2}}, - '(* where flag) .. bar', 3, - {'foo': {'bar': 3, 'flag': 1}, 'baz': {'bar': 2}}) - ]) +@pytest.mark.parametrize( + "path, data, expected_values, expected_full_paths", find_test_cases +) +@parsers +def test_find(parse, path, data, expected_values, expected_full_paths): + results = parse(path).find(data) - def test_update_descendants(self): - self.check_update_cases([ - ({'somefield': 1}, '$..somefield', 42, {'somefield': 42}), - ({'outer': {'nestedfield': 1}}, '$..nestedfield', 42, {'outer': {'nestedfield': 42}}), - ({'outs': {'bar': 1, 'ins': {'bar': 9}}, 'outs2': {'bar': 2}}, - '$..bar', 42, - {'outs': {'bar': 42, 'ins': {'bar': 42}}, 'outs2': {'bar': 42}}) - ]) + # Verify result values and full paths match expectations. + assert_value_equality(results, expected_values) + assert_full_path_equality(results, expected_full_paths) - def test_update_index(self): - self.check_update_cases([ - (['foo', 'bar', 'baz'], '[0]', 'test', ['test', 'bar', 'baz']) - ]) - def test_update_slice(self): - self.check_update_cases([ - (['foo', 'bar', 'baz'], '[0:2]', 'test', ['test', 'test', 'baz']) - ]) +find_test_cases_with_auto_id = ( + # + # * (star) + # -------- + # + ("*", {"foo": 1, "baz": 2}, {1, 2, "`this`"}), + # + # Fields + # ------ + # + ("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}, {"1", "baz"}), + # + # Roots + # ----- + # + ("$.id", {"foo": "baz"}, ["$"]), + ("foo.$.id", {"foo": "baz", "id": "bizzle"}, ["bizzle"]), + ("foo.$.baz.id", {"foo": 4, "baz": 3}, ["baz"]), + # + # This + # ---- + # + ("id", {"foo": "baz"}, ["`this`"]), + ("foo.`this`.id", {"foo": "baz"}, ["foo"]), + ("foo.`this`.baz.id", {"foo": {"baz": 3}}, ["foo.baz"]), + # + # Indexes + # ------- + # + ("[0].id", [42], ["[0]"]), + ("[2].id", [34, 65, 29, 59], ["[2]"]), + # + # Slices + # ------ + # + ("[*].id", [1, 2, 3], ["[0]", "[1]", "[2]"]), + ("[1:].id", [1, 2, 3, 4], ["[1]", "[2]", "[3]"]), + # + # Children + # -------- + # + ("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"]), + # + # Descendants + # ----------- + # + ( + "foo..baz.id", + {"foo": {"baz": 1, "bing": {"baz": 2}}}, + ["foo.baz", "foo.bing.baz"], + ), +) + + +@pytest.mark.parametrize("path, data, expected_values", find_test_cases_with_auto_id) +@parsers +def test_find_values_auto_id(auto_id_field, parse, path, data, expected_values): + result = parse(path).find(data) + assert_value_equality(result, expected_values) + + +@parsers +def test_find_full_paths_auto_id(auto_id_field, parse): + results = parse("*").find({"foo": 1, "baz": 2}) + assert_full_path_equality(results, {"foo", "baz", "id"}) + + +@pytest.mark.parametrize( + "string, target", + ( + ("m.[1].id", ["1.m.a2id"]), + ("m.[1].$.b.id", ["1.bid"]), + ("m.[0].id", ["1.m.[0]"]), + ), +) +@parsers +def test_nested_index_auto_id(auto_id_field, parse, string, target): + data = { + "id": 1, + "b": {"id": "bid", "name": "bob"}, + "m": [{"a": "a1"}, {"a": "a2", "id": "a2id"}], + } + result = parse(string).find(data) + assert_value_equality(result, target) + + +def test_invalid_hyphenation_in_key(): + with pytest.raises(JsonPathLexerError): + base_parse("foo.-baz") diff --git a/tests/test_jsonpath_rw_ext.py b/tests/test_jsonpath_rw_ext.py index 37eaa46..a4876d5 100644 --- a/tests/test_jsonpath_rw_ext.py +++ b/tests/test_jsonpath_rw_ext.py @@ -1,17 +1,3 @@ -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - """ test_jsonpath_ng_ext ---------------------------------- @@ -19,597 +5,461 @@ Tests for `jsonpath_ng_ext` module. """ -import unittest - -from jsonpath_ng import jsonpath # For setting the global auto_id_field flag +import pytest +from jsonpath_ng.exceptions import JsonPathParserError from jsonpath_ng.ext import parser - -# Example from https://docs.pytest.org/en/7.1.x/example/parametrize.html#a-quick-port-of-testscenarios -def pytest_generate_tests(metafunc): - idlist = [] - argvalues = [] - for scenario in metafunc.cls.scenarios: - idlist.append(scenario[0]) - items = scenario[1].items() - argnames = [x[0] for x in items] - argvalues.append([x[1] for x in items]) - metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class") - - -class Testjsonpath_ng_ext: - scenarios = [ - ('sorted_list', dict(string='objects.`sorted`', - data={'objects': ['alpha', 'gamma', 'beta']}, - target=[['alpha', 'beta', 'gamma']])), - ('sorted_list_indexed', dict(string='objects.`sorted`[1]', - data={'objects': [ - 'alpha', 'gamma', 'beta']}, - target='beta')), - ('sorted_dict', dict(string='objects.`sorted`', - data={'objects': {'cow': 'moo', 'horse': 'neigh', - 'cat': 'meow'}}, - target=[['cat', 'cow', 'horse']])), - ('sorted_dict_indexed', dict(string='objects.`sorted`[0]', - data={'objects': {'cow': 'moo', - 'horse': 'neigh', - 'cat': 'meow'}}, - target='cat')), - - ('len_list', dict(string='objects.`len`', - data={'objects': ['alpha', 'gamma', 'beta']}, - target=3)), - ('len_dict', dict(string='objects.`len`', - data={'objects': {'cow': 'moo', 'cat': 'neigh'}}, - target=2)), - ('len_str', dict(string='objects[0].`len`', - data={'objects': ['alpha', 'gamma']}, - target=5)), - - ('filter_list', dict(string='objects[?@="alpha"]', - data={'objects': ['alpha', 'gamma', 'beta']}, - target=['alpha'])), - ('filter_list_2', dict(string='objects[?@ =~ "a.+"]', - data={'objects': ['alpha', 'gamma', 'beta']}, - target=['alpha','gamma'])), - ('filter_list_3', dict(string='objects[?@ =~ "a.+"]', - data={'objects': [1, 2, 3]}, - target=[])), - - ('keys_list', dict(string='objects.`keys`', - data={'objects': ['alpha', 'gamma', 'beta']}, - target=[])), - ('keys_dict', dict(string='objects.`keys`', - data={'objects': {'cow': 'moo', 'cat': 'neigh'}}, - target=['cow','cat'])), - - ('filter_exists_syntax1', dict(string='objects[?cow]', - data={'objects': [{'cow': 'moo'}, - {'cat': 'neigh'}]}, - target=[{'cow': 'moo'}])), - ('filter_exists_syntax2', dict(string='objects[?@.cow]', - data={'objects': [{'cow': 'moo'}, - {'cat': 'neigh'}]}, - target=[{'cow': 'moo'}])), - ('filter_exists_syntax3', dict(string='objects[?(@.cow)]', - data={'objects': [{'cow': 'moo'}, - {'cat': 'neigh'}]}, - target=[{'cow': 'moo'}])), - ('filter_exists_syntax4', dict(string='objects[?(@."cow!?cat")]', - data={'objects': [{'cow!?cat': 'moo'}, - {'cat': 'neigh'}]}, - target=[{'cow!?cat': 'moo'}])), - ('filter_eq1', dict(string='objects[?cow="moo"]', - data={'objects': [{'cow': 'moo'}, - {'cow': 'neigh'}, - {'cat': 'neigh'}]}, - target=[{'cow': 'moo'}])), - ('filter_eq2', dict(string='objects[?(@.["cow"]="moo")]', - data={'objects': [{'cow': 'moo'}, - {'cow': 'neigh'}, - {'cat': 'neigh'}]}, - target=[{'cow': 'moo'}])), - ('filter_eq3', dict(string='objects[?cow=="moo"]', - data={'objects': [{'cow': 'moo'}, - {'cow': 'neigh'}, - {'cat': 'neigh'}]}, - target=[{'cow': 'moo'}])), - ('filter_gt', dict(string='objects[?cow>5]', - data={'objects': [{'cow': 8}, - {'cow': 7}, - {'cow': 5}, - {'cow': 'neigh'}]}, - target=[{'cow': 8}, {'cow': 7}])), - ('filter_and', dict(string='objects[?cow>5&cat=2]', - data={'objects': [{'cow': 8, 'cat': 2}, - {'cow': 7, 'cat': 2}, - {'cow': 2, 'cat': 2}, - {'cow': 5, 'cat': 3}, - {'cow': 8, 'cat': 3}]}, - target=[{'cow': 8, 'cat': 2}, - {'cow': 7, 'cat': 2}])), - ('filter_float_gt', dict( - string='objects[?confidence>=0.5].prediction', - data={ - 'objects': [ - {'confidence': 0.42, - 'prediction': 'Good'}, - {'confidence': 0.58, - 'prediction': 'Bad'}, +from .helpers import assert_value_equality + +test_cases = ( + pytest.param( + "objects.`sorted`", + {"objects": ["alpha", "gamma", "beta"]}, + [["alpha", "beta", "gamma"]], + id="sorted_list", + ), + pytest.param( + "objects.`sorted`[1]", + {"objects": ["alpha", "gamma", "beta"]}, + "beta", + id="sorted_list_indexed", + ), + pytest.param( + "objects.`sorted`", + {"objects": {"cow": "moo", "horse": "neigh", "cat": "meow"}}, + [["cat", "cow", "horse"]], + id="sorted_dict", + ), + pytest.param( + "objects.`sorted`[0]", + {"objects": {"cow": "moo", "horse": "neigh", "cat": "meow"}}, + "cat", + id="sorted_dict_indexed", + ), + pytest.param( + "objects.`len`", {"objects": ["alpha", "gamma", "beta"]}, 3, id="len_list" + ), + pytest.param( + "objects.`len`", {"objects": {"cow": "moo", "cat": "neigh"}}, 2, id="len_dict" + ), + pytest.param("objects[0].`len`", {"objects": ["alpha", "gamma"]}, 5, id="len_str"), + pytest.param( + 'objects[?@="alpha"]', + {"objects": ["alpha", "gamma", "beta"]}, + ["alpha"], + id="filter_list", + ), + pytest.param( + 'objects[?@ =~ "a.+"]', + {"objects": ["alpha", "gamma", "beta"]}, + ["alpha", "gamma"], + id="filter_list_2", + ), + pytest.param( + 'objects[?@ =~ "a.+"]', {"objects": [1, 2, 3]}, [], id="filter_list_3" + ), + pytest.param( + "objects.`keys`", {"objects": ["alpha", "gamma", "beta"]}, [], id="keys_list" + ), + pytest.param( + "objects.`keys`", + {"objects": {"cow": "moo", "cat": "neigh"}}, + ["cow", "cat"], + id="keys_dict", + ), + pytest.param( + "objects[?cow]", + {"objects": [{"cow": "moo"}, {"cat": "neigh"}]}, + [{"cow": "moo"}], + id="filter_exists_syntax1", + ), + pytest.param( + "objects[?@.cow]", + {"objects": [{"cow": "moo"}, {"cat": "neigh"}]}, + [{"cow": "moo"}], + id="filter_exists_syntax2", + ), + pytest.param( + "objects[?(@.cow)]", + {"objects": [{"cow": "moo"}, {"cat": "neigh"}]}, + [{"cow": "moo"}], + id="filter_exists_syntax3", + ), + pytest.param( + 'objects[?(@."cow!?cat")]', + {"objects": [{"cow!?cat": "moo"}, {"cat": "neigh"}]}, + [{"cow!?cat": "moo"}], + id="filter_exists_syntax4", + ), + pytest.param( + 'objects[?cow="moo"]', + {"objects": [{"cow": "moo"}, {"cow": "neigh"}, {"cat": "neigh"}]}, + [{"cow": "moo"}], + id="filter_eq1", + ), + pytest.param( + 'objects[?(@.["cow"]="moo")]', + {"objects": [{"cow": "moo"}, {"cow": "neigh"}, {"cat": "neigh"}]}, + [{"cow": "moo"}], + id="filter_eq2", + ), + pytest.param( + 'objects[?cow=="moo"]', + {"objects": [{"cow": "moo"}, {"cow": "neigh"}, {"cat": "neigh"}]}, + [{"cow": "moo"}], + id="filter_eq3", + ), + pytest.param( + "objects[?cow>5]", + {"objects": [{"cow": 8}, {"cow": 7}, {"cow": 5}, {"cow": "neigh"}]}, + [{"cow": 8}, {"cow": 7}], + id="filter_gt", + ), + pytest.param( + "objects[?cow>5&cat=2]", + { + "objects": [ + {"cow": 8, "cat": 2}, + {"cow": 7, "cat": 2}, + {"cow": 2, "cat": 2}, + {"cow": 5, "cat": 3}, + {"cow": 8, "cat": 3}, + ] + }, + [{"cow": 8, "cat": 2}, {"cow": 7, "cat": 2}], + id="filter_and", + ), + pytest.param( + "objects[?confidence>=0.5].prediction", + { + "objects": [ + {"confidence": 0.42, "prediction": "Good"}, + {"confidence": 0.58, "prediction": "Bad"}, + ] + }, + ["Bad"], + id="filter_float_gt", + ), + pytest.param( + "objects[/cow]", + { + "objects": [ + {"cat": 1, "cow": 2}, + {"cat": 2, "cow": 1}, + {"cat": 3, "cow": 3}, + ] + }, + [[{"cat": 2, "cow": 1}, {"cat": 1, "cow": 2}, {"cat": 3, "cow": 3}]], + id="sort1", + ), + pytest.param( + "objects[/cow][0].cat", + { + "objects": [ + {"cat": 1, "cow": 2}, + {"cat": 2, "cow": 1}, + {"cat": 3, "cow": 3}, + ] + }, + 2, + id="sort1_indexed", + ), + pytest.param( + "objects[\\cat]", + {"objects": [{"cat": 2}, {"cat": 1}, {"cat": 3}]}, + [[{"cat": 3}, {"cat": 2}, {"cat": 1}]], + id="sort2", + ), + pytest.param( + "objects[\\cat][-1].cat", + {"objects": [{"cat": 2}, {"cat": 1}, {"cat": 3}]}, + 1, + id="sort2_indexed", + ), + pytest.param( + "objects[/cow,\\cat]", + { + "objects": [ + {"cat": 1, "cow": 2}, + {"cat": 2, "cow": 1}, + {"cat": 3, "cow": 1}, + {"cat": 3, "cow": 3}, + ] + }, + [ + [ + {"cat": 3, "cow": 1}, + {"cat": 2, "cow": 1}, + {"cat": 1, "cow": 2}, + {"cat": 3, "cow": 3}, + ] + ], + id="sort3", + ), + pytest.param( + "objects[/cow,\\cat][0].cat", + { + "objects": [ + {"cat": 1, "cow": 2}, + {"cat": 2, "cow": 1}, + {"cat": 3, "cow": 1}, + {"cat": 3, "cow": 3}, + ] + }, + 3, + id="sort3_indexed", + ), + pytest.param( + "objects[/cat.cow]", + { + "objects": [ + {"cat": {"dog": 1, "cow": 2}}, + {"cat": {"dog": 2, "cow": 1}}, + {"cat": {"dog": 3, "cow": 3}}, + ] + }, + [ + [ + {"cat": {"dog": 2, "cow": 1}}, + {"cat": {"dog": 1, "cow": 2}}, + {"cat": {"dog": 3, "cow": 3}}, + ] + ], + id="sort4", + ), + pytest.param( + "objects[/cat.cow][0].cat.dog", + { + "objects": [ + {"cat": {"dog": 1, "cow": 2}}, + {"cat": {"dog": 2, "cow": 1}}, + {"cat": {"dog": 3, "cow": 3}}, + ] + }, + 2, + id="sort4_indexed", + ), + pytest.param( + "objects[/cat.(cow,bow)]", + { + "objects": [ + {"cat": {"dog": 1, "bow": 3}}, + {"cat": {"dog": 2, "cow": 1}}, + {"cat": {"dog": 2, "bow": 2}}, + {"cat": {"dog": 3, "cow": 2}}, + ] + }, + [ + [ + {"cat": {"dog": 2, "cow": 1}}, + {"cat": {"dog": 2, "bow": 2}}, + {"cat": {"dog": 3, "cow": 2}}, + {"cat": {"dog": 1, "bow": 3}}, + ] + ], + id="sort5_twofields", + ), + pytest.param( + "objects[/cat.(cow,bow)][0].cat.dog", + { + "objects": [ + {"cat": {"dog": 1, "bow": 3}}, + {"cat": {"dog": 2, "cow": 1}}, + {"cat": {"dog": 2, "bow": 2}}, + {"cat": {"dog": 3, "cow": 2}}, + ] + }, + 2, + id="sort5_indexed", + ), + pytest.param("3 * 3", {}, [9], id="arithmetic_number_only"), + pytest.param("$.foo * 10", {"foo": 4}, [40], id="arithmetic_mul1"), + pytest.param("10 * $.foo", {"foo": 4}, [40], id="arithmetic_mul2"), + pytest.param("$.foo * 10", {"foo": 4}, [40], id="arithmetic_mul3"), + pytest.param("$.foo * 3", {"foo": "f"}, ["fff"], id="arithmetic_mul4"), + pytest.param("foo * 3", {"foo": "f"}, ["foofoofoo"], id="arithmetic_mul5"), + pytest.param("($.foo * 10 * $.foo) + 2", {"foo": 4}, [162], id="arithmetic_mul6"), + pytest.param("$.foo * 10 * $.foo + 2", {"foo": 4}, [240], id="arithmetic_mul7"), + pytest.param( + "foo + bar", {"foo": "name", "bar": "node"}, ["foobar"], id="arithmetic_str0" + ), + pytest.param( + 'foo + "_" + bar', + {"foo": "name", "bar": "node"}, + ["foo_bar"], + id="arithmetic_str1", + ), + pytest.param( + '$.foo + "_" + $.bar', + {"foo": "name", "bar": "node"}, + ["name_node"], + id="arithmetic_str2", + ), + pytest.param( + "$.foo + $.bar", + {"foo": "name", "bar": "node"}, + ["namenode"], + id="arithmetic_str3", + ), + pytest.param( + "foo.cow + bar.cow", + {"foo": {"cow": "name"}, "bar": {"cow": "node"}}, + ["namenode"], + id="arithmetic_str4", + ), + pytest.param( + "$.objects[*].cow * 2", + {"objects": [{"cow": 1}, {"cow": 2}, {"cow": 3}]}, + [2, 4, 6], + id="arithmetic_list1", + ), + pytest.param( + "$.objects[*].cow * $.objects[*].cow", + {"objects": [{"cow": 1}, {"cow": 2}, {"cow": 3}]}, + [1, 4, 9], + id="arithmetic_list2", + ), + pytest.param( + "$.objects[*].cow * $.objects2[*].cow", + {"objects": [{"cow": 1}, {"cow": 2}, {"cow": 3}], "objects2": [{"cow": 5}]}, + [], + id="arithmetic_list_err1", + ), + pytest.param('$.objects * "foo"', {"objects": []}, [], id="arithmetic_err1"), + pytest.param('"bar" * "foo"', {}, [], id="arithmetic_err2"), + pytest.param( + "payload.metrics[?(@.name='cpu.frequency')].value * 100", + { + "payload": { + "metrics": [ + { + "timestamp": "2013-07-29T06:51:34.472416", + "name": "cpu.frequency", + "value": 1600, + "source": "libvirt.LibvirtDriver", + }, + { + "timestamp": "2013-07-29T06:51:34.472416", + "name": "cpu.user.time", + "value": 17421440000000, + "source": "libvirt.LibvirtDriver", + }, ] - }, - target=['Bad'] - )), - ('sort1', dict(string='objects[/cow]', - data={'objects': [{'cat': 1, 'cow': 2}, - {'cat': 2, 'cow': 1}, - {'cat': 3, 'cow': 3}]}, - target=[[{'cat': 2, 'cow': 1}, - {'cat': 1, 'cow': 2}, - {'cat': 3, 'cow': 3}]])), - ('sort1_indexed', dict(string='objects[/cow][0].cat', - data={'objects': [{'cat': 1, 'cow': 2}, - {'cat': 2, 'cow': 1}, - {'cat': 3, 'cow': 3}]}, - target=2)), - ('sort2', dict(string='objects[\\cat]', - data={'objects': [{'cat': 2}, {'cat': 1}, {'cat': 3}]}, - target=[[{'cat': 3}, {'cat': 2}, {'cat': 1}]])), - ('sort2_indexed', dict(string='objects[\\cat][-1].cat', - data={'objects': [{'cat': 2}, {'cat': 1}, - {'cat': 3}]}, - target=1)), - ('sort3', dict(string='objects[/cow,\\cat]', - data={'objects': [{'cat': 1, 'cow': 2}, - {'cat': 2, 'cow': 1}, - {'cat': 3, 'cow': 1}, - {'cat': 3, 'cow': 3}]}, - target=[[{'cat': 3, 'cow': 1}, - {'cat': 2, 'cow': 1}, - {'cat': 1, 'cow': 2}, - {'cat': 3, 'cow': 3}]])), - ('sort3_indexed', dict(string='objects[/cow,\\cat][0].cat', - data={'objects': [{'cat': 1, 'cow': 2}, - {'cat': 2, 'cow': 1}, - {'cat': 3, 'cow': 1}, - {'cat': 3, 'cow': 3}]}, - target=3)), - ('sort4', dict(string='objects[/cat.cow]', - data={'objects': [{'cat': {'dog': 1, 'cow': 2}}, - {'cat': {'dog': 2, 'cow': 1}}, - {'cat': {'dog': 3, 'cow': 3}}]}, - target=[[{'cat': {'dog': 2, 'cow': 1}}, - {'cat': {'dog': 1, 'cow': 2}}, - {'cat': {'dog': 3, 'cow': 3}}]])), - ('sort4_indexed', dict(string='objects[/cat.cow][0].cat.dog', - data={'objects': [{'cat': {'dog': 1, - 'cow': 2}}, - {'cat': {'dog': 2, - 'cow': 1}}, - {'cat': {'dog': 3, - 'cow': 3}}]}, - target=2)), - ('sort5_twofields', dict(string='objects[/cat.(cow,bow)]', - data={'objects': - [{'cat': {'dog': 1, 'bow': 3}}, - {'cat': {'dog': 2, 'cow': 1}}, - {'cat': {'dog': 2, 'bow': 2}}, - {'cat': {'dog': 3, 'cow': 2}}]}, - target=[[{'cat': {'dog': 2, 'cow': 1}}, - {'cat': {'dog': 2, 'bow': 2}}, - {'cat': {'dog': 3, 'cow': 2}}, - {'cat': {'dog': 1, 'bow': 3}}]])), - - ('sort5_indexed', dict(string='objects[/cat.(cow,bow)][0].cat.dog', - data={'objects': - [{'cat': {'dog': 1, 'bow': 3}}, - {'cat': {'dog': 2, 'cow': 1}}, - {'cat': {'dog': 2, 'bow': 2}}, - {'cat': {'dog': 3, 'cow': 2}}]}, - target=2)), - ('arithmetic_number_only', dict(string='3 * 3', data={}, - target=[9])), - - ('arithmetic_mul1', dict(string='$.foo * 10', data={'foo': 4}, - target=[40])), - ('arithmetic_mul2', dict(string='10 * $.foo', data={'foo': 4}, - target=[40])), - ('arithmetic_mul3', dict(string='$.foo * 10', data={'foo': 4}, - target=[40])), - ('arithmetic_mul4', dict(string='$.foo * 3', data={'foo': 'f'}, - target=['fff'])), - ('arithmetic_mul5', dict(string='foo * 3', data={'foo': 'f'}, - target=['foofoofoo'])), - ('arithmetic_mul6', dict(string='($.foo * 10 * $.foo) + 2', - data={'foo': 4}, target=[162])), - ('arithmetic_mul7', dict(string='$.foo * 10 * $.foo + 2', - data={'foo': 4}, target=[240])), - - ('arithmetic_str0', dict(string='foo + bar', - data={'foo': 'name', "bar": "node"}, - target=["foobar"])), - ('arithmetic_str1', dict(string='foo + "_" + bar', - data={'foo': 'name', "bar": "node"}, - target=["foo_bar"])), - ('arithmetic_str2', dict(string='$.foo + "_" + $.bar', - data={'foo': 'name', "bar": "node"}, - target=["name_node"])), - ('arithmetic_str3', dict(string='$.foo + $.bar', - data={'foo': 'name', "bar": "node"}, - target=["namenode"])), - ('arithmetic_str4', dict(string='foo.cow + bar.cow', - data={'foo': {'cow': 'name'}, - "bar": {'cow': "node"}}, - target=["namenode"])), - - ('arithmetic_list1', dict(string='$.objects[*].cow * 2', - data={'objects': [{'cow': 1}, - {'cow': 2}, - {'cow': 3}]}, - target=[2, 4, 6])), - - ('arithmetic_list2', dict(string='$.objects[*].cow * $.objects[*].cow', - data={'objects': [{'cow': 1}, - {'cow': 2}, - {'cow': 3}]}, - target=[1, 4, 9])), - - ('arithmetic_list_err1', dict( - string='$.objects[*].cow * $.objects2[*].cow', - data={'objects': [{'cow': 1}, {'cow': 2}, {'cow': 3}], - 'objects2': [{'cow': 5}]}, - target=[])), - - ('arithmetic_err1', dict(string='$.objects * "foo"', - data={'objects': []}, target=[])), - ('arithmetic_err2', dict(string='"bar" * "foo"', data={}, target=[])), - - ('real_life_example1', dict( - string="payload.metrics[?(@.name='cpu.frequency')].value * 100", - data={'payload': {'metrics': [ - {'timestamp': '2013-07-29T06:51:34.472416', - 'name': 'cpu.frequency', - 'value': 1600, - 'source': 'libvirt.LibvirtDriver'}, - {'timestamp': '2013-07-29T06:51:34.472416', - 'name': 'cpu.user.time', - 'value': 17421440000000, - 'source': 'libvirt.LibvirtDriver'}]}}, - target=[160000])), - - ('real_life_example2', dict( - string="payload.(id|(resource.id))", - data={'payload': {'id': 'foobar'}}, - target=['foobar'])), - ('real_life_example3', dict( - string="payload.id|(resource.id)", - data={'payload': {'resource': - {'id': 'foobar'}}}, - target=['foobar'])), - ('real_life_example4', dict( - string="payload.id|(resource.id)", - data={'payload': {'id': 'yes', - 'resource': {'id': 'foobar'}}}, - target=['yes', 'foobar'])), - - ('sub1', dict( - string="payload.`sub(/(foo\\\\d+)\\\\+(\\\\d+bar)/, \\\\2-\\\\1)`", - data={'payload': "foo5+3bar"}, - target=["3bar-foo5"] - )), - ('sub2', dict( - string='payload.`sub(/foo\\\\+bar/, repl)`', - data={'payload': "foo+bar"}, - target=["repl"] - )), - ('str1', dict( - string='payload.`str()`', - data={'payload': 1}, - target=["1"] - )), - ('split1', dict( - string='payload.`split(-, 2, -1)`', - data={'payload': "foo-bar-cat-bow"}, - target=["cat"] - )), - ('split2', dict( - string='payload.`split(-, 2, 2)`', - data={'payload': "foo-bar-cat-bow"}, - target=["cat-bow"] - )), - - ('bug-#2-correct', dict( - string='foo[?(@.baz==1)]', - data={'foo': [{'baz': 1}, {'baz': 2}]}, - target=[{'baz': 1}], - )), - - ('bug-#2-wrong', dict( - string='foo[*][?(@.baz==1)]', - data={'foo': [{'baz': 1}, {'baz': 2}]}, - target=[], - )), - - ('boolean-filter-true', dict( - string='foo[?flag = true].color', - data={'foo': [{"color": "blue", "flag": True}, - {"color": "green", "flag": False}]}, - target=['blue'] - )), - - ('boolean-filter-false', dict( - string='foo[?flag = false].color', - data={'foo': [{"color": "blue", "flag": True}, - {"color": "green", "flag": False}]}, - target=['green'] - )), - - ('boolean-filter-other-datatypes-involved', dict( - string='foo[?flag = true].color', - data={'foo': [{"color": "blue", "flag": True}, - {"color": "green", "flag": 2}, - {"color": "red", "flag": "hi"}]}, - target=['blue'] - )), - - ('boolean-filter-string-true-string-literal', dict( - string='foo[?flag = "true"].color', - data={'foo': [{"color": "blue", "flag": True}, - {"color": "green", "flag": "true"}]}, - target=['green'] - )), - ] - - def test_fields_value(self, string, data, target): - jsonpath.auto_id_field = None - result = parser.parse(string, debug=True).find(data) - if isinstance(target, list): - assert target == [r.value for r in result] - elif isinstance(target, set): - assert target == set([r.value for r in result]) - elif isinstance(target, (int, float)): - assert target == result[0].value - else: - assert target == result[0].value - -# NOTE(sileht): copy of tests/test_jsonpath.py -# to ensure we didn't break jsonpath_ng - - -class TestJsonPath(unittest.TestCase): - """Tests of the actual jsonpath functionality """ - - # - # Check that the data value returned is good - # - def check_cases(self, test_cases): - # 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 - - for string, data, target in test_cases: - print('parse("%s").find(%s) =?= %s' % (string, data, target)) - result = parser.parse(string).find(data) - if isinstance(target, list): - assert [r.value for r in result] == target - elif isinstance(target, set): - assert set([r.value for r in result]) == target - else: - 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]))]) - - jsonpath.auto_id_field = 'id' - self.check_cases([('*', {'foo': 1, 'baz': 2}, set([1, 2, '`this`']))]) - - def test_root_value(self): - jsonpath.auto_id_field = None - self.check_cases([ - ('$', {'foo': 'baz'}, [{'foo': 'baz'}]), - ('foo.$', {'foo': 'baz'}, [{'foo': 'baz'}]), - ('foo.$.foo', {'foo': 'baz'}, ['baz']), - ]) - - def test_this_value(self): - jsonpath.auto_id_field = None - self.check_cases([ - ('`this`', {'foo': 'baz'}, [{'foo': 'baz'}]), - ('foo.`this`', {'foo': 'baz'}, ['baz']), - ('foo.`this`.baz', {'foo': {'baz': 3}}, [3]), - ]) - - def test_index_value(self): - self.check_cases([ - ('[0]', [42], [42]), - ('[5]', [42], []), - ('[2]', [34, 65, 29, 59], [29]) - ]) - - def test_slice_value(self): - self.check_cases([('[*]', [1, 2, 3], [1, 2, 3]), - ('[*]', range(1, 4), [1, 2, 3]), - ('[1:]', [1, 2, 3, 4], [2, 3, 4]), - ('[:2]', [1, 2, 3, 4], [1, 2])]) - - # Funky slice hacks - self.check_cases([ - ('[*]', 1, [1]), # This is a funky hack - ('[0:]', 1, [1]), # This is a funky hack - ('[*]', {'foo': 1}, [{'foo': 1}]), # This is a funky hack - ('[*].foo', {'foo': 1}, [1]), # This is a funky hack - ]) - - 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])]) - - def test_descendants_value(self): - self.check_cases([ - ('foo..baz', {'foo': {'baz': 1, 'bing': {'baz': 2}}}, [1, 2]), - ('foo..baz', {'foo': [{'baz': 1}, {'baz': 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])]) - - def test_hyphen_key(self): - # NOTE(sileht): hyphen is now a operator - # so to use it has key we must escape it with quote - # 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])]) - 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])]) - # self.assertRaises(lexer.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): - # 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 - - for string, data, target in test_cases: - print('parse("%s").find(%s).paths =?= %s' % (string, data, target)) - result = parser.parse(string).find(data) - if isinstance(target, list): - assert [str(r.full_path) for r in result] == target - elif isinstance(target, set): - assert set([str(r.full_path) for r in result]) == target - else: - assert str(result.path) == target - - def test_filter_with_filtering(self): - data = {"foos": [{"id": 1, "name": "first"}, {"id": 2, "name": "second"}]} - result = parser.parse('$.foos[?(@.name=="second")]').filter( - lambda _: True, data - ) - names = [item["name"] for item in result["foos"]] - assert "second" not in names - - 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']))]) - - jsonpath.auto_id_field = 'id' - self.check_paths([('*', {'foo': 1, 'baz': 2}, - set(['foo', 'baz', 'id']))]) - - def test_root_paths(self): - jsonpath.auto_id_field = None - self.check_paths([ - ('$', {'foo': 'baz'}, ['$']), - ('foo.$', {'foo': 'baz'}, ['$']), - ('foo.$.foo', {'foo': 'baz'}, ['foo']), - ]) - - def test_this_paths(self): - jsonpath.auto_id_field = None - self.check_paths([ - ('`this`', {'foo': 'baz'}, ['`this`']), - ('foo.`this`', {'foo': 'baz'}, ['foo']), - ('foo.`this`.baz', {'foo': {'baz': 3}}, ['foo.baz']), - ]) - - def test_index_paths(self): - self.check_paths([('[0]', [42], ['[0]']), - ('[2]', [34, 65, 29, 59], ['[2]'])]) - - def test_slice_paths(self): - self.check_paths([('[*]', [1, 2, 3], ['[0]', '[1]', '[2]']), - ('[1:]', [1, 2, 3, 4], ['[1]', '[2]', '[3]'])]) - - 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'])]) - - def test_descendants_paths(self): - 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']))]) - - 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']), - ]) - - 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']), - ]) - - 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]'])]) - - 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]'])]) - - 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'])]) - - def test_descendants_auto_id(self): - jsonpath.auto_id_field = "id" - self.check_cases([('foo..baz.id', - {'foo': { - 'baz': 1, - 'bing': { - 'baz': 2 - } - }}, - ['foo.baz', - 'foo.bing.baz'])]) + } + }, + [160000], + id="real_life_example1", + ), + pytest.param( + "payload.(id|(resource.id))", + {"payload": {"id": "foobar"}}, + ["foobar"], + id="real_life_example2", + ), + pytest.param( + "payload.id|(resource.id)", + {"payload": {"resource": {"id": "foobar"}}}, + ["foobar"], + id="real_life_example3", + ), + pytest.param( + "payload.id|(resource.id)", + {"payload": {"id": "yes", "resource": {"id": "foobar"}}}, + ["yes", "foobar"], + id="real_life_example4", + ), + pytest.param( + "payload.`sub(/(foo\\\\d+)\\\\+(\\\\d+bar)/, \\\\2-\\\\1)`", + {"payload": "foo5+3bar"}, + ["3bar-foo5"], + id="sub1", + ), + pytest.param( + "payload.`sub(/foo\\\\+bar/, repl)`", + {"payload": "foo+bar"}, + ["repl"], + id="sub2", + ), + pytest.param("payload.`str()`", {"payload": 1}, ["1"], id="str1"), + pytest.param( + "payload.`split(-, 2, -1)`", + {"payload": "foo-bar-cat-bow"}, + ["cat"], + id="split1", + ), + pytest.param( + "payload.`split(-, 2, 2)`", + {"payload": "foo-bar-cat-bow"}, + ["cat-bow"], + id="split2", + ), + pytest.param( + "foo[?(@.baz==1)]", + {"foo": [{"baz": 1}, {"baz": 2}]}, + [{"baz": 1}], + id="bug-#2-correct", + ), + pytest.param( + "foo[*][?(@.baz==1)]", {"foo": [{"baz": 1}, {"baz": 2}]}, [], id="bug-#2-wrong" + ), + pytest.param( + "foo[?flag = true].color", + { + "foo": [ + {"color": "blue", "flag": True}, + {"color": "green", "flag": False}, + ] + }, + ["blue"], + id="boolean-filter-true", + ), + pytest.param( + "foo[?flag = false].color", + { + "foo": [ + {"color": "blue", "flag": True}, + {"color": "green", "flag": False}, + ] + }, + ["green"], + id="boolean-filter-false", + ), + pytest.param( + "foo[?flag = true].color", + { + "foo": [ + {"color": "blue", "flag": True}, + {"color": "green", "flag": 2}, + {"color": "red", "flag": "hi"}, + ] + }, + ["blue"], + id="boolean-filter-other-datatypes-involved", + ), + pytest.param( + 'foo[?flag = "true"].color', + { + "foo": [ + {"color": "blue", "flag": True}, + {"color": "green", "flag": "true"}, + ] + }, + ["green"], + id="boolean-filter-string-true-string-literal", + ), +) + + +@pytest.mark.parametrize("path, data, expected_values", test_cases) +def test_values(path, data, expected_values): + results = parser.parse(path).find(data) + assert_value_equality(results, expected_values) + + +def test_invalid_hyphenation_in_key(): + # This test is almost copied-and-pasted directly from `test_jsonpath.py`. + # However, the parsers generate different exceptions for this syntax error. + # This discrepancy needs to be resolved. + with pytest.raises(JsonPathParserError): + parser.parse("foo.-baz") diff --git a/tests/test_lexer.py b/tests/test_lexer.py index 4872334..85533f5 100644 --- a/tests/test_lexer.py +++ b/tests/test_lexer.py @@ -1,68 +1,55 @@ -import logging -import unittest - -from ply.lex import LexToken +import pytest from jsonpath_ng.lexer import JsonPathLexer, JsonPathLexerError -class TestLexer(unittest.TestCase): - - def token(self, value, ty=None): - t = LexToken() - t.type = ty if ty != None else value - t.value = value - t.lineno = -1 - t.lexpos = -1 - return t - - def assert_lex_equiv(self, s, stream2): - # NOTE: lexer fails to reset after call? - l = JsonPathLexer(debug=True) - stream1 = list(l.tokenize(s)) # Save the stream for debug output when a test fails - stream2 = list(stream2) - assert len(stream1) == len(stream2) - for token1, token2 in zip(stream1, stream2): - print(token1, token2) - assert token1.type == token2.type - assert token1.value == token2.value - - @classmethod - def setup_class(cls): - logging.basicConfig() - - def test_simple_inputs(self): - self.assert_lex_equiv('$', [self.token('$', '$')]) - self.assert_lex_equiv('"hello"', [self.token('hello', 'ID')]) - self.assert_lex_equiv("'goodbye'", [self.token('goodbye', 'ID')]) - self.assert_lex_equiv("'doublequote\"'", [self.token('doublequote"', 'ID')]) - self.assert_lex_equiv(r'"doublequote\""', [self.token('doublequote"', 'ID')]) - self.assert_lex_equiv(r"'singlequote\''", [self.token("singlequote'", 'ID')]) - self.assert_lex_equiv('"singlequote\'"', [self.token("singlequote'", 'ID')]) - self.assert_lex_equiv('fuzz', [self.token('fuzz', 'ID')]) - self.assert_lex_equiv('1', [self.token(1, 'NUMBER')]) - self.assert_lex_equiv('45', [self.token(45, 'NUMBER')]) - self.assert_lex_equiv('-1', [self.token(-1, 'NUMBER')]) - self.assert_lex_equiv(' -13 ', [self.token(-13, 'NUMBER')]) - self.assert_lex_equiv('"fuzz.bang"', [self.token('fuzz.bang', 'ID')]) - self.assert_lex_equiv('fuzz.bang', [self.token('fuzz', 'ID'), self.token('.', '.'), self.token('bang', 'ID')]) - self.assert_lex_equiv('fuzz.*', [self.token('fuzz', 'ID'), self.token('.', '.'), self.token('*', '*')]) - self.assert_lex_equiv('fuzz..bang', [self.token('fuzz', 'ID'), self.token('..', 'DOUBLEDOT'), self.token('bang', 'ID')]) - 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('|', [self.token('|', '|')]) - self.assert_lex_equiv('where', [self.token('where', 'WHERE')]) - - def test_basic_errors(self): - def tokenize(s): - l = JsonPathLexer(debug=True) - return list(l.tokenize(s)) - - self.assertRaises(JsonPathLexerError, tokenize, "'\"") - self.assertRaises(JsonPathLexerError, tokenize, '"\'') - self.assertRaises(JsonPathLexerError, tokenize, '`"') - self.assertRaises(JsonPathLexerError, tokenize, "`'") - self.assertRaises(JsonPathLexerError, tokenize, '"`') - self.assertRaises(JsonPathLexerError, tokenize, "'`") - self.assertRaises(JsonPathLexerError, tokenize, '?') - self.assertRaises(JsonPathLexerError, tokenize, '$.foo.bar.#') +token_test_cases = ( + ("$", (("$", "$"),)), + ('"hello"', (("hello", "ID"),)), + ("'goodbye'", (("goodbye", "ID"),)), + ("'doublequote\"'", (('doublequote"', "ID"),)), + (r'"doublequote\""', (('doublequote"', "ID"),)), + (r"'singlequote\''", (("singlequote'", "ID"),)), + ('"singlequote\'"', (("singlequote'", "ID"),)), + ("fuzz", (("fuzz", "ID"),)), + ("1", ((1, "NUMBER"),)), + ("45", ((45, "NUMBER"),)), + ("-1", ((-1, "NUMBER"),)), + (" -13 ", ((-13, "NUMBER"),)), + ('"fuzz.bang"', (("fuzz.bang", "ID"),)), + ("fuzz.bang", (("fuzz", "ID"), (".", "."), ("bang", "ID"))), + ("fuzz.*", (("fuzz", "ID"), (".", "."), ("*", "*"))), + ("fuzz..bang", (("fuzz", "ID"), ("..", "DOUBLEDOT"), ("bang", "ID"))), + ("&", (("&", "&"),)), + ("@", (("@", "ID"),)), + ("`this`", (("this", "NAMED_OPERATOR"),)), + ("|", (("|", "|"),)), + ("where", (("where", "WHERE"),)), +) + + +@pytest.mark.parametrize("string, expected_token_info", token_test_cases) +def test_lexer(string, expected_token_info): + lexer = JsonPathLexer(debug=True) + tokens = list(lexer.tokenize(string)) + assert len(tokens) == len(expected_token_info) + for token, (expected_value, expected_type) in zip(tokens, expected_token_info): + assert token.type == expected_type + assert token.value == expected_value + + +invalid_token_test_cases = ( + "'\"", + "\"'", + '`"', + "`'", + '"`', + "'`", + "?", + "$.foo.bar.#", +) + + +@pytest.mark.parametrize("string", invalid_token_test_cases) +def test_lexer_errors(string): + with pytest.raises(JsonPathLexerError): + list(JsonPathLexer().tokenize(string)) diff --git a/tests/test_parser.py b/tests/test_parser.py index e4a9da5..c54b489 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,39 +1,38 @@ -import unittest +import pytest +from jsonpath_ng.jsonpath import Child, Descendants, Fields, Index, Slice, Where from jsonpath_ng.lexer import JsonPathLexer from jsonpath_ng.parser import JsonPathParser -from jsonpath_ng.jsonpath import * -class TestParser(unittest.TestCase): - # TODO: This will be much more effective with a few regression tests and `arbitrary` parse . pretty testing +# Format: (string, expected_object) +parser_test_cases = ( + # + # Atomic + # ------ + # + ("foo", Fields("foo")), + ("*", Fields("*")), + ("baz,bizzle", Fields("baz", "bizzle")), + ("[1]", Index(1)), + ("[1:]", Slice(start=1)), + ("[:]", Slice()), + ("[*]", Slice()), + ("[:2]", Slice(end=2)), + ("[1:2]", Slice(start=1, end=2)), + ("[5:-2]", Slice(start=5, end=-2)), + # + # Nested + # ------ + # + ("foo.baz", Child(Fields("foo"), Fields("baz"))), + ("foo.baz,bizzle", Child(Fields("foo"), Fields("baz", "bizzle"))), + ("foo where baz", Where(Fields("foo"), Fields("baz"))), + ("foo..baz", Descendants(Fields("foo"), Fields("baz"))), + ("foo..baz.bing", Descendants(Fields("foo"), Child(Fields("baz"), Fields("bing")))), +) - @classmethod - def setup_class(cls): - logging.basicConfig() - 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 - assert parser.parse(string) == parsed - - def test_atomic(self): - self.check_parse_cases([('foo', Fields('foo')), - ('*', Fields('*')), - ('baz,bizzle', Fields('baz','bizzle')), - ('[1]', Index(1)), - ('[1:]', Slice(start=1)), - ('[:]', Slice()), - ('[*]', Slice()), - ('[:2]', Slice(end=2)), - ('[1:2]', Slice(start=1, end=2)), - ('[5:-2]', Slice(start=5, end=-2)) - ]) - - 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'))), - ('foo..baz', Descendants(Fields('foo'), Fields('baz'))), - ('foo..baz.bing', Descendants(Fields('foo'), Child(Fields('baz'), Fields('bing'))))]) +@pytest.mark.parametrize("string, expected_object", parser_test_cases) +def test_parser(string, expected_object): + parser = JsonPathParser(lexer_class=lambda: JsonPathLexer()) + assert parser.parse(string) == expected_object