deepdiff-rs 0.3.1__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/Cargo.lock +3 -3
  2. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/Cargo.toml +1 -1
  3. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/PKG-INFO +32 -21
  4. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/README.md +31 -20
  5. deepdiff_rs-0.4.1/crates/onix-core/src/datetime.rs +437 -0
  6. deepdiff_rs-0.4.1/crates/onix-core/src/datetime_tests.rs +334 -0
  7. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/array.rs +38 -13
  8. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/dispatch.rs +30 -6
  9. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/mod.rs +4 -1
  10. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/object.rs +2 -2
  11. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/options.rs +3 -3
  12. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/scalar.rs +60 -1
  13. deepdiff_rs-0.4.1/crates/onix-core/src/diff/set.rs +77 -0
  14. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/diff/tests.rs +717 -2
  15. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/error.rs +39 -0
  16. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/distance.rs +297 -24
  17. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/fxhash.rs +38 -26
  18. deepdiff_rs-0.4.1/crates/onix-core/src/ignore_order/hash.rs +716 -0
  19. deepdiff_rs-0.4.1/crates/onix-core/src/ignore_order/memo.rs +292 -0
  20. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/mod.rs +7 -4
  21. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/pairing.rs +3 -3
  22. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/ignore_order/tests.rs +1285 -9
  23. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/lcs.rs +94 -15
  24. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/lcs_tests.rs +71 -1
  25. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/lib.rs +2 -0
  26. deepdiff_rs-0.4.1/crates/onix-core/src/path.rs +928 -0
  27. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/report.rs +280 -29
  28. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/report_tests.rs +227 -0
  29. deepdiff_rs-0.4.1/crates/onix-core/src/test_support.rs +87 -0
  30. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/value.rs +370 -12
  31. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/src/value_tests.rs +428 -2
  32. deepdiff_rs-0.4.1/crates/onix-core/tests/golden.rs +424 -0
  33. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/benchmarks/bench_bindings.py +121 -4
  34. deepdiff_rs-0.4.1/crates/onix-py/src/convert.rs +967 -0
  35. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/src/deepdiff.rs +49 -36
  36. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/src/errors.rs +10 -4
  37. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/src/fast_path.rs +4 -8
  38. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/src/guard.rs +52 -92
  39. deepdiff_rs-0.4.1/crates/onix-py/tests/test_conversions.py +296 -0
  40. deepdiff_rs-0.4.1/crates/onix-py/tests/test_datetimes.py +222 -0
  41. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_depth_guard.py +244 -4
  42. deepdiff_rs-0.4.1/crates/onix-py/tests/test_differential_fuzz.py +1294 -0
  43. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_golden_parity.py +29 -8
  44. deepdiff_rs-0.4.1/crates/onix-py/tests/test_sets.py +640 -0
  45. deepdiff_rs-0.4.1/crates/onix-py/tests/test_suite_hygiene.py +39 -0
  46. deepdiff_rs-0.4.1/crates/onix-py/tests/test_tuples.py +155 -0
  47. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/pyproject.toml +6 -0
  48. deepdiff_rs-0.3.1/crates/onix-core/src/ignore_order/hash.rs +0 -165
  49. deepdiff_rs-0.3.1/crates/onix-core/src/ignore_order/memo.rs +0 -105
  50. deepdiff_rs-0.3.1/crates/onix-core/src/path.rs +0 -241
  51. deepdiff_rs-0.3.1/crates/onix-core/src/test_support.rs +0 -40
  52. deepdiff_rs-0.3.1/crates/onix-core/tests/golden.rs +0 -209
  53. deepdiff_rs-0.3.1/crates/onix-py/src/convert.rs +0 -545
  54. deepdiff_rs-0.3.1/crates/onix-py/tests/test_conversions.py +0 -144
  55. deepdiff_rs-0.3.1/crates/onix-py/tests/test_differential_fuzz.py +0 -147
  56. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/Cargo.toml +0 -0
  57. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
  58. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/tests/memory_footprint.rs +0 -0
  59. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/tests/proptest_diff.rs +0 -0
  60. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
  61. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/Cargo.toml +0 -0
  62. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/src/lib.rs +0 -0
  63. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_bindings_memory.py +0 -0
  64. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_signed_zero.py +0 -0
  65. {deepdiff_rs-0.3.1 → deepdiff_rs-0.4.1}/crates/onix-py/tests/test_smoke.py +0 -0
