deepdiff-rs 0.7.0__tar.gz → 0.7.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.7.0 → deepdiff_rs-0.7.1}/Cargo.lock +11 -4
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/Cargo.toml +1 -1
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/PKG-INFO +2 -2
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/README.md +1 -1
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/Cargo.toml +1 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/path.rs +125 -25
- deepdiff_rs-0.7.1/crates/onix-py/.python-version +1 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_sets.py +77 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/pyproject.toml +3 -5
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-arrow/Cargo.toml +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-arrow/examples/row_diff_rss.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-arrow/examples/type_stack_cost.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-arrow/src/error.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-arrow/src/lib.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-arrow/src/options.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-arrow/src/row_diff.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-arrow/src/schema.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-arrow/src/table_diff.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/datetime.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/datetime_tests.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/diff/array.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/diff/dispatch.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/diff/mod.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/diff/object.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/diff/options.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/diff/scalar.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/diff/set.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/diff/tests.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/error.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/ignore_order/distance.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/ignore_order/fxhash.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/ignore_order/hash.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/ignore_order/memo.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/ignore_order/mod.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/ignore_order/pairing.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/ignore_order/tests.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/lcs.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/lcs_tests.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/lib.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/report.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/report_tests.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/test_support.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/unified_diff.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/unified_diff_tests.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/value.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/src/value_tests.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/tests/golden.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/tests/ignore_order_memory.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/tests/memory_footprint.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/tests/proptest_diff.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/Cargo.toml +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/benchmarks/bench_bindings.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/src/arrow.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/src/convert.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/src/deepdiff.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/src/errors.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/src/fast_path.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/src/guard.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/src/lib.rs +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_bindings_memory.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_conversions.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_datetimes.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_depth_guard.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_differential_fuzz.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_golden_parity.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_signed_zero.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_smoke.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_suite_hygiene.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_table_diff.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_table_row_diff.py +0 -0
- {deepdiff_rs-0.7.0 → deepdiff_rs-0.7.1}/crates/onix-py/tests/test_tuples.py +0 -0
|
@@ -629,7 +629,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
629
629
|
|
|
630
630
|
[[package]]
|
|
631
631
|
name = "onix-arrow"
|
|
632
|
-
version = "0.7.
|
|
632
|
+
version = "0.7.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.7.
|
|
649
|
+
version = "0.7.1"
|
|
650
650
|
dependencies = [
|
|
651
651
|
"onix-core",
|
|
652
652
|
"serde_json",
|
|
@@ -654,17 +654,18 @@ dependencies = [
|
|
|
654
654
|
|
|
655
655
|
[[package]]
|
|
656
656
|
name = "onix-core"
|
|
657
|
-
version = "0.7.
|
|
657
|
+
version = "0.7.1"
|
|
658
658
|
dependencies = [
|
|
659
659
|
"proptest",
|
|
660
660
|
"serde",
|
|
661
661
|
"serde_json",
|
|
662
662
|
"stats_alloc",
|
|
663
|
+
"unicode-general-category",
|
|
663
664
|
]
|
|
664
665
|
|
|
665
666
|
[[package]]
|
|
666
667
|
name = "onix-py"
|
|
667
|
-
version = "0.7.
|
|
668
|
+
version = "0.7.1"
|
|
668
669
|
dependencies = [
|
|
669
670
|
"arrow-array",
|
|
670
671
|
"arrow-ipc",
|
|
@@ -1112,6 +1113,12 @@ version = "0.1.4"
|
|
|
1112
1113
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1113
1114
|
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
|
1114
1115
|
|
|
1116
|
+
[[package]]
|
|
1117
|
+
name = "unicode-general-category"
|
|
1118
|
+
version = "1.1.0"
|
|
1119
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1120
|
+
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
|
1121
|
+
|
|
1115
1122
|
[[package]]
|
|
1116
1123
|
name = "unicode-ident"
|
|
1117
1124
|
version = "1.0.24"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepdiff-rs
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Rust
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -160,7 +160,7 @@ Type comparison uses the full logical Arrow type (timestamp unit and timezone, d
|
|
|
160
160
|
- **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).
|
|
161
161
|
- **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.
|
|
162
162
|
- 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.
|
|
163
|
-
- A `str` inside a `tuple` or `frozenset` set item is
|
|
163
|
+
- 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.
|
|
164
164
|
- 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).
|
|
165
165
|
- `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).
|
|
166
166
|
- A `values_changed` between two multi-line strings runs a `difflib`-style `O(N*M)` line diff on the default path with no opt-out, worst when changes are spread evenly through the text (about 35 s for a heavily edited 1 MB string and growing quadratically, so a few megabytes is minutes), so bound the size of untrusted strings yourself. See [`crates/onix-core/src/unified_diff.rs`](crates/onix-core/src/unified_diff.rs).
|
|
@@ -142,7 +142,7 @@ Type comparison uses the full logical Arrow type (timestamp unit and timezone, d
|
|
|
142
142
|
- **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).
|
|
143
143
|
- **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.
|
|
144
144
|
- 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.
|
|
145
|
-
- A `str` inside a `tuple` or `frozenset` set item is
|
|
145
|
+
- 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.
|
|
146
146
|
- 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).
|
|
147
147
|
- `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).
|
|
148
148
|
- A `values_changed` between two multi-line strings runs a `difflib`-style `O(N*M)` line diff on the default path with no opt-out, worst when changes are spread evenly through the text (about 35 s for a heavily edited 1 MB string and growing quadratically, so a few megabytes is minutes), so bound the size of untrusted strings yourself. See [`crates/onix-core/src/unified_diff.rs`](crates/onix-core/src/unified_diff.rs).
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
use std::fmt::Write as _;
|
|
12
12
|
|
|
13
|
+
use unicode_general_category::{GeneralCategory, get_general_category};
|
|
14
|
+
|
|
13
15
|
use crate::datetime::{SECONDS_PER_DAY, div_rem_euclid};
|
|
14
16
|
use crate::value::{Number, Value};
|
|
15
17
|
|
|
@@ -209,20 +211,6 @@ pub fn set_item_repr(item: &Value) -> String {
|
|
|
209
211
|
/// [`Value`]'s own stack-safety posture: nothing bounds how deep a value a
|
|
210
212
|
/// caller may render, and a natively recursive renderer would be an
|
|
211
213
|
/// unguarded overflow sink on adversarially nested input.
|
|
212
|
-
///
|
|
213
|
-
/// # Divergence from Python: non-printable non-ASCII characters
|
|
214
|
-
///
|
|
215
|
-
/// Python's `repr()` escapes any character it considers non-printable (the
|
|
216
|
-
/// Unicode `Cc`/`Cf`/`Cs`/`Co`/`Cn`/`Zl`/`Zp` categories, plus `Zs` other
|
|
217
|
-
/// than a plain space) as `\xXX`/`\uXXXX`/`\UXXXXXXXX`. This port escapes
|
|
218
|
-
/// exactly those below `U+0100` (every ASCII control, `U+007F`–`U+00A0`,
|
|
219
|
-
/// and the soft hyphen `U+00AD` — the complete non-printable set in that
|
|
220
|
-
/// range) and passes everything above through literally, which is what
|
|
221
|
-
/// Python does for all *printable* text. Reproducing the rest would mean
|
|
222
|
-
/// carrying a Unicode category table for characters (zero-width joiners,
|
|
223
|
-
/// unassigned code points) that no realistic diff input holds; this is an
|
|
224
|
-
/// accepted, narrow, documented limitation, in the same class as
|
|
225
|
-
/// `crate::lcs`'s `2^53` numeric bound.
|
|
226
214
|
#[must_use]
|
|
227
215
|
pub fn python_repr(value: &Value) -> String {
|
|
228
216
|
let mut out = String::new();
|
|
@@ -387,8 +375,8 @@ fn push_sequence<'a>(
|
|
|
387
375
|
|
|
388
376
|
/// Python's `repr()` for a `str`: single quotes unless the string contains a
|
|
389
377
|
/// single quote and no double quote (then double quotes), with `\`, the
|
|
390
|
-
/// wrapping quote and
|
|
391
|
-
///
|
|
378
|
+
/// wrapping quote and every non-printable code point escaped as `\xXX`,
|
|
379
|
+
/// `\uXXXX` or `\UXXXXXXXX` per [`escape_non_printable`].
|
|
392
380
|
fn python_repr_str(s: &str) -> String {
|
|
393
381
|
let quote = if s.contains('\'') && !s.contains('"') {
|
|
394
382
|
'"'
|
|
@@ -408,10 +396,7 @@ fn python_repr_str(s: &str) -> String {
|
|
|
408
396
|
out.push('\\');
|
|
409
397
|
out.push(c);
|
|
410
398
|
}
|
|
411
|
-
c if
|
|
412
|
-
// Writing into a `String` is infallible.
|
|
413
|
-
let _ = write!(out, "\\x{:02x}", u32::from(c));
|
|
414
|
-
}
|
|
399
|
+
c if is_non_printable(c) => escape_non_printable(&mut out, c),
|
|
415
400
|
c => out.push(c),
|
|
416
401
|
}
|
|
417
402
|
}
|
|
@@ -437,10 +422,43 @@ fn number_repr(n: &Number) -> String {
|
|
|
437
422
|
.to_string()
|
|
438
423
|
}
|
|
439
424
|
|
|
440
|
-
///
|
|
441
|
-
///
|
|
442
|
-
|
|
443
|
-
|
|
425
|
+
/// Whether `c` is one of the code points Python's `repr()` escapes: every
|
|
426
|
+
/// character in Unicode general categories `Cc`, `Cf`, `Cs`, `Co`, `Cn`,
|
|
427
|
+
/// `Zl`, `Zp` or `Zs`, except the plain space (`U+0020`), which is `Zs` but
|
|
428
|
+
/// stays printable. This is `CPython`'s own rule
|
|
429
|
+
/// (`Tools/unicode/makeunicodedata.py`'s `PRINTABLE_MASK`, read from the
|
|
430
|
+
/// `unicode-general-category` table pinned to Unicode 16.0.0 — the same
|
|
431
|
+
/// version Python 3.14's `unicodedata` module ships).
|
|
432
|
+
fn is_non_printable(c: char) -> bool {
|
|
433
|
+
if c == ' ' {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
matches!(
|
|
437
|
+
get_general_category(c),
|
|
438
|
+
GeneralCategory::Control
|
|
439
|
+
| GeneralCategory::Format
|
|
440
|
+
| GeneralCategory::Surrogate
|
|
441
|
+
| GeneralCategory::PrivateUse
|
|
442
|
+
| GeneralCategory::Unassigned
|
|
443
|
+
| GeneralCategory::LineSeparator
|
|
444
|
+
| GeneralCategory::ParagraphSeparator
|
|
445
|
+
| GeneralCategory::SpaceSeparator
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/// Appends `c`'s `repr()` escape to `out`: `\xXX` below `U+0100`, `\uXXXX`
|
|
450
|
+
/// up to `U+FFFF`, `\UXXXXXXXX` above — the same three widths
|
|
451
|
+
/// `Objects/unicodeobject.c`'s `unicode_repr` picks by.
|
|
452
|
+
fn escape_non_printable(out: &mut String, c: char) {
|
|
453
|
+
let code_point = u32::from(c);
|
|
454
|
+
// Writing into a `String` is infallible.
|
|
455
|
+
let _ = if code_point < 0x100 {
|
|
456
|
+
write!(out, "\\x{code_point:02x}")
|
|
457
|
+
} else if code_point < 0x1_0000 {
|
|
458
|
+
write!(out, "\\u{code_point:04x}")
|
|
459
|
+
} else {
|
|
460
|
+
write!(out, "\\U{code_point:08x}")
|
|
461
|
+
};
|
|
444
462
|
}
|
|
445
463
|
|
|
446
464
|
/// Python's `repr()` for a `float`, which is `float_repr_style="short"`: the
|
|
@@ -525,7 +543,9 @@ fn python_float_repr(value: f64) -> String {
|
|
|
525
543
|
|
|
526
544
|
#[cfg(test)]
|
|
527
545
|
mod tests {
|
|
528
|
-
use super::{
|
|
546
|
+
use super::{
|
|
547
|
+
PathSegment, escape_non_printable, python_repr, quote_key, render_path, set_item_repr,
|
|
548
|
+
};
|
|
529
549
|
use crate::test_support::{cdate, cdt_at};
|
|
530
550
|
use crate::value::{Builder, Number, SetItems, Value};
|
|
531
551
|
|
|
@@ -813,6 +833,86 @@ mod tests {
|
|
|
813
833
|
}
|
|
814
834
|
}
|
|
815
835
|
|
|
836
|
+
/// Every expectation here is real Python `repr()` output, verified
|
|
837
|
+
/// against `CPython` 3.14 (Unicode 16.0.0).
|
|
838
|
+
#[test]
|
|
839
|
+
fn python_repr_str_escapes_non_printable_code_points_above_u0100() {
|
|
840
|
+
let cases = [
|
|
841
|
+
// U+00FF: printable Latin-1, the code point right after the old
|
|
842
|
+
// below-U+0100 ceiling — left bare, not escaped.
|
|
843
|
+
("a\u{ff}b", "'a\u{ff}b'"),
|
|
844
|
+
// U+200B: Cf (zero width space) — \uXXXX width.
|
|
845
|
+
("a\u{200b}b", r"'a\u200bb'"),
|
|
846
|
+
// U+2028: Zl (line separator) — \uXXXX width.
|
|
847
|
+
("a\u{2028}b", r"'a\u2028b'"),
|
|
848
|
+
// U+E000: Co (private use) — \uXXXX width.
|
|
849
|
+
("a\u{e000}b", r"'a\ue000b'"),
|
|
850
|
+
// U+0378: Cn (unassigned in Unicode 16.0.0) — \uXXXX width.
|
|
851
|
+
("a\u{378}b", r"'a\u0378b'"),
|
|
852
|
+
// U+FFFF: Cn (a BMP noncharacter) — the top of the \uXXXX width.
|
|
853
|
+
("a\u{ffff}b", r"'a\uffffb'"),
|
|
854
|
+
// U+10000: Lo, printable astral text — left bare.
|
|
855
|
+
("a\u{10000}b", "'a\u{10000}b'"),
|
|
856
|
+
// U+1F600: So, a printable astral emoji — left bare.
|
|
857
|
+
("a\u{1f600}b", "'a\u{1f600}b'"),
|
|
858
|
+
// U+F0000: Co (a supplementary private-use plane) — \UXXXXXXXX
|
|
859
|
+
// width, the one the old port could not reach at all.
|
|
860
|
+
("a\u{f0000}b", r"'a\U000f0000b'"),
|
|
861
|
+
// U+10FFFF: Cn, the last valid Unicode scalar value.
|
|
862
|
+
("a\u{10ffff}b", r"'a\U0010ffffb'"),
|
|
863
|
+
];
|
|
864
|
+
for (input, expected) in cases {
|
|
865
|
+
let item = Value::Tuple(Box::new([Value::Str(input.into())]));
|
|
866
|
+
assert_eq!(
|
|
867
|
+
python_repr(&item),
|
|
868
|
+
format!("({expected},)"),
|
|
869
|
+
"for {input:?}"
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/// The plain space (`U+0020`) is `Zs` like every other escaped space
|
|
875
|
+
/// separator, but Python's own rule carves it out as printable — the
|
|
876
|
+
/// one exception `is_non_printable` must apply.
|
|
877
|
+
#[test]
|
|
878
|
+
fn python_repr_str_does_not_escape_plain_space() {
|
|
879
|
+
let item = Value::Tuple(Box::new([Value::Str("a b".into())]));
|
|
880
|
+
assert_eq!(python_repr(&item), "('a b',)");
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/// Exercises `escape_non_printable` directly, at the two code points
|
|
884
|
+
/// each escape width's `<` comparison must reject vs. accept: `U+00FF`
|
|
885
|
+
/// stays `\xXX`, `U+0100` switches to `\uXXXX`; `U+FFFF` stays `\uXXXX`,
|
|
886
|
+
/// `U+10000` switches to `\UXXXXXXXX`.
|
|
887
|
+
#[test]
|
|
888
|
+
fn escape_non_printable_widths_switch_exactly_at_their_boundaries() {
|
|
889
|
+
let cases = [
|
|
890
|
+
(0xff, "\\xff"),
|
|
891
|
+
(0x100, "\\u0100"),
|
|
892
|
+
(0xffff, "\\uffff"),
|
|
893
|
+
(0x1_0000, "\\U00010000"),
|
|
894
|
+
];
|
|
895
|
+
for (code_point, expected) in cases {
|
|
896
|
+
let mut out = String::new();
|
|
897
|
+
escape_non_printable(
|
|
898
|
+
&mut out,
|
|
899
|
+
char::from_u32(code_point).expect("valid code point"),
|
|
900
|
+
);
|
|
901
|
+
assert_eq!(out, expected, "for U+{code_point:04X}");
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/// Fails `make check` if a `unicode-general-category` bump ever changes
|
|
906
|
+
/// the table `is_non_printable` reads, so a Unicode-version drift is
|
|
907
|
+
/// caught here rather than only by the Python bindings' own
|
|
908
|
+
/// interpreter-version-gated differential test (`test_sets.py`'s
|
|
909
|
+
/// `test_bmp_printability_table_matches_the_running_interpreter_on_3_14`),
|
|
910
|
+
/// which needs a Python 3.14 interpreter to run at all.
|
|
911
|
+
#[test]
|
|
912
|
+
fn unicode_general_category_stays_pinned_to_16_0_0() {
|
|
913
|
+
assert_eq!(unicode_general_category::UNICODE_VERSION, (16, 0, 0));
|
|
914
|
+
}
|
|
915
|
+
|
|
816
916
|
/// Python's `repr()` for a calendar value — the form a container holding
|
|
817
917
|
/// one shows, and the form issue #21 will need once a calendar value may
|
|
818
918
|
/// be a set member. Every expectation here is real Python `repr()`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -18,6 +18,8 @@ import json
|
|
|
18
18
|
import math
|
|
19
19
|
import random
|
|
20
20
|
import struct
|
|
21
|
+
import sys
|
|
22
|
+
import unicodedata
|
|
21
23
|
from typing import Final
|
|
22
24
|
|
|
23
25
|
import pytest
|
|
@@ -30,6 +32,26 @@ from deepdiff_rs import DeepDiff, diff_json
|
|
|
30
32
|
FLOAT_CASE_COUNT: Final[int] = 1_000_000
|
|
31
33
|
FLOAT_BATCH_SIZE: Final[int] = 5_000
|
|
32
34
|
|
|
35
|
+
# How many BMP code points are rendered per diff call in the str-repr
|
|
36
|
+
# differential below (one call per batch keeps the full sweep fast).
|
|
37
|
+
BMP_BATCH_SIZE: Final[int] = 2_000
|
|
38
|
+
|
|
39
|
+
# Beyond-BMP sample: one code point per category needing the `\UXXXXXXXX`
|
|
40
|
+
# escape width, plus three printable ones left bare.
|
|
41
|
+
SUPPLEMENTARY_SAMPLE: Final[list[int]] = [
|
|
42
|
+
0x10000, # Lo, printable
|
|
43
|
+
0x1F600, # So, printable (emoji)
|
|
44
|
+
0x30000, # Lo, printable
|
|
45
|
+
0xE01EF, # Mn, printable (combining, astral)
|
|
46
|
+
0xE0001, # Cf, non-printable
|
|
47
|
+
0xF0000, # Co, non-printable (private-use plane)
|
|
48
|
+
0x10FFFD, # Co, non-printable (private-use plane)
|
|
49
|
+
0x2FFFE, # Cn, non-printable (unassigned noncharacter)
|
|
50
|
+
0x3FFFD, # Cn, non-printable (unassigned)
|
|
51
|
+
0x40000, # Cn, non-printable (unassigned)
|
|
52
|
+
0x10FFFF, # Cn, non-printable (last valid code point)
|
|
53
|
+
]
|
|
54
|
+
|
|
33
55
|
|
|
34
56
|
def test_set_items_are_reported_as_added_and_removed_paths() -> None:
|
|
35
57
|
"""The two set categories are lists of path strings, sorted by onix."""
|
|
@@ -204,6 +226,61 @@ def test_float_set_item_paths_match_python_repr_over_a_million_bit_patterns() ->
|
|
|
204
226
|
)
|
|
205
227
|
|
|
206
228
|
|
|
229
|
+
def _rendered_tuple_str_path(s: str) -> str:
|
|
230
|
+
"""
|
|
231
|
+
Render one `(s,)` tuple set item's path.
|
|
232
|
+
|
|
233
|
+
:param s: A single-character string.
|
|
234
|
+
:return: The rendered ``set_item_removed`` entry.
|
|
235
|
+
"""
|
|
236
|
+
entries = DeepDiff({(s,)}, {("sentinel",)}).to_dict()["set_item_removed"]
|
|
237
|
+
assert len(entries) == 1, entries
|
|
238
|
+
return entries[0]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _assert_batch_matches_python_repr(code_points: list[int]) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Diff one batch of one-element `(chr(cp),)` tuple set items against real Python `repr()`.
|
|
244
|
+
|
|
245
|
+
:param code_points: The batch to check, distinct code points.
|
|
246
|
+
"""
|
|
247
|
+
strings = [chr(cp) for cp in code_points]
|
|
248
|
+
items = {(s,) for s in strings}
|
|
249
|
+
rendered = set(DeepDiff(items, {("sentinel",)}).to_dict()["set_item_removed"])
|
|
250
|
+
expected = {f"root[{(s,)!r}]" for s in strings}
|
|
251
|
+
if rendered == expected:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
for cp in code_points:
|
|
255
|
+
s = chr(cp)
|
|
256
|
+
want = f"root[{(s,)!r}]"
|
|
257
|
+
actual = _rendered_tuple_str_path(s)
|
|
258
|
+
assert actual == want, f"U+{cp:04X} rendered differently: expected {want!r}, got {actual!r}"
|
|
259
|
+
pytest.fail("batch-level mismatch, but no single code point in it diverged")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_bmp_printability_table_matches_the_running_interpreter_on_3_14() -> None:
|
|
263
|
+
"""Pins the assumption the sweep below relies on: 3.14 must ship Unicode 16.0.0."""
|
|
264
|
+
if sys.version_info[:2] != (3, 14):
|
|
265
|
+
pytest.skip("only meaningful on the crate's pinned reference interpreter, 3.14")
|
|
266
|
+
assert unicodedata.unidata_version == "16.0.0"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_str_inside_tuple_matches_python_repr_over_the_full_bmp() -> None:
|
|
270
|
+
"""Every BMP code point escapes exactly like Python `repr()`, against Unicode 16.0.0."""
|
|
271
|
+
if unicodedata.unidata_version != "16.0.0":
|
|
272
|
+
pytest.skip("valid only against Unicode 16.0.0 (Python 3.14); see tests/golden/README.md")
|
|
273
|
+
|
|
274
|
+
code_points = [cp for cp in range(0x10000) if not 0xD800 <= cp <= 0xDFFF]
|
|
275
|
+
for start in range(0, len(code_points), BMP_BATCH_SIZE):
|
|
276
|
+
_assert_batch_matches_python_repr(code_points[start : start + BMP_BATCH_SIZE])
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_str_inside_tuple_matches_python_repr_beyond_the_bmp() -> None:
|
|
280
|
+
"""The `\\UXXXXXXXX` escape width, and printable astral text left bare."""
|
|
281
|
+
_assert_batch_matches_python_repr(SUPPLEMENTARY_SAMPLE)
|
|
282
|
+
|
|
283
|
+
|
|
207
284
|
def test_a_set_holding_an_unhashable_subclass_is_refused() -> None:
|
|
208
285
|
"""A list subclass that defines __hash__ can be a set member; onix refuses it by path."""
|
|
209
286
|
|
|
@@ -39,11 +39,9 @@ Repository = "https://github.com/ksco92/onix"
|
|
|
39
39
|
#
|
|
40
40
|
# `deepdiff==9.1.0` itself requires Python >=3.10, tighter than this
|
|
41
41
|
# package's own `requires-python` (>=3.9) — the marker below keeps `uv
|
|
42
|
-
# sync`'s universal (all-supported-versions) resolution satisfiable.
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# version): `uv sync` resolves whatever Python >=3.10 it finds/manages, and
|
|
46
|
-
# the abi3 wheel this crate builds works identically across all of them.
|
|
42
|
+
# sync`'s universal (all-supported-versions) resolution satisfiable. The
|
|
43
|
+
# test venv itself IS pinned, via this directory's `.python-version`:
|
|
44
|
+
# Pinned to 3.14, Unicode 16.0.0; see tests/golden/README.md, Pinned versions.
|
|
47
45
|
# polars and duckdb are here (test group) only, never as runtime deps: the
|
|
48
46
|
# Arrow table-diff tests feed the same data through pyarrow, polars, and
|
|
49
47
|
# DuckDB and assert identical results, which needs all three installed to run
|
|
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
|
|
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
|