deepdiff-rs 0.9.0__tar.gz → 0.9.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.9.0 → deepdiff_rs-0.9.1}/Cargo.lock +8 -8
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/Cargo.toml +1 -1
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/PKG-INFO +3 -2
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/README.md +2 -1
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/scalar.rs +7 -4
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/distance.rs +1 -1
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/fxhash.rs +8 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/hash.rs +42 -10
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/memo.rs +8 -6
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/tests.rs +48 -6
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/lcs.rs +33 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/lcs_tests.rs +61 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/path.rs +13 -2
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/test_support.rs +1 -1
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/value.rs +112 -31
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/value_tests.rs +83 -16
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/deepdiff_rs.pyi +2 -1
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/src/convert.rs +8 -12
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/src/deepdiff.rs +3 -2
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/src/guard.rs +107 -6
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_conversions.py +13 -13
- deepdiff_rs-0.9.1/crates/onix-py/tests/test_non_finite.py +318 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/Cargo.toml +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/examples/row_diff_rss.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/examples/type_stack_cost.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/error.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/json_rows.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/lib.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/options.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/row_diff.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/schema.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-arrow/src/table_diff.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/Cargo.toml +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/datetime.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/datetime_tests.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/array.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/dispatch.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/mod.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/object.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/options.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/set.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/diff/tests.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/error.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/mod.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/ignore_order/pairing.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/lib.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/report.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/report_tests.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/unified_diff.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/src/unified_diff_tests.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/tests/golden.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/tests/ignore_order_memory.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/tests/memory_footprint.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/tests/proptest_diff.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/.python-version +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/Cargo.toml +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/benchmarks/bench_bindings.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/src/arrow.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/src/errors.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/src/fast_path.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/src/lib.rs +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/conftest.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_bindings_memory.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_datetimes.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_depth_guard.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_differential_fuzz.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_golden_parity.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_sets.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_signed_zero.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_smoke.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_stub_mypy.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_stub_signatures.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_suite_hygiene.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_table_diff.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_table_row_diff.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_timedeltas.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_times.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_tuples.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/crates/onix-py/tests/test_wheel_contents.py +0 -0
- {deepdiff_rs-0.9.0 → deepdiff_rs-0.9.1}/pyproject.toml +0 -0
|
@@ -629,7 +629,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
629
629
|
|
|
630
630
|
[[package]]
|
|
631
631
|
name = "onix-arrow"
|
|
632
|
-
version = "0.9.
|
|
632
|
+
version = "0.9.1"
|
|
633
633
|
dependencies = [
|
|
634
634
|
"arrow-array",
|
|
635
635
|
"arrow-buffer",
|
|
@@ -646,7 +646,7 @@ dependencies = [
|
|
|
646
646
|
|
|
647
647
|
[[package]]
|
|
648
648
|
name = "onix-cli"
|
|
649
|
-
version = "0.9.
|
|
649
|
+
version = "0.9.1"
|
|
650
650
|
dependencies = [
|
|
651
651
|
"onix-core",
|
|
652
652
|
"serde_json",
|
|
@@ -654,7 +654,7 @@ dependencies = [
|
|
|
654
654
|
|
|
655
655
|
[[package]]
|
|
656
656
|
name = "onix-core"
|
|
657
|
-
version = "0.9.
|
|
657
|
+
version = "0.9.1"
|
|
658
658
|
dependencies = [
|
|
659
659
|
"proptest",
|
|
660
660
|
"serde",
|
|
@@ -665,7 +665,7 @@ dependencies = [
|
|
|
665
665
|
|
|
666
666
|
[[package]]
|
|
667
667
|
name = "onix-py"
|
|
668
|
-
version = "0.9.
|
|
668
|
+
version = "0.9.1"
|
|
669
669
|
dependencies = [
|
|
670
670
|
"arrow-array",
|
|
671
671
|
"arrow-ipc",
|
|
@@ -997,7 +997,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|
|
997
997
|
dependencies = [
|
|
998
998
|
"proc-macro2",
|
|
999
999
|
"quote",
|
|
1000
|
-
"syn 3.0.
|
|
1000
|
+
"syn 3.0.5",
|
|
1001
1001
|
]
|
|
1002
1002
|
|
|
1003
1003
|
[[package]]
|
|
@@ -1050,9 +1050,9 @@ dependencies = [
|
|
|
1050
1050
|
|
|
1051
1051
|
[[package]]
|
|
1052
1052
|
name = "syn"
|
|
1053
|
-
version = "3.0.
|
|
1053
|
+
version = "3.0.5"
|
|
1054
1054
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1055
|
-
checksum = "
|
|
1055
|
+
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
|
1056
1056
|
dependencies = [
|
|
1057
1057
|
"proc-macro2",
|
|
1058
1058
|
"quote",
|
|
@@ -1199,7 +1199,7 @@ dependencies = [
|
|
|
1199
1199
|
"bumpalo",
|
|
1200
1200
|
"proc-macro2",
|
|
1201
1201
|
"quote",
|
|
1202
|
-
"syn 3.0.
|
|
1202
|
+
"syn 3.0.5",
|
|
1203
1203
|
"wasm-bindgen-shared",
|
|
1204
1204
|
]
|
|
1205
1205
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepdiff-rs
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Rust
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -236,10 +236,11 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
236
236
|
## Known limitations
|
|
237
237
|
|
|
238
238
|
- Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
|
|
239
|
-
- Supported value types are `None`, `bool`, `int`, `float
|
|
239
|
+
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested). `int`s must fit in `i64`/`u64`, and anything else — a custom object or an arbitrary-precision `int` — raises `TypeError`/`ValueError` naming the exact path it was found at. A non-`str` key's path renders via Python's own `repr()`, except a `tuple` key, which splits into one bracket group per element (`root[1][2]` for `(1, 2)`, never `root[(1, 2)]`, with one deliberate exception for a real DeepDiff bug on the empty tuple, and another on a non-finite-float key — see [`tests/golden/README.md`](tests/golden/README.md)). A nested dict value's own `bool`/`None`/`int`/`float` key stringifies the way `json.dumps` does, but its `datetime`/`date`/`tuple` key renders that same `repr()` text where DeepDiff's own `to_json()` raises `TypeError` on one — a superset, not a difference in the findings (see [`tests/golden/README.md`](tests/golden/README.md)). `1`/`1.0`/`True` match as the same key between two dicts (Python `dict`/`set` equality). The **Datetimes**, **Sets** and **Non-finite floats** bullets below cover the deliberate divergences for those types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
|
|
240
240
|
- A subclass of a supported type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date`/`time`/`timedelta` 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).
|
|
241
241
|
- **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` is not supported; the normalized-versus-raw split is documented there too. A `time`/`timedelta`, unlike a datetime, is never normalized for report (DeepDiff compares `time`/`date`/`timedelta` with a plain `!=`), and a naive `time` is never equal to an aware one; `to_json()` renders a `time` as `time.isoformat()`'s bytes and a `timedelta` as `str(timedelta)`'s, both supersets. Under `ignore_order`, `DeepHash` hashes a `time` by whole seconds-of-day only — dropping the microsecond and any offset, a confirmed upstream quirk — while a `timedelta` hashes exactly; see [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
242
242
|
- **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.
|
|
243
|
+
- **Non-finite floats** (`NaN`, `Infinity`, `-Infinity`) compare and hash like real Python: two `NaN`s are never equal, `Infinity == Infinity`, `to_json()` renders the same bare `NaN`/`Infinity`/`-Infinity` tokens Python's `json.dumps` does, and `ignore_order` matching treats every `NaN` as one shared item, matching `DeepHash`. The one divergence, always deterministic: this crate's value model carries no Python object identity, so two independently-obtained `NaN`s always compare unequal here, where DeepDiff sometimes reports no difference (`t1 is t2`) or lets one collapse into another (a `set` member, an ordered-list match) when the two objects, or their containers, happen to be the same one. See [`tests/golden/README.md`](tests/golden/README.md)'s "Non-finite floats" section.
|
|
243
244
|
- 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.
|
|
244
245
|
- A `str` inside a `tuple` or `frozenset` set item is escaped exactly as Python's `repr()` escapes it, against Unicode 16.0.0; on a Python older than 3.14 (an older `unicodedata` table), a code point assigned to Unicode after that Python's own version is escaped by DeepDiff and rendered literally by onix. See [`tests/golden/README.md`](tests/golden/README.md)'s "Pinned versions" section.
|
|
245
246
|
- 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).
|
|
@@ -218,10 +218,11 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
218
218
|
## Known limitations
|
|
219
219
|
|
|
220
220
|
- Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
|
|
221
|
-
- Supported value types are `None`, `bool`, `int`, `float
|
|
221
|
+
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested). `int`s must fit in `i64`/`u64`, and anything else — a custom object or an arbitrary-precision `int` — raises `TypeError`/`ValueError` naming the exact path it was found at. A non-`str` key's path renders via Python's own `repr()`, except a `tuple` key, which splits into one bracket group per element (`root[1][2]` for `(1, 2)`, never `root[(1, 2)]`, with one deliberate exception for a real DeepDiff bug on the empty tuple, and another on a non-finite-float key — see [`tests/golden/README.md`](tests/golden/README.md)). A nested dict value's own `bool`/`None`/`int`/`float` key stringifies the way `json.dumps` does, but its `datetime`/`date`/`tuple` key renders that same `repr()` text where DeepDiff's own `to_json()` raises `TypeError` on one — a superset, not a difference in the findings (see [`tests/golden/README.md`](tests/golden/README.md)). `1`/`1.0`/`True` match as the same key between two dicts (Python `dict`/`set` equality). The **Datetimes**, **Sets** and **Non-finite floats** bullets below cover the deliberate divergences for those types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
|
|
222
222
|
- A subclass of a supported type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date`/`time`/`timedelta` 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).
|
|
223
223
|
- **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` is not supported; the normalized-versus-raw split is documented there too. A `time`/`timedelta`, unlike a datetime, is never normalized for report (DeepDiff compares `time`/`date`/`timedelta` with a plain `!=`), and a naive `time` is never equal to an aware one; `to_json()` renders a `time` as `time.isoformat()`'s bytes and a `timedelta` as `str(timedelta)`'s, both supersets. Under `ignore_order`, `DeepHash` hashes a `time` by whole seconds-of-day only — dropping the microsecond and any offset, a confirmed upstream quirk — while a `timedelta` hashes exactly; see [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
224
224
|
- **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.
|
|
225
|
+
- **Non-finite floats** (`NaN`, `Infinity`, `-Infinity`) compare and hash like real Python: two `NaN`s are never equal, `Infinity == Infinity`, `to_json()` renders the same bare `NaN`/`Infinity`/`-Infinity` tokens Python's `json.dumps` does, and `ignore_order` matching treats every `NaN` as one shared item, matching `DeepHash`. The one divergence, always deterministic: this crate's value model carries no Python object identity, so two independently-obtained `NaN`s always compare unequal here, where DeepDiff sometimes reports no difference (`t1 is t2`) or lets one collapse into another (a `set` member, an ordered-list match) when the two objects, or their containers, happen to be the same one. See [`tests/golden/README.md`](tests/golden/README.md)'s "Non-finite floats" section.
|
|
225
226
|
- 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.
|
|
226
227
|
- A `str` inside a `tuple` or `frozenset` set item is escaped exactly as Python's `repr()` escapes it, against Unicode 16.0.0; on a Python older than 3.14 (an older `unicodedata` table), a code point assigned to Unicode after that Python's own version is escaped by DeepDiff and rendered literally by onix. See [`tests/golden/README.md`](tests/golden/README.md)'s "Pinned versions" section.
|
|
227
228
|
- 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).
|
|
@@ -207,14 +207,17 @@ pub(crate) fn numbers_equal(old: &Number, new: &Number) -> bool {
|
|
|
207
207
|
}
|
|
208
208
|
/// Compares two floats for exact equality.
|
|
209
209
|
///
|
|
210
|
-
/// This mirrors Python's `==` semantics for floats
|
|
211
|
-
///
|
|
212
|
-
/// for
|
|
210
|
+
/// This mirrors Python's `==` semantics for floats: `0.0 == -0.0`, and —
|
|
211
|
+
/// with no special-casing needed — `NaN != NaN` (matching real `DeepDiff`
|
|
212
|
+
/// for two independently-obtained `NaN` values; see `tests/golden/README.md`'s
|
|
213
|
+
/// "Non-finite floats" section for the one case this crate cannot
|
|
214
|
+
/// reproduce, where `DeepDiff` short-circuits on `t1 is t2`), no implicit
|
|
215
|
+
/// epsilon, and `Infinity == Infinity`. Exact IEEE-754 `==` is the correct
|
|
213
216
|
/// (and only) rule here.
|
|
214
217
|
fn floats_equal(a: f64, b: f64) -> bool {
|
|
215
218
|
#[allow(
|
|
216
219
|
clippy::float_cmp,
|
|
217
|
-
reason = "exact IEEE-754 equality is the intended rule (Python == semantics, including 0.0 == -0.0
|
|
220
|
+
reason = "exact IEEE-754 equality is the intended rule (Python == semantics, including 0.0 == -0.0 and NaN != NaN)"
|
|
218
221
|
)]
|
|
219
222
|
{
|
|
220
223
|
a == b
|
|
@@ -519,7 +519,7 @@ fn coerce_for_type_change(old_value: &Value, new_value: &Value) -> Option<Value>
|
|
|
519
519
|
match new_value {
|
|
520
520
|
Value::Bool(_) => Some(Value::Bool(is_truthy(old_value))),
|
|
521
521
|
Value::Number(n) if n.is_f64() => {
|
|
522
|
-
coerce_to_f64(old_value).
|
|
522
|
+
coerce_to_f64(old_value).map(|f| Value::Number(Number::from_f64(f)))
|
|
523
523
|
}
|
|
524
524
|
Value::Number(_) => coerce_to_i64(old_value).map(|i| Value::Number(Number::from_i64(i))),
|
|
525
525
|
Value::Str(_) => coerce_to_python_str(old_value).map(|s| Value::Str(s.into_boxed_str())),
|
|
@@ -59,6 +59,14 @@ pub(crate) type HashSet<T> = std::collections::HashSet<T, BuildHasherDefault<FxH
|
|
|
59
59
|
/// hence `mix_float_bits`, so the hazard does not reach the new key either — a
|
|
60
60
|
/// *non-adversarial* run of integral/half-integer floats, whose raw bit
|
|
61
61
|
/// patterns share ~50 trailing zeros, does not accidentally collide.
|
|
62
|
+
/// `ItemKey::Float` additionally maps *every* `NaN`, regardless of its own
|
|
63
|
+
/// bits, onto one fixed representative (`deephash_float_bits`, matching
|
|
64
|
+
/// `DeepHash`'s `str(obj)`-based digest — see that function's own doc): a
|
|
65
|
+
/// deliberate full key collapse, not merely a hash collision, so a run of
|
|
66
|
+
/// `NaN`s in one of these tables costs *less* than the equivalent run of
|
|
67
|
+
/// distinct floats would (they fold to a single entry), not more — this
|
|
68
|
+
/// changes which values these tables treat as the same item, not their
|
|
69
|
+
/// per-lookup cost.
|
|
62
70
|
///
|
|
63
71
|
/// For those remaining `ignore_order`-only tables the trade is deliberate and
|
|
64
72
|
/// measured. Re-keying them to `SipHash` (`RandomState`) added a material,
|
|
@@ -189,9 +189,11 @@ pub(crate) enum ItemKey {
|
|
|
189
189
|
/// `serde_json::Number`'s non-float representation is always an `i64`
|
|
190
190
|
/// or `u64`, both of which fit losslessly in `i128`.
|
|
191
191
|
Int(i128),
|
|
192
|
-
/// A
|
|
193
|
-
///
|
|
194
|
-
/// never collides with `Int(5)`; see this type's own doc)
|
|
192
|
+
/// A float, keyed by [`deephash_float_bits`] — its exact bit pattern for
|
|
193
|
+
/// any finite value (kept as its own bucket even when whole-numbered:
|
|
194
|
+
/// `5.0` never collides with `Int(5)`; see this type's own doc), but one
|
|
195
|
+
/// fixed representative for *every* `NaN` regardless of its bits, since
|
|
196
|
+
/// `DeepHash` digests any `NaN` the same way (see that function's doc).
|
|
195
197
|
Float(u64),
|
|
196
198
|
Str(String),
|
|
197
199
|
/// A `datetime`, keyed by its instant with a naive value read as UTC —
|
|
@@ -678,15 +680,44 @@ fn scalar_content_key(value: &Value) -> ItemKey {
|
|
|
678
680
|
}
|
|
679
681
|
}
|
|
680
682
|
|
|
683
|
+
/// The bit pattern [`ItemKey::Float`] keys a float by:
|
|
684
|
+
/// [`crate::value::fold_signed_zero`] for every finite value (agreeing with
|
|
685
|
+
/// [`crate::value::Number`]'s `PartialEq` and `canonical_cmp`'s
|
|
686
|
+
/// `number_cmp`), and one fixed representative for *every* `NaN`,
|
|
687
|
+
/// regardless of its own sign or payload bits.
|
|
688
|
+
///
|
|
689
|
+
/// This is deliberately **coarser** than [`crate::value::fold_signed_zero`]
|
|
690
|
+
/// (which leaves a `NaN`'s bits alone) and coarser than
|
|
691
|
+
/// [`crate::value::Number`]'s own `PartialEq` (under which no two `NaN`s are
|
|
692
|
+
/// ever equal): `DeepHash._prep_number`
|
|
693
|
+
/// builds its digest from `str(obj)` when `significant_digits` is unset (the
|
|
694
|
+
/// only mode this crate reproduces), and `str()` of *any* `NaN` — whatever
|
|
695
|
+
/// its bits — is the same three characters, `"nan"`. Collapsing every `NaN`
|
|
696
|
+
/// onto one [`ItemKey`] is what makes `onix`'s `ignore_order` item/member
|
|
697
|
+
/// matching agree with that: two list elements, dict values, or set members
|
|
698
|
+
/// that are each independently a `NaN` hash-match here exactly as they do
|
|
699
|
+
/// under real `DeepHash`, confirmed against `deepdiff==9.1.0` (`[nan_a]` vs
|
|
700
|
+
/// `[nan_b]`, two *distinct* `NaN` objects, is `{}` under `ignore_order`).
|
|
701
|
+
/// `Infinity`/`-Infinity` need no such collapse: `str(inf)` and `str(-inf)`
|
|
702
|
+
/// already differ, matching their (already distinct) bit patterns.
|
|
703
|
+
fn deephash_float_bits(f: f64) -> u64 {
|
|
704
|
+
if f.is_nan() {
|
|
705
|
+
f64::NAN.to_bits()
|
|
706
|
+
} else {
|
|
707
|
+
crate::value::fold_signed_zero(f).to_bits()
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
681
711
|
/// The type-distinct key for a bare number, mirroring [`ItemKey`]'s own
|
|
682
|
-
/// number rule (see [`
|
|
683
|
-
///
|
|
712
|
+
/// number rule (see [`deephash_float_bits`] for the float rule, including
|
|
713
|
+
/// its `NaN` collapse, and [`crate::value::fold_signed_zero`] for the
|
|
714
|
+
/// signed-zero note).
|
|
684
715
|
fn number_key(n: &crate::value::Number) -> ItemKey {
|
|
685
716
|
if n.is_f64() {
|
|
686
717
|
let f = n
|
|
687
718
|
.as_f64()
|
|
688
719
|
.expect("Number::is_f64 guarantees as_f64 succeeds");
|
|
689
|
-
return ItemKey::Float(
|
|
720
|
+
return ItemKey::Float(deephash_float_bits(f));
|
|
690
721
|
}
|
|
691
722
|
if let Some(i) = n.as_i64() {
|
|
692
723
|
return ItemKey::Int(i128::from(i));
|
|
@@ -742,13 +773,14 @@ fn keyed(value: &Value, memo: &IgnoreOrderMemo, want_part: bool) -> (ItemKey, Op
|
|
|
742
773
|
let f = n
|
|
743
774
|
.as_f64()
|
|
744
775
|
.expect("Number::is_f64 guarantees as_f64 succeeds");
|
|
745
|
-
// See `
|
|
746
|
-
//
|
|
776
|
+
// See `deephash_float_bits`: it is the identity on every
|
|
777
|
+
// finite float but `-0.0`, so an integral float like `2.0`
|
|
747
778
|
// keeps a distinct `Float` key from the integer `2` (this
|
|
748
779
|
// deliberately does NOT take the ordered path's `ScalarKey`
|
|
749
780
|
// integral-to-`Int` canonicalization — the two paths have
|
|
750
|
-
// genuinely different number semantics)
|
|
751
|
-
|
|
781
|
+
// genuinely different number semantics), and it collapses
|
|
782
|
+
// every `NaN` onto one shared key regardless of bits.
|
|
783
|
+
ItemKey::Float(deephash_float_bits(f))
|
|
752
784
|
} else if let Some(i) = n.as_i64() {
|
|
753
785
|
ItemKey::Int(i128::from(i))
|
|
754
786
|
} else {
|
|
@@ -177,12 +177,14 @@ pub(crate) struct IgnoreOrderMemo {
|
|
|
177
177
|
node_table: RefCell<BTreeMap<MemberHashKey, (NodeId, RepId)>>,
|
|
178
178
|
/// Set-member content interning: each distinct [`MemberContent`] gets one
|
|
179
179
|
/// [`RepId`] (its `usize` index), so content-equal members — a naive and an
|
|
180
|
-
/// aware datetime at one instant
|
|
181
|
-
///
|
|
182
|
-
///
|
|
183
|
-
///
|
|
184
|
-
///
|
|
185
|
-
/// `
|
|
180
|
+
/// aware datetime at one instant, and (via the [`ItemKey::Float`] a
|
|
181
|
+
/// `MemberContent::Scalar` embeds) every `NaN` regardless of its own
|
|
182
|
+
/// bits — collapse to one id. A [`BTreeMap`] for the same
|
|
183
|
+
/// collision-immunity reason as `node_table`; per-lookup cost is
|
|
184
|
+
/// `O(log n)` comparisons, each a full walk of the probed
|
|
185
|
+
/// `MemberContent` — a `MemberContent::UnhashableDict` key is itself
|
|
186
|
+
/// keyed by each of the dict's own keys' `ItemKey` trees (a `tuple`
|
|
187
|
+
/// dict key included), not a cheap string ordering.
|
|
186
188
|
member_content: RefCell<BTreeMap<MemberContent, RepId>>,
|
|
187
189
|
enabled: bool,
|
|
188
190
|
/// Total number of times [`Self::put`] has actually run — every distance
|
|
@@ -3065,7 +3065,7 @@ fn dist_hash(value: &CValue) -> u64 {
|
|
|
3065
3065
|
/// aware pair shifted by its offset to the same moment).
|
|
3066
3066
|
#[test]
|
|
3067
3067
|
fn dist_key_hash_agrees_with_equality_on_tricky_equal_values() {
|
|
3068
|
-
let float = |f: f64| CValue::Number(crate::value::Number::from_f64(f)
|
|
3068
|
+
let float = |f: f64| CValue::Number(crate::value::Number::from_f64(f));
|
|
3069
3069
|
let nested_set = |order: [i64; 3]| {
|
|
3070
3070
|
CValue::Set(SetItems::new(vec![
|
|
3071
3071
|
ctup(&[json!(order[0])]),
|
|
@@ -3116,6 +3116,37 @@ fn dist_key_hash_agrees_with_equality_on_tricky_equal_values() {
|
|
|
3116
3116
|
}
|
|
3117
3117
|
}
|
|
3118
3118
|
|
|
3119
|
+
/// `NaN` cannot appear among the pairs above — no two `NaN`s are ever
|
|
3120
|
+
/// `Value`-equal (`NaN != NaN`, matching Python), so there is no equal-values
|
|
3121
|
+
/// case to add. What NEEDS pinning instead is the deliberately *coarser*
|
|
3122
|
+
/// hash: `DistKey`'s `Hash` collapses every `NaN` bit pattern onto one bucket
|
|
3123
|
+
/// (`crate::ignore_order::hash::number_key`, matching `DeepHash`'s own
|
|
3124
|
+
/// `NaN`-insensitive digest — see that function's own doc), so two
|
|
3125
|
+
/// genuinely distinct, never-`Value`-equal `NaN`s hash *equal* here — the
|
|
3126
|
+
/// opposite direction from the property above, and safe only because a hash
|
|
3127
|
+
/// collision is not itself a lookup match: `DistKey`'s `Eq` is `Value`'s
|
|
3128
|
+
/// real equality, and it must still tell the two apart, or the memo would
|
|
3129
|
+
/// silently hand one `NaN`'s cached distance to the other.
|
|
3130
|
+
#[test]
|
|
3131
|
+
fn dist_key_hash_collision_on_distinct_nans_never_becomes_equality() {
|
|
3132
|
+
let nan_a = CValue::Number(crate::value::Number::from_f64(f64::NAN));
|
|
3133
|
+
let nan_b = CValue::Number(crate::value::Number::from_f64(-f64::NAN));
|
|
3134
|
+
|
|
3135
|
+
assert_ne!(nan_a, nan_b, "two NaN values are never Value-equal");
|
|
3136
|
+
assert_eq!(
|
|
3137
|
+
dist_hash(&nan_a),
|
|
3138
|
+
dist_hash(&nan_b),
|
|
3139
|
+
"NaN's dist-key hash deliberately ignores sign/payload"
|
|
3140
|
+
);
|
|
3141
|
+
|
|
3142
|
+
let key_a = super::hash::DistKey::new(&nan_a);
|
|
3143
|
+
let key_b = super::hash::DistKey::new(&nan_b);
|
|
3144
|
+
assert!(
|
|
3145
|
+
key_a != key_b,
|
|
3146
|
+
"a hash collision must not make DistKey::eq treat two distinct NaNs as the same cache entry"
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3119
3150
|
/// An `arbitrary` compact value covering every equality class the distance-key
|
|
3120
3151
|
/// hash must respect — including the ones JSON cannot express, so they are
|
|
3121
3152
|
/// actually generated: tuples, sets, frozensets, datetimes (naive and aware),
|
|
@@ -3137,15 +3168,28 @@ fn arb_cvalue() -> impl Strategy<Value = CValue> {
|
|
|
3137
3168
|
)
|
|
3138
3169
|
.prop_map(|(y, mo, d, h, mi, s, off)| cdt_at(y, mo, d, h, mi, s, 0, off));
|
|
3139
3170
|
let arb_date = (2000i32..2025, 1u8..=12, 1u8..=28).prop_map(|(y, m, d)| cdate(y, m, d));
|
|
3171
|
+
// `NaN` is excluded, not `any::<f64>()`'s non-finite values generally:
|
|
3172
|
+
// `Infinity`/`-Infinity` are ordinary equal-to-themselves floats and
|
|
3173
|
+
// `structural_twin` leaves them untouched (only a signed zero gets
|
|
3174
|
+
// perturbed), so they exercise this property harmlessly. A `NaN` cannot:
|
|
3175
|
+
// `Value::eq` never calls two `NaN`s equal (matching Python's
|
|
3176
|
+
// `nan != nan`), so `prop_assert_eq!(&value, &twin)` below would fail on
|
|
3177
|
+
// any tree containing one, structural twin or not — this proptest is
|
|
3178
|
+
// about the hash/equality *agreement*, which a `NaN` leaf has no
|
|
3179
|
+
// meaningful instance of (see
|
|
3180
|
+
// `dist_key_hash_collision_on_distinct_nans_never_becomes_equality` for
|
|
3181
|
+
// the coverage a `NaN` does need).
|
|
3140
3182
|
let arb_float = prop_oneof![
|
|
3141
3183
|
Just(0.0f64),
|
|
3142
3184
|
Just(-0.0f64),
|
|
3143
3185
|
Just(1.0f64),
|
|
3144
3186
|
Just(2.0f64),
|
|
3187
|
+
Just(f64::INFINITY),
|
|
3188
|
+
Just(f64::NEG_INFINITY),
|
|
3145
3189
|
any::<f64>(),
|
|
3146
3190
|
]
|
|
3147
|
-
.prop_filter_map("
|
|
3148
|
-
crate::value::Number::from_f64(f)
|
|
3191
|
+
.prop_filter_map("not NaN", |f| {
|
|
3192
|
+
(!f.is_nan()).then(|| CValue::Number(crate::value::Number::from_f64(f)))
|
|
3149
3193
|
});
|
|
3150
3194
|
let leaf = prop_oneof![
|
|
3151
3195
|
Just(CValue::Null),
|
|
@@ -3283,9 +3327,7 @@ fn structural_twin(value: &CValue, in_set: bool) -> CValue {
|
|
|
3283
3327
|
CValue::Number(n) if n.is_f64() => {
|
|
3284
3328
|
let f = n.as_f64().expect("is_f64 guarantees as_f64");
|
|
3285
3329
|
let flipped = if f == 0.0 { -f } else { f };
|
|
3286
|
-
CValue::Number(
|
|
3287
|
-
crate::value::Number::from_f64(flipped).expect("finite float stays finite"),
|
|
3288
|
-
)
|
|
3330
|
+
CValue::Number(crate::value::Number::from_f64(flipped))
|
|
3289
3331
|
}
|
|
3290
3332
|
scalar => scalar.clone(),
|
|
3291
3333
|
}
|
|
@@ -92,6 +92,9 @@ pub(crate) enum ScalarKey {
|
|
|
92
92
|
/// Bit pattern of a non-integral (or too-large-to-be-exact) float —
|
|
93
93
|
/// hashed through [`mix_float_bits`]; see this type's hand-written `Hash`.
|
|
94
94
|
Float(u64),
|
|
95
|
+
/// A `NaN`, keyed by the address of the [`Value`] node it was read from
|
|
96
|
+
/// — see [`python_scalar_key`]'s `NaN` case for why.
|
|
97
|
+
Nan(usize),
|
|
95
98
|
/// A `datetime`, keyed by whether it is aware and by its instant —
|
|
96
99
|
/// Python's own `datetime.__eq__`/`__hash__` pair, which compares two
|
|
97
100
|
/// aware values by instant (so `10:00+00:00 == 12:00+02:00`) but never
|
|
@@ -150,6 +153,13 @@ impl std::hash::Hash for ScalarKey {
|
|
|
150
153
|
Self::Str(s) => s.hash(state),
|
|
151
154
|
Self::Int(i) => i.hash(state),
|
|
152
155
|
Self::Float(bits) => mix_float_bits(*bits).hash(state),
|
|
156
|
+
// A `Value` node's address is 8/16-byte-aligned like any other
|
|
157
|
+
// pointer, so its low bits carry no entropy; avalanche it the
|
|
158
|
+
// same way as a trailing-zero-heavy float bit pattern (see
|
|
159
|
+
// `mix_float_bits`'s own doc) rather than trust the raw address.
|
|
160
|
+
// `usize as u64` never truncates: `u64` is at least as wide as
|
|
161
|
+
// `usize` on every target this crate builds for.
|
|
162
|
+
Self::Nan(id) => mix_float_bits(*id as u64).hash(state),
|
|
153
163
|
Self::DateTime { aware, instant } | Self::Time { aware, instant } => {
|
|
154
164
|
aware.hash(state);
|
|
155
165
|
instant.hash(state);
|
|
@@ -219,6 +229,26 @@ fn scalar_key(value: &Value) -> ScalarKey {
|
|
|
219
229
|
/// `==`; `crate::ignore_order` needs the same rule twice more, for
|
|
220
230
|
/// `DeepHash`'s cache identity and for the `list(t1) == t2` coercion test,
|
|
221
231
|
/// and shares this one rather than restating it.
|
|
232
|
+
///
|
|
233
|
+
/// A `NaN` gets [`ScalarKey::Nan`], keyed by `value`'s own address, because
|
|
234
|
+
/// no bit-pattern-based key could be right here: `NaN != NaN` in Python
|
|
235
|
+
/// regardless of bits, confirmed against `deepdiff==9.1.0` — two distinct
|
|
236
|
+
/// `NaN` objects with the *same* bits still fail `difflib`'s `==`-based
|
|
237
|
+
/// match (`[1, nan_a, 2]` vs `[1, 2, nan_b]` reports a `type_changes` at the
|
|
238
|
+
/// `nan`/`2` positions rather than treating the insertion as a clean shift,
|
|
239
|
+
/// which a bit-based key that let `nan_a` match `nan_b` would get wrong).
|
|
240
|
+
/// Each call therefore has to hand back a key that cannot equal *any* other
|
|
241
|
+
/// call's — including another `NaN` read from the exact same bits — and an
|
|
242
|
+
/// address is the only per-call-distinct, already-available `usize` this
|
|
243
|
+
/// function has: `value` is borrowed from a [`Value`] tree that outlives the
|
|
244
|
+
/// whole comparison this key feeds, so the address is stable for exactly as
|
|
245
|
+
/// long as the key needs to be looked up, and (being a fresh allocation per
|
|
246
|
+
/// converted Python object) two different `NaN` occurrences can never share
|
|
247
|
+
/// one. See `tests/golden/README.md`'s "Non-finite floats" section for the
|
|
248
|
+
/// one case this cannot reproduce (the *same* Python `NaN` object compared
|
|
249
|
+
/// or hashed against itself, which real `DeepDiff`/`CPython` sometimes treat
|
|
250
|
+
/// as matching via object identity — a concept this crate's value model
|
|
251
|
+
/// does not carry).
|
|
222
252
|
pub(crate) fn python_scalar_key(value: &Value) -> Option<ScalarKey> {
|
|
223
253
|
Some(match value {
|
|
224
254
|
Value::Null => ScalarKey::Null,
|
|
@@ -233,6 +263,9 @@ pub(crate) fn python_scalar_key(value: &Value) -> Option<ScalarKey> {
|
|
|
233
263
|
return Some(ScalarKey::Int(i));
|
|
234
264
|
}
|
|
235
265
|
let f = n.as_f64().expect("a serde_json Number is i64, u64, or f64");
|
|
266
|
+
if f.is_nan() {
|
|
267
|
+
return Some(ScalarKey::Nan(std::ptr::from_ref(value) as usize));
|
|
268
|
+
}
|
|
236
269
|
if f.fract() == 0.0 && f.abs() <= MAX_EXACT_F64_INT {
|
|
237
270
|
#[allow(
|
|
238
271
|
clippy::cast_possible_truncation,
|
|
@@ -104,6 +104,67 @@ fn non_integral_floats_compare_by_value() {
|
|
|
104
104
|
assert!(!python_scalar_eq(&json!(1.5), &json!(2.5)));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// `NaN` cannot arrive via `json!`/`cv` (`serde_json::Number` is finite by
|
|
108
|
+
// construction), so these build the compact `Value` directly rather than
|
|
109
|
+
// through the `serde_json`-literal wrapper the tests above share.
|
|
110
|
+
#[test]
|
|
111
|
+
fn two_distinct_nans_are_never_python_equal() {
|
|
112
|
+
use crate::value::{Number, Value};
|
|
113
|
+
|
|
114
|
+
let a = Value::Number(Number::from_f64(f64::NAN));
|
|
115
|
+
let b = Value::Number(Number::from_f64(f64::NAN));
|
|
116
|
+
assert_ne!(
|
|
117
|
+
super::python_scalar_key(&a),
|
|
118
|
+
super::python_scalar_key(&b),
|
|
119
|
+
"two independently-built NaN values must never share a ScalarKey"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn the_same_nan_value_reference_yields_the_same_scalar_key() {
|
|
125
|
+
use crate::value::{Number, Value};
|
|
126
|
+
|
|
127
|
+
let a = Value::Number(Number::from_f64(f64::NAN));
|
|
128
|
+
assert_eq!(
|
|
129
|
+
super::python_scalar_key(&a),
|
|
130
|
+
super::python_scalar_key(&a),
|
|
131
|
+
"hashing/keying the same Value node twice must agree with itself"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn nan_and_infinity_never_share_a_scalar_key() {
|
|
137
|
+
use crate::value::{Number, Value};
|
|
138
|
+
|
|
139
|
+
let nan = Value::Number(Number::from_f64(f64::NAN));
|
|
140
|
+
let inf = Value::Number(Number::from_f64(f64::INFINITY));
|
|
141
|
+
assert_ne!(
|
|
142
|
+
super::python_scalar_key(&nan),
|
|
143
|
+
super::python_scalar_key(&inf)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn scalar_key_nan_hashes_and_is_usable_as_a_hashmap_key() {
|
|
149
|
+
use std::collections::HashMap;
|
|
150
|
+
|
|
151
|
+
use crate::value::{Number, Value};
|
|
152
|
+
|
|
153
|
+
let a = Value::Number(Number::from_f64(f64::NAN));
|
|
154
|
+
let b = Value::Number(Number::from_f64(f64::NAN));
|
|
155
|
+
let key_a = super::python_scalar_key(&a).expect("NaN is a scalar");
|
|
156
|
+
let key_b = super::python_scalar_key(&b).expect("NaN is a scalar");
|
|
157
|
+
|
|
158
|
+
let mut map: HashMap<super::ScalarKey, &str> = HashMap::new();
|
|
159
|
+
map.insert(key_a.clone(), "a");
|
|
160
|
+
map.insert(key_b.clone(), "b");
|
|
161
|
+
// Two distinct NaNs never collapse to one entry, matching Python's own
|
|
162
|
+
// `nan != nan` (see `super::ScalarKey::Nan`'s own doc).
|
|
163
|
+
assert_eq!(map.len(), 2);
|
|
164
|
+
assert_eq!(map.get(&key_a), Some(&"a"));
|
|
165
|
+
assert_eq!(map.get(&key_b), Some(&"b"));
|
|
166
|
+
}
|
|
167
|
+
|
|
107
168
|
// --- compute_opcodes -------------------------------------------------
|
|
108
169
|
|
|
109
170
|
fn vals(items: &[i64]) -> Vec<serde_json::Value> {
|
|
@@ -601,6 +601,17 @@ fn escape_non_printable(out: &mut String, c: char) {
|
|
|
601
601
|
/// is involved. Verified against real Python `repr()` over a million random
|
|
602
602
|
/// bit patterns in the bindings suite.
|
|
603
603
|
pub(crate) fn python_float_repr(value: f64) -> String {
|
|
604
|
+
if !value.is_finite() {
|
|
605
|
+
// `{:e}` has no exponent form for these, so the digit-count logic
|
|
606
|
+
// below (which expects an `e` separator) never applies to them.
|
|
607
|
+
return if value.is_nan() {
|
|
608
|
+
"nan".to_string()
|
|
609
|
+
} else if value.is_sign_positive() {
|
|
610
|
+
"inf".to_string()
|
|
611
|
+
} else {
|
|
612
|
+
"-inf".to_string()
|
|
613
|
+
};
|
|
614
|
+
}
|
|
604
615
|
let shortest = format!("{value:e}");
|
|
605
616
|
let significant = shortest
|
|
606
617
|
.split_once('e')
|
|
@@ -879,7 +890,7 @@ mod tests {
|
|
|
879
890
|
];
|
|
880
891
|
for (input, expected) in cases {
|
|
881
892
|
assert_eq!(
|
|
882
|
-
set_item_repr(&Value::Number(Number::from_f64(input)
|
|
893
|
+
set_item_repr(&Value::Number(Number::from_f64(input))),
|
|
883
894
|
expected,
|
|
884
895
|
"for {input:?}"
|
|
885
896
|
);
|
|
@@ -1135,7 +1146,7 @@ mod tests {
|
|
|
1135
1146
|
];
|
|
1136
1147
|
for (input, expected) in cases {
|
|
1137
1148
|
assert_eq!(
|
|
1138
|
-
set_item_repr(&Value::Number(Number::from_f64(input)
|
|
1149
|
+
set_item_repr(&Value::Number(Number::from_f64(input))),
|
|
1139
1150
|
expected,
|
|
1140
1151
|
"for {input:?}"
|
|
1141
1152
|
);
|
|
@@ -109,6 +109,6 @@ pub(crate) fn cnum(n: &serde_json::Number) -> Number {
|
|
|
109
109
|
} else if let Some(i) = n.as_i64() {
|
|
110
110
|
Number::from_i64(i)
|
|
111
111
|
} else {
|
|
112
|
-
Number::from_f64(n.as_f64().expect("serde Number is u64/i64/f64"))
|
|
112
|
+
Number::from_f64(n.as_f64().expect("serde Number is u64/i64/f64"))
|
|
113
113
|
}
|
|
114
114
|
}
|