@@ -127,7 +127,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
127
127
 
128
128
  [[package]]
129
129
  name = "onix-cli"
130
- version = "0.3.1"
130
+ version = "0.4.1"
131
131
  dependencies = [
132
132
  "onix-core",
133
133
  "serde_json",
@@ -135,7 +135,7 @@ dependencies = [
135
135
 
136
136
  [[package]]
137
137
  name = "onix-core"
138
- version = "0.3.1"
138
+ version = "0.4.1"
139
139
  dependencies = [
140
140
  "proptest",
141
141
  "serde",
@@ -145,7 +145,7 @@ dependencies = [
145
145
 
146
146
  [[package]]
147
147
  name = "onix-py"
148
- version = "0.3.1"
148
+ version = "0.4.1"
149
149
  dependencies = [
150
150
  "onix-core",
151
151
  "pyo3",
@@ -3,7 +3,7 @@ resolver = "3"
3
3
  members = ["crates/onix-core", "crates/onix-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.1"
6
+ version = "0.4.1"
7
7
  edition = "2024"
8
8
  license = "MIT"
9
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepdiff-rs
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -114,34 +114,45 @@ Pass `--ignore-order` to compare every list by value instead of by position, mir
114
114
  ## Known limitations
115
115
 
116
116
  - Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
117
- - Supported value types are `None`, `bool`, `int`, `float`, `str`, `dict` (with `str` keys), and `list`; `int`s must fit in `i64`/`u64`, `float`s must be finite, and anything else (tuple, set, date, custom object, non-`str` dict key) raises `TypeError` or `ValueError` naming the exact path it was found at. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs).
117
+ - Supported value types are `None`, `bool`, `int`, `float`, `str`, `dict` (with `str` keys), `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, and `datetime.date`; 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. `int`s must fit in `i64`/`u64`, `float`s must be finite, and anything else — `time`, `timedelta`, a non-`str` dict key, a custom object, an arbitrary-precision `int`, or a non-finite `float` — raises `TypeError`/`ValueError` naming the exact path it was found at. The **Datetimes** and **Sets** bullets below cover the deliberate divergences for those two types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
118
+ - A subclass of a supported type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date` subclass such as pandas' `Timestamp`) raises `TypeError` rather than being diffed as its base type, because DeepDiff reports each value's own type name. A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. Both are described in [`tests/golden/README.md`](tests/golden/README.md).
119
+ - **Datetimes** compare by instant, with a naive value read as UTC, matching DeepDiff. A changed pair is reported normalized to UTC (`to_json()` renders `...+00:00`, `to_dict()` returns UTC-aware `datetime`s); everywhere else a datetime keeps its raw value. Three deliberate departures: `to_json()` renders a `date` as `YYYY-MM-DD` where DeepDiff's own `to_json()` raises `TypeError` (a documented superset); a `zoneinfo`/`pytz` tzinfo comes back from `to_dict()` as a fixed-offset `datetime.timezone` carrying the offset it was in force at, not the original zone object; and a set holding both a naive and an aware value at one instant reports both as members, where DeepDiff's own digest cache can report only one (see [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs)). Comparing two datetimes whose UTC form would leave year 1..=9999 raises `ValueError` naming the path, where DeepDiff raises `OverflowError`; under `ignore_order` DeepDiff's hasher normalizes every datetime and so raises for such a value even when it is only added, removed, or shuffled, where onix hashes by instant and reports it normally (see [`tests/golden/README.md`](tests/golden/README.md)). `truncate_datetime`, `time` and `timedelta` are not supported. The normalized-versus-raw split is documented in [`tests/golden/README.md`](tests/golden/README.md).
120
+ - **Sets** are diffed deterministically, where DeepDiff's own answers depend on the order the running process happens to iterate a set in (hash order, and `PYTHONHASHSEED`-dependent for `str` members) or on how its digest cache/computation handles a tuple, frozenset, or calendar member independently of Python's own `==`. Each consequence — entry order, which member of an equality class is reported, set-versus-sequence coercion, and a tuple/frozenset member's own (positional, not order-/repetition-insensitive) matching rule — is shown with both tools' output in [`tests/golden/README.md`](tests/golden/README.md)'s "Set iteration order" section. A report holding a `frozenset` value also serializes to JSON here, where DeepDiff's own `to_json()` raises `TypeError` — a superset, not a difference in the findings.
121
+ - A `str` containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) raises `ValueError` naming the exact path on either side, before the two values are ever compared — including a pair DeepDiff would call equal and report as no change, since DeepDiff's scalar equality is plain Python `==` and never hits the encoding problem; DeepDiff does report a plain change for a *differing* pair, and crashes with an unhandled `UnicodeEncodeError` if such a string is ever hashed (a `set`/`frozenset` member). See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
122
+ - A `str` inside a `tuple` or `frozenset` set item is rendered with Python's `repr()`, which escapes every non-printable character; onix escapes those below `U+0100` (the complete set in that range) and passes higher non-printable code points through literally, since escaping them would mean carrying a Unicode category table. Exact for all of ASCII and all printable text. See [`crates/onix-core/src/path.rs`](crates/onix-core/src/path.rs).
118
123
  - Adversarially deep input raises `MaxDepthError` instead of crashing: the default `max_depth` is 512 and the hard ceiling is `MAX_DEPTH_CEILING` (20,000). See [`crates/onix-py/src/guard.rs`](crates/onix-py/src/guard.rs).
119
124
  - `ignore_order` pairing is `O(N^2)` in unpaired elements per side and carries a polynomial cost in both time and memory with input depth; it has no `max_passes`/`max_diffs` cutoff, so bound the size and depth of untrusted input yourself. See [`crates/onix-core/src/ignore_order/mod.rs`](crates/onix-core/src/ignore_order/mod.rs).
120
- - Output is byte-identical to DeepDiff except for one boundary case: integers past `2^53` (the limit of exact `f64` representation) inside ordered scalar lists. See [`tests/golden/README.md`](tests/golden/README.md).
125
+ - Output is byte-identical to DeepDiff except for the cases listed above and two path-rendering quirks; [`tests/golden/README.md`](tests/golden/README.md) enumerates every accepted exception, including integers past `2^53` (the limit of exact `f64` representation) inside ordered scalar lists and `ignore_order` pairing among naive datetimes, which DeepDiff ranks using the *process's local timezone* while onix reads a naive value as UTC everywhere.
121
126
 
122
127
  ## Performance
123
128
 
124
129
  Two committed, regenerable reports back the numbers below; every figure here is copied verbatim from them.
125
130
 
126
- The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side):
131
+ The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side, run on 2026-09-04):
127
132
 
128
133
  | Shape | deepdiff | deepdiff_rs | Speedup |
129
134
  | --- | --- | --- | --- |
130
- | `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) | 3128.94ms | 82.57ms | **37.89x** |
131
- |   peak RSS | 228.0 MB | 141.2 MB | **1.61x** |
132
- |   CPU seconds | 3.128 s | 0.083 s | **37.88x** |
133
- | Heterogeneous API-payload records, n=20,000 (live objects) | 3466.52ms | 149.18ms | **23.24x** |
134
- |   peak RSS | 117.5 MB | 148.3 MB | **0.79x** |
135
- |   CPU seconds | 3.465 s | 0.149 s | **23.24x** |
136
- | Same `ignore_order` shape, via `diff_json` (JSON-string path) | 3133.45ms | 83.16ms | **37.68x** |
137
- |   peak RSS | 228.9 MB | 141.7 MB | **1.62x** |
138
- |   CPU seconds | 3.131 s | 0.083 s | **37.68x** |
139
- | Same API-payload shape, via `diff_json` (JSON-string path) | 4568.33ms | 86.33ms | **52.92x** |
140
- |   peak RSS | 139.0 MB | 141.7 MB | **0.98x** |
141
- |   CPU seconds | 4.566 s | 0.086 s | **52.90x** |
142
- | Same API-payload shape, both tools reading two JSON files from disk | 4571.28ms | 85.35ms | **53.56x** |
143
- |   peak RSS | 139.0 MB | 141.8 MB | **0.98x** |
144
- |   CPU seconds | 4.569 s | 0.085 s | **53.54x** |
135
+ | `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) | 3164.29ms | 89.08ms | **35.52x** |
136
+ |   peak RSS | 228.4 MB | 141.5 MB | **1.61x** |
137
+ |   CPU seconds | 3.162 s | 0.089 s | **35.50x** |
138
+ | Heterogeneous API-payload records, n=20,000 (live objects) | 3451.01ms | 151.84ms | **22.73x** |
139
+ |   peak RSS | 118.1 MB | 147.7 MB | **0.80x** |
140
+ |   CPU seconds | 3.449 s | 0.152 s | **22.73x** |
141
+ | Typed records (datetime/tuple/set fields), n=10,000 (live objects) | 792.96ms | 47.87ms | **16.57x** |
142
+ |   peak RSS | 60.2 MB | 62.1 MB | **0.97x** |
143
+ |   CPU seconds | 0.792 s | 0.048 s | **16.56x** |
144
+ | Same typed-records shape, `ignore_order` (live objects) | 60483.86ms | 1272.04ms | **47.55x** |
145
+ |   peak RSS | 112.1 MB | 1005.6 MB | **0.11x** |
146
+ |   CPU seconds | 60.452 s | 1.271 s | **47.55x** |
147
+ | Same `ignore_order` shape, via `diff_json` (JSON-string path) | 3168.05ms | 86.95ms | **36.44x** |
148
+ |   peak RSS | 228.7 MB | 142.3 MB | **1.61x** |
149
+ |   CPU seconds | 3.166 s | 0.087 s | **36.46x** |
150
+ | Same API-payload shape, via `diff_json` (JSON-string path) | 4568.00ms | 85.37ms | **53.51x** |
151
+ |   peak RSS | 139.5 MB | 140.9 MB | **0.99x** |
152
+ |   CPU seconds | 4.566 s | 0.085 s | **53.50x** |
153
+ | Same API-payload shape, both tools reading two JSON files from disk | 4565.46ms | 89.43ms | **51.05x** |
154
+ |   peak RSS | 139.5 MB | 141.0 MB | **0.99x** |
155
+ |   CPU seconds | 4.564 s | 0.089 s | **51.03x** |
145
156
 
146
157
  The engine's own diff-only time and peak resident memory against pinned `deepdiff` 9.1.0 (source: [`perf/RESULTS.md`](perf/RESULTS.md), same machine, median over tier-appropriate runs, diff time excluding process startup and JSON parsing on both sides):
147
158
 
@@ -164,14 +175,14 @@ Both reports carry their full methodology, fairness rules, and the reproduce com
164
175
 
165
176
  **Python API.** The public surface is `DeepDiff`, `diff_json`, `MaxDepthError`, and `MAX_DEPTH_CEILING`.
166
177
 
167
- - `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects; `.to_json()` returns the DeepDiff-compatible JSON string, `.to_dict()` the same report as a dict, and the instance is falsy when there is no difference.
178
+ - `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects of supported value types — `None`, `bool`, `int`, `float`, `str`, `dict` (with `str` keys), `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, and `datetime.date` (see [Known limitations](#known-limitations) for the exact restrictions and exclusions); `.to_json()` returns the DeepDiff-compatible JSON string, `.to_dict()` the same report as a dict — with Python types preserved, so a value the diff found in a `tuple`, `set` or `frozenset` comes back as one and a `datetime`/`date` comes back as a real `datetime`/`date` — and the instance is falsy when there is no difference. The `set_item_added`/`set_item_removed` categories are lists of path strings, each ending in the item itself (`root['a'][2]`, `root['x']`, `root[(1, 2)]`).
168
179
  - `diff_json(a, b, ignore_order=False, max_depth=None) -> str`: diffs two JSON strings entirely in Rust and returns the report as a JSON string.
169
180
  - `MaxDepthError` (a `ValueError` subclass) is raised when input exceeds `max_depth`; `MAX_DEPTH_CEILING` (20,000) is the hard upper bound on `max_depth`.
170
181
 
171
182
  **CLI.** `onix diff <a.json> <b.json> [--max-depth N] [--ignore-order] [--timing]` reads both files as JSON and prints a compact, single-line DeepDiff-compatible report to stdout (`{}` when there is no difference).
172
183
 
173
184
  - `--max-depth N` overrides the recursion-depth bound (default: the `ONIX_MAX_DEPTH` environment variable if set, else 512).
174
- - `--ignore-order` compares every list/tuple by hash-based matching instead of by position, mirroring `DeepDiff(..., ignore_order=True)`.
185
+ - `--ignore-order` compares every list by hash-based matching instead of by position, mirroring `DeepDiff(..., ignore_order=True)`.
175
186
  - `--timing` prints one line of JSON (`{"parse_ns": N, "diff_ns": N}`) to stderr.
176
187
 
177
188
  Exit codes:
@@ -98,34 +98,45 @@ Pass `--ignore-order` to compare every list by value instead of by position, mir
98
98
  ## Known limitations
99
99
 
100
100
  - Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
101
- - Supported value types are `None`, `bool`, `int`, `float`, `str`, `dict` (with `str` keys), and `list`; `int`s must fit in `i64`/`u64`, `float`s must be finite, and anything else (tuple, set, date, custom object, non-`str` dict key) raises `TypeError` or `ValueError` naming the exact path it was found at. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs).
101
+ - Supported value types are `None`, `bool`, `int`, `float`, `str`, `dict` (with `str` keys), `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, and `datetime.date`; 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. `int`s must fit in `i64`/`u64`, `float`s must be finite, and anything else — `time`, `timedelta`, a non-`str` dict key, a custom object, an arbitrary-precision `int`, or a non-finite `float` — raises `TypeError`/`ValueError` naming the exact path it was found at. The **Datetimes** and **Sets** bullets below cover the deliberate divergences for those two types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
102
+ - A subclass of a supported type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date` subclass such as pandas' `Timestamp`) raises `TypeError` rather than being diffed as its base type, because DeepDiff reports each value's own type name. A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. Both are described in [`tests/golden/README.md`](tests/golden/README.md).
103
+ - **Datetimes** compare by instant, with a naive value read as UTC, matching DeepDiff. A changed pair is reported normalized to UTC (`to_json()` renders `...+00:00`, `to_dict()` returns UTC-aware `datetime`s); everywhere else a datetime keeps its raw value. Three deliberate departures: `to_json()` renders a `date` as `YYYY-MM-DD` where DeepDiff's own `to_json()` raises `TypeError` (a documented superset); a `zoneinfo`/`pytz` tzinfo comes back from `to_dict()` as a fixed-offset `datetime.timezone` carrying the offset it was in force at, not the original zone object; and a set holding both a naive and an aware value at one instant reports both as members, where DeepDiff's own digest cache can report only one (see [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs)). Comparing two datetimes whose UTC form would leave year 1..=9999 raises `ValueError` naming the path, where DeepDiff raises `OverflowError`; under `ignore_order` DeepDiff's hasher normalizes every datetime and so raises for such a value even when it is only added, removed, or shuffled, where onix hashes by instant and reports it normally (see [`tests/golden/README.md`](tests/golden/README.md)). `truncate_datetime`, `time` and `timedelta` are not supported. The normalized-versus-raw split is documented in [`tests/golden/README.md`](tests/golden/README.md).
104
+ - **Sets** are diffed deterministically, where DeepDiff's own answers depend on the order the running process happens to iterate a set in (hash order, and `PYTHONHASHSEED`-dependent for `str` members) or on how its digest cache/computation handles a tuple, frozenset, or calendar member independently of Python's own `==`. Each consequence — entry order, which member of an equality class is reported, set-versus-sequence coercion, and a tuple/frozenset member's own (positional, not order-/repetition-insensitive) matching rule — is shown with both tools' output in [`tests/golden/README.md`](tests/golden/README.md)'s "Set iteration order" section. A report holding a `frozenset` value also serializes to JSON here, where DeepDiff's own `to_json()` raises `TypeError` — a superset, not a difference in the findings.
105
+ - A `str` containing a lone (unpaired) surrogate code point (e.g. `'\udc80'`, legal in Python but not encodable as UTF-8) raises `ValueError` naming the exact path on either side, before the two values are ever compared — including a pair DeepDiff would call equal and report as no change, since DeepDiff's scalar equality is plain Python `==` and never hits the encoding problem; DeepDiff does report a plain change for a *differing* pair, and crashes with an unhandled `UnicodeEncodeError` if such a string is ever hashed (a `set`/`frozenset` member). See [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
106
+ - A `str` inside a `tuple` or `frozenset` set item is rendered with Python's `repr()`, which escapes every non-printable character; onix escapes those below `U+0100` (the complete set in that range) and passes higher non-printable code points through literally, since escaping them would mean carrying a Unicode category table. Exact for all of ASCII and all printable text. See [`crates/onix-core/src/path.rs`](crates/onix-core/src/path.rs).
102
107
  - Adversarially deep input raises `MaxDepthError` instead of crashing: the default `max_depth` is 512 and the hard ceiling is `MAX_DEPTH_CEILING` (20,000). See [`crates/onix-py/src/guard.rs`](crates/onix-py/src/guard.rs).
103
108
  - `ignore_order` pairing is `O(N^2)` in unpaired elements per side and carries a polynomial cost in both time and memory with input depth; it has no `max_passes`/`max_diffs` cutoff, so bound the size and depth of untrusted input yourself. See [`crates/onix-core/src/ignore_order/mod.rs`](crates/onix-core/src/ignore_order/mod.rs).
104
- - Output is byte-identical to DeepDiff except for one boundary case: integers past `2^53` (the limit of exact `f64` representation) inside ordered scalar lists. See [`tests/golden/README.md`](tests/golden/README.md).
109
+ - Output is byte-identical to DeepDiff except for the cases listed above and two path-rendering quirks; [`tests/golden/README.md`](tests/golden/README.md) enumerates every accepted exception, including integers past `2^53` (the limit of exact `f64` representation) inside ordered scalar lists and `ignore_order` pairing among naive datetimes, which DeepDiff ranks using the *process's local timezone* while onix reads a naive value as UTC everywhere.
105
110
 
106
111
  ## Performance
107
112
 
108
113
  Two committed, regenerable reports back the numbers below; every figure here is copied verbatim from them.
109
114
 
110
- The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side):
115
+ The Python bindings against real `deepdiff` on **live Python objects**, the number a real caller pays (source: [`crates/onix-py/benchmarks/bench_bindings.py`](crates/onix-py/benchmarks/bench_bindings.py), macOS 26.5.1, Apple M5 Max, median of 11 isolated subprocess runs per side, run on 2026-09-04):
111
116
 
112
117
  | Shape | deepdiff | deepdiff_rs | Speedup |
113
118
  | --- | --- | --- | --- |
114
- | `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) | 3128.94ms | 82.57ms | **37.89x** |
115
- | &nbsp;&nbsp;peak RSS | 228.0 MB | 141.2 MB | **1.61x** |
116
- | &nbsp;&nbsp;CPU seconds | 3.128 s | 0.083 s | **37.88x** |
117
- | Heterogeneous API-payload records, n=20,000 (live objects) | 3466.52ms | 149.18ms | **23.24x** |
118
- | &nbsp;&nbsp;peak RSS | 117.5 MB | 148.3 MB | **0.79x** |
119
- | &nbsp;&nbsp;CPU seconds | 3.465 s | 0.149 s | **23.24x** |
120
- | Same `ignore_order` shape, via `diff_json` (JSON-string path) | 3133.45ms | 83.16ms | **37.68x** |
121
- | &nbsp;&nbsp;peak RSS | 228.9 MB | 141.7 MB | **1.62x** |
122
- | &nbsp;&nbsp;CPU seconds | 3.131 s | 0.083 s | **37.68x** |
123
- | Same API-payload shape, via `diff_json` (JSON-string path) | 4568.33ms | 86.33ms | **52.92x** |
124
- | &nbsp;&nbsp;peak RSS | 139.0 MB | 141.7 MB | **0.98x** |
125
- | &nbsp;&nbsp;CPU seconds | 4.566 s | 0.086 s | **52.90x** |
126
- | Same API-payload shape, both tools reading two JSON files from disk | 4571.28ms | 85.35ms | **53.56x** |
127
- | &nbsp;&nbsp;peak RSS | 139.0 MB | 141.8 MB | **0.98x** |
128
- | &nbsp;&nbsp;CPU seconds | 4.569 s | 0.085 s | **53.54x** |
119
+ | `ignore_order`, 10k shuffled ints, ~5% mutated (live objects) | 3164.29ms | 89.08ms | **35.52x** |
120
+ | &nbsp;&nbsp;peak RSS | 228.4 MB | 141.5 MB | **1.61x** |
121
+ | &nbsp;&nbsp;CPU seconds | 3.162 s | 0.089 s | **35.50x** |
122
+ | Heterogeneous API-payload records, n=20,000 (live objects) | 3451.01ms | 151.84ms | **22.73x** |
123
+ | &nbsp;&nbsp;peak RSS | 118.1 MB | 147.7 MB | **0.80x** |
124
+ | &nbsp;&nbsp;CPU seconds | 3.449 s | 0.152 s | **22.73x** |
125
+ | Typed records (datetime/tuple/set fields), n=10,000 (live objects) | 792.96ms | 47.87ms | **16.57x** |
126
+ | &nbsp;&nbsp;peak RSS | 60.2 MB | 62.1 MB | **0.97x** |
127
+ | &nbsp;&nbsp;CPU seconds | 0.792 s | 0.048 s | **16.56x** |
128
+ | Same typed-records shape, `ignore_order` (live objects) | 60483.86ms | 1272.04ms | **47.55x** |
129
+ | &nbsp;&nbsp;peak RSS | 112.1 MB | 1005.6 MB | **0.11x** |
130
+ | &nbsp;&nbsp;CPU seconds | 60.452 s | 1.271 s | **47.55x** |
131
+ | Same `ignore_order` shape, via `diff_json` (JSON-string path) | 3168.05ms | 86.95ms | **36.44x** |
132
+ | &nbsp;&nbsp;peak RSS | 228.7 MB | 142.3 MB | **1.61x** |
133
+ | &nbsp;&nbsp;CPU seconds | 3.166 s | 0.087 s | **36.46x** |
134
+ | Same API-payload shape, via `diff_json` (JSON-string path) | 4568.00ms | 85.37ms | **53.51x** |
135
+ | &nbsp;&nbsp;peak RSS | 139.5 MB | 140.9 MB | **0.99x** |
136
+ | &nbsp;&nbsp;CPU seconds | 4.566 s | 0.085 s | **53.50x** |
137
+ | Same API-payload shape, both tools reading two JSON files from disk | 4565.46ms | 89.43ms | **51.05x** |
138
+ | &nbsp;&nbsp;peak RSS | 139.5 MB | 141.0 MB | **0.99x** |
139
+ | &nbsp;&nbsp;CPU seconds | 4.564 s | 0.089 s | **51.03x** |
129
140
 
