deepdiff-rust 0.1.0__cp39-abi3-win_amd64.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.
@@ -0,0 +1,6 @@
1
+ """Rust-backed DeepDiff-compatible comparison engine."""
2
+
3
+ from ._native import MAX_DEPTH_CEILING, MaxDepthError, __version__, diff_json
4
+ from ._diff import DeepDiff
5
+
6
+ __all__ = ["DeepDiff", "diff_json", "MaxDepthError", "MAX_DEPTH_CEILING", "__version__"]
@@ -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