Skip to content

Commit 48c4944

Browse files
committed
Delta can now read from flat dicts dump
1 parent 8c86424 commit 48c4944

File tree

5 files changed

+312
-61
lines changed

5 files changed

+312
-61
lines changed

deepdiff/delta.py

+94-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
np_ndarray, np_array_factory, numpy_dtypes, get_doc,
1111
not_found, numpy_dtype_string_to_type, dict_,
1212
)
13-
from deepdiff.path import _path_to_elements, _get_nested_obj, _get_nested_obj_and_force, GET, GETATTR, parse_path
13+
from deepdiff.path import (
14+
_path_to_elements, _get_nested_obj, _get_nested_obj_and_force,
15+
GET, GETATTR, parse_path, stringify_path, DEFAULT_FIRST_ELEMENT
16+
)
1417
from deepdiff.anyset import AnySet
1518

1619

@@ -55,6 +58,10 @@ class DeltaNumpyOperatorOverrideError(ValueError):
5558
pass
5659

5760

61+
class _ObjDoesNotExist:
62+
pass
63+
64+
5865
class Delta:
5966

6067
__doc__ = doc
@@ -64,6 +71,7 @@ def __init__(
6471
diff=None,
6572
delta_path=None,
6673
delta_file=None,
74+
flat_dict_list=None,
6775
deserializer=pickle_load,
6876
log_errors=True,
6977
mutate=False,
@@ -79,6 +87,8 @@ def __init__(
7987
def _deserializer(obj, safe_to_import=None):
8088
return deserializer(obj)
8189

90+
self._reversed_diff = None
91+
8292
if diff is not None:
8393
if isinstance(diff, DeepDiff):
8494
self.diff = diff._to_delta_dict(directed=not verify_symmetry)
@@ -96,6 +106,8 @@ def _deserializer(obj, safe_to_import=None):
96106
except UnicodeDecodeError as e:
97107
raise ValueError(BINIARY_MODE_NEEDED_MSG.format(e)) from None
98108
self.diff = _deserializer(content, safe_to_import=safe_to_import)
109+
elif flat_dict_list:
110+
self.diff = self._from_flat_dicts(flat_dict_list)
99111
else:
100112
raise ValueError(DELTA_AT_LEAST_ONE_ARG_NEEDED)
101113

@@ -161,7 +173,7 @@ def _do_verify_changes(self, path, expected_old_value, current_old_value):
161173
self._raise_or_log(VERIFICATION_MSG.format(
162174
path, expected_old_value, current_old_value, VERIFY_SYMMETRY_MSG))
163175

164-
def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expected_old_value, elem=None, action=None):
176+
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):
165177
try:
166178
if action == GET:
167179
current_old_value = obj[elem]
@@ -171,12 +183,12 @@ def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expect
171183
raise DeltaError(INVALID_ACTION_WHEN_CALLING_GET_ELEM.format(action))
172184
except (KeyError, IndexError, AttributeError, TypeError) as e:
173185
if self.force:
174-
forced_old_value = {}
186+
_forced_old_value = {} if forced_old_value is None else forced_old_value
175187
if action == GET:
176-
obj[elem] = forced_old_value
188+
obj[elem] = _forced_old_value
177189
elif action == GETATTR:
178-
setattr(obj, elem, forced_old_value)
179-
return forced_old_value
190+
setattr(obj, elem, _forced_old_value)
191+
return _forced_old_value
180192
current_old_value = not_found
181193
if isinstance(path_for_err_reporting, (list, tuple)):
182194
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):
475487
parent = self.get_nested_obj(obj=self, elements=elements[:-1])
476488
elem, action = elements[-1]
477489
obj = self._get_elem_and_compare_to_old_value(
478-
parent, path_for_err_reporting=path, expected_old_value=None, elem=elem, action=action)
490+
parent, path_for_err_reporting=path, expected_old_value=None, elem=elem, action=action, forced_old_value=set())
479491
new_value = getattr(obj, func)(value)
480492
self._simple_set_elem_value(parent, path_for_err_reporting=path, elem=elem, value=new_value, action=action)
481493

@@ -568,6 +580,9 @@ def _do_ignore_order(self):
568580
self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem,
569581
value=new_obj, action=parent_to_obj_action)
570582

