deepdiff-rust 0.1.0__cp39-abi3-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepdiff_rust/__init__.py +6 -0
- deepdiff_rust/__init__.pyi +91 -0
- deepdiff_rust/_callbacks.py +111 -0
- deepdiff_rust/_diff.py +285 -0
- deepdiff_rust/_dual.py +130 -0
- deepdiff_rust/_group_by.py +55 -0
- deepdiff_rust/_native.abi3.so +0 -0
- deepdiff_rust/_options.py +93 -0
- deepdiff_rust/_paths.py +152 -0
- deepdiff_rust/_view.py +131 -0
- deepdiff_rust/py.typed +0 -0
- deepdiff_rust-0.1.0.dist-info/METADATA +191 -0
- deepdiff_rust-0.1.0.dist-info/RECORD +17 -0
- deepdiff_rust-0.1.0.dist-info/WHEEL +4 -0
- deepdiff_rust-0.1.0.dist-info/licenses/LICENSE +21 -0
- deepdiff_rust-0.1.0.dist-info/licenses/LICENSE-ONIX +21 -0
- deepdiff_rust-0.1.0.dist-info/sboms/deepdiff-python.cyclonedx.json +1065 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from re import Pattern
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
__version__: str
|
|
6
|
+
|
|
7
|
+
class MaxDepthError(ValueError):
|
|
8
|
+
"""Raised when diffing two values would need to recurse past the configured ``max_depth``."""
|
|
9
|
+
|
|
10
|
+
MAX_DEPTH_CEILING: int
|
|
11
|
+
|
|
12
|
+
class DeepDiff(dict[str, Any]):
|
|
13
|
+
"""A drop-in ``deepdiff.DeepDiff``: the differences between ``t1`` and ``t2`` as a dict."""
|
|
14
|
+
|
|
15
|
+
t1: Any
|
|
16
|
+
t2: Any
|
|
17
|
+
verbose_level: int
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
t1: Any,
|
|
22
|
+
t2: Any,
|
|
23
|
+
*,
|
|
24
|
+
ignore_order: bool = ...,
|
|
25
|
+
max_depth: int | None = ...,
|
|
26
|
+
verbose_level: int = ...,
|
|
27
|
+
view: str = ...,
|
|
28
|
+
exclude_paths: str | Iterable[str] | None = ...,
|
|
29
|
+
exclude_regex_paths: str | Pattern[str] | Iterable[str | Pattern[str]] | None = ...,
|
|
30
|
+
include_paths: str | Iterable[str] | None = ...,
|
|
31
|
+
exclude_types: Iterable[type] | None = ...,
|
|
32
|
+
ignore_private_variables: bool = ...,
|
|
33
|
+
significant_digits: int | None = ...,
|
|
34
|
+
number_format_notation: str = ...,
|
|
35
|
+
math_epsilon: float | None = ...,
|
|
36
|
+
use_log_scale: bool = ...,
|
|
37
|
+
log_scale_similarity_threshold: float = ...,
|
|
38
|
+
ignore_nan_inequality: bool = ...,
|
|
39
|
+
ignore_string_case: bool = ...,
|
|
40
|
+
ignore_numeric_type_changes: bool = ...,
|
|
41
|
+
ignore_string_type_changes: bool = ...,
|
|
42
|
+
ignore_type_in_groups: Any = ...,
|
|
43
|
+
ignore_type_subclasses: bool = ...,
|
|
44
|
+
truncate_datetime: str | None = ...,
|
|
45
|
+
ignore_uuid_types: bool = ...,
|
|
46
|
+
use_enum_value: bool = ...,
|
|
47
|
+
threshold_to_diff_deeper: float = ...,
|
|
48
|
+
cutoff_distance_for_pairs: float = ...,
|
|
49
|
+
cutoff_intersection_for_pairs: float = ...,
|
|
50
|
+
zip_ordered_iterables: bool = ...,
|
|
51
|
+
get_deep_distance: bool = ...,
|
|
52
|
+
group_by: Any = ...,
|
|
53
|
+
group_by_sort_key: Any = ...,
|
|
54
|
+
report_repetition: bool = ...,
|
|
55
|
+
max_diffs: int | None = ...,
|
|
56
|
+
max_passes: int = ...,
|
|
57
|
+
custom_operators: list[Any] | None = ...,
|
|
58
|
+
exclude_obj_callback: Any | None = ...,
|
|
59
|
+
exclude_obj_callback_strict: Any | None = ...,
|
|
60
|
+
include_obj_callback: Any | None = ...,
|
|
61
|
+
include_obj_callback_strict: Any | None = ...,
|
|
62
|
+
iterable_compare_func: Any | None = ...,
|
|
63
|
+
) -> None: ...
|
|
64
|
+
def to_dict(self, verbose_level: int | None = ...) -> dict[str, Any]:
|
|
65
|
+
"""The differences as a plain dict, with ``type_changes`` types as type objects."""
|
|
66
|
+
|
|
67
|
+
def to_json(
|
|
68
|
+
self,
|
|
69
|
+
default_mapping: dict[type, Any] | None = ...,
|
|
70
|
+
force_use_builtin_json: bool = ...,
|
|
71
|
+
verbose_level: int | None = ...,
|
|
72
|
+
**kwargs: Any,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""The differences as JSON; extra keyword arguments go to ``json.dumps``."""
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def affected_paths(self) -> set[str]:
|
|
78
|
+
"""The paths of every difference in this result."""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def affected_root_keys(self) -> set[Any]:
|
|
82
|
+
"""The root keys, indices or attribute names the differences sit under."""
|
|
83
|
+
|
|
84
|
+
def diff_json(
|
|
85
|
+
a: str,
|
|
86
|
+
b: str,
|
|
87
|
+
ignore_order: bool = ...,
|
|
88
|
+
max_depth: int | None = ...,
|
|
89
|
+
) -> str:
|
|
90
|
+
"""Diffs two JSON documents and returns a ``DeepDiff``-compatible JSON report string (``verbose_level=2`` shape)."""
|
|
91
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""DeepDiff callback helpers: Level, activity, object-callback skip."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
|
|
5
|
+
from ._paths import _elements
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Level:
|
|
9
|
+
"""Minimal DeepDiff-compatible level for operators and iterable_compare_func."""
|
|
10
|
+
|
|
11
|
+
__slots__ = ("t1", "t2", "_path")
|
|
12
|
+
|
|
13
|
+
def __init__(self, t1, t2, path="root"):
|
|
14
|
+
self.t1 = t1
|
|
15
|
+
self.t2 = t2
|
|
16
|
+
self._path = path
|
|
17
|
+
|
|
18
|
+
def path(self):
|
|
19
|
+
return self._path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def callbacks_active(
|
|
23
|
+
*,
|
|
24
|
+
custom_operators=None,
|
|
25
|
+
exclude_obj_callback=None,
|
|
26
|
+
exclude_obj_callback_strict=None,
|
|
27
|
+
include_obj_callback=None,
|
|
28
|
+
include_obj_callback_strict=None,
|
|
29
|
+
iterable_compare_func=None,
|
|
30
|
+
):
|
|
31
|
+
return any(
|
|
32
|
+
(
|
|
33
|
+
custom_operators,
|
|
34
|
+
exclude_obj_callback,
|
|
35
|
+
exclude_obj_callback_strict,
|
|
36
|
+
include_obj_callback,
|
|
37
|
+
include_obj_callback_strict,
|
|
38
|
+
iterable_compare_func,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _child(obj, seg):
|
|
44
|
+
if isinstance(obj, Mapping):
|
|
45
|
+
return obj[seg]
|
|
46
|
+
if isinstance(seg, int) and isinstance(obj, (list, tuple)):
|
|
47
|
+
return obj[seg]
|
|
48
|
+
return getattr(obj, seg)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve(root, path):
|
|
52
|
+
if path == "root":
|
|
53
|
+
return root
|
|
54
|
+
cur = root
|
|
55
|
+
for seg in _elements(path):
|
|
56
|
+
cur = _child(cur, seg)
|
|
57
|
+
return cur
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def compose_skip(
|
|
61
|
+
base_skip,
|
|
62
|
+
t1,
|
|
63
|
+
t2,
|
|
64
|
+
*,
|
|
65
|
+
exclude_obj_callback=None,
|
|
66
|
+
exclude_obj_callback_strict=None,
|
|
67
|
+
include_obj_callback=None,
|
|
68
|
+
include_obj_callback_strict=None,
|
|
69
|
+
):
|
|
70
|
+
"""Retain-filter skip plus DeepDiff object-callback branch of `_skip_this`."""
|
|
71
|
+
if not any(
|
|
72
|
+
(
|
|
73
|
+
exclude_obj_callback,
|
|
74
|
+
exclude_obj_callback_strict,
|
|
75
|
+
include_obj_callback,
|
|
76
|
+
include_obj_callback_strict,
|
|
77
|
+
)
|
|
78
|
+
):
|
|
79
|
+
return base_skip
|
|
80
|
+
|
|
81
|
+
def skip_obj(path):
|
|
82
|
+
try:
|
|
83
|
+
o1, o2 = resolve(t1, path), resolve(t2, path)
|
|
84
|
+
except (KeyError, IndexError, AttributeError, TypeError):
|
|
85
|
+
o1 = o2 = None
|
|
86
|
+
if exclude_obj_callback and (
|
|
87
|
+
exclude_obj_callback(o1, path) or exclude_obj_callback(o2, path)
|
|
88
|
+
):
|
|
89
|
+
return True
|
|
90
|
+
if exclude_obj_callback_strict and (
|
|
91
|
+
exclude_obj_callback_strict(o1, path)
|
|
92
|
+
and exclude_obj_callback_strict(o2, path)
|
|
93
|
+
):
|
|
94
|
+
return True
|
|
95
|
+
if include_obj_callback and path != "root":
|
|
96
|
+
return not (
|
|
97
|
+
include_obj_callback(o1, path) or include_obj_callback(o2, path)
|
|
98
|
+
)
|
|
99
|
+
if include_obj_callback_strict and path != "root":
|
|
100
|
+
return not (
|
|
101
|
+
include_obj_callback_strict(o1, path)
|
|
102
|
+
and include_obj_callback_strict(o2, path)
|
|
103
|
+
)
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def skip(prefixes, typed):
|
|
107
|
+
if base_skip and base_skip(prefixes, typed):
|
|
108
|
+
return True
|
|
109
|
+
return any(skip_obj(path) for path in prefixes)
|
|
110
|
+
|
|
111
|
+
return skip
|
deepdiff_rust/_diff.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""The `DeepDiff` result: a `dict` of findings, like `deepdiff.DeepDiff`."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import datetime
|
|
5
|
+
import decimal
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from . import _native
|
|
10
|
+
from ._callbacks import callbacks_active, compose_skip
|
|
11
|
+
from ._dual import run_dual
|
|
12
|
+
from ._group_by import group_pair
|
|
13
|
+
from ._options import native_options
|
|
14
|
+
from ._paths import skip_filter
|
|
15
|
+
from ._view import build_tree, pretty_from_tree, tree_as_result
|
|
16
|
+
|
|
17
|
+
_BUILTIN_TYPES = {
|
|
18
|
+
t.__name__: t
|
|
19
|
+
for t in (
|
|
20
|
+
type(None), bool, int, float, str, list, tuple, set, frozenset, dict,
|
|
21
|
+
datetime.datetime, datetime.date, datetime.time, datetime.timedelta,
|
|
22
|
+
bytes, complex, decimal.Decimal, uuid.UUID,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _check_verbose_level(verbose_level):
|
|
28
|
+
if verbose_level not in (0, 1, 2):
|
|
29
|
+
raise ValueError("verbose_level should be 0, 1, or 2.")
|
|
30
|
+
return verbose_level
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_NOT_PRESENT = object()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _root_key(path):
|
|
37
|
+
"""The key, index or attribute name right after `root` in a rendered path."""
|
|
38
|
+
rest = path[len("root"):]
|
|
39
|
+
if rest.startswith("."):
|
|
40
|
+
ends = [i for i in (rest.find(".", 1), rest.find("[")) if i > 0]
|
|
41
|
+
return rest[1 : min(ends, default=len(rest))]
|
|
42
|
+
# ponytail: tries each "]" until the key parses as a literal; a key whose
|
|
43
|
+
# repr is not a Python literal is skipped.
|
|
44
|
+
end = rest.find("]")
|
|
45
|
+
while end != -1:
|
|
46
|
+
try:
|
|
47
|
+
return ast.literal_eval(rest[1:end])
|
|
48
|
+
except (ValueError, SyntaxError):
|
|
49
|
+
end = rest.find("]", end + 1)
|
|
50
|
+
return _NOT_PRESENT
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DeepDiff(dict):
|
|
54
|
+
"""A drop-in `deepdiff.DeepDiff`: the differences between `t1` and `t2` as a dict."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self, t1, t2, *, ignore_order=False, max_depth=None, verbose_level=1, view="text",
|
|
58
|
+
exclude_paths=None, exclude_regex_paths=None, include_paths=None, exclude_types=None,
|
|
59
|
+
ignore_private_variables=True, significant_digits=None, number_format_notation="f",
|
|
60
|
+
math_epsilon=None, use_log_scale=False, log_scale_similarity_threshold=0.1,
|
|
61
|
+
ignore_nan_inequality=False, ignore_string_case=False, ignore_numeric_type_changes=False,
|
|
62
|
+
ignore_string_type_changes=False, ignore_type_in_groups=None, ignore_type_subclasses=False,
|
|
63
|
+
truncate_datetime=None, ignore_uuid_types=False, use_enum_value=False,
|
|
64
|
+
threshold_to_diff_deeper=0.33, cutoff_distance_for_pairs=0.3,
|
|
65
|
+
cutoff_intersection_for_pairs=0.7, zip_ordered_iterables=False, get_deep_distance=False,
|
|
66
|
+
group_by=None, group_by_sort_key=None, report_repetition=False, max_diffs=None,
|
|
67
|
+
max_passes=10000000, custom_operators=None, exclude_obj_callback=None,
|
|
68
|
+
exclude_obj_callback_strict=None, include_obj_callback=None,
|
|
69
|
+
include_obj_callback_strict=None, iterable_compare_func=None,
|
|
70
|
+
):
|
|
71
|
+
if view not in ("text", "tree"):
|
|
72
|
+
raise NotImplementedError(
|
|
73
|
+
f"view={view!r} is not supported yet; only view='text' and view='tree' are."
|
|
74
|
+
)
|
|
75
|
+
self.view = view
|
|
76
|
+
self.verbose_level = _check_verbose_level(verbose_level)
|
|
77
|
+
if not 0 <= float(cutoff_distance_for_pairs) <= 1:
|
|
78
|
+
raise ValueError("cutoff_distance_for_pairs needs to be a positive float max 1.")
|
|
79
|
+
if t1 is t2 and group_by is None:
|
|
80
|
+
self.t1 = t1
|
|
81
|
+
self.t2 = t2
|
|
82
|
+
self._native = None
|
|
83
|
+
self._bridge = None
|
|
84
|
+
self.tree = {}
|
|
85
|
+
super().__init__()
|
|
86
|
+
return
|
|
87
|
+
t1, t2 = group_pair(t1, t2, group_by, group_by_sort_key)
|
|
88
|
+
self.t1 = t1
|
|
89
|
+
self.t2 = t2
|
|
90
|
+
self.custom_operators = custom_operators or []
|
|
91
|
+
self.exclude_obj_callback = exclude_obj_callback
|
|
92
|
+
self.exclude_obj_callback_strict = exclude_obj_callback_strict
|
|
93
|
+
self.include_obj_callback = include_obj_callback
|
|
94
|
+
self.include_obj_callback_strict = include_obj_callback_strict
|
|
95
|
+
self.iterable_compare_func = iterable_compare_func
|
|
96
|
+
self._callbacks_active = callbacks_active(
|
|
97
|
+
custom_operators=custom_operators,
|
|
98
|
+
exclude_obj_callback=exclude_obj_callback,
|
|
99
|
+
exclude_obj_callback_strict=exclude_obj_callback_strict,
|
|
100
|
+
include_obj_callback=include_obj_callback,
|
|
101
|
+
include_obj_callback_strict=include_obj_callback_strict,
|
|
102
|
+
iterable_compare_func=iterable_compare_func,
|
|
103
|
+
)
|
|
104
|
+
exclude_types = tuple(exclude_types) if exclude_types else None
|
|
105
|
+
if custom_operators or iterable_compare_func:
|
|
106
|
+
def native_factory(a, b):
|
|
107
|
+
return dict(
|
|
108
|
+
_native.DeepDiff(
|
|
109
|
+
a, b,
|
|
110
|
+
ignore_order=ignore_order, max_depth=max_depth,
|
|
111
|
+
exclude_types=exclude_types,
|
|
112
|
+
ignore_private_variables=ignore_private_variables,
|
|
113
|
+
use_enum_value=use_enum_value,
|
|
114
|
+
threshold_to_diff_deeper=float(threshold_to_diff_deeper or 0),
|
|
115
|
+
cutoff_distance_for_pairs=float(cutoff_distance_for_pairs),
|
|
116
|
+
cutoff_intersection_for_pairs=float(cutoff_intersection_for_pairs),
|
|
117
|
+
zip_ordered_iterables=zip_ordered_iterables,
|
|
118
|
+
get_deep_distance=False, report_repetition=report_repetition,
|
|
119
|
+
max_diffs=max_diffs, max_passes=max_passes,
|
|
120
|
+
**native_options(
|
|
121
|
+
_BUILTIN_TYPES, significant_digits=significant_digits,
|
|
122
|
+
number_format_notation=number_format_notation,
|
|
123
|
+
math_epsilon=math_epsilon, use_log_scale=use_log_scale,
|
|
124
|
+
log_scale_similarity_threshold=log_scale_similarity_threshold,
|
|
125
|
+
ignore_nan_inequality=ignore_nan_inequality,
|
|
126
|
+
ignore_string_case=ignore_string_case,
|
|
127
|
+
ignore_numeric_type_changes=ignore_numeric_type_changes,
|
|
128
|
+
ignore_string_type_changes=ignore_string_type_changes,
|
|
129
|
+
ignore_type_in_groups=ignore_type_in_groups,
|
|
130
|
+
ignore_type_subclasses=ignore_type_subclasses,
|
|
131
|
+
truncate_datetime=truncate_datetime,
|
|
132
|
+
ignore_uuid_types=ignore_uuid_types,
|
|
133
|
+
),
|
|
134
|
+
).to_dict(verbose_level)
|
|
135
|
+
)
|
|
136
|
+
self._native = None
|
|
137
|
+
self._bridge = None
|
|
138
|
+
self._finish_init(run_dual(self, t1, t2, native_factory))
|
|
139
|
+
return
|
|
140
|
+
skip = skip_filter(exclude_paths, include_paths, exclude_regex_paths, exclude_types)
|
|
141
|
+
skip = compose_skip(
|
|
142
|
+
skip, t1, t2,
|
|
143
|
+
exclude_obj_callback=exclude_obj_callback,
|
|
144
|
+
exclude_obj_callback_strict=exclude_obj_callback_strict,
|
|
145
|
+
include_obj_callback=include_obj_callback,
|
|
146
|
+
include_obj_callback_strict=include_obj_callback_strict,
|
|
147
|
+
)
|
|
148
|
+
self._bridge = None
|
|
149
|
+
self._native = _native.DeepDiff(
|
|
150
|
+
t1, t2, ignore_order=ignore_order, max_depth=max_depth, exclude_types=exclude_types,
|
|
151
|
+
ignore_private_variables=ignore_private_variables, skip=skip, use_enum_value=use_enum_value,
|
|
152
|
+
threshold_to_diff_deeper=float(threshold_to_diff_deeper or 0),
|
|
153
|
+
cutoff_distance_for_pairs=float(cutoff_distance_for_pairs),
|
|
154
|
+
cutoff_intersection_for_pairs=float(cutoff_intersection_for_pairs),
|
|
155
|
+
zip_ordered_iterables=zip_ordered_iterables, get_deep_distance=get_deep_distance,
|
|
156
|
+
report_repetition=report_repetition, max_diffs=max_diffs, max_passes=max_passes,
|
|
157
|
+
**native_options(
|
|
158
|
+
_BUILTIN_TYPES, significant_digits=significant_digits,
|
|
159
|
+
number_format_notation=number_format_notation, math_epsilon=math_epsilon,
|
|
160
|
+
use_log_scale=use_log_scale,
|
|
161
|
+
log_scale_similarity_threshold=log_scale_similarity_threshold,
|
|
162
|
+
ignore_nan_inequality=ignore_nan_inequality, ignore_string_case=ignore_string_case,
|
|
163
|
+
ignore_numeric_type_changes=ignore_numeric_type_changes,
|
|
164
|
+
ignore_string_type_changes=ignore_string_type_changes,
|
|
165
|
+
ignore_type_in_groups=ignore_type_in_groups,
|
|
166
|
+
ignore_type_subclasses=ignore_type_subclasses,
|
|
167
|
+
truncate_datetime=truncate_datetime, ignore_uuid_types=ignore_uuid_types,
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
self._finish_init(self.to_dict())
|
|
171
|
+
|
|
172
|
+
def _finish_init(self, report):
|
|
173
|
+
"""Store text report, build `.tree`, and optionally switch to view='tree'."""
|
|
174
|
+
text = dict(report)
|
|
175
|
+
self.tree = build_tree(text, self.verbose_level)
|
|
176
|
+
if self.view == "tree":
|
|
177
|
+
super().__init__(tree_as_result(self.tree))
|
|
178
|
+
else:
|
|
179
|
+
super().__init__(text)
|
|
180
|
+
|
|
181
|
+
def to_dict(self, verbose_level=None):
|
|
182
|
+
"""The differences as a plain dict, with `type_changes` types as type objects."""
|
|
183
|
+
if self._native is None:
|
|
184
|
+
if self._bridge is not None:
|
|
185
|
+
return self._bridge.to_dict(verbose_level=self._level(verbose_level))
|
|
186
|
+
if getattr(self, "view", "text") == "tree":
|
|
187
|
+
# Rebuild a text-shaped dict from levels when asked.
|
|
188
|
+
out = {}
|
|
189
|
+
for key, levels in self.tree.items():
|
|
190
|
+
if key.endswith(("_added", "_removed")) and self._level(verbose_level) < 2:
|
|
191
|
+
out[key] = [level.path() for level in levels]
|
|
192
|
+
elif key in ("values_changed", "type_changes"):
|
|
193
|
+
out[key] = {
|
|
194
|
+
level.path(): {
|
|
195
|
+
"old_value": level.t1,
|
|
196
|
+
"new_value": level.t2,
|
|
197
|
+
**(
|
|
198
|
+
{"old_type": type(level.t1), "new_type": type(level.t2)}
|
|
199
|
+
if key == "type_changes"
|
|
200
|
+
else {}
|
|
201
|
+
),
|
|
202
|
+
}
|
|
203
|
+
for level in levels
|
|
204
|
+
}
|
|
205
|
+
else:
|
|
206
|
+
out[key] = {
|
|
207
|
+
level.path(): level.t2 if key.endswith("_added") else level.t1
|
|
208
|
+
for level in levels
|
|
209
|
+
}
|
|
210
|
+
return out
|
|
211
|
+
return dict(self)
|
|
212
|
+
report = self._native.to_dict(self._level(verbose_level))
|
|
213
|
+
if "type_changes" in report:
|
|
214
|
+
classes = {**_BUILTIN_TYPES, **self._native._types}
|
|
215
|
+
for change in report["type_changes"].values():
|
|
216
|
+
for key in ("old_type", "new_type"):
|
|
217
|
+
change[key] = classes.get(change[key], change[key])
|
|
218
|
+
if self._native.deep_distance is not None:
|
|
219
|
+
report["deep_distance"] = self._native.deep_distance
|
|
220
|
+
return report
|
|
221
|
+
|
|
222
|
+
def to_json(self, default_mapping=None, force_use_builtin_json=False, verbose_level=None, **kwargs):
|
|
223
|
+
"""The differences as JSON; extra keyword arguments go to `json.dumps`."""
|
|
224
|
+
if default_mapping is not None:
|
|
225
|
+
raise NotImplementedError("default_mapping is not supported yet.")
|
|
226
|
+
if self._native is None:
|
|
227
|
+
if self._bridge is not None:
|
|
228
|
+
return self._bridge.to_json(verbose_level=self._level(verbose_level), **kwargs)
|
|
229
|
+
return json.dumps(self.to_dict(verbose_level), **kwargs) if kwargs else json.dumps(self.to_dict(verbose_level))
|
|
230
|
+
text = self._native.to_json(self._level(verbose_level))
|
|
231
|
+
if self._native.deep_distance is not None:
|
|
232
|
+
report = json.loads(text)
|
|
233
|
+
report["deep_distance"] = self._native.deep_distance
|
|
234
|
+
return json.dumps(report, **kwargs)
|
|
235
|
+
return json.dumps(json.loads(text), **kwargs) if kwargs else text
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def affected_paths(self):
|
|
239
|
+
"""The paths of every difference in this result."""
|
|
240
|
+
paths = set()
|
|
241
|
+
for key, findings in self.items():
|
|
242
|
+
if key == "deep_distance":
|
|
243
|
+
continue
|
|
244
|
+
if getattr(self, "view", "text") == "tree":
|
|
245
|
+
paths.update(level.path() for level in findings)
|
|
246
|
+
elif isinstance(findings, dict):
|
|
247
|
+
paths.update(findings)
|
|
248
|
+
else:
|
|
249
|
+
paths.update(findings)
|
|
250
|
+
return paths
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def affected_root_keys(self):
|
|
254
|
+
"""The root keys, indices or attribute names the differences sit under."""
|
|
255
|
+
report = self.to_dict(2)
|
|
256
|
+
keys = set()
|
|
257
|
+
for findings in report.values():
|
|
258
|
+
if isinstance(findings, dict):
|
|
259
|
+
keys.update(_root_key(path) for path in findings)
|
|
260
|
+
else:
|
|
261
|
+
keys.update(_root_key(path) for path in findings)
|
|
262
|
+
keys.discard(_NOT_PRESENT)
|
|
263
|
+
return keys
|
|
264
|
+
|
|
265
|
+
def pretty(self, prefix=None):
|
|
266
|
+
"""Human-readable multi-line summary of the differences."""
|
|
267
|
+
tree = getattr(self, "tree", None) or build_tree(self.to_dict(), self.verbose_level)
|
|
268
|
+
return pretty_from_tree(tree, self.verbose_level, prefix)
|
|
269
|
+
|
|
270
|
+
def custom_report_result(self, report_type, level, extra_info=None):
|
|
271
|
+
path = level.path()
|
|
272
|
+
payload = dict(extra_info or {})
|
|
273
|
+
if report_type == "values_changed":
|
|
274
|
+
payload.setdefault("old_value", level.t1)
|
|
275
|
+
payload.setdefault("new_value", level.t2)
|
|
276
|
+
bucket = getattr(self, "_walk_report", self).setdefault(report_type, {})
|
|
277
|
+
bucket[path] = payload
|
|
278
|
+
if self._native is None and getattr(self, "_bridge", None) is None:
|
|
279
|
+
self.setdefault(report_type, {})[path] = payload
|
|
280
|
+
|
|
281
|
+
def _report_result(self, report_type, change_level, local_tree=None):
|
|
282
|
+
self.custom_report_result(report_type, change_level)
|
|
283
|
+
|
|
284
|
+
def _level(self, verbose_level):
|
|
285
|
+
return self.verbose_level if verbose_level is None else _check_verbose_level(verbose_level)
|
deepdiff_rust/_dual.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Native dual walk for custom_operators and iterable_compare_func."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
|
|
5
|
+
from ._callbacks import Level
|
|
6
|
+
|
|
7
|
+
_SENTINEL = object()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_seq(x):
|
|
11
|
+
return isinstance(x, Sequence) and not isinstance(x, (str, bytes, bytearray))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _remap(path, prefix):
|
|
15
|
+
if path == "root":
|
|
16
|
+
return prefix
|
|
17
|
+
return prefix + path[4:]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _merge_native(into, native_dict, prefix):
|
|
21
|
+
for category, findings in native_dict.items():
|
|
22
|
+
if category == "deep_distance":
|
|
23
|
+
continue
|
|
24
|
+
bucket = into.setdefault(category, {})
|
|
25
|
+
if isinstance(findings, dict):
|
|
26
|
+
for path, payload in findings.items():
|
|
27
|
+
bucket[_remap(path, prefix)] = payload
|
|
28
|
+
else:
|
|
29
|
+
bucket.update(findings)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _matching_pairs(t1, t2, level, compare_func):
|
|
33
|
+
if compare_func is None:
|
|
34
|
+
from itertools import zip_longest
|
|
35
|
+
return [
|
|
36
|
+
((i, i), (x, y))
|
|
37
|
+
for i, (x, y) in enumerate(zip_longest(t1, t2, fillvalue=_SENTINEL))
|
|
38
|
+
]
|
|
39
|
+
matches = []
|
|
40
|
+
y_matched = set()
|
|
41
|
+
for i, x in enumerate(t1):
|
|
42
|
+
found = False
|
|
43
|
+
for j, y in enumerate(t2):
|
|
44
|
+
if j in y_matched:
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
ok = compare_func(x, y, level)
|
|
48
|
+
except Exception:
|
|
49
|
+
ok = False
|
|
50
|
+
if ok:
|
|
51
|
+
y_matched.add(j)
|
|
52
|
+
matches.append(((i, j), (x, y)))
|
|
53
|
+
found = True
|
|
54
|
+
break
|
|
55
|
+
if not found:
|
|
56
|
+
matches.append(((i, -1), (x, _SENTINEL)))
|
|
57
|
+
for j, y in enumerate(t2):
|
|
58
|
+
if j not in y_matched:
|
|
59
|
+
matches.append(((-1, j), (_SENTINEL, y)))
|
|
60
|
+
return matches
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def walk_diff(diff_instance, t1, t2, path, native_factory):
|
|
64
|
+
level = Level(t1, t2, path)
|
|
65
|
+
for operator in diff_instance.custom_operators:
|
|
66
|
+
if operator.match(level) and operator.give_up_diffing(level, diff_instance):
|
|
67
|
+
return
|
|
68
|
+
if _skip_obj(diff_instance, t1, t2, path):
|
|
69
|
+
return
|
|
70
|
+
if t1 is t2:
|
|
71
|
+
return
|
|
72
|
+
if isinstance(t1, Mapping) and isinstance(t2, Mapping):
|
|
73
|
+
_walk_mapping(diff_instance, t1, t2, path, native_factory)
|
|
74
|
+
return
|
|
75
|
+
if _is_seq(t1) and _is_seq(t2):
|
|
76
|
+
_walk_seq(diff_instance, t1, t2, path, native_factory)
|
|
77
|
+
return
|
|
78
|
+
_merge_native(diff_instance._walk_report, native_factory(t1, t2), path)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _skip_obj(diff_instance, t1, t2, path):
|
|
82
|
+
ex = diff_instance.exclude_obj_callback
|
|
83
|
+
if ex and (ex(t1, path) or ex(t2, path)):
|
|
84
|
+
return True
|
|
85
|
+
exs = diff_instance.exclude_obj_callback_strict
|
|
86
|
+
if exs and exs(t1, path) and exs(t2, path):
|
|
87
|
+
return True
|
|
88
|
+
inc = diff_instance.include_obj_callback
|
|
89
|
+
if inc and path != "root" and not (inc(t1, path) or inc(t2, path)):
|
|
90
|
+
return True
|
|
91
|
+
incs = diff_instance.include_obj_callback_strict
|
|
92
|
+
if incs and path != "root" and not (incs(t1, path) and incs(t2, path)):
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _walk_mapping(diff_instance, t1, t2, path, native_factory):
|
|
98
|
+
for key in set(t1) | set(t2):
|
|
99
|
+
child = f"{path}[{key!r}]"
|
|
100
|
+
if key in t1 and key in t2:
|
|
101
|
+
walk_diff(diff_instance, t1[key], t2[key], child, native_factory)
|
|
102
|
+
elif key in t1:
|
|
103
|
+
diff_instance._walk_report.setdefault("dictionary_item_removed", {})[child] = t1[key]
|
|
104
|
+
else:
|
|
105
|
+
diff_instance._walk_report.setdefault("dictionary_item_added", {})[child] = t2[key]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _walk_seq(diff_instance, t1, t2, path, native_factory):
|
|
109
|
+
level = Level(t1, t2, path)
|
|
110
|
+
pairs = _matching_pairs(t1, t2, level, diff_instance.iterable_compare_func)
|
|
111
|
+
for (i, j), (x, y) in pairs:
|
|
112
|
+
if x is _SENTINEL:
|
|
113
|
+
child = f"{path}[{j}]"
|
|
114
|
+
diff_instance._walk_report.setdefault("iterable_item_added", {})[child] = y
|
|
115
|
+
elif y is _SENTINEL:
|
|
116
|
+
child = f"{path}[{i}]"
|
|
117
|
+
diff_instance._walk_report.setdefault("iterable_item_removed", {})[child] = x
|
|
118
|
+
else:
|
|
119
|
+
moved = i != j and diff_instance.iterable_compare_func
|
|
120
|
+
child = f"{path}[{j if moved else i}]"
|
|
121
|
+
if moved and diff_instance.verbose_level > 1:
|
|
122
|
+
bucket = diff_instance._walk_report.setdefault("iterable_item_moved", {})
|
|
123
|
+
bucket[f"{path}[{i}]"] = {"new_path": f"{path}[{j}]", "value": y}
|
|
124
|
+
walk_diff(diff_instance, x, y, child, native_factory)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def run_dual(diff_instance, t1, t2, native_factory):
|
|
128
|
+
diff_instance._walk_report = {}
|
|
129
|
+
walk_diff(diff_instance, t1, t2, "root", native_factory)
|
|
130
|
+
return diff_instance._walk_report
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""`group_by`: a list of dicts regrouped into a dict before the diff, as DeepDiff does."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _key(row, group_by):
|
|
8
|
+
return group_by(row) if callable(group_by) else row.pop(group_by)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _add(result, key, row, sort_key):
|
|
12
|
+
if sort_key is None:
|
|
13
|
+
result[key] = row
|
|
14
|
+
elif row not in result.setdefault(key, []):
|
|
15
|
+
result[key].append(row)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _group(item, group_by, sort_key):
|
|
19
|
+
"""DeepDiff's `_group_iterable_to_dict`; raises `KeyError` or `ValueError` when it cannot."""
|
|
20
|
+
level1, level2 = group_by, None
|
|
21
|
+
if isinstance(group_by, (list, tuple)):
|
|
22
|
+
level1, level2 = group_by[0], group_by[1] if len(group_by) > 1 else None
|
|
23
|
+
if not isinstance(item, Iterable) or isinstance(item, Mapping):
|
|
24
|
+
raise ValueError(f"Unable to group by {group_by}")
|
|
25
|
+
result = {}
|
|
26
|
+
for row in deepcopy(item):
|
|
27
|
+
if not isinstance(row, Mapping):
|
|
28
|
+
raise ValueError(f"Unable to group by {level1}: {row!r} is not a dictionary.")
|
|
29
|
+
key1 = _key(row, level1)
|
|
30
|
+
if level2:
|
|
31
|
+
_add(result.setdefault(key1, {}), _key(row, level2), row, sort_key)
|
|
32
|
+
else:
|
|
33
|
+
_add(result, key1, row, sort_key)
|
|
34
|
+
if sort_key is not None:
|
|
35
|
+
groups = [g for sub in result.values() for g in sub.values()] if level2 else result.values()
|
|
36
|
+
for rows in groups:
|
|
37
|
+
rows.sort(key=sort_key)
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def group_pair(t1, t2, group_by, group_by_sort_key):
|
|
42
|
+
"""`(t1, t2)` grouped, or both unchanged when either side cannot be grouped."""
|
|
43
|
+
if not group_by:
|
|
44
|
+
return t1, t2
|
|
45
|
+
sort_key = group_by_sort_key
|
|
46
|
+
if sort_key is not None and not callable(sort_key):
|
|
47
|
+
name = sort_key
|
|
48
|
+
|
|
49
|
+
def sort_key(row):
|
|
50
|
+
return row[name]
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
return _group(t1, group_by, sort_key), _group(t2, group_by, sort_key)
|
|
54
|
+
except (KeyError, ValueError):
|
|
55
|
+
return t1, t2
|
|
Binary file
|