diff --git a/jmespath/visitor.py b/jmespath/visitor.py index 15fb1774..5404fda0 100644 --- a/jmespath/visitor.py +++ b/jmespath/visitor.py @@ -6,10 +6,24 @@ def _equals(x, y): - if _is_special_number_case(x, y): - return False - else: - return x == y + pending = [(x, y)] + while pending: + x, y = pending.pop() + if _is_special_number_case(x, y): + return False + elif isinstance(x, list) and isinstance(y, list): + if len(x) != len(y): + return False + if x is not y: + pending.extend(zip(reversed(x), reversed(y))) + elif isinstance(x, dict) and isinstance(y, dict): + if x.keys() != y.keys(): + return False + if x is not y: + pending.extend((x[key], y[key]) for key in reversed(x)) + elif not (x == y): + return False + return True def _is_special_number_case(x, y): diff --git a/tests/test_search.py b/tests/test_search.py index 4832079b..fd3fa664 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,5 +1,6 @@ import sys import decimal +import json from tests import unittest, OrderedDict import jmespath @@ -62,3 +63,52 @@ def test_can_handle_decimals_as_numeric_type(self): result = decimal.Decimal('3') self.assertEqual(jmespath.search('[?a >= `1`].a', [{'a': result}]), [result]) + + +class TestNestedEquality(unittest.TestCase): + def test_nested_booleans_are_not_numbers(self): + for left, right in [ + ([True], [1]), + ([False], [0]), + ({'value': True}, {'value': 1}), + ({'value': [False]}, {'value': [0]}), + ([{'value': True}], [{'value': 1.0}])]: + for a, b in [(left, right), (right, left)]: + data = {'a': a, 'b': b} + self.assertFalse(jmespath.search('a == b', data)) + self.assertTrue(jmespath.search('a != b', data)) + + def test_nested_equality_preserves_json_semantics(self): + cases = [ + ([1], [1.0], True), + ({'a': [True, 1]}, {'a': [True, 1.0]}, True), + ({'a': 1, 'b': 2}, {'b': 2, 'a': 1}, True), + ([1], [1, 2], False), + ({'a': None}, {'b': None}, False), + ([], {}, False), + ([], [], True), + ({}, {}, True), + ] + for a, b, expected in cases: + self.assertEqual(jmespath.search('a == b', {'a': a, 'b': b}), + expected) + + def test_filter_rejects_nested_boolean_number_matches(self): + data = [{'value': [True]}, {'value': [1]}, {'value': [1.0]}] + self.assertEqual(jmespath.search('[?value == `[1]`]', data), + data[1:]) + + + def test_deeply_nested_json_equality(self): + for opening, closing in [('[', ']'), ('{"value":', '}')]: + for left, right, expected in [ + ('1', '1.0', True), + ('true', '1', False), + ('1', '2', False), + ('[]', '{}', False)]: + a = json.loads(opening * 600 + left + closing * 600) + b = json.loads(opening * 600 + right + closing * 600) + self.assertIsNot(a, b) + data = {'a': a, 'b': b} + self.assertEqual(jmespath.search('a == b', data), expected) + self.assertEqual(jmespath.search('a != b', data), not expected)