583+
def _reverse_diff(self):
584+
pass
585+
571586
def dump(self, file):
572587
"""
573588
Dump into file object
@@ -604,6 +619,78 @@ def _get_flat_row(action, info, _parse_path, keys_and_funcs):
604619
row[new_key] = details[key]
605620
yield row
606621

622+
@staticmethod
623+
def _from_flat_dicts(flat_dict_list):
624+
"""
625+
Create the delta's diff object from the flat_dict_list
626+
"""
627+
result = {}
628+
629+
DEFLATTENING_NEW_ACTION_MAP = {
630+
'iterable_item_added': 'iterable_items_added_at_indexes',
631+
'iterable_item_removed': 'iterable_items_removed_at_indexes',
632+
}
633+
for flat_dict in flat_dict_list:
634+
index = None
635+
action = flat_dict.get("action")
636+
path = flat_dict.get("path")
637+
value = flat_dict.get('value')
638+
old_value = flat_dict.get('old_value', _ObjDoesNotExist)
639+
if not action:
640+
raise ValueError("Flat dict need to include the 'action'.")
641+
if path is None:
642+
raise ValueError("Flat dict need to include the 'path'.")
643+
if action in DEFLATTENING_NEW_ACTION_MAP:
644+
action = DEFLATTENING_NEW_ACTION_MAP[action]
645+
index = path.pop()
646+
if action in {'attribute_added', 'attribute_removed'}:
647+
root_element = ('root', GETATTR)
648+
else:
649+
root_element = ('root', GET)
650+
path_str = stringify_path(path, root_element=root_element) # We need the string path
651+
if action not in result:
652+
result[action] = {}
653+
if action in {'iterable_items_added_at_indexes', 'iterable_items_removed_at_indexes'}:
654+
if path_str not in result[action]:
655+
result[action][path_str] = {}
656+
result[action][path_str][index] = value
657+
elif action in {'set_item_added', 'set_item_removed'}:
658+
if path_str not in result[action]:
659+
result[action][path_str] = set()
660+
result[action][path_str].add(value)
661+
elif action in {
662+
'dictionary_item_added', 'dictionary_item_removed', 'iterable_item_added',
663+
'iterable_item_removed', 'attribute_removed', 'attribute_added'
664+
}:
665+
result[action][path_str] = value
666+
elif action == 'values_changed':
667+
if old_value is _ObjDoesNotExist:
668+
result[action][path_str] = {'new_value': value}
669+
else:
670+
result[action][path_str] = {'new_value': value, 'old_value': old_value}
671+
elif action == 'type_changes':
672+
type_ = flat_dict.get('type', _ObjDoesNotExist)
673+
old_type = flat_dict.get('old_type', _ObjDoesNotExist)
674+
675+
result[action][path_str] = {'new_value': value}
676+
for elem, elem_value in [
677+
('new_type', type_),
678+
('old_type', old_type),
679+
('old_value', old_value),
680+
]:
681+
if elem_value is not _ObjDoesNotExist:
682+
result[action][path_str][elem] = elem_value
683+
elif action == 'iterable_item_moved':
684+
result[action][path_str] = {
685+
'new_path': stringify_path(
686+
flat_dict.get('new_path', ''),
687+
root_element=('root', GET)
688+
),
689+
'value': value,
690+
}
691+
692+
return result
693+
607694
def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True):
608695
"""
609696
Returns a flat list of actions that is easily machine readable.

deepdiff/model.py

+2-15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from deepdiff.helper import (
66
RemapDict, strings, short_repr, notpresent, get_type, numpy_numbers, np, literal_eval_extended,
77
dict_)
8+
from deepdiff.path import stringify_element
89

910
logger = logging.getLogger(__name__)
1011

@@ -874,21 +875,7 @@ def stringify_param(self, force=None):
874875
"""
875876
param = self.param
876877
if isinstance(param, strings):
877-
has_quote = "'" in param
878-
has_double_quote = '"' in param
879-
if has_quote and has_double_quote:
880-
new_param = []
881-
for char in param:
882-
if char in {'"', "'"}:
883-
new_param.append('\\')
884-
new_param.append(char)
885-
param = ''.join(new_param)
886-
elif has_quote:
887-
result = f'"{param}"'
888-
elif has_double_quote:
889-
result = f"'{param}'"
890-
else:
891-
result = param if self.quote_str is None else self.quote_str.format(param)
878+
result = stringify_element(param, quote_str=self.quote_str)
892879
elif isinstance(param, tuple): # Currently only for numpy ndarrays
893880
result = ']['.join(map(repr, param))
894881
else:

deepdiff/path.py

+59-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ def _add_to_elements(elements, elem, inside):
2121
if not elem:
2222
return
2323
if not elem.startswith('__'):
24-
try:
25-
elem = literal_eval(elem)
26-
except (ValueError, SyntaxError):
27-
pass
24+
remove_quotes = False
25+
if '\\' in elem:
26+
remove_quotes = True
27+
else:
28+
try:
29+
elem = literal_eval(elem)
30+
remove_quotes = False
31+
except (ValueError, SyntaxError):
32+
remove_quotes = True
33+
if remove_quotes and elem[0] == elem[-1] and elem[0] in {'"', "'"}:
34+
elem = elem[1: -1]
2835
action = GETATTR if inside == '.' else GET
2936
elements.append((elem, action))
3037

@@ -229,3 +236,51 @@ def parse_path(path, root_element=DEFAULT_FIRST_ELEMENT, include_actions=False):
229236
if include_actions is False:
230237
return [i[0] for i in result]
231238
return [{'element': i[0], 'action': i[1]} for i in result]
239+
240+
241+
def stringify_element(param, quote_str=None):
242+
has_quote = "'" in param
243+
has_double_quote = '"' in param
244+
if has_quote and has_double_quote:
245+
new_param = []
246+
for char in param:
247+
if char in {'"', "'"}:
248+
new_param.append('\\')
249+
new_param.append(char)
250+
param = ''.join(new_param)
251+
elif has_quote:
252+
result = f'"{param}"'
253+
elif has_double_quote:
254+
result = f"'{param}'"
255+
else:
256+
result = param if quote_str is None else quote_str.format(param)
257+
return result
258+
259+
260+
def stringify_path(path, root_element=DEFAULT_FIRST_ELEMENT, quote_str="'{}'"):
261+
"""
262+
Gets the path as an string.
263+
264+
For example [1, 2, 'age'] should become
265+
root[1][2]['age']
266+
"""
267+
if not path:
268+
return root_element[0]
269+
result = [root_element[0]]
270+
has_actions = False
271+
try:
272+
if path[0][1] in {GET, GETATTR}:
273+
has_actions = True
274+
except (KeyError, IndexError, TypeError):
275+
pass
276+
if not has_actions:
277+
path = [(i, GET) for i in path]
278+
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.
279+
for element, action in path:
280+
if isinstance(element, str) and action == GET:
281+
element = stringify_element(element, quote_str)
282+
if action == GET:
283+
result.append(f"[{element}]")
284+
else:
285+
result.append(f".{element}")
286+
return ''.join(result)

0 commit comments

Comments
 (0)