|
1 | 1 | import logging
|
| 2 | +from functools import partial |
2 | 3 | from collections.abc import Mapping
|
3 | 4 | from copy import deepcopy
|
4 | 5 | from ordered_set import OrderedSet
|
|
9 | 10 | np_ndarray, np_array_factory, numpy_dtypes, get_doc,
|
10 | 11 | not_found, numpy_dtype_string_to_type, dict_,
|
11 | 12 | )
|
12 |
| -from deepdiff.path import _path_to_elements, _get_nested_obj, _get_nested_obj_and_force, GET, GETATTR |
| 13 | +from deepdiff.path import _path_to_elements, _get_nested_obj, _get_nested_obj_and_force, GET, GETATTR, parse_path |
13 | 14 | from deepdiff.anyset import AnySet
|
14 | 15 |
|
15 | 16 |
|
@@ -591,6 +592,155 @@ def dumps(self):
|
591 | 592 | def to_dict(self):
|
592 | 593 | return dict(self.diff)
|
593 | 594 |
|
| 595 | + @staticmethod |
| 596 | + def _get_flat_row(action, info, _parse_path, keys_and_funcs): |
| 597 | + for path, details in info.items(): |
| 598 | + row = {'path': _parse_path(path), 'action': action} |
| 599 | + for key, new_key, func in keys_and_funcs: |
| 600 | + if key in details: |
| 601 | + if func: |
| 602 | + row[new_key] = func(details[key]) |
| 603 | + else: |
| 604 | + row[new_key] = details[key] |
| 605 | + yield row |
| 606 | + |
| 607 | + def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True): |
| 608 | + """ |
| 609 | + Returns a flat list of actions that is easily machine readable. |
| 610 | +
|
| 611 | + For example: |
| 612 | + {'iterable_item_added': {'root[3]': 5, 'root[2]': 3}} |
| 613 | +
|
| 614 | + Becomes: |
| 615 | + [ |
| 616 | + {'path': [3], 'value': 5, 'action': 'iterable_item_added'}, |
| 617 | + {'path': [2], 'value': 3, 'action': 'iterable_item_added'}, |
| 618 | + ] |
| 619 | +
|
| 620 | + |
| 621 | + **Parameters** |
| 622 | +
|
| 623 | + include_action_in_path : Boolean, default=False |
| 624 | + When False, we translate DeepDiff's paths like root[3].attribute1 into a [3, 'attribute1']. |
| 625 | + When True, we include the action to retrieve the item in the path: [(3, 'GET'), ('attribute1', 'GETATTR')] |
| 626 | +
|
| 627 | + report_type_changes : Boolean, default=True |
| 628 | + If False, we don't report the type change. Instead we report the value change. |
| 629 | +
|
| 630 | + Example: |
| 631 | + t1 = {"a": None} |
| 632 | + t2 = {"a": 1} |
| 633 | +
|
| 634 | + dump = Delta(DeepDiff(t1, t2)).dumps() |
| 635 | + delta = Delta(dump) |
| 636 | + assert t2 == delta + t1 |
| 637 | +
|
| 638 | + flat_result = delta.to_flat_dicts() |
| 639 | + flat_expected = [{'path': ['a'], 'action': 'type_changes', 'value': 1, 'new_type': int, 'old_type': type(None)}] |
| 640 | + assert flat_expected == flat_result |
| 641 | +
|
| 642 | + flat_result2 = delta.to_flat_dicts(report_type_changes=False) |
| 643 | + flat_expected2 = [{'path': ['a'], 'action': 'values_changed', 'value': 1}] |
| 644 | +
|
| 645 | + **List of actions** |
| 646 | +
|
| 647 | + Here are the list of actions that the flat dictionary can return. |
| 648 | + iterable_item_added |
| 649 | + iterable_item_removed |
| 650 | + values_changed |
| 651 | + type_changes |
| 652 | + set_item_added |
| 653 | + set_item_removed |
| 654 | + dictionary_item_added |
| 655 | + dictionary_item_removed |
| 656 | + attribute_added |
| 657 | + attribute_removed |
| 658 | + """ |
| 659 | + result = [] |
| 660 | + if include_action_in_path: |
| 661 | + _parse_path = partial(parse_path, include_actions=True) |
| 662 | + else: |
| 663 | + _parse_path = parse_path |
| 664 | + if report_type_changes: |
| 665 | + keys_and_funcs = [ |
| 666 | + ('value', 'value', None), |
| 667 | + ('new_value', 'value', None), |
| 668 | + ('old_value', 'old_value', None), |
| 669 | + ('new_type', 'type', None), |
| 670 | + ('old_type', 'old_type', None), |
| 671 | + ('new_path', 'new_path', _parse_path), |
| 672 | + ] |
| 673 | + action_mapping = {} |
| 674 | + else: |
| 675 | + keys_and_funcs = [ |
| 676 | + ('value', 'value', None), |
| 677 | + ('new_value', 'value', None), |
| 678 | + ('old_value', 'old_value', None), |
| 679 | + ('new_path', 'new_path', _parse_path), |
| 680 | + ] |
| 681 | + action_mapping = {'type_changes': 'values_changed'} |
| 682 | + |
| 683 | + FLATTENING_NEW_ACTION_MAP = { |
| 684 | + 'iterable_items_added_at_indexes': 'iterable_item_added', |
| 685 | + 'iterable_items_removed_at_indexes': 'iterable_item_removed', |
| 686 | + } |
| 687 | + for action, info in self.diff.items(): |
| 688 | + if action in FLATTENING_NEW_ACTION_MAP: |
| 689 | + new_action = FLATTENING_NEW_ACTION_MAP[action] |
| 690 | + for path, index_to_value in info.items(): |
| 691 | + path = _parse_path(path) |
| 692 | + for index, value in index_to_value.items(): |
| 693 | + path2 = path.copy() |
| 694 | + if include_action_in_path: |
| 695 | + path2.append((index, 'GET')) |
| 696 | + else: |
| 697 | + path2.append(index) |
| 698 | + result.append( |
| 699 | + {'path': path2, 'value': value, 'action': new_action} |
| 700 | + ) |
| 701 | + elif action in {'set_item_added', 'set_item_removed'}: |
| 702 | + for path, values in info.items(): |
| 703 | + path = _parse_path(path) |
| 704 | + for value in values: |
| 705 | + result.append( |
| 706 | + {'path': path, 'value': value, 'action': action} |
| 707 | + ) |
| 708 | + elif action == 'dictionary_item_added': |
| 709 | + for path, value in info.items(): |
| 710 | + path = _parse_path(path) |
| 711 | + if isinstance(value, dict) and len(value) == 1: |
| 712 | + new_key = next(iter(value)) |
| 713 | + path.append(new_key) |
| 714 | + value = value[new_key] |
| 715 | + elif isinstance(value, (list, tuple)) and len(value) == 1: |
| 716 | + value = value[0] |
| 717 | + path.append(0) |
| 718 | + action = 'iterable_item_added' |
| 719 | + elif isinstance(value, set) and len(value) == 1: |
| 720 | + value = value.pop() |
| 721 | + action = 'set_item_added' |
| 722 | + result.append( |
| 723 | + {'path': path, 'value': value, 'action': action} |
| 724 | + ) |
| 725 | + elif action in { |
| 726 | + 'dictionary_item_removed', 'iterable_item_added', |
| 727 | + 'iterable_item_removed', 'attribute_removed', 'attribute_added' |
| 728 | + }: |
| 729 | + for path, value in info.items(): |
| 730 | + path = _parse_path(path) |
| 731 | + result.append( |
| 732 | + {'path': path, 'value': value, 'action': action} |
| 733 | + ) |
| 734 | + else: |
| 735 | + for row in self._get_flat_row( |
| 736 | + action=action_mapping.get(action, action), |
| 737 | + info=info, |
| 738 | + _parse_path=_parse_path, |
| 739 | + keys_and_funcs=keys_and_funcs, |
| 740 | + ): |
| 741 | + result.append(row) |
| 742 | + return result |
| 743 | + |
594 | 744 |
|
595 | 745 | if __name__ == "__main__": # pragma: no cover
|
596 | 746 | import doctest
|
|
0 commit comments