From 230f225735748f8550eee215c4c6e4ac87c4d7b8 Mon Sep 17 00:00:00 2001 From: Seperman Date: Sun, 5 Nov 2023 13:00:21 -0800 Subject: [PATCH] fixes #420 Where if the key of a dictionary contains the characters used in the path the path is actually corrupted. --- deepdiff/delta.py | 1 + deepdiff/model.py | 16 +++++++++++++++- deepdiff/path.py | 14 ++++++++++---- tests/test_delta.py | 7 +++++++ tests/test_diff_text.py | 11 +++++++++++ tests/test_path.py | 14 ++++++++++++-- tests/test_serialization.py | 2 ++ 7 files changed, 58 insertions(+), 7 deletions(-) diff --git a/deepdiff/delta.py b/deepdiff/delta.py index 0ee1ed84..bb358258 100644 --- a/deepdiff/delta.py +++ b/deepdiff/delta.py @@ -623,6 +623,7 @@ def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True): include_action_in_path : Boolean, default=False When False, we translate DeepDiff's paths like root[3].attribute1 into a [3, 'attribute1']. When True, we include the action to retrieve the item in the path: [(3, 'GET'), ('attribute1', 'GETATTR')] + Note that the "action" here is the different than the action reported by to_flat_dicts. The action here is just about the "path" output. report_type_changes : Boolean, default=True If False, we don't report the type change. Instead we report the value change. diff --git a/deepdiff/model.py b/deepdiff/model.py index 4b846b21..3723e2ba 100644 --- a/deepdiff/model.py +++ b/deepdiff/model.py @@ -874,7 +874,21 @@ def stringify_param(self, force=None): """ param = self.param if isinstance(param, strings): - result = param if self.quote_str is None else self.quote_str.format(param) + has_quote = "'" in param + has_double_quote = '"' in param + if has_quote and has_double_quote: + new_param = [] + for char in param: + if char in {'"', "'"}: + new_param.append('\\') + new_param.append(char) + param = ''.join(new_param) + elif has_quote: + result = f'"{param}"' + elif has_double_quote: + result = f"'{param}'" + else: + result = param if self.quote_str is None else self.quote_str.format(param) elif isinstance(param, tuple): # Currently only for numpy ndarrays result = ']['.join(map(repr, param)) else: diff --git a/deepdiff/path.py b/deepdiff/path.py index a228d0ab..0c941cfe 100644 --- a/deepdiff/path.py +++ b/deepdiff/path.py @@ -53,15 +53,21 @@ def _path_to_elements(path, root_element=DEFAULT_FIRST_ELEMENT): path = path[4:] # removing "root from the beginning" brackets = [] inside_quotes = False + quote_used = '' for char in path: if prev_char == '\\': elem += char elif char in {'"', "'"}: elem += char - inside_quotes = not inside_quotes - if not inside_quotes: - _add_to_elements(elements, elem, inside) - elem = '' + # If we are inside and the quote is not what we expected, the quote is not closing + if not(inside_quotes and quote_used != char): + inside_quotes = not inside_quotes + if inside_quotes: + quote_used = char + else: + _add_to_elements(elements, elem, inside) + elem = '' + quote_used = '' elif inside_quotes: elem += char elif char == '[': diff --git a/tests/test_delta.py b/tests/test_delta.py index dcb2bd71..79c9b7d8 100644 --- a/tests/test_delta.py +++ b/tests/test_delta.py @@ -681,6 +681,13 @@ def test_delta_dict_items_added_retain_order(self): 'to_delta_kwargs': {'directed': True}, 'expected_delta_dict': {'iterable_item_removed': {'root[4]': 4}} }, + 'delta_case20_quotes_in_path': { + 't1': {"a']['b']['c": 1}, + 't2': {"a']['b']['c": 2}, + 'deepdiff_kwargs': {}, + 'to_delta_kwargs': {'directed': True}, + 'expected_delta_dict': {'values_changed': {'root["a\'][\'b\'][\'c"]': {'new_value': 2}}} + }, } diff --git a/tests/test_diff_text.py b/tests/test_diff_text.py index 9f750b29..d47b0f3c 100755 --- a/tests/test_diff_text.py +++ b/tests/test_diff_text.py @@ -297,6 +297,17 @@ def test_string_difference_ignore_case(self): result = {} assert result == ddiff + def test_diff_quote_in_string(self): + t1 = { + "a']['b']['c": 1 + } + t2 = { + "a']['b']['c": 2 + } + diff = DeepDiff(t1, t2) + expected = {'values_changed': {'''root["a']['b']['c"]''': {'new_value': 2, 'old_value': 1}}} + assert expected == diff + def test_bytes(self): t1 = { 1: 1, diff --git a/tests/test_path.py b/tests/test_path.py index ee65963d..c98f616a 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -24,10 +24,20 @@ def test_path_to_elements(path, expected): 5), ({1: [{'2': 'b'}, 3], 2: {4, 5}}, "root[1][0]['2']", - 'b'), + 'b' + ), ({'test [a]': 'b'}, "root['test [a]']", - 'b'), + 'b' + ), + ({"a']['b']['c": 1}, + """root["a\\'][\\'b\\'][\\'c"]""", + 1 + ), + ({"a']['b']['c": 1}, + """root["a']['b']['c"]""", + 1 + ), ]) def test_get_item(obj, path, expected): result = extract(obj, path) diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 2d3a6365..8a9c02f5 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -321,6 +321,8 @@ def test_pretty_form_method(self, expected, verbose_level): (3, {'10': Decimal(2017)}, None), (4, Decimal(2017.1), None), (5, {1, 2, 10}, set), + (6, datetime.datetime(2023, 10, 11), datetime.datetime.fromisoformat), + (7, datetime.datetime.utcnow(), datetime.datetime.fromisoformat), ]) def test_json_dumps_and_loads(self, test_num, value, func_to_convert_back): serialized = json_dumps(value)