130
141
  The engine's own diff-only time and peak resident memory against pinned `deepdiff` 9.1.0 (source: [`perf/RESULTS.md`](perf/RESULTS.md), same machine, median over tier-appropriate runs, diff time excluding process startup and JSON parsing on both sides):
131
142
 
@@ -148,14 +159,14 @@ Both reports carry their full methodology, fairness rules, and the reproduce com
148
159
 
149
160
  **Python API.** The public surface is `DeepDiff`, `diff_json`, `MaxDepthError`, and `MAX_DEPTH_CEILING`.
150
161
 
151
- - `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects; `.to_json()` returns the DeepDiff-compatible JSON string, `.to_dict()` the same report as a dict, and the instance is falsy when there is no difference.
162
+ - `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects of supported value types — `None`, `bool`, `int`, `float`, `str`, `dict` (with `str` keys), `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, and `datetime.date` (see [Known limitations](#known-limitations) for the exact restrictions and exclusions); `.to_json()` returns the DeepDiff-compatible JSON string, `.to_dict()` the same report as a dict — with Python types preserved, so a value the diff found in a `tuple`, `set` or `frozenset` comes back as one and a `datetime`/`date` comes back as a real `datetime`/`date` — and the instance is falsy when there is no difference. The `set_item_added`/`set_item_removed` categories are lists of path strings, each ending in the item itself (`root['a'][2]`, `root['x']`, `root[(1, 2)]`).
152
163
  - `diff_json(a, b, ignore_order=False, max_depth=None) -> str`: diffs two JSON strings entirely in Rust and returns the report as a JSON string.
153
164
  - `MaxDepthError` (a `ValueError` subclass) is raised when input exceeds `max_depth`; `MAX_DEPTH_CEILING` (20,000) is the hard upper bound on `max_depth`.
154
165
 
155
166
  **CLI.** `onix diff <a.json> <b.json> [--max-depth N] [--ignore-order] [--timing]` reads both files as JSON and prints a compact, single-line DeepDiff-compatible report to stdout (`{}` when there is no difference).
156
167
 
157
168
  - `--max-depth N` overrides the recursion-depth bound (default: the `ONIX_MAX_DEPTH` environment variable if set, else 512).
158
- - `--ignore-order` compares every list/tuple by hash-based matching instead of by position, mirroring `DeepDiff(..., ignore_order=True)`.
169
+ - `--ignore-order` compares every list by hash-based matching instead of by position, mirroring `DeepDiff(..., ignore_order=True)`.
159
170
  - `--timing` prints one line of JSON (`{"parse_ns": N, "diff_ns": N}`) to stderr.
160
171
 
161
172
  Exit codes: