Skip to content

Commit 12c8714

Browse files
committed
fixing ignore order for deltas and tuple
1 parent d711b4e commit 12c8714

File tree

6 files changed

+282
-51
lines changed

6 files changed

+282
-51
lines changed

deepdiff/delta.py

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from collections import defaultdict
23
from collections.abc import Mapping
34
from copy import deepcopy
45
from decimal import Decimal
@@ -18,9 +19,10 @@
1819

1920

2021
VERIFICATION_MSG = 'Expected the previous value for {} to be {} but it is {}. Due to {}'
21-
ELEM_NOT_FOUND_TO_ADD_MSG = 'Key or index of {} is not found for {} for insertion operation.'
22+
ELEM_NOT_FOUND_TO_ADD_MSG = 'Key or index of {} is not found for {} for setting operation.'
2223
TYPE_CHANGE_FAIL_MSG = 'Unable to do the type change for {} from to type {} due to {}'
2324
VERIFY_SYMMETRY_MSG = 'that the original objects that the delta is made from must be different than what the delta is applied to.'
25+
FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG = 'Failed to remove index[{}] on {}. It was expected to be {} but got {}'
2426

2527

2628
class _NotFound:
@@ -134,6 +136,7 @@ def __add__(self, other):
134136
# all the other iterables to match the reverse of order of operations in DeepDiff
135137
self._do_iterable_item_removed()
136138
self._do_iterable_item_added()
139+
self._do_ignore_order()
137140
self._do_dictionary_item_added()
138141
self._do_dictionary_item_removed()
139142
self._do_attribute_added()
@@ -169,6 +172,8 @@ def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expect
169172
except (KeyError, IndexError, AttributeError) as e:
170173
current_old_value = not_found
171174
if self.verify_symmetry:
175+
if isinstance(path_for_err_reporting, (list, tuple)):
176+
path_for_err_reporting = '.'.join([i[0] for i in path_for_err_reporting])
172177
self._raise_or_log(VERIFICATION_MSG.format(
173178
path_for_err_reporting,
174179
expected_old_value, current_old_value, e))
@@ -357,14 +362,14 @@ def _do_attribute_removed(self):
357362
def _do_set_item_added(self):
358363
items = self.diff.get('set_item_added')
359364
if items:
360-
self._do_set_item(items, func='union')
365+
self._do_set_or_frozenset_item(items, func='union')
361366

362367
def _do_set_item_removed(self):
363368
items = self.diff.get('set_item_removed')
364369
if items:
365-
self._do_set_item(items, func='difference')
370+
self._do_set_or_frozenset_item(items, func='difference')
366371

367-
def _do_set_item(self, items, func):
372+
def _do_set_or_frozenset_item(self, items, func):
368373
for path, value in items.items():
369374
elements = _path_to_elements(path)
370375
parent = _get_nested_obj(obj=self, elements=elements[:-1])
@@ -374,6 +379,81 @@ def _do_set_item(self, items, func):
374379
new_value = getattr(obj, func)(value)
375380
self._simple_set_elem_value(parent, path_for_err_reporting=path, elem=elem, value=new_value, action=action)
376381

