deepdiff-rs 0.9.2__tar.gz → 0.9.3__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.2 → deepdiff_rs-0.9.3}/Cargo.lock +4 -4
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/Cargo.toml +1 -1
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/PKG-INFO +3 -3
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/README.md +2 -2
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/dispatch.rs +3 -3
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/object.rs +2 -2
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/scalar.rs +1 -1
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/tests.rs +13 -7
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/distance.rs +41 -20
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/hash.rs +9 -5
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/tests.rs +2 -2
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/lcs.rs +4 -2
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/path.rs +150 -68
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/report.rs +18 -21
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/report_tests.rs +10 -7
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/test_support.rs +1 -1
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/unified_diff.rs +11 -1
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/value.rs +530 -47
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/value_tests.rs +461 -8
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/tests/golden.rs +11 -1
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/tests/ignore_order_memory.rs +4 -4
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/src/convert.rs +107 -55
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/src/deepdiff.rs +35 -16
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/src/fast_path.rs +5 -2
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/src/guard.rs +116 -49
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_conversions.py +92 -39
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_differential_fuzz.py +159 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/Cargo.toml +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/examples/row_diff_rss.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/examples/type_stack_cost.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/error.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/json_rows.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/lib.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/options.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/row_diff.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/schema.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/table_diff.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/Cargo.toml +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/datetime.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/datetime_tests.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/array.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/mod.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/options.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/set.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/error.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/fxhash.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/memo.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/mod.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/pairing.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/lcs_tests.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/lib.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/src/unified_diff_tests.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/tests/memory_footprint.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/tests/proptest_diff.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/.python-version +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/Cargo.toml +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/benchmarks/bench_bindings.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/deepdiff_rs.pyi +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/src/arrow.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/src/errors.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/src/lib.rs +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/conftest.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_bindings_memory.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_datetimes.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_depth_guard.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_golden_parity.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_non_finite.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_sets.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_signed_zero.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_smoke.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_stub_mypy.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_stub_signatures.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_suite_hygiene.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_table_diff.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_table_row_diff.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_timedeltas.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_times.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_tuples.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_wheel_contents.py +0 -0
- {deepdiff_rs-0.9.2 → deepdiff_rs-0.9.3}/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.3"
|
|
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.3"
|
|
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.3"
|
|
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.3"
|
|
669
669
|
dependencies = [
|
|
670
670
|
"arrow-array",
|
|
671
671
|
"arrow-ipc",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepdiff-rs
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Rust
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -148,7 +148,7 @@ A key appearing more than once on either side is reported in `duplicate_keys` (w
|
|
|
148
148
|
|
|
149
149
|
Type comparison uses the full logical Arrow type (timestamp unit and timezone, decimal precision and scale, and so on), but physical encodings that carry the same logical type compare equal — a dictionary-encoded string equals a plain string, polars' `Utf8View` equals pyarrow's `Utf8`, the list variants normalize together, and a map compares equal however a library spells it — so the same table read through pyarrow, polars, or DuckDB reports no spurious type changes. The full normalization rules are documented on `normalized_type` (and `map_entries`) in [`crates/onix-arrow/src/schema.rs`](crates/onix-arrow/src/schema.rs); nullability is ignored but reported in each record. Column names must be unique on each side; a repeated name raises `ValueError`. `diff.schema_arrow` is the same result as an Arrow table: it implements `__arrow_c_stream__`, so `polars.DataFrame(diff.schema_arrow)` consumes it with no pyarrow needed, and `diff.schema_arrow.to_pyarrow()` returns a `pyarrow.Table`; `pandas.api.interchange.from_dataframe(diff.schema_arrow)` also works, but pandas' own implementation of that protocol needs pyarrow installed regardless of which path you take.
|
|
150
150
|
|
|
151
|
-
`pyarrow` is optional: install it with `pip install deepdiff-rs[arrow]`. It is needed only for `to_pyarrow()` and for passing pyarrow objects in — importing `deepdiff_rs` and diffing polars or DuckDB tables need it not at all. Passing an object that implements neither Arrow protocol raises `TypeError`; calling `to_pyarrow()` without pyarrow installed raises `ImportError` naming the extra
|
|
151
|
+
`pyarrow` is optional: install it with `pip install deepdiff-rs[arrow]`. It is needed only for `to_pyarrow()` and for passing pyarrow objects in — importing `deepdiff_rs` and diffing polars or DuckDB tables need it not at all. Passing an object that implements neither Arrow protocol raises `TypeError`; calling `to_pyarrow()` without pyarrow installed raises `ImportError` naming the extra; `diff.to_json()` gives the whole diff — schema, summary, and `rows_added`/`rows_removed`/`cells_changed`/`duplicate_keys` in full, one JSON object per row — as a single string with no pyarrow, polars, or pandas needed at all; see [Known limitations](#known-limitations) for its row cap.
|
|
152
152
|
|
|
153
153
|
## Performance
|
|
154
154
|
|
|
@@ -241,7 +241,7 @@ perf/ # cross-language benchmark harness and RESULTS.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
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.
|
|
244
|
-
- A `str` containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8)
|
|
244
|
+
- A `str` (or `dict` key) containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) is compared and reported exactly like any other `str`, matching DeepDiff's plain `==`; `to_json()` renders it with `json.dumps`'s own single-backslash `\uXXXX` escape. The one accepted divergence, always deterministic: hashing one — a `set`/`frozenset` member, or *any* value at all once `ignore_order=True` (`DeepHash` hashes every value there, not just a set's members) — crashes real DeepDiff with an unhandled `UnicodeEncodeError`, where onix hashes by code point and reports normally. See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
245
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.
|
|
246
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).
|
|
247
247
|
- `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).
|
|
@@ -130,7 +130,7 @@ A key appearing more than once on either side is reported in `duplicate_keys` (w
|
|
|
130
130
|
|
|
131
131
|
Type comparison uses the full logical Arrow type (timestamp unit and timezone, decimal precision and scale, and so on), but physical encodings that carry the same logical type compare equal — a dictionary-encoded string equals a plain string, polars' `Utf8View` equals pyarrow's `Utf8`, the list variants normalize together, and a map compares equal however a library spells it — so the same table read through pyarrow, polars, or DuckDB reports no spurious type changes. The full normalization rules are documented on `normalized_type` (and `map_entries`) in [`crates/onix-arrow/src/schema.rs`](crates/onix-arrow/src/schema.rs); nullability is ignored but reported in each record. Column names must be unique on each side; a repeated name raises `ValueError`. `diff.schema_arrow` is the same result as an Arrow table: it implements `__arrow_c_stream__`, so `polars.DataFrame(diff.schema_arrow)` consumes it with no pyarrow needed, and `diff.schema_arrow.to_pyarrow()` returns a `pyarrow.Table`; `pandas.api.interchange.from_dataframe(diff.schema_arrow)` also works, but pandas' own implementation of that protocol needs pyarrow installed regardless of which path you take.
|
|
132
132
|
|
|
133
|
-
`pyarrow` is optional: install it with `pip install deepdiff-rs[arrow]`. It is needed only for `to_pyarrow()` and for passing pyarrow objects in — importing `deepdiff_rs` and diffing polars or DuckDB tables need it not at all. Passing an object that implements neither Arrow protocol raises `TypeError`; calling `to_pyarrow()` without pyarrow installed raises `ImportError` naming the extra
|
|
133
|
+
`pyarrow` is optional: install it with `pip install deepdiff-rs[arrow]`. It is needed only for `to_pyarrow()` and for passing pyarrow objects in — importing `deepdiff_rs` and diffing polars or DuckDB tables need it not at all. Passing an object that implements neither Arrow protocol raises `TypeError`; calling `to_pyarrow()` without pyarrow installed raises `ImportError` naming the extra; `diff.to_json()` gives the whole diff — schema, summary, and `rows_added`/`rows_removed`/`cells_changed`/`duplicate_keys` in full, one JSON object per row — as a single string with no pyarrow, polars, or pandas needed at all; see [Known limitations](#known-limitations) for its row cap.
|
|
134
134
|
|
|
135
135
|
## Performance
|
|
136
136
|
|
|
@@ -223,7 +223,7 @@ perf/ # cross-language benchmark harness and RESULTS.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
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.
|
|
226
|
-
- A `str` containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8)
|
|
226
|
+
- A `str` (or `dict` key) containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) is compared and reported exactly like any other `str`, matching DeepDiff's plain `==`; `to_json()` renders it with `json.dumps`'s own single-backslash `\uXXXX` escape. The one accepted divergence, always deterministic: hashing one — a `set`/`frozenset` member, or *any* value at all once `ignore_order=True` (`DeepHash` hashes every value there, not just a set's members) — crashes real DeepDiff with an unhandled `UnicodeEncodeError`, where onix hashes by code point and reports normally. See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
227
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.
|
|
228
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).
|
|
229
229
|
- `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).
|
|
@@ -222,7 +222,7 @@ pub(crate) fn check_value_depth(
|
|
|
222
222
|
) -> Result<(), Error> {
|
|
223
223
|
if deeper_than(value, max_depth.saturating_sub(depth)) {
|
|
224
224
|
return Err(Error::MaxDepthExceeded {
|
|
225
|
-
path: render_path(path),
|
|
225
|
+
path: render_path(path).to_string(),
|
|
226
226
|
max_depth,
|
|
227
227
|
});
|
|
228
228
|
}
|
|
@@ -266,7 +266,7 @@ pub(crate) fn check_map_depth(
|
|
|
266
266
|
) -> Result<(), Error> {
|
|
267
267
|
if map_deeper_than(map, max_depth.saturating_sub(depth)) {
|
|
268
268
|
return Err(Error::MaxDepthExceeded {
|
|
269
|
-
path: render_path(path),
|
|
269
|
+
path: render_path(path).to_string(),
|
|
270
270
|
max_depth,
|
|
271
271
|
});
|
|
272
272
|
}
|
|
@@ -294,7 +294,7 @@ pub(crate) fn check_traversal_depth(
|
|
|
294
294
|
) -> Result<(), Error> {
|
|
295
295
|
if depth > max_depth {
|
|
296
296
|
return Err(Error::MaxDepthExceeded {
|
|
297
|
-
path: render_path(path),
|
|
297
|
+
path: render_path(path).to_string(),
|
|
298
298
|
max_depth,
|
|
299
299
|
});
|
|
300
300
|
}
|
|
@@ -113,7 +113,7 @@ pub(crate) fn object_diff(
|
|
|
113
113
|
// own path sits at), not the *parent* dict's `depth`.
|
|
114
114
|
for (key, old_value) in a {
|
|
115
115
|
scoped(path, key_segment(key), |path| -> Result<(), Error> {
|
|
116
|
-
match
|
|
116
|
+
match b.get(key) {
|
|
117
117
|
None => check_value_depth(path, old_value, depth + 1, opts.max_depth).map(|()| {
|
|
118
118
|
report.insert_dictionary_item_removed(path.clone(), old_value.clone());
|
|
119
119
|
}),
|
|
@@ -124,7 +124,7 @@ pub(crate) fn object_diff(
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
for (key, new_value) in b {
|
|
127
|
-
if
|
|
127
|
+
if !a.contains_key(key) {
|
|
128
128
|
scoped(path, key_segment(key), |path| {
|
|
129
129
|
check_value_depth(path, new_value, depth + 1, opts.max_depth).map(|()| {
|
|
130
130
|
report.insert_dictionary_item_added(path.clone(), new_value.clone());
|
|
@@ -1235,20 +1235,23 @@ fn threshold_collapse_rejects_a_deep_side_on_a_constrained_stack_instead_of_cras
|
|
|
1235
1235
|
};
|
|
1236
1236
|
let a = CValue::Object(CObject::from_pairs(vec![
|
|
1237
1237
|
(
|
|
1238
|
-
ObjectKey::Str(std::sync::Arc::from("p")),
|
|
1238
|
+
ObjectKey::Str(crate::value::Key::Utf8(std::sync::Arc::from("p"))),
|
|
1239
1239
|
CValue::from(json!(1)),
|
|
1240
1240
|
),
|
|
1241
1241
|
(
|
|
1242
|
-
ObjectKey::Str(std::sync::Arc::from("q")),
|
|
1242
|
+
ObjectKey::Str(crate::value::Key::Utf8(std::sync::Arc::from("q"))),
|
|
1243
1243
|
CValue::from(json!(2)),
|
|
1244
1244
|
),
|
|
1245
1245
|
]));
|
|
1246
1246
|
let b = CValue::Object(CObject::from_pairs(vec![
|
|
1247
1247
|
(
|
|
1248
|
-
ObjectKey::Str(std::sync::Arc::from("r")),
|
|
1248
|
+
ObjectKey::Str(crate::value::Key::Utf8(std::sync::Arc::from("r"))),
|
|
1249
1249
|
CValue::from(json!(3)),
|
|
1250
1250
|
),
|
|
1251
|
-
(
|
|
1251
|
+
(
|
|
1252
|
+
ObjectKey::Str(crate::value::Key::Utf8(std::sync::Arc::from("deep"))),
|
|
1253
|
+
deep,
|
|
1254
|
+
),
|
|
1252
1255
|
]));
|
|
1253
1256
|
|
|
1254
1257
|
let err = super::diff_with_max_depth(&a, &b, 1).unwrap_err();
|
|
@@ -1992,7 +1995,7 @@ fn list_vs_tuple_nested_in_a_dict_is_a_type_change() {
|
|
|
1992
1995
|
let mut a = Map::new();
|
|
1993
1996
|
a.insert("a".to_string(), json!([1, 2]));
|
|
1994
1997
|
let b = CValue::Object(CObject::from_pairs(vec![(
|
|
1995
|
-
ObjectKey::Str(std::sync::Arc::from("a")),
|
|
1998
|
+
ObjectKey::Str(crate::value::Key::Utf8(std::sync::Arc::from("a"))),
|
|
1996
1999
|
ctup(&[json!(1), json!(2)]),
|
|
1997
2000
|
)]));
|
|
1998
2001
|
let report = super::diff(&cv(&Value::Object(a)), &b).unwrap();
|
|
@@ -2127,7 +2130,7 @@ fn a_dict_value_that_is_a_too_deep_tuple_errors_instead_of_being_cloned() {
|
|
|
2127
2130
|
deep = ctuple(vec![deep]);
|
|
2128
2131
|
}
|
|
2129
2132
|
let b = CValue::Object(CObject::from_pairs(vec![(
|
|
2130
|
-
ObjectKey::Str(std::sync::Arc::from("a")),
|
|
2133
|
+
ObjectKey::Str(crate::value::Key::Utf8(std::sync::Arc::from("a"))),
|
|
2131
2134
|
deep,
|
|
2132
2135
|
)]));
|
|
2133
2136
|
let error = super::diff_with_max_depth(&cv(&json!({})), &b, 4).unwrap_err();
|
|
@@ -2711,7 +2714,10 @@ fn two_set_subclasses_of_the_same_class_diff_by_membership() {
|
|
|
2711
2714
|
|
|
2712
2715
|
#[test]
|
|
2713
2716
|
fn an_object_subclass_versus_the_base_type_is_a_type_change_at_equal_value() {
|
|
2714
|
-
let entries = vec![(
|
|
2717
|
+
let entries = vec![(
|
|
2718
|
+
ObjectKey::Str(crate::value::Key::Utf8(std::sync::Arc::from("a"))),
|
|
2719
|
+
cv(&json!(1)),
|
|
2720
|
+
)];
|
|
2715
2721
|
let base = CValue::Object(CObject::from_pairs(entries.clone()));
|
|
2716
2722
|
let subclass = CValue::Object(
|
|
2717
2723
|
CObject::from_pairs(entries).with_type_name(Some(std::sync::Arc::from("MyDict"))),
|
|
@@ -256,9 +256,11 @@ pub(crate) fn item_length(value: &Value) -> usize {
|
|
|
256
256
|
/// "new value" is a whole map, not a [`Value`]) can share it directly.
|
|
257
257
|
fn item_length_of_map(map: &Object) -> usize {
|
|
258
258
|
map.iter()
|
|
259
|
-
// A non-`str` key
|
|
260
|
-
//
|
|
261
|
-
//
|
|
259
|
+
// A non-`str` key, and a `str` key holding a lone surrogate (never
|
|
260
|
+
// equal to one of the plain-ASCII names below — see `ObjectKey::
|
|
261
|
+
// as_str`, `None` for both), can never match the literal exclusion
|
|
262
|
+
// list, so it is always counted — `Option::is_none_or` reads as
|
|
263
|
+
// "excluded only when this is a plain `str` key that matches".
|
|
262
264
|
.filter(|(key, _)| key.as_str().is_none_or(|s| !is_length_excluded_key(s)))
|
|
263
265
|
.map(|(_, v)| item_length(v))
|
|
264
266
|
.sum()
|
|
@@ -524,7 +526,7 @@ fn coerce_for_type_change(old_value: &Value, new_value: &Value) -> Option<Value>
|
|
|
524
526
|
coerce_to_f64(old_value).map(|f| Value::Number(Number::from_f64(f)))
|
|
525
527
|
}
|
|
526
528
|
Value::Number(_) => coerce_to_i64(old_value).map(|i| Value::Number(Number::from_i64(i))),
|
|
527
|
-
Value::Str(_) => coerce_to_python_str(old_value).map(|s| Value::Str(s.
|
|
529
|
+
Value::Str(_) => coerce_to_python_str(old_value).map(|s| Value::Str(s.into())),
|
|
528
530
|
Value::Null
|
|
529
531
|
| Value::DateTime(_)
|
|
530
532
|
| Value::Date(_)
|
|
@@ -561,6 +563,9 @@ fn is_truthy(value: &Value) -> bool {
|
|
|
561
563
|
}
|
|
562
564
|
Value::Bool(b) => *b,
|
|
563
565
|
Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
|
|
566
|
+
// Never empty by construction for `Str::Wtf8` (see `Str::is_empty`),
|
|
567
|
+
// so this is always `true` there — matching `bool("\udc80") ==
|
|
568
|
+
// True` in real Python.
|
|
564
569
|
Value::Str(s) => !s.is_empty(),
|
|
565
570
|
Value::Array(items) | Value::Tuple(items) => !items.is_empty(),
|
|
566
571
|
Value::Set(items) | Value::FrozenSet(items) => !items.is_empty(),
|
|
@@ -587,7 +592,9 @@ fn coerce_to_f64(value: &Value) -> Option<f64> {
|
|
|
587
592
|
| Value::Object(_) => None,
|
|
588
593
|
Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
|
|
589
594
|
Value::Number(n) => n.as_f64(),
|
|
590
|
-
|
|
595
|
+
// A `Str::Wtf8` (a lone surrogate present) is never a valid float
|
|
596
|
+
// literal, matching `float("\udc80")` raising in real Python.
|
|
597
|
+
Value::Str(s) => s.as_utf8().and_then(|s| s.trim().parse::<f64>().ok()),
|
|
591
598
|
}
|
|
592
599
|
}
|
|
593
600
|
|
|
@@ -642,7 +649,8 @@ fn coerce_to_i64(value: &Value) -> Option<i64> {
|
|
|
642
649
|
.or_else(|| n.as_u64().and_then(|u| i64::try_from(u).ok()))
|
|
643
650
|
}
|
|
644
651
|
}
|
|
645
|
-
|
|
652
|
+
// See `coerce_to_f64`'s doc for the `Str::Wtf8` case.
|
|
653
|
+
Value::Str(s) => s.as_utf8().and_then(|s| s.trim().parse::<i64>().ok()),
|
|
646
654
|
}
|
|
647
655
|
}
|
|
648
656
|
|
|
@@ -690,7 +698,13 @@ fn coerce_to_python_str(value: &Value) -> Option<String> {
|
|
|
690
698
|
n.as_u64().map(|u| u.to_string())
|
|
691
699
|
}
|
|
692
700
|
}
|
|
693
|
-
|
|
701
|
+
// `str(x)` is the identity for a value already a `str`, including
|
|
702
|
+
// one holding a lone surrogate — but this coercion only ever needs
|
|
703
|
+
// to build a `String`, which cannot hold one, so a `Str::Wtf8`
|
|
704
|
+
// falls through to `None` here (an accepted, narrow gap: it only
|
|
705
|
+
// costs an unnecessary type-change inclusion, never a wrong one —
|
|
706
|
+
// see this function's own doc).
|
|
707
|
+
Value::Str(s) => s.as_utf8().map(ToString::to_string),
|
|
694
708
|
}
|
|
695
709
|
}
|
|
696
710
|
|
|
@@ -731,7 +745,7 @@ enum DictKeyIdentity {
|
|
|
731
745
|
fn dict_key_identity(key: &ObjectKey) -> Option<DictKeyIdentity> {
|
|
732
746
|
match key {
|
|
733
747
|
ObjectKey::Str(s) => Some(DictKeyIdentity::Scalar(crate::lcs::ScalarKey::Str(
|
|
734
|
-
s.
|
|
748
|
+
s.as_bytes().to_vec(),
|
|
735
749
|
))),
|
|
736
750
|
ObjectKey::Other(value) => match value.as_ref() {
|
|
737
751
|
Value::Tuple(items) => items
|
|
@@ -831,20 +845,27 @@ pub(crate) fn is_below_threshold_to_diff_deeper(a: &Object, b: &Object) -> bool
|
|
|
831
845
|
matched.shared.len(),
|
|
832
846
|
)
|
|
833
847
|
} else {
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
// `as_str`
|
|
848
|
+
// Every key here is an `ObjectKey::Str`, so structural and
|
|
849
|
+
// python-equality matching coincide. Counted by WTF-8 bytes, not
|
|
850
|
+
// `ObjectKey::as_str` (which is `None` for a lone-surrogate key —
|
|
851
|
+
// `ObjectKey` has no `Hash` impl at all, see its own doc, so this
|
|
852
|
+
// is also the only way to put one in a `HashSet` here), so a
|
|
853
|
+
// surrogate key is counted correctly instead of silently dropped.
|
|
854
|
+
fn key_bytes(key: &ObjectKey) -> &[u8] {
|
|
855
|
+
match key {
|
|
856
|
+
ObjectKey::Str(s) => s.as_bytes(),
|
|
857
|
+
ObjectKey::Other(_) => {
|
|
858
|
+
unreachable!("has_non_str_keys() is false on both sides in this branch")
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
837
862
|
let union_len = a
|
|
838
863
|
.keys()
|
|
839
|
-
.
|
|
840
|
-
.chain(b.keys().
|
|
864
|
+
.map(key_bytes)
|
|
865
|
+
.chain(b.keys().map(key_bytes))
|
|
841
866
|
.collect::<HashSet<_>>()
|
|
842
867
|
.len();
|
|
843
|
-
let intersect_len = a
|
|
844
|
-
.keys()
|
|
845
|
-
.filter_map(ObjectKey::as_str)
|
|
846
|
-
.filter(|key| b.contains_key_str(key))
|
|
847
|
-
.count();
|
|
868
|
+
let intersect_len = a.keys().filter(|key| b.contains_key(key)).count();
|
|
848
869
|
(union_len, intersect_len)
|
|
849
870
|
};
|
|
850
871
|
#[allow(
|
|
@@ -890,13 +911,13 @@ pub(crate) fn count_object_diff_leaves(
|
|
|
890
911
|
let mut total = 0;
|
|
891
912
|
|
|
892
913
|
for (key, old_value) in a {
|
|
893
|
-
total += match
|
|
914
|
+
total += match b.get(key) {
|
|
894
915
|
None => item_length(old_value),
|
|
895
916
|
Some(new_value) => count_diff_leaves(old_value, new_value, depth + 1, opts, memo),
|
|
896
917
|
};
|
|
897
918
|
}
|
|
898
919
|
for (key, new_value) in b {
|
|
899
|
-
if
|
|
920
|
+
if !a.contains_key(key) {
|
|
900
921
|
total += item_length(new_value);
|
|
901
922
|
}
|
|
902
923
|
}
|
|
@@ -195,7 +195,10 @@ pub(crate) enum ItemKey {
|
|
|
195
195
|
/// fixed representative for *every* `NaN` regardless of its bits, since
|
|
196
196
|
/// `DeepHash` digests any `NaN` the same way (see that function's doc).
|
|
197
197
|
Float(u64),
|
|
198
|
-
Str
|
|
198
|
+
/// WTF-8 bytes (see `crate::value::Str`) rather than `String`, so a
|
|
199
|
+
/// lone surrogate still has a distinct, correct content key instead of
|
|
200
|
+
/// failing to compile or silently colliding with a different string.
|
|
201
|
+
Str(Vec<u8>),
|
|
199
202
|
/// A `datetime`, keyed by its instant with a naive value read as UTC —
|
|
200
203
|
/// `DeepHash._prep_datetime` runs `datetime_normalize` before formatting
|
|
201
204
|
/// its digest string, so a naive and an aware value at the same moment
|
|
@@ -257,7 +260,8 @@ pub(crate) enum ItemKey {
|
|
|
257
260
|
/// keyed too — a plain `String` cannot represent a non-`str` dict key
|
|
258
261
|
/// (`ItemKey` already covers every key kind this crate's dicts allow,
|
|
259
262
|
/// scalar or a `tuple` of scalars, via the same recursion [`item_key`]
|
|
260
|
-
/// runs on a value)
|
|
263
|
+
/// runs on a value) and, via [`ItemKey::Str`]'s own WTF-8 bytes, a
|
|
264
|
+
/// `str` key holding a lone surrogate code point too.
|
|
261
265
|
Dict(BTreeMap<ItemKey, ItemKey>),
|
|
262
266
|
}
|
|
263
267
|
|
|
@@ -667,7 +671,7 @@ fn scalar_content_key(value: &Value) -> ItemKey {
|
|
|
667
671
|
Value::Null => ItemKey::Null,
|
|
668
672
|
Value::Bool(b) => ItemKey::Bool(*b),
|
|
669
673
|
Value::Number(n) => number_key(n),
|
|
670
|
-
Value::Str(s) => ItemKey::Str(s.
|
|
674
|
+
Value::Str(s) => ItemKey::Str(s.as_bytes().to_vec()),
|
|
671
675
|
Value::DateTime(dt) => ItemKey::DateTime(dt.instant()),
|
|
672
676
|
Value::Date(date) => ItemKey::Date(date.ordinal()),
|
|
673
677
|
Value::Time(time) => ItemKey::Time(time.hash_seconds_of_day()),
|
|
@@ -763,7 +767,7 @@ fn keyed(value: &Value, memo: &IgnoreOrderMemo, want_part: bool) -> (ItemKey, Op
|
|
|
763
767
|
match value {
|
|
764
768
|
Value::Null => (ItemKey::Null, part()),
|
|
765
769
|
Value::Bool(b) => (ItemKey::Bool(*b), part()),
|
|
766
|
-
Value::Str(s) => (ItemKey::Str(s.
|
|
770
|
+
Value::Str(s) => (ItemKey::Str(s.as_bytes().to_vec()), part()),
|
|
767
771
|
Value::DateTime(value) => (ItemKey::DateTime(value.instant()), part()),
|
|
768
772
|
Value::Date(value) => (ItemKey::Date(value.ordinal()), part()),
|
|
769
773
|
Value::Time(value) => (ItemKey::Time(value.hash_seconds_of_day()), part()),
|
|
@@ -833,7 +837,7 @@ fn keyed(value: &Value, memo: &IgnoreOrderMemo, want_part: bool) -> (ItemKey, Op
|
|
|
833
837
|
/// inline match, for the unkeyed `DistKey` case) instead.
|
|
834
838
|
fn object_key_item_key(key: &crate::value::ObjectKey, memo: &IgnoreOrderMemo) -> ItemKey {
|
|
835
839
|
match key {
|
|
836
|
-
crate::value::ObjectKey::Str(s) => ItemKey::Str(s.
|
|
840
|
+
crate::value::ObjectKey::Str(s) => ItemKey::Str(s.as_bytes().to_vec()),
|
|
837
841
|
crate::value::ObjectKey::Other(value) => item_key(value, memo),
|
|
838
842
|
}
|
|
839
843
|
}
|
|
@@ -1865,7 +1865,7 @@ fn tuple_and_list_leaf_lengths_follow_python_equality() {
|
|
|
1865
1865
|
/// `serde_json` literal cannot express.
|
|
1866
1866
|
fn cobj_of(key: &str, value: CValue) -> CValue {
|
|
1867
1867
|
CValue::Object(crate::value::Object::from_pairs(vec![(
|
|
1868
|
-
ObjectKey::Str(std::sync::Arc::from(key)),
|
|
1868
|
+
ObjectKey::Str(crate::value::Key::Utf8(std::sync::Arc::from(key))),
|
|
1869
1869
|
value,
|
|
1870
1870
|
)]))
|
|
1871
1871
|
}
|
|
@@ -3166,7 +3166,7 @@ fn arb_cvalue() -> impl Strategy<Value = CValue> {
|
|
|
3166
3166
|
any::<bool>().prop_map(CValue::Bool),
|
|
3167
3167
|
any::<i64>().prop_map(|i| CValue::Number(crate::value::Number::from_i64(i))),
|
|
3168
3168
|
arb_float,
|
|
3169
|
-
"[a-z]{0,3}".prop_map(|s| CValue::Str(s.
|
|
3169
|
+
"[a-z]{0,3}".prop_map(|s| CValue::Str(s.into())),
|
|
3170
3170
|
arb_datetime,
|
|
3171
3171
|
arb_date,
|
|
3172
3172
|
];
|
|
@@ -87,7 +87,9 @@ use crate::value::Value;
|
|
|
87
87
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
88
88
|
pub(crate) enum ScalarKey {
|
|
89
89
|
Null,
|
|
90
|
-
Str
|
|
90
|
+
/// WTF-8 bytes (see `crate::value::Str`), so a lone surrogate still
|
|
91
|
+
/// compares correctly instead of failing to compile or colliding.
|
|
92
|
+
Str(Vec<u8>),
|
|
91
93
|
Int(i128),
|
|
92
94
|
/// Bit pattern of a non-integral (or too-large-to-be-exact) float —
|
|
93
95
|
/// hashed through [`mix_float_bits`]; see this type's hand-written `Hash`.
|
|
@@ -252,7 +254,7 @@ fn scalar_key(value: &Value) -> ScalarKey {
|
|
|
252
254
|
pub(crate) fn python_scalar_key(value: &Value) -> Option<ScalarKey> {
|
|
253
255
|
Some(match value {
|
|
254
256
|
Value::Null => ScalarKey::Null,
|
|
255
|
-
Value::Str(s) => ScalarKey::Str(s.
|
|
257
|
+
Value::Str(s) => ScalarKey::Str(s.as_bytes().to_vec()),
|
|
256
258
|
Value::Bool(b) => ScalarKey::Int(i128::from(*b)),
|
|
257
259
|
Value::Number(n) => {
|
|
258
260
|
if let Some(i) = n
|