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.
- 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.pyd +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,93 @@
|
|
|
1
|
+
"""deepdiff 9.1.0's value-normalization options, as the native diff takes them."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import decimal
|
|
5
|
+
import fractions
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
# deepdiff's `numbers` and `strings`, plus numpy scalars when numpy is importable.
|
|
9
|
+
_NUMBERS = [
|
|
10
|
+
int, float, complex, decimal.Decimal, fractions.Fraction,
|
|
11
|
+
datetime.datetime, datetime.date, datetime.timedelta, datetime.time,
|
|
12
|
+
]
|
|
13
|
+
try:
|
|
14
|
+
import numpy as _np
|
|
15
|
+
|
|
16
|
+
_NUMBERS.extend(
|
|
17
|
+
t for t in (
|
|
18
|
+
getattr(_np, "integer", None),
|
|
19
|
+
getattr(_np, "floating", None),
|
|
20
|
+
getattr(_np, "complexfloating", None),
|
|
21
|
+
)
|
|
22
|
+
if t is not None
|
|
23
|
+
)
|
|
24
|
+
except ImportError:
|
|
25
|
+
pass
|
|
26
|
+
_NUMBERS = tuple(_NUMBERS)
|
|
27
|
+
_STRINGS = (str, bytes, memoryview)
|
|
28
|
+
_UUID_STR = (uuid.UUID, str)
|
|
29
|
+
_ZEROED = ("hour", "minute", "second", "microsecond")
|
|
30
|
+
_TRUNCATE_FROM = {"day": 0, "hour": 1, "minute": 2, "second": 3}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _type_groups(groups, ignore_string_type_changes, ignore_numeric_type_changes, ignore_uuid_types):
|
|
34
|
+
"""`get_ignore_types_in_groups`: each group a tuple of types."""
|
|
35
|
+
if groups and isinstance(groups[0], type):
|
|
36
|
+
groups = [groups]
|
|
37
|
+
result = [
|
|
38
|
+
tuple(type(item) if item is None or not isinstance(item, type) else item for item in group)
|
|
39
|
+
for group in groups or []
|
|
40
|
+
]
|
|
41
|
+
if ignore_string_type_changes and _STRINGS not in result:
|
|
42
|
+
result.append(_STRINGS)
|
|
43
|
+
if ignore_numeric_type_changes and _NUMBERS not in result:
|
|
44
|
+
result.append(_NUMBERS)
|
|
45
|
+
if ignore_uuid_types and _UUID_STR not in result:
|
|
46
|
+
result.append(_UUID_STR)
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def native_options(
|
|
51
|
+
builtin_types, *, significant_digits, number_format_notation, math_epsilon, use_log_scale,
|
|
52
|
+
log_scale_similarity_threshold, ignore_nan_inequality, ignore_string_case,
|
|
53
|
+
ignore_numeric_type_changes, ignore_string_type_changes, ignore_type_in_groups,
|
|
54
|
+
ignore_type_subclasses, truncate_datetime, ignore_uuid_types=False,
|
|
55
|
+
):
|
|
56
|
+
"""The native `DeepDiff` keyword arguments for these options."""
|
|
57
|
+
if significant_digits is not None and significant_digits < 0:
|
|
58
|
+
raise ValueError("significant_digits must be None or a non-negative integer")
|
|
59
|
+
if significant_digits is None and ignore_numeric_type_changes:
|
|
60
|
+
significant_digits = 12
|
|
61
|
+
if significant_digits is not None and number_format_notation not in ("f", "e"):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"number_format_notation got invalid value of {number_format_notation}. "
|
|
64
|
+
"The valid values are 'f' and 'e'"
|
|
65
|
+
)
|
|
66
|
+
if truncate_datetime not in {None, *_TRUNCATE_FROM}:
|
|
67
|
+
raise ValueError("truncate_datetime must be second, minute, hour or day")
|
|
68
|
+
options = {
|
|
69
|
+
"truncate_datetime": truncate_datetime
|
|
70
|
+
and dict.fromkeys(_ZEROED[_TRUNCATE_FROM[truncate_datetime]:], 0),
|
|
71
|
+
"significant_digits": significant_digits,
|
|
72
|
+
"exponent_notation": number_format_notation == "e",
|
|
73
|
+
"math_epsilon": math_epsilon,
|
|
74
|
+
"log_scale_threshold": log_scale_similarity_threshold if use_log_scale else None,
|
|
75
|
+
"ignore_nan_inequality": ignore_nan_inequality,
|
|
76
|
+
"ignore_string_case": ignore_string_case,
|
|
77
|
+
"ignore_numeric_type_changes": ignore_numeric_type_changes,
|
|
78
|
+
}
|
|
79
|
+
groups = _type_groups(
|
|
80
|
+
ignore_type_in_groups, ignore_string_type_changes, ignore_numeric_type_changes,
|
|
81
|
+
ignore_uuid_types,
|
|
82
|
+
)
|
|
83
|
+
if groups:
|
|
84
|
+
# ponytail: a u64 mask, so at most 64 groups.
|
|
85
|
+
def group_mask(cls):
|
|
86
|
+
return sum(
|
|
87
|
+
1 << i for i, group in enumerate(groups)
|
|
88
|
+
if (cls in group if ignore_type_subclasses else issubclass(cls, group))
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
options["type_groups"] = {name: group_mask(cls) for name, cls in builtin_types.items()}
|
|
92
|
+
options["group_mask"] = group_mask
|
|
93
|
+
return options
|
deepdiff_rust/_paths.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Path filters: deepdiff 9.1.0's `_skip_this` over rendered path strings."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import re
|
|
5
|
+
from functools import cache
|
|
6
|
+
|
|
7
|
+
_WILDCARD_RE = re.compile(r"\[\*\*?\]|\.\*\*?(?=[.\[]|$)")
|
|
8
|
+
_ONE, _ANY = object(), object()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _element(text, literal):
|
|
12
|
+
if text in ("*", "**"):
|
|
13
|
+
return _ONE if text == "*" else _ANY
|
|
14
|
+
if not literal:
|
|
15
|
+
return text
|
|
16
|
+
try:
|
|
17
|
+
return ast.literal_eval(text)
|
|
18
|
+
except (ValueError, SyntaxError):
|
|
19
|
+
return text
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cache
|
|
23
|
+
def _elements(path):
|
|
24
|
+
"""`root['a'][0].b` -> ('a', 0, 'b'); `*`/`**` become wildcard markers."""
|
|
25
|
+
out, i, n = [], 4, len(path)
|
|
26
|
+
while i < n:
|
|
27
|
+
j = i + 1
|
|
28
|
+
if path[i] == ".":
|
|
29
|
+
while j < n and path[j] not in ".[":
|
|
30
|
+
j += 1
|
|
31
|
+
out.append(_element(path[i + 1 : j], literal=False))
|
|
32
|
+
i = j
|
|
33
|
+
continue
|
|
34
|
+
quote = None
|
|
35
|
+
while j < n and (quote or path[j] != "]"):
|
|
36
|
+
if quote and path[j] == "\\":
|
|
37
|
+
j += 1
|
|
38
|
+
elif path[j] == quote:
|
|
39
|
+
quote = None
|
|
40
|
+
elif not quote and path[j] in "'\"":
|
|
41
|
+
quote = path[j]
|
|
42
|
+
j += 1
|
|
43
|
+
out.append(_element(path[i + 1 : j], literal=True))
|
|
44
|
+
i = j + 1
|
|
45
|
+
return tuple(out)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _matches(pattern, target, prefix):
|
|
49
|
+
"""`pattern` matches `target`, or (`prefix`) some prefix of it."""
|
|
50
|
+
|
|
51
|
+
@cache
|
|
52
|
+
def go(p, t):
|
|
53
|
+
if p == len(pattern):
|
|
54
|
+
return prefix or t == len(target)
|
|
55
|
+
if pattern[p] is _ANY:
|
|
56
|
+
return any(go(p + 1, k) for k in range(t, len(target) + 1))
|
|
57
|
+
if t == len(target):
|
|
58
|
+
return False
|
|
59
|
+
return (pattern[p] is _ONE or pattern[p] == target[t]) and go(p + 1, t + 1)
|
|
60
|
+
|
|
61
|
+
return go(0, 0)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _could_reach(pattern, target):
|
|
65
|
+
"""`target` is an ancestor of some path `pattern` matches."""
|
|
66
|
+
|
|
67
|
+
@cache
|
|
68
|
+
def go(p, t):
|
|
69
|
+
if t == len(target):
|
|
70
|
+
return p < len(pattern)
|
|
71
|
+
if p == len(pattern):
|
|
72
|
+
return False
|
|
73
|
+
if pattern[p] is _ANY:
|
|
74
|
+
return go(p + 1, t) or go(p, t + 1)
|
|
75
|
+
return (pattern[p] is _ONE or pattern[p] == target[t]) and go(p + 1, t + 1)
|
|
76
|
+
|
|
77
|
+
return go(0, 0)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _root_paths(paths):
|
|
81
|
+
"""deepdiff's `add_root_to_paths`: a bare key also means `root[key]`."""
|
|
82
|
+
result = set()
|
|
83
|
+
for path in paths:
|
|
84
|
+
if path.startswith("root"):
|
|
85
|
+
result.add(path)
|
|
86
|
+
elif path.isdigit():
|
|
87
|
+
result |= {f"root['{path}']", f"root[{path}]"}
|
|
88
|
+
elif path[0].isdigit():
|
|
89
|
+
result.add(f"root['{path}']")
|
|
90
|
+
else:
|
|
91
|
+
result |= {f"root.{path}", f"root['{path}']"}
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _split(paths):
|
|
96
|
+
"""(exact paths, glob patterns as element tuples), as deepdiff separates them."""
|
|
97
|
+
if not paths:
|
|
98
|
+
return set(), []
|
|
99
|
+
paths = {paths} if isinstance(paths, str) else set(paths)
|
|
100
|
+
globs = []
|
|
101
|
+
for path in paths:
|
|
102
|
+
if _WILDCARD_RE.search(path):
|
|
103
|
+
if not path.startswith("root"):
|
|
104
|
+
raise ValueError(f"Wildcard paths must start with 'root'. Got: {path}")
|
|
105
|
+
globs.append(_elements(path))
|
|
106
|
+
exact = {path for path in paths if not _WILDCARD_RE.search(path)}
|
|
107
|
+
return _root_paths(exact), globs
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _regexes(items):
|
|
111
|
+
if not items:
|
|
112
|
+
return []
|
|
113
|
+
items = [items] if isinstance(items, (str, re.Pattern)) else list(items)
|
|
114
|
+
return [item if isinstance(item, re.Pattern) else re.compile(item) for item in items]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def skip_filter(exclude_paths, include_paths, exclude_regex_paths, exclude_types):
|
|
118
|
+
"""`skip(prefixes, typed)` for a finding: `prefixes` are its path's rendered
|
|
119
|
+
prefixes from `root`, `typed[k]` whether `prefixes[k]` holds an
|
|
120
|
+
`exclude_types` instance. `None` when nothing filters."""
|
|
121
|
+
ex_exact, ex_globs = _split(exclude_paths)
|
|
122
|
+
in_exact, in_globs = _split(include_paths)
|
|
123
|
+
regexes = _regexes(exclude_regex_paths)
|
|
124
|
+
if not (ex_exact or ex_globs or in_exact or in_globs or regexes or exclude_types):
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def included(path):
|
|
128
|
+
return path in in_exact or any(p in path or path in p for p in in_exact)
|
|
129
|
+
|
|
130
|
+
@cache
|
|
131
|
+
def skip_level(path, typed):
|
|
132
|
+
if path in ex_exact or any(_matches(g, _elements(path), False) for g in ex_globs):
|
|
133
|
+
return True
|
|
134
|
+
if (in_exact or in_globs) and path != "root":
|
|
135
|
+
if included(path):
|
|
136
|
+
return False
|
|
137
|
+
target = _elements(path)
|
|
138
|
+
return not any(_matches(g, target, False) or _could_reach(g, target) for g in in_globs)
|
|
139
|
+
if any(regex.search(path) for regex in regexes):
|
|
140
|
+
return True
|
|
141
|
+
return typed
|
|
142
|
+
|
|
143
|
+
def skip_report(path):
|
|
144
|
+
# `_skip_report_for_include_glob`: a glob ancestor is walked, not reported.
|
|
145
|
+
if not in_globs or path in in_exact or any(p in path for p in in_exact):
|
|
146
|
+
return False
|
|
147
|
+
return not any(_matches(g, _elements(path), True) for g in in_globs)
|
|
148
|
+
|
|
149
|
+
def skip(prefixes, typed):
|
|
150
|
+
return any(map(skip_level, prefixes, typed)) or skip_report(prefixes[-1])
|
|
151
|
+
|
|
152
|
+
return skip
|
deepdiff_rust/_view.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""pretty() formatting and a minimal view='tree' DiffLevel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DiffLevel:
|
|
7
|
+
"""Minimal DeepDiff tree node: path, t1/t2, report_type."""
|
|
8
|
+
|
|
9
|
+
__slots__ = ("t1", "t2", "report_type", "_path", "additional")
|
|
10
|
+
|
|
11
|
+
def __init__(self, path, t1, t2, report_type):
|
|
12
|
+
self._path = path
|
|
13
|
+
self.t1 = t1
|
|
14
|
+
self.t2 = t2
|
|
15
|
+
self.report_type = report_type
|
|
16
|
+
self.additional = {}
|
|
17
|
+
|
|
18
|
+
def path(self, root="root", **_kwargs):
|
|
19
|
+
if root == "root":
|
|
20
|
+
return self._path
|
|
21
|
+
if self._path == "root":
|
|
22
|
+
return root
|
|
23
|
+
return root + self._path[4:]
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return f"<{self._path} t1:{self.t1!r}, t2:{self.t2!r}>"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_PRETTY = {
|
|
30
|
+
"type_changes": (
|
|
31
|
+
"Type of {diff_path} changed from {type_t1} to {type_t2} "
|
|
32
|
+
"and value changed from {val_t1} to {val_t2}."
|
|
33
|
+
),
|
|
34
|
+
"values_changed": "Value of {diff_path} changed from {val_t1} to {val_t2}.",
|
|
35
|
+
"dictionary_item_added": "Item {diff_path} added to dictionary.",
|
|
36
|
+
"dictionary_item_removed": "Item {diff_path} removed from dictionary.",
|
|
37
|
+
"iterable_item_added": "Item {diff_path} added to iterable.",
|
|
38
|
+
"iterable_item_removed": "Item {diff_path} removed from iterable.",
|
|
39
|
+
"attribute_added": "Attribute {diff_path} added.",
|
|
40
|
+
"attribute_removed": "Attribute {diff_path} removed.",
|
|
41
|
+
"set_item_added": "Item root[{val_t2}] added to set.",
|
|
42
|
+
"set_item_removed": "Item root[{val_t1}] removed from set.",
|
|
43
|
+
"repetition_change": "Repetition change for item {diff_path}.",
|
|
44
|
+
"iterable_item_moved": "Item {diff_path} moved to {val_t2}.",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_PRETTY_V2 = {
|
|
48
|
+
**_PRETTY,
|
|
49
|
+
"dictionary_item_added": "Item {diff_path} ({val_t2}) added to dictionary.",
|
|
50
|
+
"dictionary_item_removed": "Item {diff_path} ({val_t1}) removed from dictionary.",
|
|
51
|
+
"iterable_item_added": "Item {diff_path} ({val_t2}) added to iterable.",
|
|
52
|
+
"iterable_item_removed": "Item {diff_path} ({val_t1}) removed from iterable.",
|
|
53
|
+
"attribute_added": "Attribute {diff_path} ({val_t2}) added.",
|
|
54
|
+
"attribute_removed": "Attribute {diff_path} ({val_t1}) removed.",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _fmt_val(value):
|
|
59
|
+
if isinstance(value, str):
|
|
60
|
+
return f'"{value}"'
|
|
61
|
+
return str(value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _iter_findings(findings):
|
|
65
|
+
if isinstance(findings, dict):
|
|
66
|
+
yield from findings.items()
|
|
67
|
+
else:
|
|
68
|
+
for path in findings:
|
|
69
|
+
yield path, None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _level_from_finding(report_type, path, payload):
|
|
73
|
+
if report_type == "values_changed" and isinstance(payload, dict):
|
|
74
|
+
return DiffLevel(path, payload.get("old_value"), payload.get("new_value"), report_type)
|
|
75
|
+
if report_type == "type_changes" and isinstance(payload, dict):
|
|
76
|
+
return DiffLevel(path, payload.get("old_value"), payload.get("new_value"), report_type)
|
|
77
|
+
if report_type == "iterable_item_moved" and isinstance(payload, dict):
|
|
78
|
+
return DiffLevel(path, payload.get("value"), payload.get("new_path"), report_type)
|
|
79
|
+
if report_type == "repetition_change" and isinstance(payload, dict):
|
|
80
|
+
return DiffLevel(path, payload.get("value"), payload.get("value"), report_type)
|
|
81
|
+
if report_type.endswith("_added"):
|
|
82
|
+
return DiffLevel(path, None, payload, report_type)
|
|
83
|
+
if report_type.endswith("_removed"):
|
|
84
|
+
return DiffLevel(path, payload, None, report_type)
|
|
85
|
+
return DiffLevel(path, payload, payload, report_type)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_tree(report: dict, verbose_level: int = 1) -> dict:
|
|
89
|
+
"""text-report dict → {category: [DiffLevel, ...]} like DeepDiff.tree."""
|
|
90
|
+
tree = {}
|
|
91
|
+
for report_type, findings in report.items():
|
|
92
|
+
if report_type == "deep_distance":
|
|
93
|
+
continue
|
|
94
|
+
levels = [
|
|
95
|
+
_level_from_finding(report_type, path, payload)
|
|
96
|
+
for path, payload in _iter_findings(findings)
|
|
97
|
+
]
|
|
98
|
+
if levels:
|
|
99
|
+
tree[report_type] = levels
|
|
100
|
+
return tree
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def tree_as_result(tree: dict) -> dict:
|
|
104
|
+
"""DeepDiff view='tree' shape: category → list of DiffLevel."""
|
|
105
|
+
return {key: list(levels) for key, levels in tree.items()}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def pretty_from_tree(tree: dict, verbose_level: int = 1, prefix=None) -> str:
|
|
109
|
+
forms = _PRETTY_V2 if verbose_level == 2 else _PRETTY
|
|
110
|
+
if prefix is None:
|
|
111
|
+
prefix = ""
|
|
112
|
+
lines = []
|
|
113
|
+
for report_type in sorted(tree):
|
|
114
|
+
for level in tree[report_type]:
|
|
115
|
+
template = forms.get(level.report_type, "")
|
|
116
|
+
if not template:
|
|
117
|
+
continue
|
|
118
|
+
type_t1 = type(level.t1).__name__
|
|
119
|
+
type_t2 = type(level.t2).__name__
|
|
120
|
+
line = template.format(
|
|
121
|
+
diff_path=level.path(),
|
|
122
|
+
type_t1=type_t1,
|
|
123
|
+
type_t2=type_t2,
|
|
124
|
+
val_t1=_fmt_val(level.t1),
|
|
125
|
+
val_t2=_fmt_val(level.t2),
|
|
126
|
+
)
|
|
127
|
+
if callable(prefix):
|
|
128
|
+
lines.append(f"{prefix(diff=level)}{line}")
|
|
129
|
+
else:
|
|
130
|
+
lines.append(f"{prefix}{line}")
|
|
131
|
+
return "\n".join(lines)
|
deepdiff_rust/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepdiff-rust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: MacOS
|
|
8
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Dist: pytest>=8 ; extra == 'test'
|
|
21
|
+
Requires-Dist: deepdiff==9.1.0 ; extra == 'test'
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: LICENSE-ONIX
|
|
25
|
+
Summary: Rust-backed DeepDiff-compatible comparison for Python
|
|
26
|
+
Keywords: deepdiff,diff,rust,performance,comparison
|
|
27
|
+
Author-email: viktormokhov <110980778+viktormokhov@users.noreply.github.com>
|
|
28
|
+
License: MIT
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
31
|
+
Project-URL: Homepage, https://github.com/viktormokhov/deepdiff-rust
|
|
32
|
+
Project-URL: Issues, https://github.com/viktormokhov/deepdiff-rust/issues
|
|
33
|
+
Project-URL: Repository, https://github.com/viktormokhov/deepdiff-rust
|
|
34
|
+
|
|
35
|
+
# deepdiff-rust
|
|
36
|
+
|
|
37
|
+
Rust-backed [DeepDiff](https://github.com/seperman/deepdiff) for Python.
|
|
38
|
+
Forked from [Onix](https://github.com/ksco92/onix) (`deepdiff-rs`) and extended
|
|
39
|
+
toward full `deepdiff==9.1.0` parity for the comparison API.
|
|
40
|
+
|
|
41
|
+
- **Distribution:** `deepdiff-rust`
|
|
42
|
+
- **Import:** `deepdiff_rust` (does not shadow PyPI `deepdiff` or `deepdiff-rs`)
|
|
43
|
+
- **Runtime deps:** none (CPython 3.10+)
|
|
44
|
+
- **Branch:** `fork-onix` is the active development line
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from deepdiff_rust import DeepDiff
|
|
48
|
+
|
|
49
|
+
print(DeepDiff({"a": 1, "b": [1, 2]}, {"a": 2, "b": [1, 3]}))
|
|
50
|
+
# {'values_changed': {"root['a']": {...}, "root['b'][1]": {...}}}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
From a local wheel (Windows / manylinux x86_64 abi3):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install --force-reinstall --no-deps path/to/deepdiff_rust-0.1.0-*.whl
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
From source (Rust toolchain + Maturin):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/viktormokhov/deepdiff-rust.git
|
|
65
|
+
cd deepdiff-rust
|
|
66
|
+
git checkout fork-onix
|
|
67
|
+
python -m venv .venv
|
|
68
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
69
|
+
pip install maturin
|
|
70
|
+
maturin develop --release
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Status
|
|
74
|
+
|
|
75
|
+
**Core `DeepDiff(...)` comparison is largely done** and differentially tested
|
|
76
|
+
against live `deepdiff==9.1.0`. Not a complete drop-in for the whole DeepDiff
|
|
77
|
+
*package* yet (no DeepHash / DeepSearch / Delta).
|
|
78
|
+
|
|
79
|
+
### Supported (tested vs DeepDiff 9.1.0)
|
|
80
|
+
|
|
81
|
+
| Area | Options / behavior |
|
|
82
|
+
|---|---|
|
|
83
|
+
| Result API | `dict`-like result, `to_dict` / `to_json`, `affected_paths`, `affected_root_keys`, `t1` / `t2`, `verbose_level` 0/1/2 |
|
|
84
|
+
| Order | `ignore_order`, `zip_ordered_iterables`, pairing cutoffs, `threshold_to_diff_deeper`, `report_repetition`, `group_by` |
|
|
85
|
+
| Limits | `max_diffs`, `max_passes`, `max_depth` (extension beyond DeepDiff) |
|
|
86
|
+
| Numbers / strings | `significant_digits`, `math_epsilon`, `use_log_scale`, `ignore_nan_inequality`, `ignore_string_case`, type-ignore groups |
|
|
87
|
+
| Filters | `exclude_paths` / `include_paths` (incl. `*`/`**`), `exclude_regex_paths`, `exclude_types`, `ignore_private_variables` |
|
|
88
|
+
| Callbacks | `exclude_obj_callback`(+`_strict`), `include_obj_callback`(+`_strict`), `iterable_compare_func`, `custom_operators` |
|
|
89
|
+
| Types | `None`, bool, int, float, str, bytes, list/tuple/set/frozenset, dict, Decimal, UUID, complex, datetime/date/time/timedelta, Enum / IntEnum (`use_enum_value`), numpy int/float scalars + `ndarray`, many custom objects by attributes |
|
|
90
|
+
| Other | `get_deep_distance`, `view='tree'`, `pretty()`, `diff_json` (JSON fast path in Rust) |
|
|
91
|
+
|
|
92
|
+
Details and known divergences: [docs/compatibility.md](docs/compatibility.md).
|
|
93
|
+
|
|
94
|
+
### Not yet
|
|
95
|
+
|
|
96
|
+
- `DeepHash` / `hasher`, `DeepSearch`, `Delta` / patch
|
|
97
|
+
- numpy `bool_` / `datetime64` / complex scalars
|
|
98
|
+
- Onix extras we did not port: `diff_tables` (Arrow), `onix` CLI
|
|
99
|
+
- Some exotic report shapes for failed/opaque objects may still differ from DeepDiff
|
|
100
|
+
|
|
101
|
+
## vs `deepdiff-rs` (Onix) and `deepdiff`
|
|
102
|
+
|
|
103
|
+
| | `deepdiff` | `deepdiff-rs` (Onix) | **this package** |
|
|
104
|
+
|---|---|---|---|
|
|
105
|
+
| Engine | pure Python | Rust | Rust (Onix fork) |
|
|
106
|
+
| Import | `deepdiff` | `deepdiff_rs` | `deepdiff_rust` |
|
|
107
|
+
| Pip runtime deps | several | none (+ optional pyarrow) | none |
|
|
108
|
+
| Full DeepDiff option set | yes | core only | most comparison options |
|
|
109
|
+
| DeepHash / Delta / Search | yes | no | no (planned) |
|
|
110
|
+
| Arrow `diff_tables` | no | yes | no |
|
|
111
|
+
|
|
112
|
+
Keep stock `deepdiff` installed only if you want differential tests or a reference oracle.
|
|
113
|
+
|
|
114
|
+
## Architecture
|
|
115
|
+
|
|
116
|
+
- `crates/deepdiff-core` — comparison engine, paths, reports
|
|
117
|
+
- `crates/deepdiff-python` — PyO3 extension (`deepdiff_rust._native`)
|
|
118
|
+
- `python/deepdiff_rust` — public Python API
|
|
119
|
+
- `tests` — API + differential tests against `deepdiff==9.1.0`
|
|
120
|
+
- `docs/` — compatibility matrix and design notes
|
|
121
|
+
|
|
122
|
+
Live Python objects are converted into a native value model, diffed in Rust,
|
|
123
|
+
then rendered as DeepDiff-compatible reports. JSON strings can skip the
|
|
124
|
+
Python object round-trip via `diff_json`.
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install -e ".[test]" # pytest + deepdiff==9.1.0
|
|
130
|
+
cargo test --workspace
|
|
131
|
+
cargo fmt --all --check
|
|
132
|
+
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
|
133
|
+
maturin develop --release
|
|
134
|
+
pytest -q
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Publishing to PyPI
|
|
138
|
+
|
|
139
|
+
Workflow [`.github/workflows/publish.yml`](.github/workflows/publish.yml) builds
|
|
140
|
+
abi3 wheels (Linux x86_64/aarch64, Windows x64, macOS Intel/ARM) plus an sdist.
|
|
141
|
+
|
|
142
|
+
### Secrets (GitHub → Settings → Secrets and variables → Actions)
|
|
143
|
+
|
|
144
|
+
| Secret | Purpose |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `TEST_PYPI_API_TOKEN` | Upload to https://test.pypi.org |
|
|
147
|
+
| `PYPI_API_TOKEN` | Upload to https://pypi.org |
|
|
148
|
+
|
|
149
|
+
Create tokens on TestPyPI / PyPI (Account → API tokens). Store the raw token
|
|
150
|
+
value; maturin authenticates as user `__token__`.
|
|
151
|
+
|
|
152
|
+
### Dry run on TestPyPI
|
|
153
|
+
|
|
154
|
+
1. Register on https://test.pypi.org and create a token → secret
|
|
155
|
+
`TEST_PYPI_API_TOKEN`.
|
|
156
|
+
2. **Actions → Publish → Run workflow** → target `testpypi`.
|
|
157
|
+
3. Install and smoke-test:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ deepdiff-rust==0.1.0
|
|
161
|
+
python -c "from deepdiff_rust import DeepDiff; print(DeepDiff({'a':1},{'a':2}))"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
(`--extra-index-url` is only needed if a future release gains PyPI dependencies.)
|
|
165
|
+
|
|
166
|
+
### Production release
|
|
167
|
+
|
|
168
|
+
1. Version in `pyproject.toml` and `[workspace.package]` must match the tag
|
|
169
|
+
without the leading `v` (e.g. `0.1.0` ↔ `v0.1.0`).
|
|
170
|
+
2. Push the tag (or run the workflow with target `pypi`):
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
git tag v0.1.0
|
|
174
|
+
git push origin v0.1.0
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
3. After the job finishes: `pip install deepdiff-rust`.
|
|
178
|
+
|
|
179
|
+
A version string can be uploaded to PyPI only once — fix mistakes with a new
|
|
180
|
+
version number.
|
|
181
|
+
|
|
182
|
+
## Third-party code
|
|
183
|
+
|
|
184
|
+
Comparison engine and Python conversion modules are forked from Onix,
|
|
185
|
+
Copyright (c) 2026 Rodrigo Carvajal, MIT. See
|
|
186
|
+
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and [LICENSE-ONIX](LICENSE-ONIX).
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT. Upstream licenses in third-party notices still apply.
|
|
191
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
deepdiff_rust/__init__.py,sha256=q3PDPu7ZJBqo9_T-birDkHjvY-p91ULOd3punAxKvi8,260
|
|
2
|
+
deepdiff_rust/__init__.pyi,sha256=NaPyHP4kLZINdwsa-gAKVYeLAFGP1wEqdZ1hQmdRHDo,3326
|
|
3
|
+
deepdiff_rust/_callbacks.py,sha256=61TpMIQdYQmMoJRw49pHCHIbAbdWtp_fc6e_wNfyTKA,3008
|
|
4
|
+
deepdiff_rust/_diff.py,sha256=p872e2pnYoFyA8kL6RnRTRnBGxQ14tuAGNaP4uTNUZs,14170
|
|
5
|
+
deepdiff_rust/_dual.py,sha256=56m4Vfx0BE1MCkOCbg5fGTBpjNmR7x_tElVbg7-0QRY,4697
|
|
6
|
+
deepdiff_rust/_group_by.py,sha256=tjSQah_qNcCtkUzlnUTe49WVbeCpCparw55g1mWIGp8,1993
|
|
7
|
+
deepdiff_rust/_native.pyd,sha256=E_gn4hQiNANnYK4CgW2dHylKXtUWwXZDMzMOyq9EJfU,1552384
|
|
8
|
+
deepdiff_rust/_options.py,sha256=VReNz-JZjHfBh8kxHYYAeXE-V2Ixfz1rQgb3TwAFaqY,3868
|
|
9
|
+
deepdiff_rust/_paths.py,sha256=aXa90axI6yVv0mhI3dbc32LGP6gWvOBD-l_7XJsqLtA,5162
|
|
10
|
+
deepdiff_rust/_view.py,sha256=icf6kZoThV9Tya55JUulxqce6ThRoJ-3CzPg045dg1s,5070
|
|
11
|
+
deepdiff_rust/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
deepdiff_rust-0.1.0.dist-info/METADATA,sha256=tEe1Q2FOc6xXP-8E9YVTfhIrzOyTNScqeatzG9hWOgU,7558
|
|
13
|
+
deepdiff_rust-0.1.0.dist-info/WHEEL,sha256=xw8F9xBWUOgm5ym2S9tihj_bLJOb723bjhA0kdANo2I,95
|
|
14
|
+
deepdiff_rust-0.1.0.dist-info/licenses/LICENSE,sha256=bBESMrPM6AtsqS38rSgpBMXoBLhBRMDcE_bHw_Ivie0,1091
|
|
15
|
+
deepdiff_rust-0.1.0.dist-info/licenses/LICENSE-ONIX,sha256=-9RD2QdrTE077HE1bP2OPj29mVd9n63ywdnqxzsN5yg,1094
|
|
16
|
+
deepdiff_rust-0.1.0.dist-info/sboms/deepdiff-python.cyclonedx.json,sha256=nI7vnQda7asAVzuRkwxQfgCmbQm8AE87Jdcacr2NFWU,32576
|
|
17
|
+
deepdiff_rust-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Viktor Mokhov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rodrigo Carvajal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|