382+
def _do_ignore_order_get_old(self, obj, remove_indexes_per_path, fixed_indexes_values, path_for_err_reporting):
383+
"""
384+
A generator that gets the old values in an iterable when the order was supposed to be ignored.
385+
"""
386+
old_obj_index = -1
387+
max_len = len(obj) - 1
388+
while old_obj_index < max_len:
389+
old_obj_index += 1
390+
current_old_obj = obj[old_obj_index]
391+
if current_old_obj in fixed_indexes_values:
392+
continue
393+
if old_obj_index in remove_indexes_per_path:
394+
expected_obj_to_delete = remove_indexes_per_path.pop(old_obj_index)
395+
if current_old_obj == expected_obj_to_delete:
396+
continue
397+
else:
398+
self._raise_or_log(FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG.format(old_obj_index, path_for_err_reporting, expected_obj_to_delete, current_old_obj))
399+
yield current_old_obj
400+
401+
def _do_ignore_order(self):
402+
"""
403+
404+
't1': [5, 1, 1, 1, 6],
405+
't2': [7, 1, 1, 1, 8],
406+
407+
'ignore_order_fixed_indexes': {
408+
'root': {
409+
0: 7,
410+
4: 8
411+
}
412+
},
413+
'ignore_order_remove_indexes': {
414+
'root': {
415+
4: 6,
416+
0: 5
417+
}
418+
}
419+
420+
"""
421+
fixed_indexes = self.diff.get('ignore_order_fixed_indexes', {})
422+
remove_indexes = self.diff.get('ignore_order_remove_indexes', {})
423+
424+
paths = set(fixed_indexes.keys()) | set(remove_indexes.keys())
425+
for path in paths:
426+
# In the case of ignore_order reports, we are pointing to the container object.
427+
# Thus we add a [0] to the elements so we can get the required objects and discard what we don't need.
428+
_, parent, parent_to_obj_elem, parent_to_obj_action, obj, _, _ = self._get_elements_and_details("{}[0]".format(path))
429+
fixed_indexes_per_path = fixed_indexes.get(path)
430+
remove_indexes_per_path = remove_indexes.get(path)
431+
fixed_indexes_values = set(fixed_indexes_per_path.values()) # TODO: this needs to be changed to use deephash
432+
433+
new_obj = []
434+
there_are_old_items = bool(obj)
435+
old_item_gen = self._do_ignore_order_get_old(
436+
obj, remove_indexes_per_path, fixed_indexes_values, path_for_err_reporting=path)
437+
while there_are_old_items or fixed_indexes_per_path:
438+
new_obj_index = len(new_obj)
439+
if new_obj_index in fixed_indexes_per_path:
440+
new_item = fixed_indexes_per_path.pop(new_obj_index)
441+
new_obj.append(new_item)
442+
else:
443+
try:
444+
new_item = next(old_item_gen)
445+
except StopIteration:
446+
there_are_old_items = False
447+
else:
448+
new_obj.append(new_item)
449+
450+
if isinstance(obj, tuple):
451+
new_obj = tuple(new_obj)
452+
# Making sure that the object is re-instated inside the parent especially if it was immutable
453+
# and we had to turn it into a mutable one. In such cases the object has a new id.
454+
self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem,
455+
value=new_obj, action=parent_to_obj_action)
456+
377457

378458
if __name__ == "__main__": # pragma: no cover
379459
import doctest

deepdiff/diff.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ def __iterables_subscriptable(t1, t2):
347347

348348
def __diff_iterable(self, level, parents_ids=frozenset({})):
349349
"""Difference of iterables"""
350+
351+
if self.ignore_order:
352+
self.__diff_iterable_with_deephash(level)
353+
return
354+
350355
# We're handling both subscriptable and non-subscriptable iterables. Which one is it?
351356
subscriptable = self.__iterables_subscriptable(level.t1, level.t2)
352357
if subscriptable:
@@ -666,10 +671,7 @@ def __diff(self, level, parents_ids=frozenset({})):
666671
self.__diff_numpy(level, parents_ids)
667672

668673
elif isinstance(level.t1, Iterable):
669-
if self.ignore_order:
670-
self.__diff_iterable_with_deephash(level)
671-
else:
672-
self.__diff_iterable(level, parents_ids)
674+
self.__diff_iterable(level, parents_ids)
673675

