From 48c4944f499386a6f8b12e8397da3cfdfd10b472 Mon Sep 17 00:00:00 2001 From: Seperman Date: Mon, 6 Nov 2023 16:24:38 -0800 Subject: [PATCH] Delta can now read from flat dicts dump --- deepdiff/delta.py | 101 ++++++++++++++++++++++++++++++--- deepdiff/model.py | 17 +----- deepdiff/path.py | 63 +++++++++++++++++++-- tests/test_delta.py | 133 ++++++++++++++++++++++++++++++++++++++------ tests/test_path.py | 59 +++++++++++++------- 5 files changed, 312 insertions(+), 61 deletions(-) diff --git a/deepdiff/delta.py b/deepdiff/delta.py index bb35825..82b99ca 100644 --- a/deepdiff/delta.py +++ b/deepdiff/delta.py @@ -10,7 +10,10 @@ np_ndarray, np_array_factory, numpy_dtypes, get_doc, not_found, numpy_dtype_string_to_type, dict_, ) -from deepdiff.path import _path_to_elements, _get_nested_obj, _get_nested_obj_and_force, GET, GETATTR, parse_path +from deepdiff.path import ( + _path_to_elements, _get_nested_obj, _get_nested_obj_and_force, + GET, GETATTR, parse_path, stringify_path, DEFAULT_FIRST_ELEMENT +) from deepdiff.anyset import AnySet @@ -55,6 +58,10 @@ class DeltaNumpyOperatorOverrideError(ValueError): pass +class _ObjDoesNotExist: + pass + + class Delta: __doc__ = doc @@ -64,6 +71,7 @@ def __init__( diff=None, delta_path=None, delta_file=None, + flat_dict_list=None, deserializer=pickle_load, log_errors=True, mutate=False, @@ -79,6 +87,8 @@ def __init__( def _deserializer(obj, safe_to_import=None): return deserializer(obj) + self._reversed_diff = None + if diff is not None: if isinstance(diff, DeepDiff): self.diff = diff._to_delta_dict(directed=not verify_symmetry) @@ -96,6 +106,8 @@ def _deserializer(obj, safe_to_import=None): except UnicodeDecodeError as e: raise ValueError(BINIARY_MODE_NEEDED_MSG.format(e)) from None self.diff = _deserializer(content, safe_to_import=safe_to_import) + elif flat_dict_list: + self.diff = self._from_flat_dicts(flat_dict_list) else: raise ValueError(DELTA_AT_LEAST_ONE_ARG_NEEDED) @@ -161,7 +173,7 @@ def _do_verify_changes(self, path, expected_old_value, current_old_value): self._raise_or_log(VERIFICATION_MSG.format( path, expected_old_value, current_old_value, VERIFY_SYMMETRY_MSG)) - def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expected_old_value, elem=None, action=None): + def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expected_old_value, elem=None, action=None, forced_old_value=None): try: if action == GET: current_old_value = obj[elem] @@ -171,12 +183,12 @@ def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expect raise DeltaError(INVALID_ACTION_WHEN_CALLING_GET_ELEM.format(action)) except (KeyError, IndexError, AttributeError, TypeError) as e: if self.force: - forced_old_value = {} + _forced_old_value = {} if forced_old_value is None else forced_old_value if action == GET: - obj[elem] = forced_old_value + obj[elem] = _forced_old_value elif action == GETATTR: - setattr(obj, elem, forced_old_value) - return forced_old_value + setattr(obj, elem, _forced_old_value) + return _forced_old_value current_old_value = not_found if isinstance(path_for_err_reporting, (list, tuple)): path_for_err_reporting = '.'.join([i[0] for i in path_for_err_reporting]) @@ -475,7 +487,7 @@ def _do_set_or_frozenset_item(self, items, func): parent = self.get_nested_obj(obj=self, elements=elements[:-1]) elem, action = elements[-1] obj = self._get_elem_and_compare_to_old_value( - parent, path_for_err_reporting=path, expected_old_value=None, elem=elem, action=action) + parent, path_for_err_reporting=path, expected_old_value=None, elem=elem, action=action, forced_old_value=set()) new_value = getattr(obj, func)(value) self._simple_set_elem_value(parent, path_for_err_reporting=path, elem=elem, value=new_value, action=action) @@ -568,6 +580,9 @@ def _do_ignore_order(self): self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem, value=new_obj, action=parent_to_obj_action) + def _reverse_diff(self): + pass + def dump(self, file): """ Dump into file object @@ -604,6 +619,78 @@ def _get_flat_row(action, info, _parse_path, keys_and_funcs): row[new_key] = details[key] yield row + @staticmethod + def _from_flat_dicts(flat_dict_list): + """ + Create the delta's diff object from the flat_dict_list + """ + result = {} + + DEFLATTENING_NEW_ACTION_MAP = { + 'iterable_item_added': 'iterable_items_added_at_indexes', + 'iterable_item_removed': 'iterable_items_removed_at_indexes', + } + for flat_dict in flat_dict_list: + index = None + action = flat_dict.get("action") + path = flat_dict.get("path") + value = flat_dict.get('value') + old_value = flat_dict.get('old_value', _ObjDoesNotExist) + if not action: + raise ValueError("Flat dict need to include the 'action'.") + if path is None: + raise ValueError("Flat dict need to include the 'path'.") + if action in DEFLATTENING_NEW_ACTION_MAP: + action = DEFLATTENING_NEW_ACTION_MAP[action] + index = path.pop() + if action in {'attribute_added', 'attribute_removed'}: + root_element = ('root', GETATTR) + else: + root_element = ('root', GET) + path_str = stringify_path(path, root_element=root_element) # We need the string path + if action not in result: + result[action] = {} + if action in {'iterable_items_added_at_indexes', 'iterable_items_removed_at_indexes'}: + if path_str not in result[action]: + result[action][path_str] = {} + result[action][path_str][index] = value + elif action in {'set_item_added', 'set_item_removed'}: + if path_str not in result[action]: + result[action][path_str] = set() + result[action][path_str].add(value) + elif action in { + 'dictionary_item_added', 'dictionary_item_removed', 'iterable_item_added', + 'iterable_item_removed', 'attribute_removed', 'attribute_added' + }: + result[action][path_str] = value + elif action == 'values_changed': + if old_value is _ObjDoesNotExist: + result[action][path_str] = {'new_value': value} + else: + result[action][path_str] = {'new_value': value, 'old_value': old_value} + elif action == 'type_changes': + type_ = flat_dict.get('type', _ObjDoesNotExist) + old_type = flat_dict.get('old_type', _ObjDoesNotExist) + + result[action][path_str] = {'new_value': value} + for elem, elem_value in [ + ('new_type', type_), + ('old_type', old_type), + ('old_value', old_value), + ]: + if elem_value is not _ObjDoesNotExist: + result[action][path_str][elem] = elem_value + elif action == 'iterable_item_moved': + result[action][path_str] = { + 'new_path': stringify_path( + flat_dict.get('new_path', ''), + root_element=('root', GET) + ), + 'value': value, + } + + return result + def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True): """ Returns a flat list of actions that is easily machine readable. diff --git a/deepdiff/model.py b/deepdiff/model.py index 3723e2b..34e6aed 100644 --- a/deepdiff/model.py +++ b/deepdiff/model.py @@ -5,6 +5,7 @@ from deepdiff.helper import ( RemapDict, strings, short_repr, notpresent, get_type, numpy_numbers, np, literal_eval_extended, dict_) +from deepdiff.path import stringify_element logger = logging.getLogger(__name__) @@ -874,21 +875,7 @@ def stringify_param(self, force=None): """ param = self.param if isinstance(param, strings): - 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) + result = stringify_element(param, quote_str=self.quote_str) 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 0c941cf..0390a6d 100644 --- a/deepdiff/path.py +++ b/deepdiff/path.py @@ -21,10 +21,17 @@ def _add_to_elements(elements, elem, inside): if not elem: return if not elem.startswith('__'): - try: - elem = literal_eval(elem) - except (ValueError, SyntaxError): - pass + remove_quotes = False + if '\\' in elem: + remove_quotes = True + else: + try: + elem = literal_eval(elem) + remove_quotes = False + except (ValueError, SyntaxError): + remove_quotes = True + if remove_quotes and elem[0] == elem[-1] and elem[0] in {'"', "'"}: + elem = elem[1: -1] action = GETATTR if inside == '.' else GET elements.append((elem, action)) @@ -229,3 +236,51 @@ def parse_path(path, root_element=DEFAULT_FIRST_ELEMENT, include_actions=False): if include_actions is False: return [i[0] for i in result] return [{'element': i[0], 'action': i[1]} for i in result] + + +def stringify_element(param, quote_str=None): + 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 quote_str is None else quote_str.format(param) + return result + + +def stringify_path(path, root_element=DEFAULT_FIRST_ELEMENT, quote_str="'{}'"): + """ + Gets the path as an string. + + For example [1, 2, 'age'] should become + root[1][2]['age'] + """ + if not path: + return root_element[0] + result = [root_element[0]] + has_actions = False + try: + if path[0][1] in {GET, GETATTR}: + has_actions = True + except (KeyError, IndexError, TypeError): + pass + if not has_actions: + path = [(i, GET) for i in path] + path[0] = (path[0][0], root_element[1]) # The action for the first element might be a GET or GETATTR. We update the action based on the root_element. + for element, action in path: + if isinstance(element, str) and action == GET: + element = stringify_element(element, quote_str) + if action == GET: + result.append(f"[{element}]") + else: + result.append(f".{element}") + return ''.join(result) diff --git a/tests/test_delta.py b/tests/test_delta.py index 79c9b7d..7fe552a 100644 --- a/tests/test_delta.py +++ b/tests/test_delta.py @@ -1226,6 +1226,9 @@ def test_list_ignore_order_various_deltas2(self): ] assert flat_expected1 == flat_result1 + delta1_again = Delta(flat_dict_list=flat_expected1) + assert delta1.diff == delta1_again.diff + flat_result2 = delta2.to_flat_dicts() flat_expected2 = [ {'path': [1], 'value': 4, 'action': 'iterable_item_added'}, @@ -1236,6 +1239,9 @@ def test_list_ignore_order_various_deltas2(self): ] assert flat_expected2 == flat_result2 + delta2_again = Delta(flat_dict_list=flat_expected2) + assert delta2.diff == delta2_again.diff + def test_delta_view_and_to_delta_dict_are_equal_when_parameteres_passed(self): """ This is a test that passes parameters in a dictionary instead of kwargs. @@ -1369,11 +1375,17 @@ def test_apply_delta_to_incompatible_object6_value_change(self): flat_expected2 = [{'path': [1, 2, 0], 'action': 'values_changed', 'value': 5}] assert flat_expected2 == flat_result2 + delta2_again = Delta(flat_dict_list=flat_expected2) + assert delta2.diff == delta2_again.diff + delta3 = Delta(diff, raise_errors=False, verify_symmetry=True) flat_result3 = delta3.to_flat_dicts() flat_expected3 = [{'path': [1, 2, 0], 'action': 'values_changed', 'value': 5, 'old_value': 4}] assert flat_expected3 == flat_result3 + delta3_again = Delta(flat_dict_list=flat_expected3) + assert delta3.diff == delta3_again.diff + def test_apply_delta_to_incompatible_object7_type_change(self): t1 = ['1'] t2 = [1] @@ -1479,6 +1491,9 @@ def test_delta_to_dict(self): flat_expected = [{'action': 'iterable_item_removed', 'path': [2], 'value': 'B'}] assert flat_expected == flat_result + delta_again = Delta(flat_dict_list=flat_expected) + assert delta.diff == delta_again.diff + def test_class_type_change(self): t1 = CustomClass t2 = CustomClass2 @@ -1531,6 +1546,9 @@ def test_none_in_delta_object(self): flat_expected = [{'path': ['a'], 'action': 'type_changes', 'value': 1, 'type': int, 'old_type': type(None)}] assert flat_expected == flat_result + delta_again = Delta(flat_dict_list=flat_expected) + assert delta.diff == delta_again.diff + flat_result2 = delta.to_flat_dicts(report_type_changes=False) flat_expected2 = [{'path': ['a'], 'action': 'values_changed', 'value': 1}] assert flat_expected2 == flat_result2 @@ -1551,6 +1569,9 @@ def test_delta_set_in_objects(self): flat_result.sort(key=lambda x: str(x['value'])) assert flat_expected == flat_result + delta_again = Delta(flat_dict_list=flat_expected) + assert delta.diff == delta_again.diff + def test_delta_with_json_serializer(self): t1 = {"a": 1} t2 = {"a": 2} @@ -1663,6 +1684,45 @@ def test_compare_func_with_duplicates_removed(self): ] assert flat_expected == flat_result + Delta.DEBUG = True + delta_again = Delta(flat_dict_list=flat_expected) + expected_delta_dict = { + 'iterable_items_removed_at_indexes': { + 'root': { + 2: { + 'id': 1, + 'val': 3 + }, + 0: { + 'id': 1, + 'val': 3 + }, + 3: { + 'id': 3, + 'val': 3 + } + } + }, + 'iterable_item_moved': { + 'root[0]': { + 'new_path': 'root[2]', + 'value': { + 'id': 1, + 'val': 3 + } + }, + 'root[3]': { + 'new_path': 'root[0]', + 'value': { + 'id': 3, + 'val': 3 + } + } + } + } + assert expected_delta_dict == delta_again.diff + assert t2 == t1 + delta_again + def test_compare_func_with_duplicates_added(self): t1 = [{'id': 3, 'val': 3}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}] t2 = [{'id': 1, 'val': 1}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}, {'id': 3, 'val': 3}] @@ -1818,11 +1878,19 @@ def test_flatten_dict_with_one_key_added(self): diff = DeepDiff(t1, t2) delta = Delta(diff=diff) flat_result = delta.to_flat_dicts(report_type_changes=False) - expected_result = [ + flat_expected = [ {'path': ['field2', 'jimmy'], 'value': 'Jimmy', 'action': 'dictionary_item_added'}, {'path': ['field1', 'joe'], 'action': 'values_changed', 'value': 'Joe Nobody'}, ] - assert expected_result == flat_result + assert flat_expected == flat_result + + delta_again = Delta(flat_dict_list=flat_expected, force=True) # We need to enable force so it creates the dictionary when added to t1 + expected_data_again_diff = {'dictionary_item_added': {"root['field2']['jimmy']": 'Jimmy'}, 'values_changed': {"root['field1']['joe']": {'new_value': 'Joe Nobody'}}} + + assert delta.diff != delta_again.diff, "Since a dictionary containing a single field was created, the flat dict acted like one key was added." + assert expected_data_again_diff == delta_again.diff, "Since a dictionary containing a single field was created, the flat dict acted like one key was added." + + assert t2 == t1 + delta_again def test_flatten_dict_with_multiple_keys_added(self): t1 = {"field1": {"joe": "Joe"}} @@ -1830,11 +1898,14 @@ def test_flatten_dict_with_multiple_keys_added(self): diff = DeepDiff(t1, t2) delta = Delta(diff=diff) flat_result = delta.to_flat_dicts(report_type_changes=False) - expected_result = [ + flat_expected = [ {'path': ['field2'], 'value': {'jimmy': 'Jimmy', 'sar': 'Sarah'}, 'action': 'dictionary_item_added'}, {'path': ['field1', 'joe'], 'action': 'values_changed', 'value': 'Joe Nobody'}, ] - assert expected_result == flat_result + assert flat_expected == flat_result + + delta_again = Delta(flat_dict_list=flat_expected) + assert delta.diff == delta_again.diff def test_flatten_list_with_one_item_added(self): t1 = {"field1": {"joe": "Joe"}} @@ -1843,14 +1914,23 @@ def test_flatten_list_with_one_item_added(self): diff = DeepDiff(t1, t2) delta = Delta(diff=diff) flat_result = delta.to_flat_dicts(report_type_changes=False) - expected_result = [{'path': ['field2', 0], 'value': 'James', 'action': 'iterable_item_added'}] - assert expected_result == flat_result + flat_expected = [{'path': ['field2', 0], 'value': 'James', 'action': 'iterable_item_added'}] + assert flat_expected == flat_result - diff = DeepDiff(t2, t3) - delta2 = Delta(diff=diff) + delta_again = Delta(flat_dict_list=flat_expected, force=True) + assert {'iterable_items_added_at_indexes': {"root['field2']": {0: 'James'}}} == delta_again.diff + assert t2 == t1 + delta_again + + diff2 = DeepDiff(t2, t3) + delta2 = Delta(diff=diff2) flat_result2 = delta2.to_flat_dicts(report_type_changes=False) - expected_result2 = [{'path': ['field2', 1], 'value': 'Jack', 'action': 'iterable_item_added'}] - assert expected_result2 == flat_result2 + flat_expected2 = [{'path': ['field2', 1], 'value': 'Jack', 'action': 'iterable_item_added'}] + assert flat_expected2 == flat_result2 + + delta_again2 = Delta(flat_dict_list=flat_expected2, force=True) + + assert {'iterable_items_added_at_indexes': {"root['field2']": {1: 'Jack'}}} == delta_again2.diff + assert t3 == t2 + delta_again2 def test_flatten_set_with_one_item_added(self): t1 = {"field1": {"joe": "Joe"}} @@ -1858,15 +1938,24 @@ def test_flatten_set_with_one_item_added(self): t3 = {"field1": {"joe": "Joe"}, "field2": {"James", "Jack"}} diff = DeepDiff(t1, t2) delta = Delta(diff=diff) + assert t2 == t1 + delta flat_result = delta.to_flat_dicts(report_type_changes=False) - expected_result = [{'path': ['field2'], 'value': 'James', 'action': 'set_item_added'}] - assert expected_result == flat_result + flat_expected = [{'path': ['field2'], 'value': 'James', 'action': 'set_item_added'}] + assert flat_expected == flat_result + + delta_again = Delta(flat_dict_list=flat_expected, force=True) + assert {'set_item_added': {"root['field2']": {'James'}}} == delta_again.diff + assert t2 == t1 + delta_again diff = DeepDiff(t2, t3) delta2 = Delta(diff=diff) flat_result2 = delta2.to_flat_dicts(report_type_changes=False) - expected_result2 = [{'path': ['field2'], 'value': 'Jack', 'action': 'set_item_added'}] - assert expected_result2 == flat_result2 + flat_expected2 = [{'path': ['field2'], 'value': 'Jack', 'action': 'set_item_added'}] + assert flat_expected2 == flat_result2 + + delta_again2 = Delta(flat_dict_list=flat_expected2, force=True) + assert {'set_item_added': {"root['field2']": {'Jack'}}} == delta_again2.diff + assert t3 == t2 + delta_again2 def test_flatten_tuple_with_one_item_added(self): t1 = {"field1": {"joe": "Joe"}} @@ -1874,9 +1963,12 @@ def test_flatten_tuple_with_one_item_added(self): t3 = {"field1": {"joe": "Joe"}, "field2": ("James", "Jack")} diff = DeepDiff(t1, t2) delta = Delta(diff=diff) - flat_result = delta.to_flat_dicts(report_type_changes=False) + flat_expected = delta.to_flat_dicts(report_type_changes=False) expected_result = [{'path': ['field2', 0], 'value': 'James', 'action': 'iterable_item_added'}] - assert expected_result == flat_result + assert expected_result == flat_expected + + delta_again = Delta(flat_dict_list=flat_expected) + assert {'iterable_items_added_at_indexes': {"root['field2']": {0: 'James'}}} == delta_again.diff diff = DeepDiff(t2, t3) delta2 = Delta(diff=diff) @@ -1884,6 +1976,9 @@ def test_flatten_tuple_with_one_item_added(self): expected_result2 = [{'path': ['field2', 1], 'value': 'Jack', 'action': 'iterable_item_added'}] assert expected_result2 == flat_result2 + delta_again2 = Delta(flat_dict_list=flat_result2) + assert {'iterable_items_added_at_indexes': {"root['field2']": {1: 'Jack'}}} == delta_again2.diff + def test_flatten_list_with_multiple_item_added(self): t1 = {"field1": {"joe": "Joe"}} t2 = {"field1": {"joe": "Joe"}, "field2": ["James", "Jack"]} @@ -1897,6 +1992,9 @@ def test_flatten_list_with_multiple_item_added(self): flat_result2 = delta2.to_flat_dicts(report_type_changes=False) assert expected_result == flat_result2 + delta_again = Delta(flat_dict_list=flat_result) + assert delta.diff == delta_again.diff + def test_flatten_attribute_added(self): t1 = picklalbe_obj_without_item t2 = PicklableClass(10) @@ -1905,3 +2003,6 @@ def test_flatten_attribute_added(self): flat_result = delta.to_flat_dicts(report_type_changes=False) expected_result = [{'path': ['item'], 'value': 10, 'action': 'attribute_added'}] assert expected_result == flat_result + + delta_again = Delta(flat_dict_list=flat_result) + assert delta.diff == delta_again.diff diff --git a/tests/test_path.py b/tests/test_path.py index c98f616..edb2784 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,21 +1,26 @@ import pytest -from deepdiff.path import _path_to_elements, GET, GETATTR, extract, parse_path - - -@pytest.mark.parametrize('path, expected', [ - ("root[4]['b'][3]", [(4, GET), ('b', GET), (3, GET)]), - ("root[4].b[3]", [(4, GET), ('b', GETATTR), (3, GET)]), - ("root[4].b['a3']", [(4, GET), ('b', GETATTR), ('a3', GET)]), - ("root[4.3].b['a3']", [(4.3, GET), ('b', GETATTR), ('a3', GET)]), - ("root.a.b", [('a', GETATTR), ('b', GETATTR)]), - ("root.hello", [('hello', GETATTR)]), - (r"root['a\rb']", [('a\rb', GET)]), - ("root", []), - (((4, GET), ('b', GET)), ((4, GET), ('b', GET))), +from deepdiff.path import _path_to_elements, GET, GETATTR, extract, parse_path, stringify_path, _add_to_elements + + +@pytest.mark.parametrize('test_num, path, expected', [ + (1, "root[4]['b'][3]", [(4, GET), ('b', GET), (3, GET)]), + (2, "root[4].b[3]", [(4, GET), ('b', GETATTR), (3, GET)]), + (3, "root[4].b['a3']", [(4, GET), ('b', GETATTR), ('a3', GET)]), + (4, "root[4.3].b['a3']", [(4.3, GET), ('b', GETATTR), ('a3', GET)]), + (5, "root.a.b", [('a', GETATTR), ('b', GETATTR)]), + (6, "root.hello", [('hello', GETATTR)]), + (7, "root['h']", [('h', GET)]), + (8, "root['a\rb']", [('a\rb', GET)]), + (9, "root['a\\rb']", [('a\\rb', GET)]), + (10, "root", []), + (11, ((4, GET), ('b', GET)), ((4, GET), ('b', GET))), ]) -def test_path_to_elements(path, expected): +def test_path_to_elements(test_num, path, expected): result = _path_to_elements(path, root_element=None) - assert tuple(expected) == result + assert tuple(expected) == result, f"test_path_to_elements #{test_num} failed" + if isinstance(path, str): + path_again = stringify_path(path=result) + assert path == path_again, f"test_path_to_elements #{test_num} failed" @pytest.mark.parametrize('obj, path, expected', [ @@ -30,10 +35,6 @@ def test_path_to_elements(path, expected): "root['test [a]']", 'b' ), - ({"a']['b']['c": 1}, - """root["a\\'][\\'b\\'][\\'c"]""", - 1 - ), ({"a']['b']['c": 1}, """root["a']['b']['c"]""", 1 @@ -53,3 +54,23 @@ def test_parse_path(): assert ['joe', 'age'] == result3 result4 = parse_path("root['joe'].age", include_actions=True) assert [{'element': 'joe', 'action': 'GET'}, {'element': 'age', 'action': 'GETATTR'}] == result4 + + +@pytest.mark.parametrize('test_num, elem, inside, expected', [ + ( + 1, + "'hello'", + None, + [('hello', GET)], + ), + ( + 2, + "'a\rb'", + None, + [('a\rb', GET)], + ), +]) +def test__add_to_elements(test_num, elem, inside, expected): + elements = [] + _add_to_elements(elements, elem, inside) + assert expected == elements