deepdiff-rs 0.4.0__tar.gz → 0.4.1__tar.gz
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_rs-0.4.0 → deepdiff_rs-0.4.1}/Cargo.lock +3 -3
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/Cargo.toml +1 -1
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/PKG-INFO +24 -18
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/README.md +23 -17
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/value.rs +7 -7
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/benchmarks/bench_bindings.py +121 -4
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/src/convert.rs +36 -4
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_conversions.py +64 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/Cargo.toml +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/datetime.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/datetime_tests.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/array.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/dispatch.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/mod.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/object.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/options.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/scalar.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/set.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/tests.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/error.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/distance.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/fxhash.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/hash.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/memo.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/mod.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/pairing.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/tests.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/lcs.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/lcs_tests.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/lib.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/path.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/report.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/report_tests.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/test_support.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/src/value_tests.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/tests/golden.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/tests/memory_footprint.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/tests/proptest_diff.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/Cargo.toml +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/src/deepdiff.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/src/errors.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/src/fast_path.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/src/guard.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/src/lib.rs +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_bindings_memory.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_datetimes.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_depth_guard.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_differential_fuzz.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_golden_parity.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_sets.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_signed_zero.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_smoke.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_suite_hygiene.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_tuples.py +0 -0
- {deepdiff_rs-0.4.0 → deepdiff_rs-0.4.1}/pyproject.toml +0 -0
|
@@ -127,7 +127,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
127
127
|
|
|
128
128
|
[[package]]
|
|
129
129
|
name = "onix-cli"
|
|
130
|
-
version = "0.4.
|
|
130
|
+
version = "0.4.1"
|
|
131
131
|
dependencies = [
|
|
132
132
|
"onix-core",
|
|
133
133
|
"serde_json",
|
|
@@ -135,7 +135,7 @@ dependencies = [
|
|
|
135
135
|
|
|
136
136
|
[[package]]
|
|
137
137
|
name = "onix-core"
|
|
138
|
-
version = "0.4.
|
|
138
|
+
version = "0.4.1"
|
|
139
139
|
dependencies = [
|
|
140
140
|
"proptest",
|
|
141
141
|
"serde",
|
|
@@ -145,7 +145,7 @@ dependencies = [
|
|
|
145
145
|
|
|
146
146
|
[[package]]
|
|
147
147
|
name = "onix-py"
|
|
148
|
-
version = "0.4.
|
|
148
|
+
version = "0.4.1"
|
|
149
149
|
dependencies = [
|
|
150
150
|
"onix-core",
|
|
151
151
|
"pyo3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepdiff-rs
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Rust
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -118,7 +118,7 @@ Pass `--ignore-order` to compare every list by value instead of by position, mir
|
|
|
118
118
|
- A subclass of a supported type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date` subclass such as pandas' `Timestamp`) raises `TypeError` rather than being diffed as its base type, because DeepDiff reports each value's own type name. A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. Both are described in [`tests/golden/README.md`](tests/golden/README.md).
|
|
119
119
|
- **Datetimes** compare by instant, with a naive value read as UTC, matching DeepDiff. A changed pair is reported normalized to UTC (`to_json()` renders `...+00:00`, `to_dict()` returns UTC-aware `datetime`s); everywhere else a datetime keeps its raw value. Three deliberate departures: `to_json()` renders a `date` as `YYYY-MM-DD` where DeepDiff's own `to_json()` raises `TypeError` (a documented superset); a `zoneinfo`/`pytz` tzinfo comes back from `to_dict()` as a fixed-offset `datetime.timezone` carrying the offset it was in force at, not the original zone object; and a set holding both a naive and an aware value at one instant reports both as members, where DeepDiff's own digest cache can report only one (see [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs)). Comparing two datetimes whose UTC form would leave year 1..=9999 raises `ValueError` naming the path, where DeepDiff raises `OverflowError`; under `ignore_order` DeepDiff's hasher normalizes every datetime and so raises for such a value even when it is only added, removed, or shuffled, where onix hashes by instant and reports it normally (see [`tests/golden/README.md`](tests/golden/README.md)). `truncate_datetime`, `time` and `timedelta` are not supported. The normalized-versus-raw split is documented in [`tests/golden/README.md`](tests/golden/README.md).
|
|
120
120
|
- **Sets** are diffed deterministically, where DeepDiff's own answers depend on the order the running process happens to iterate a set in (hash order, and `PYTHONHASHSEED`-dependent for `str` members) or on how its digest cache/computation handles a tuple, frozenset, or calendar member independently of Python's own `==`. Each consequence — entry order, which member of an equality class is reported, set-versus-sequence coercion, and a tuple/frozenset member's own (positional, not order-/repetition-insensitive) matching rule — is shown with both tools' output in [`tests/golden/README.md`](tests/golden/README.md)'s "Set iteration order" section. A report holding a `frozenset` value also serializes to JSON here, where DeepDiff's own `to_json()` raises `TypeError` — a superset, not a difference in the findings.
|
|
121
|
-
-
|
|
121
|
+
- A `str` containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) raises `ValueError` naming the exact path on either side, before the two values are ever compared — including a pair DeepDiff would call equal and report as no change, since DeepDiff's scalar equality is plain Python `==` and never hits the encoding problem; DeepDiff does report a plain change for a *differing* pair, and crashes with an unhandled `UnicodeEncodeError` if such a string is ever hashed (a `set`/`frozenset` member). See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
122
122
|
- A `str` inside a `tuple` or `frozenset` set item is rendered with Python's `repr()`, which escapes every non-printable character; onix escapes those below `U+0100` (the complete set in that range) and passes higher non-printable code points through literally, since escaping them would mean carrying a Unicode category table. Exact for all of ASCII and all printable text. See [`crates/onix-core/src/path.rs`](crates/onix-core/src/path.rs).
|
|
123
123
|
- Adversarially deep input raises `MaxDepthError` instead of crashing: the default `max_depth` is 512 and the hard ceiling is `MAX_DEPTH_CEILING` (20,000). See [`crates/onix-py/src/guard.rs`](crates/onix-py/src/guard.rs).
|
|
124
124
|
- `ignore_order` pairing is `O(N^2)` in unpaired elements per side and carries a polynomial cost in both time and memory with input depth; it has no `max_passes`/`max_diffs` cutoff, so bound the size and depth of untrusted input yourself. See [`crates/onix-core/src/ignore_order/mod.rs`](crates/onix-core/src/ignore_order/mod.rs).
|
|
@@ -128,25 +128,31 @@ Pass `--ignore-order` to compare every list by value instead of by position, mir
|
|
|
128
128
|
|
|
129
129
|
Two committed, regenerable reports back the numbers below; every figure here is copied verbatim from them.
|
|
130
130
|
|
|
131
|
-
The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side):
|
|
131
|
+
The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side, run on 2026-09-04):
|
|
132
132
|
|
|
133
133
|
| Shape | deepdiff | deepdiff_rs | Speedup |
|
|
134
134
|
| --- | --- | --- | --- |
|
|
135
|
-
| `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) |
|
|
136
|
-
| peak RSS | 228.
|
|
137
|
-
| CPU seconds | 3.
|
|
138
|
-
| Heterogeneous API-payload records, n=20,000 (live objects) |
|
|
139
|
-
| peak RSS |
|
|
140
|
-
| CPU seconds | 3.
|
|
141
|
-
|
|
|
142
|
-
| peak RSS |
|
|
143
|
-
| CPU seconds |
|
|
144
|
-
| Same
|
|
145
|
-
| peak RSS |
|
|
146
|
-
| CPU seconds |
|
|
147
|
-
| Same
|
|
148
|
-
| peak RSS |
|
|
149
|
-
| CPU seconds |
|
|
135
|
+
| `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) | 3164.29ms | 89.08ms | **35.52x** |
|
|
136
|
+
| peak RSS | 228.4 MB | 141.5 MB | **1.61x** |
|
|
137
|
+
| CPU seconds | 3.162 s | 0.089 s | **35.50x** |
|
|
138
|
+
| Heterogeneous API-payload records, n=20,000 (live objects) | 3451.01ms | 151.84ms | **22.73x** |
|
|
139
|
+
| peak RSS | 118.1 MB | 147.7 MB | **0.80x** |
|
|
140
|
+
| CPU seconds | 3.449 s | 0.152 s | **22.73x** |
|
|
141
|
+
| Typed records (datetime/tuple/set fields), n=10,000 (live objects) | 792.96ms | 47.87ms | **16.57x** |
|
|
142
|
+
| peak RSS | 60.2 MB | 62.1 MB | **0.97x** |
|
|
143
|
+
| CPU seconds | 0.792 s | 0.048 s | **16.56x** |
|
|
144
|
+
| Same typed-records shape, `ignore_order` (live objects) | 60483.86ms | 1272.04ms | **47.55x** |
|
|
145
|
+
| peak RSS | 112.1 MB | 1005.6 MB | **0.11x** |
|
|
146
|
+
| CPU seconds | 60.452 s | 1.271 s | **47.55x** |
|
|
147
|
+
| Same `ignore_order` shape, via `diff_json` (JSON-string path) | 3168.05ms | 86.95ms | **36.44x** |
|
|
148
|
+
| peak RSS | 228.7 MB | 142.3 MB | **1.61x** |
|
|
149
|
+
| CPU seconds | 3.166 s | 0.087 s | **36.46x** |
|
|
150
|
+
| Same API-payload shape, via `diff_json` (JSON-string path) | 4568.00ms | 85.37ms | **53.51x** |
|
|
151
|
+
| peak RSS | 139.5 MB | 140.9 MB | **0.99x** |
|
|
152
|
+
| CPU seconds | 4.566 s | 0.085 s | **53.50x** |
|
|
153
|
+
| Same API-payload shape, both tools reading two JSON files from disk | 4565.46ms | 89.43ms | **51.05x** |
|
|
154
|
+
| peak RSS | 139.5 MB | 141.0 MB | **0.99x** |
|
|
155
|
+
| CPU seconds | 4.564 s | 0.089 s | **51.03x** |
|
|
150
156
|
|
|
151
157
|
The engine's own diff-only time and peak resident memory against pinned `deepdiff` 9.1.0 (source: [`perf/RESULTS.md`](perf/RESULTS.md), same machine, median over tier-appropriate runs, diff time excluding process startup and JSON parsing on both sides):
|
|
152
158
|
|
|
@@ -102,7 +102,7 @@ Pass `--ignore-order` to compare every list by value instead of by position, mir
|
|
|
102
102
|
- A subclass of a supported type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date` subclass such as pandas' `Timestamp`) raises `TypeError` rather than being diffed as its base type, because DeepDiff reports each value's own type name. A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. Both are described in [`tests/golden/README.md`](tests/golden/README.md).
|
|
103
103
|
- **Datetimes** compare by instant, with a naive value read as UTC, matching DeepDiff. A changed pair is reported normalized to UTC (`to_json()` renders `...+00:00`, `to_dict()` returns UTC-aware `datetime`s); everywhere else a datetime keeps its raw value. Three deliberate departures: `to_json()` renders a `date` as `YYYY-MM-DD` where DeepDiff's own `to_json()` raises `TypeError` (a documented superset); a `zoneinfo`/`pytz` tzinfo comes back from `to_dict()` as a fixed-offset `datetime.timezone` carrying the offset it was in force at, not the original zone object; and a set holding both a naive and an aware value at one instant reports both as members, where DeepDiff's own digest cache can report only one (see [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs)). Comparing two datetimes whose UTC form would leave year 1..=9999 raises `ValueError` naming the path, where DeepDiff raises `OverflowError`; under `ignore_order` DeepDiff's hasher normalizes every datetime and so raises for such a value even when it is only added, removed, or shuffled, where onix hashes by instant and reports it normally (see [`tests/golden/README.md`](tests/golden/README.md)). `truncate_datetime`, `time` and `timedelta` are not supported. The normalized-versus-raw split is documented in [`tests/golden/README.md`](tests/golden/README.md).
|
|
104
104
|
- **Sets** are diffed deterministically, where DeepDiff's own answers depend on the order the running process happens to iterate a set in (hash order, and `PYTHONHASHSEED`-dependent for `str` members) or on how its digest cache/computation handles a tuple, frozenset, or calendar member independently of Python's own `==`. Each consequence — entry order, which member of an equality class is reported, set-versus-sequence coercion, and a tuple/frozenset member's own (positional, not order-/repetition-insensitive) matching rule — is shown with both tools' output in [`tests/golden/README.md`](tests/golden/README.md)'s "Set iteration order" section. A report holding a `frozenset` value also serializes to JSON here, where DeepDiff's own `to_json()` raises `TypeError` — a superset, not a difference in the findings.
|
|
105
|
-
-
|
|
105
|
+
- A `str` containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) raises `ValueError` naming the exact path on either side, before the two values are ever compared — including a pair DeepDiff would call equal and report as no change, since DeepDiff's scalar equality is plain Python `==` and never hits the encoding problem; DeepDiff does report a plain change for a *differing* pair, and crashes with an unhandled `UnicodeEncodeError` if such a string is ever hashed (a `set`/`frozenset` member). See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
106
106
|
- A `str` inside a `tuple` or `frozenset` set item is rendered with Python's `repr()`, which escapes every non-printable character; onix escapes those below `U+0100` (the complete set in that range) and passes higher non-printable code points through literally, since escaping them would mean carrying a Unicode category table. Exact for all of ASCII and all printable text. See [`crates/onix-core/src/path.rs`](crates/onix-core/src/path.rs).
|
|
107
107
|
- Adversarially deep input raises `MaxDepthError` instead of crashing: the default `max_depth` is 512 and the hard ceiling is `MAX_DEPTH_CEILING` (20,000). See [`crates/onix-py/src/guard.rs`](crates/onix-py/src/guard.rs).
|
|
108
108
|
- `ignore_order` pairing is `O(N^2)` in unpaired elements per side and carries a polynomial cost in both time and memory with input depth; it has no `max_passes`/`max_diffs` cutoff, so bound the size and depth of untrusted input yourself. See [`crates/onix-core/src/ignore_order/mod.rs`](crates/onix-core/src/ignore_order/mod.rs).
|
|
@@ -112,25 +112,31 @@ Pass `--ignore-order` to compare every list by value instead of by position, mir
|
|
|
112
112
|
|
|
113
113
|
Two committed, regenerable reports back the numbers below; every figure here is copied verbatim from them.
|
|
114
114
|
|
|
115
|
-
The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side):
|
|
115
|
+
The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side, run on 2026-09-04):
|
|
116
116
|
|
|
117
117
|
| Shape | deepdiff | deepdiff_rs | Speedup |
|
|
118
118
|
| --- | --- | --- | --- |
|
|
119
|
-
| `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) |
|
|
120
|
-
| peak RSS | 228.
|
|
121
|
-
| CPU seconds | 3.
|
|
122
|
-
| Heterogeneous API-payload records, n=20,000 (live objects) |
|
|
123
|
-
| peak RSS |
|
|
124
|
-
| CPU seconds | 3.
|
|
125
|
-
|
|
|
126
|
-
| peak RSS |
|
|
127
|
-
| CPU seconds |
|
|
128
|
-
| Same
|
|
129
|
-
| peak RSS |
|
|
130
|
-
| CPU seconds |
|
|
131
|
-
| Same
|
|
132
|
-
| peak RSS |
|
|
133
|
-
| CPU seconds |
|
|
119
|
+
| `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) | 3164.29ms | 89.08ms | **35.52x** |
|
|
120
|
+
| peak RSS | 228.4 MB | 141.5 MB | **1.61x** |
|
|
121
|
+
| CPU seconds | 3.162 s | 0.089 s | **35.50x** |
|
|
122
|
+
| Heterogeneous API-payload records, n=20,000 (live objects) | 3451.01ms | 151.84ms | **22.73x** |
|
|
123
|
+
| peak RSS | 118.1 MB | 147.7 MB | **0.80x** |
|
|
124
|
+
| CPU seconds | 3.449 s | 0.152 s | **22.73x** |
|
|
125
|
+
| Typed records (datetime/tuple/set fields), n=10,000 (live objects) | 792.96ms | 47.87ms | **16.57x** |
|
|
126
|
+
| peak RSS | 60.2 MB | 62.1 MB | **0.97x** |
|
|
127
|
+
| CPU seconds | 0.792 s | 0.048 s | **16.56x** |
|
|
128
|
+
| Same typed-records shape, `ignore_order` (live objects) | 60483.86ms | 1272.04ms | **47.55x** |
|
|
129
|
+
| peak RSS | 112.1 MB | 1005.6 MB | **0.11x** |
|
|
130
|
+
| CPU seconds | 60.452 s | 1.271 s | **47.55x** |
|
|
131
|
+
| Same `ignore_order` shape, via `diff_json` (JSON-string path) | 3168.05ms | 86.95ms | **36.44x** |
|
|
132
|
+
| peak RSS | 228.7 MB | 142.3 MB | **1.61x** |
|
|
133
|
+
| CPU seconds | 3.166 s | 0.087 s | **36.46x** |
|
|
134
|
+
| Same API-payload shape, via `diff_json` (JSON-string path) | 4568.00ms | 85.37ms | **53.51x** |
|
|
135
|
+
| peak RSS | 139.5 MB | 140.9 MB | **0.99x** |
|
|
136
|
+
| CPU seconds | 4.566 s | 0.085 s | **53.50x** |
|
|
137
|
+
| Same API-payload shape, both tools reading two JSON files from disk | 4565.46ms | 89.43ms | **51.05x** |
|
|
138
|
+
| peak RSS | 139.5 MB | 141.0 MB | **0.99x** |
|
|
139
|
+
| CPU seconds | 4.564 s | 0.089 s | **51.03x** |
|
|
134
140
|
|
|
135
141
|
The engine's own diff-only time and peak resident memory against pinned `deepdiff` 9.1.0 (source: [`perf/RESULTS.md`](perf/RESULTS.md), same machine, median over tier-appropriate runs, diff time excluding process startup and JSON parsing on both sides):
|
|
136
142
|
|
|
@@ -522,13 +522,13 @@ impl SetItems {
|
|
|
522
522
|
/// dropping any member equal to an earlier one.
|
|
523
523
|
///
|
|
524
524
|
/// A real Python `set` cannot hold two equal members, but this
|
|
525
|
-
/// constructor cannot assume it was handed one:
|
|
526
|
-
///
|
|
527
|
-
///
|
|
528
|
-
///
|
|
529
|
-
///
|
|
530
|
-
///
|
|
531
|
-
///
|
|
525
|
+
/// constructor cannot assume it was handed one: this type is public, so
|
|
526
|
+
/// a caller building a [`Value`] directly can still hand it two
|
|
527
|
+
/// structurally equal members. Two equal members would render to the
|
|
528
|
+
/// same path segment and so to the same *structural* report path, which
|
|
529
|
+
/// [`crate::report::Report`] requires to be unique; dropping the later
|
|
530
|
+
/// one is what a Python set would have done with the pair in the first
|
|
531
|
+
/// place.
|
|
532
532
|
///
|
|
533
533
|
/// Equality here is the structural one `canonical_cmp` decides, which
|
|
534
534
|
/// is exactly what "renders to the same path segment" means. It is
|
|
@@ -56,6 +56,7 @@ import tempfile
|
|
|
56
56
|
import time
|
|
57
57
|
from collections.abc import Callable
|
|
58
58
|
from dataclasses import dataclass
|
|
59
|
+
from datetime import datetime, timedelta, timezone
|
|
59
60
|
from pathlib import Path
|
|
60
61
|
from typing import Final
|
|
61
62
|
|
|
@@ -65,6 +66,9 @@ from deepdiff_rs import DeepDiff as OnixDeepDiff
|
|
|
65
66
|
from deepdiff_rs import diff_json
|
|
66
67
|
|
|
67
68
|
type JsonValue = dict[str, "JsonValue"] | list["JsonValue"] | str | int | float | bool | None
|
|
69
|
+
# `id`/`name` (int/str), `created_at` (datetime), `coordinate` (a tuple pair),
|
|
70
|
+
# `tags` (a string set) — the fields `_make_typed_record` builds.
|
|
71
|
+
type TypedRecord = dict[str, int | str | datetime | tuple[int, int] | tuple[float, float] | set[str]]
|
|
68
72
|
|
|
69
73
|
##############################################
|
|
70
74
|
##############################################
|
|
@@ -78,12 +82,18 @@ type JsonValue = dict[str, "JsonValue"] | list["JsonValue"] | str | int | float
|
|
|
78
82
|
SEED: Final[int] = 20260901
|
|
79
83
|
IGNORE_ORDER_SIZE: Final[int] = 10_000
|
|
80
84
|
RECORD_COUNT: Final[int] = 20_000
|
|
85
|
+
TYPED_RECORD_COUNT: Final[int] = 10_000
|
|
81
86
|
VALUE_CHANGE_RATE: Final[float] = 0.05
|
|
82
87
|
RUNS: Final[int] = 11
|
|
83
88
|
|
|
84
89
|
_ORIGINAL_INT_RANGE: Final[tuple[int, int]] = (0, 1_000_000)
|
|
85
90
|
_CHANGED_INT_RANGE: Final[tuple[int, int]] = (10_000_000, 20_000_000)
|
|
86
91
|
|
|
92
|
+
# A fixed offset, never the process's local zone, so the fixture's aware
|
|
93
|
+
# half is deterministic across machines.
|
|
94
|
+
_TYPED_RECORD_TZ: Final[timezone] = timezone(timedelta(hours=-5))
|
|
95
|
+
_TAG_POOL: Final[tuple[str, ...]] = ("alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta")
|
|
96
|
+
|
|
87
97
|
|
|
88
98
|
##############################################
|
|
89
99
|
##############################################
|
|
@@ -92,6 +102,17 @@ _CHANGED_INT_RANGE: Final[tuple[int, int]] = (10_000_000, 20_000_000)
|
|
|
92
102
|
# Fixture generation (live Python objects)
|
|
93
103
|
|
|
94
104
|
|
|
105
|
+
def _mutation_indices(count: int, rng: random.Random) -> list[int]:
|
|
106
|
+
"""
|
|
107
|
+
Sample the indices to mutate for a `VALUE_CHANGE_RATE` batch.
|
|
108
|
+
|
|
109
|
+
:param count: Total element count to sample from.
|
|
110
|
+
:param rng: Seeded random source.
|
|
111
|
+
:return: The indices to mutate.
|
|
112
|
+
"""
|
|
113
|
+
return rng.sample(range(count), int(count * VALUE_CHANGE_RATE))
|
|
114
|
+
|
|
115
|
+
|
|
95
116
|
def build_ignore_order_case() -> tuple[JsonValue, JsonValue]:
|
|
96
117
|
"""
|
|
97
118
|
Build the `ignore_order_10k` shape: a shuffled, ~5%-mutated int list.
|
|
@@ -102,9 +123,8 @@ def build_ignore_order_case() -> tuple[JsonValue, JsonValue]:
|
|
|
102
123
|
a: list[JsonValue] = [rng.randint(*_ORIGINAL_INT_RANGE) for _ in range(IGNORE_ORDER_SIZE)]
|
|
103
124
|
b = list(a)
|
|
104
125
|
rng.shuffle(b)
|
|
105
|
-
change_n = int(IGNORE_ORDER_SIZE * VALUE_CHANGE_RATE)
|
|
106
126
|
|
|
107
|
-
for index in
|
|
127
|
+
for index in _mutation_indices(IGNORE_ORDER_SIZE, rng):
|
|
108
128
|
b[index] = rng.randint(*_CHANGED_INT_RANGE)
|
|
109
129
|
|
|
110
130
|
return a, b
|
|
@@ -184,9 +204,8 @@ def build_api_payloads_case() -> tuple[JsonValue, JsonValue]:
|
|
|
184
204
|
rng = random.Random(SEED + 1)
|
|
185
205
|
a: list[JsonValue] = [_make_record(i, rng) for i in range(RECORD_COUNT)]
|
|
186
206
|
b = copy.deepcopy(a)
|
|
187
|
-
change_n = int(RECORD_COUNT * VALUE_CHANGE_RATE)
|
|
188
207
|
|
|
189
|
-
for index in
|
|
208
|
+
for index in _mutation_indices(RECORD_COUNT, rng):
|
|
190
209
|
record = b[index]
|
|
191
210
|
assert isinstance(record, dict)
|
|
192
211
|
b[index] = _mutate_record(record, rng)
|
|
@@ -194,6 +213,96 @@ def build_api_payloads_case() -> tuple[JsonValue, JsonValue]:
|
|
|
194
213
|
return a, b
|
|
195
214
|
|
|
196
215
|
|
|
216
|
+
def _make_typed_record(index: int, rng: random.Random) -> TypedRecord:
|
|
217
|
+
"""
|
|
218
|
+
Build one record whose fields exercise 0.4.0's typed-conversion path: a
|
|
219
|
+
datetime (naive or aware), a numeric-pair tuple, and a small string set.
|
|
220
|
+
|
|
221
|
+
:param index: The record's position (used for its `id` and to alternate
|
|
222
|
+
naive/aware).
|
|
223
|
+
:param rng: Seeded random source.
|
|
224
|
+
:return: The built record.
|
|
225
|
+
"""
|
|
226
|
+
coordinate: tuple[int, int] | tuple[float, float]
|
|
227
|
+
|
|
228
|
+
if rng.random() < 0.5:
|
|
229
|
+
coordinate = (round(rng.uniform(-90.0, 90.0), 4), round(rng.uniform(-180.0, 180.0), 4))
|
|
230
|
+
else:
|
|
231
|
+
coordinate = (rng.randint(-1000, 1000), rng.randint(-1000, 1000))
|
|
232
|
+
|
|
233
|
+
# Drawn into a variable rather than inline in the dict literal below:
|
|
234
|
+
# dict values evaluate in key order, and `tags` must draw before
|
|
235
|
+
# `created_at` for this function's RNG consumption to stay positionally
|
|
236
|
+
# fixed regardless of how the dict literal is ordered or edited.
|
|
237
|
+
tags = set(rng.sample(_TAG_POOL, rng.randint(1, 4)))
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"id": index,
|
|
241
|
+
"name": f"typed_{index:07d}",
|
|
242
|
+
"created_at": datetime(
|
|
243
|
+
2020 + rng.randint(0, 5),
|
|
244
|
+
rng.randint(1, 12),
|
|
245
|
+
rng.randint(1, 28),
|
|
246
|
+
rng.randint(0, 23),
|
|
247
|
+
rng.randint(0, 59),
|
|
248
|
+
rng.randint(0, 59),
|
|
249
|
+
tzinfo=_TYPED_RECORD_TZ if index % 2 == 0 else None,
|
|
250
|
+
),
|
|
251
|
+
"coordinate": coordinate,
|
|
252
|
+
"tags": tags,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _mutate_typed_record(record: TypedRecord, rng: random.Random) -> TypedRecord:
|
|
257
|
+
"""
|
|
258
|
+
Mutate one typed record for a "value changed" entry: shift its datetime
|
|
259
|
+
and add a tag drawn from the pool. The tag draw is a no-op when the
|
|
260
|
+
record already holds that tag, so the datetime always changes but only
|
|
261
|
+
roughly 70% of mutated records also gain a set change.
|
|
262
|
+
|
|
263
|
+
:param record: The original record; not mutated in place.
|
|
264
|
+
:param rng: Seeded random source.
|
|
265
|
+
:return: The mutated copy.
|
|
266
|
+
"""
|
|
267
|
+
mutated = dict(record)
|
|
268
|
+
created_at = record["created_at"]
|
|
269
|
+
tags = record["tags"]
|
|
270
|
+
assert isinstance(created_at, datetime)
|
|
271
|
+
assert isinstance(tags, set)
|
|
272
|
+
mutated["created_at"] = created_at + timedelta(days=rng.randint(1, 30))
|
|
273
|
+
mutated["tags"] = tags | {rng.choice(_TAG_POOL)}
|
|
274
|
+
|
|
275
|
+
return mutated
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def build_typed_records_case(*, shuffle: bool = False) -> tuple[list[TypedRecord], list[TypedRecord]]:
|
|
279
|
+
"""
|
|
280
|
+
Build the `typed_records` shape: `TYPED_RECORD_COUNT` records, each
|
|
281
|
+
carrying a datetime, a tuple coordinate, and a string-set tags field,
|
|
282
|
+
~5% mutated at record granularity.
|
|
283
|
+
|
|
284
|
+
`b` is `copy.deepcopy(a)`, for the same identity-sharing reason
|
|
285
|
+
documented on `build_api_payloads_case`. With `shuffle=True`, `b` is
|
|
286
|
+
reordered before mutation (matching `build_ignore_order_case`'s
|
|
287
|
+
shuffle-then-mutate order) for the `ignore_order` variant; the ordered
|
|
288
|
+
variant leaves `b` in `a`'s order, matching `build_api_payloads_case`.
|
|
289
|
+
|
|
290
|
+
:param shuffle: Whether to shuffle `b`'s record order before mutating.
|
|
291
|
+
:return: The `(a, b)` pair, as live Python lists of dicts.
|
|
292
|
+
"""
|
|
293
|
+
rng = random.Random(SEED + 2)
|
|
294
|
+
a = [_make_typed_record(i, rng) for i in range(TYPED_RECORD_COUNT)]
|
|
295
|
+
b = copy.deepcopy(a)
|
|
296
|
+
|
|
297
|
+
if shuffle:
|
|
298
|
+
rng.shuffle(b)
|
|
299
|
+
|
|
300
|
+
for index in _mutation_indices(TYPED_RECORD_COUNT, rng):
|
|
301
|
+
b[index] = _mutate_typed_record(b[index], rng)
|
|
302
|
+
|
|
303
|
+
return a, b
|
|
304
|
+
|
|
305
|
+
|
|
197
306
|
##############################################
|
|
198
307
|
##############################################
|
|
199
308
|
##############################################
|
|
@@ -214,6 +323,8 @@ def build_api_payloads_case() -> tuple[JsonValue, JsonValue]:
|
|
|
214
323
|
CASE_LABELS: Final[dict[str, str]] = {
|
|
215
324
|
"ignore_order": "`ignore_order`, 10k shuffled ints, ~5% mutated (live objects)",
|
|
216
325
|
"api_payloads": "Heterogeneous API-payload records, n=20,000 (live objects)",
|
|
326
|
+
"typed_records": "Typed records (datetime/tuple/set fields), n=10,000 (live objects)",
|
|
327
|
+
"typed_records_ignore_order": "Same typed-records shape, `ignore_order` (live objects)",
|
|
217
328
|
"ignore_order_json": "Same `ignore_order` shape, via `diff_json` (JSON-string path)",
|
|
218
329
|
"api_payloads_json": "Same API-payload shape, via `diff_json` (JSON-string path)",
|
|
219
330
|
"api_payloads_file": "Same API-payload shape, both tools reading two JSON files from disk",
|
|
@@ -269,6 +380,12 @@ def _diff_callable(tool: str, case: str) -> Callable[[], object]:
|
|
|
269
380
|
if case in ("ignore_order", "ignore_order_json"):
|
|
270
381
|
a, b = build_ignore_order_case()
|
|
271
382
|
ignore_order = True
|
|
383
|
+
elif case == "typed_records":
|
|
384
|
+
a, b = build_typed_records_case()
|
|
385
|
+
ignore_order = False
|
|
386
|
+
elif case == "typed_records_ignore_order":
|
|
387
|
+
a, b = build_typed_records_case(shuffle=True)
|
|
388
|
+
ignore_order = True
|
|
272
389
|
else:
|
|
273
390
|
a, b = build_api_payloads_case()
|
|
274
391
|
ignore_order = False
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//! | `bool` | `Bool` | checked before `int` — `bool` is a Python `int` subclass |
|
|
14
14
|
//! | `int` | `Number` | must fit in `i64` or `u64`; see below |
|
|
15
15
|
//! | `float` | `Number` | must be finite; see below |
|
|
16
|
-
//! | `str` | `Str` | |
|
|
16
|
+
//! | `str` | `Str` | must be encodable as UTF-8; see below |
|
|
17
17
|
//! | `dict` (`str` keys only) | `Object` | keys interned across the whole walk |
|
|
18
18
|
//! | `list` | `Array` | |
|
|
19
19
|
//! | `tuple` | `Tuple` | exactly `tuple`; every subclass is rejected, see below |
|
|
@@ -29,8 +29,14 @@
|
|
|
29
29
|
//! `DeepDiff` supports them natively).
|
|
30
30
|
//! - A `NaN` or infinite `float` raises [`PyValueError`] (JSON has no
|
|
31
31
|
//! representation for either).
|
|
32
|
+
//! - A `str` containing a lone (unpaired) surrogate code point (e.g.
|
|
33
|
+
//! `"\udc80"`) raises [`PyValueError`] naming the exact path: it has no
|
|
34
|
+
//! UTF-8 encoding. See `tests/golden/README.md` for why this diverges from
|
|
35
|
+
//! real `DeepDiff`.
|
|
32
36
|
//! - A `dict` key that is not a `str` raises [`PyTypeError`] naming the
|
|
33
|
-
//! key's type and the path to the dict containing it
|
|
37
|
+
//! key's type and the path to the dict containing it; a `str` key with a
|
|
38
|
+
//! lone surrogate raises [`PyValueError`] the same way, naming the dict's
|
|
39
|
+
//! path (the key itself has no path segment of its own).
|
|
34
40
|
//! - A `tzinfo` whose `utcoffset()` is not a whole number of seconds raises
|
|
35
41
|
//! [`PyValueError`]: the value model carries an offset in seconds.
|
|
36
42
|
//! - A `set`/`frozenset` member that is not one of the types this MVP allows
|
|
@@ -279,7 +285,8 @@ fn classify<'py>(
|
|
|
279
285
|
}
|
|
280
286
|
|
|
281
287
|
if let Ok(s) = current.cast::<PyString>() {
|
|
282
|
-
|
|
288
|
+
let s = s.to_cow().map_err(|_| lone_surrogate_error(path, false))?;
|
|
289
|
+
return Ok(Step::Done(CValue::Str(s.into_owned().into_boxed_str())));
|
|
283
290
|
}
|
|
284
291
|
|
|
285
292
|
// Exact, and `datetime` before `date`: see the module doc. A `date` cast
|
|
@@ -562,7 +569,12 @@ fn next_dict_entry<'py>(
|
|
|
562
569
|
))
|
|
563
570
|
})?;
|
|
564
571
|
|
|
565
|
-
|
|
572
|
+
let key = key
|
|
573
|
+
.to_cow()
|
|
574
|
+
.map_err(|_| lone_surrogate_error(dict_path, true))?
|
|
575
|
+
.into_owned();
|
|
576
|
+
|
|
577
|
+
Ok(Some((key, value)))
|
|
566
578
|
}
|
|
567
579
|
|
|
568
580
|
/// Reads a `date`'s (or a `datetime`'s) `year`/`month`/`day` attributes.
|
|
@@ -636,6 +648,26 @@ fn out_of_range_error(type_name: &str, path: &[PathSegment]) -> PyErr {
|
|
|
636
648
|
))
|
|
637
649
|
}
|
|
638
650
|
|
|
651
|
+
/// `to_cow`'s own `UnicodeEncodeError` is discarded in favor of this, so the
|
|
652
|
+
/// message names the exact path the way every other conversion error in this
|
|
653
|
+
/// module does; see the module doc and `tests/golden/README.md` for why this
|
|
654
|
+
/// diverges from real `DeepDiff`.
|
|
655
|
+
///
|
|
656
|
+
/// `is_key` distinguishes the two call sites' wording: a dict key that fails
|
|
657
|
+
/// this check has no path segment of its own yet (like a non-`str` key, see
|
|
658
|
+
/// [`next_dict_entry`]), so `path` there is the path to the *dict*, not the
|
|
659
|
+
/// entry, and the message says so explicitly to avoid implying otherwise.
|
|
660
|
+
fn lone_surrogate_error(path: &[PathSegment], is_key: bool) -> PyErr {
|
|
661
|
+
let subject = if is_key { "dict key" } else { "str" };
|
|
662
|
+
let path = render_path(path);
|
|
663
|
+
|
|
664
|
+
PyValueError::new_err(format!(
|
|
665
|
+
"{subject} at {path} contains a lone (unpaired) surrogate code point, which has no \
|
|
666
|
+
UTF-8 representation; onix's internal value model is UTF-8 and cannot represent it, \
|
|
667
|
+
unlike Python's str"
|
|
668
|
+
))
|
|
669
|
+
}
|
|
670
|
+
|
|
639
671
|
fn int_to_value(i: &Bound<'_, PyInt>) -> PyResult<CValue> {
|
|
640
672
|
if let Ok(v) = i.extract::<i64>() {
|
|
641
673
|
return Ok(CValue::Number(CNumber::from_i64(v)));
|
|
@@ -217,6 +217,70 @@ def test_non_str_dict_key_error_reports_path_to_the_dict() -> None:
|
|
|
217
217
|
DeepDiff({"a": {1: "x"}}, {"a": {1: "y"}})
|
|
218
218
|
|
|
219
219
|
|
|
220
|
+
# lone (unpaired) surrogates: legal in a Python str, not representable as UTF-8; see
|
|
221
|
+
# tests/golden/README.md for the documented divergence from real DeepDiff.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_lone_surrogate_value_raises_value_error() -> None:
|
|
225
|
+
"""A lone surrogate value raises ValueError instead of silently comparing equal."""
|
|
226
|
+
with pytest.raises(ValueError, match=r"str at root contains a lone"):
|
|
227
|
+
DeepDiff("\udc80", "\udc81")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_distinct_lone_surrogates_both_raise_the_same_way() -> None:
|
|
231
|
+
"""A different lone surrogate pair is refused identically, not silently equated."""
|
|
232
|
+
with pytest.raises(ValueError, match=r"str at root contains a lone"):
|
|
233
|
+
DeepDiff("\udc81", "\udc82")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_identical_lone_surrogate_value_still_raises() -> None:
|
|
237
|
+
"""An identical pair still raises, even though real DeepDiff reports no change for it."""
|
|
238
|
+
with pytest.raises(ValueError, match=r"str at root contains a lone"):
|
|
239
|
+
DeepDiff("\udc80", "\udc80")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_identical_lone_surrogate_dict_key_still_raises() -> None:
|
|
243
|
+
"""The same holds for a dict key equal on both sides: conversion still validates it."""
|
|
244
|
+
with pytest.raises(ValueError, match=r"dict key at root\['a'\] contains a lone"):
|
|
245
|
+
DeepDiff({"a": {"\udc80": 1}}, {"a": {"\udc80": 1}})
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_identical_lone_surrogate_set_item_still_raises() -> None:
|
|
249
|
+
"""The same holds for a set member equal on both sides: real DeepDiff would still crash."""
|
|
250
|
+
with pytest.raises(ValueError, match=r"str at root\[<set member>\] contains a lone"):
|
|
251
|
+
DeepDiff({"\udc80"}, {"\udc80"})
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_lone_surrogate_nested_in_list_reports_its_path() -> None:
|
|
255
|
+
"""The error names the exact path, like every other conversion error in this module."""
|
|
256
|
+
with pytest.raises(ValueError, match=r"str at root\['a'\]\[1\] contains a lone"):
|
|
257
|
+
DeepDiff({"a": [1, "\udc80"]}, {"a": [1, "ok"]})
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_lone_surrogate_dict_key_raises_value_error_naming_the_dict() -> None:
|
|
261
|
+
"""A lone surrogate dict key raises ValueError naming the containing dict's path."""
|
|
262
|
+
with pytest.raises(ValueError, match=r"dict key at root\['a'\] contains a lone"):
|
|
263
|
+
DeepDiff({"a": {"\udc80": 1}}, {"a": {"ok": 1}})
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_lone_surrogate_set_item_raises_value_error() -> None:
|
|
267
|
+
"""A lone surrogate set member raises ValueError; real DeepDiff crashes hashing one instead."""
|
|
268
|
+
with pytest.raises(ValueError, match=r"str at root\[<set member>\] contains a lone"):
|
|
269
|
+
DeepDiff({"\udc80"}, {"ok"})
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_lone_surrogate_tuple_item_raises_value_error() -> None:
|
|
273
|
+
"""A lone surrogate inside a tuple raises ValueError naming its index."""
|
|
274
|
+
with pytest.raises(ValueError, match=r"str at root\[0\] contains a lone"):
|
|
275
|
+
DeepDiff(("\udc80",), ("ok",))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_non_bmp_character_is_accepted() -> None:
|
|
279
|
+
"""A genuine non-BMP character converts fine: only an unpaired surrogate is refused."""
|
|
280
|
+
diff = DeepDiff("😀", "😁")
|
|
281
|
+
assert diff.to_dict()["values_changed"]["root"] == {"new_value": "😁", "old_value": "😀"}
|
|
282
|
+
|
|
283
|
+
|
|
220
284
|
# diff_json's own error path (JSON parsing, not Python-object conversion)
|
|
221
285
|
|
|
222
286
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|