674676
else:
675677
self.__diff_obj(level, parents_ids)
@@ -754,25 +756,19 @@ def to_delta_dict(self, directed=True):
754756
755757
t1 + delta == t2
756758
757-
758-
**Example**
759-
760-
Directed Delta
761-
>>> class A:
762-
... pass
763-
...
764-
>>> class B:
765759
"""
766-
result = DeltaResult(tree_results=self.tree, verbose_level=2)
760+
result = DeltaResult(tree_results=self.tree, ignore_order=self.ignore_order)
767761
result.cleanup() # clean up text-style result dictionary
762+
if self.ignore_order:
763+
if not self.report_repetition:
764+
raise ValueError('report_repetition must be set to True when ignore_order is True to create the delta object.')
768765
if directed:
769766
for report_key, report_value in result.items():
770767
if isinstance(report_value, Mapping):
771768
for path, value in report_value.items():
772769
if isinstance(value, Mapping) and 'old_value' in value:
773770
del value['old_value']
774-
if self.ignore_order:
775-
result['ignore_order'] = True
771+
776772
return result
777773

778774

deepdiff/diff_doc.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ ignore_order : Boolean, default=False
2323
report_repetition : Boolean, default=False
2424
reports repetitions when set True
2525
ONLY when ignore_order is set True too. This works for iterables.
26-
This feature currently is experimental and is not production ready.
2726

2827
significant_digits : int >= 0, default=None
2928
By default the significant_digits compares only that many digits AFTER the decimal point. However you can set override that by setting the number_format_notation="e" which will make it mean the digits in scientific notation.

deepdiff/model.py

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -199,28 +199,69 @@ class DeltaResult(TextResult):
199199

200200
ADD_QUOTES_TO_STRINGS = False
201201

202-
def __init__(self, tree_results=None, verbose_level=1):
203-
self.verbose_level = verbose_level
202+
def __init__(self, tree_results=None, ignore_order=None):
203+
self.ignore_order = ignore_order
204204

205205
self.update({
206206
"type_changes": {},
207207
"dictionary_item_added": {},
208-
"dictionary_item_removed": self.__set_or_dict(),
208+
"dictionary_item_removed": {},
209209
"values_changed": {},
210210
"iterable_item_added": {},
211-
"iterable_item_removed": self.__set_or_dict(),
211+
"iterable_item_removed": {},
212212
"attribute_added": {},
213-
"attribute_removed": self.__set_or_dict(),
213+
"attribute_removed": {},
214214
"set_item_removed": {},
215215
"set_item_added": {},
216-
"repetition_change": {}
216+
"ignore_order_fixed_indexes": {},
217+
"ignore_order_remove_indexes": {},
217218
})
218219

219220
if tree_results:
220221
self._from_tree_results(tree_results)
221222

222-
def __set_or_dict(self):
223-
return {} if self.verbose_level >= 2 else PrettyOrderedSet()
223+
def _from_tree_results(self, tree):
224+
"""
225+
Populate this object by parsing an existing reference-style result dictionary.
226+
:param tree: A TreeResult
227+
:return:
228+
"""
229+
self._from_tree_type_changes(tree)
230+
self._from_tree_default(tree, 'dictionary_item_added')
231+
self._from_tree_default(tree, 'dictionary_item_removed')
232+
self._from_tree_value_changed(tree)
233+
if self.ignore_order:
234+
self._from_tree_iterable_item_added(
235+
tree, 'iterable_item_added', delta_report_key='ignore_order_fixed_indexes')
236+
self._from_tree_iterable_item_added(
237+
tree, 'iterable_item_removed', delta_report_key='ignore_order_remove_indexes')
238+
else:
239+
self._from_tree_default(tree, 'iterable_item_added')
240+
self._from_tree_default(tree, 'iterable_item_removed')
241+
self._from_tree_default(tree, 'attribute_added')
242+
self._from_tree_default(tree, 'attribute_removed')
243+
self._from_tree_set_item_removed(tree)
244+
self._from_tree_set_item_added(tree)
245+
self._from_tree_repetition_change(tree)
246+
247+
def _from_tree_iterable_item_added(self, tree, report_type, delta_report_key):
248+
if report_type in tree:
249+
for change in tree[report_type]: # report each change
250+
# determine change direction (added or removed)
251+
# Report t2 (the new one) whenever possible.
252+
# In cases where t2 doesn't exist (i.e. stuff removed), report t1.
253+
if change.t2 is not notpresent:
254+
item = change.t2
255+
else:
256+
item = change.t1
257+
258+
# do the reporting
259+
path, param, _ = change.path(force=FORCE_DEFAULT, get_parent_too=True)
260+
try:
261+
ignore_order_fixed_indexes = self[delta_report_key][path]
262+
except KeyError:
263+
ignore_order_fixed_indexes = self[delta_report_key][path] = {}
264+
ignore_order_fixed_indexes[param] = item
224265

225266
def _from_tree_type_changes(self, tree):
226267
if 'type_changes' in tree:
@@ -247,7 +288,7 @@ def _from_tree_type_changes(self, tree):
247288
})
248289
self['type_changes'][change.path(
249290
force=FORCE_DEFAULT)] = remap_dict
250-
if self.verbose_level and include_values:
291+
if include_values:
251292
remap_dict.update(old_value=change.t1, new_value=change.t2)
252293

253294
def _from_tree_value_changed(self, tree):
@@ -259,8 +300,26 @@ def _from_tree_value_changed(self, tree):
259300
if 'diff' in change.additional:
260301
the_changed.update({'diff': change.additional['diff']})
261302

262-
def _from_tree_unprocessed(self, tree):
263-
pass
303+
def _from_tree_repetition_change(self, tree):
304+
if 'repetition_change' in tree:
305+
for change in tree['repetition_change']:
306+
path, _, _ = change.path(get_parent_too=True)
307+
repetition = RemapDict(change.additional['repetition'])
308+
value = change.t1
309+
try:
310+
ignore_order_fixed_indexes = self['ignore_order_fixed_indexes'][path]
311+
except KeyError:
312+
ignore_order_fixed_indexes = self['ignore_order_fixed_indexes'][path] = {}
313+
for index in repetition['new_indexes']:
314+
ignore_order_fixed_indexes[index] = value
315+
# self['repetition_change'][path][]
316+
# old_indexes = set(repetition['old_indexes'])
317+
# new_indexes = set(repetition['new_indexes'])
318+
# value['old_indexes'] = old_indexes - new_indexes
319+
# value['new_indexes'] = new_indexes - old_indexes
320+
# self['repetition_change'][path] = RemapDict(change.additional[
321+
# 'repetition'])
322+
# self['repetition_change'][path]['value'] = change.t1
264323

265324

266325
class DiffLevel:
@@ -474,7 +533,11 @@ def all_down(self):
474533
level = level.down
475534
return level
476535

477-
def path(self, root="root", force=None):
536+
@staticmethod
537+
def _format_result(root, result):
538+
return None if result is None else "{}{}".format(root, result)
539+
540+
def path(self, root="root", force=None, get_parent_too=False):
478541
"""
479542
A python syntax string describing how to descend to this level, assuming the top level object is called root.
480543
Returns None if the path is not representable as a string.
@@ -496,11 +559,16 @@ def path(self, root="root", force=None):
496559
This will pretend all iterables are subscriptable, for example.
497560
"""
498561
# TODO: We could optimize this by building on top of self.up's path if it is cached there
499-
if force in self._path:
500-
result = self._path[force]
501-
return None if result is None else "{}{}".format(root, result)
562+
cache_key = "{}{}".format(force, get_parent_too)
563+
if cache_key in self._path:
564+
result = self._path[cache_key]
565+
if get_parent_too:
566+
# parent, param, result
567+
return (self._format_result(root, result[0]), result[1], self._format_result(root, result[2]))
568+
else:
569+
return self._format_result(root, result)
502570

503-
result = ""
571+
result = parent = param = ""
504572
level = self.all_up # start at the root
505573

506574
# traverse all levels of this relationship
@@ -515,6 +583,8 @@ def path(self, root="root", force=None):
515583
# Build path for this level
516584
item = next_rel.get_param_repr(force)
517585
if item:
586+
parent = result
587+
param = next_rel.param
518588
result += item
519589
else:
520590
# it seems this path is not representable as a string
@@ -525,7 +595,10 @@ def path(self, root="root", force=None):
525595
level = level.down
526596

527597
self._path[force] = result
528-
result = None if result is None else "{}{}".format(root, result)
598+
result = self._format_result(root, result)
599+
if get_parent_too:
600+
parent = self._format_result(root, parent)
601+
return (parent, param, result)
529602
return result
530603

531604
def create_deeper(self,

deepdiff/path.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def _path_to_elements(path, first_element=DEFAULT_FIRST_ELEMENT):
4040
>>> _path_to_elements(path, first_element=None)
4141
[(4.3, 'GET'), ('b', 'GETATTR'), ('a3', 'GET')]
4242
"""
43-
if isinstance(path, tuple):
43+
if isinstance(path, (tuple, list)):
4444
return path
4545
elements = []
4646
if first_element:

0 commit comments

Comments
 (0)