deepdiff-rs 0.9.1__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.1 → deepdiff_rs-0.9.3}/Cargo.lock +4 -4
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/Cargo.toml +1 -1
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/PKG-INFO +7 -7
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/README.md +6 -6
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/array.rs +32 -11
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/dispatch.rs +63 -20
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/mod.rs +2 -2
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/object.rs +2 -2
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/scalar.rs +17 -7
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/tests.rs +263 -65
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/distance.rs +44 -21
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/hash.rs +11 -7
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/tests.rs +53 -87
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/lcs.rs +5 -3
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/path.rs +186 -96
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/report.rs +22 -22
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/report_tests.rs +10 -7
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/test_support.rs +28 -6
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/unified_diff.rs +11 -1
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/value.rs +758 -61
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/value_tests.rs +478 -19
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/tests/golden.rs +20 -7
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/tests/ignore_order_memory.rs +9 -8
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/deepdiff_rs.pyi +7 -1
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/src/convert.rs +416 -173
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/src/deepdiff.rs +40 -17
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/src/fast_path.rs +5 -2
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/src/guard.rs +116 -49
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/src/lib.rs +1 -0
- deepdiff_rs-0.9.3/crates/onix-py/tests/test_conversions.py +564 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_datetimes.py +76 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_differential_fuzz.py +311 -9
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_sets.py +49 -11
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_stub_signatures.py +20 -2
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_tuples.py +41 -22
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/pyproject.toml +9 -6
- deepdiff_rs-0.9.1/crates/onix-py/tests/test_conversions.py +0 -404
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/Cargo.toml +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/examples/row_diff_rss.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/examples/type_stack_cost.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/error.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/json_rows.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/lib.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/options.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/row_diff.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/schema.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-arrow/src/table_diff.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/Cargo.toml +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/datetime.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/datetime_tests.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/options.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/diff/set.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/error.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/fxhash.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/memo.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/mod.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/ignore_order/pairing.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/lcs_tests.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/lib.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/src/unified_diff_tests.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/tests/memory_footprint.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/tests/proptest_diff.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/.python-version +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/Cargo.toml +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/benchmarks/bench_bindings.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/src/arrow.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/src/errors.rs +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/conftest.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_bindings_memory.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_depth_guard.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_golden_parity.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_non_finite.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_signed_zero.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_smoke.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_stub_mypy.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_suite_hygiene.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_table_diff.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_table_row_diff.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_timedeltas.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_times.py +0 -0
- {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.3}/crates/onix-py/tests/test_wheel_contents.py +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
|
|
|
@@ -195,11 +195,11 @@ The engine's own diff-only time and peak resident memory against pinned `deepdif
|
|
|
195
195
|
| `ignore_order_10k` | 73.475 ms (71.672 ms-73.940 ms) | 12.976 s (12.900 s-13.005 s) | 176.60x | 60.11 MB | 345.19 MB | 5.74x | ✅ |
|
|
196
196
|
| `identical_1m` | 9.254 ms (6.726 ms-9.875 ms) | 15.790 s (15.660 s-15.989 s) | 1706.35x | 315.41 MB | 503.19 MB | 1.60x | ✅ |
|
|
197
197
|
|
|
198
|
-
Both reports carry their full methodology, fairness rules, and the reproduce command. `perf/RESULTS.md` is an upper bound (JSON parsed straight into the engine, no Python-object conversion); the bindings table is the product-surface number. Regenerate them with `perf/run_bench.sh` and `crates/onix-py/benchmarks/bench_bindings.py` (see [CONTRIBUTING.md](CONTRIBUTING.md)).
|
|
198
|
+
Both reports carry their full methodology, fairness rules, and the reproduce command. `perf/RESULTS.md` is an upper bound (JSON parsed straight into the engine, no Python-object conversion); the bindings table is the product-surface number. Regenerate them with `perf/run_bench.sh` and `crates/onix-py/benchmarks/bench_bindings.py` (see [CONTRIBUTING.md](CONTRIBUTING.md)). The Arrow table diff's cost against two hand-rolled baselines — a DuckDB SQL join and a polars anti-join/inner-join diff, on a seeded 5 GB parquet pair and its 1M-row subset — is in [`perf/arrow/RESULTS.md`](perf/arrow/RESULTS.md): onix's single-threaded row diff trails both parallelized baselines at both sizes; see the linked results for the figures; regenerated with `perf/arrow/bench_tables.py`.
|
|
199
199
|
|
|
200
200
|
## Reference
|
|
201
201
|
|
|
202
|
-
**Python API.** The public surface is `DeepDiff`, `diff_json`, `diff_tables` (returning a `TableDiff`), `MaxDepthError`, and `
|
|
202
|
+
**Python API.** The public surface is `DeepDiff`, `diff_json`, `diff_tables` (returning a `TableDiff`), `MaxDepthError`, `MAX_DEPTH_CEILING`, and `__version__` (a `str` matching the installed `deepdiff-rs` distribution version).
|
|
203
203
|
|
|
204
204
|
- `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects of supported value types — `None`, `bool`, `int`, `float`, `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta` (see [Known limitations](#known-limitations) for the exact restrictions and exclusions, including which types a `dict` key may be); `.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`/`time`/`timedelta` comes back as a real one of those — 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)]`).
|
|
205
205
|
- `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.
|
|
@@ -236,12 +236,12 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
236
236
|
## Known limitations
|
|
237
237
|
|
|
238
238
|
- Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
|
|
239
|
-
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested). `int`s must fit in `i64`/`u64`, and anything else — a custom object or an arbitrary-precision `int` — raises `TypeError`/`ValueError` naming the exact path it was found at. A non-`str` key's path renders via Python's own `repr()`, except a `tuple` key, which splits into one bracket group per element (`root[1][2]` for `(1, 2)`, never `root[(1, 2)]`, with one deliberate exception for a real DeepDiff bug on the empty tuple, and another on a non-finite-float key — see [`tests/golden/README.md`](tests/golden/README.md)). A nested dict value's own `bool`/`None`/`int`/`float` key stringifies the way `json.dumps` does, but its `datetime`/`date`/`tuple` key renders that same `repr()` text where DeepDiff's own `to_json()` raises `TypeError` on one — a superset, not a difference in the findings (see [`tests/golden/README.md`](tests/golden/README.md)). `1`/`1.0`/`True` match as the same key between two dicts (Python `dict`/`set` equality). The **Datetimes**, **Sets** and **Non-finite floats** bullets below cover the deliberate divergences for those types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
|
|
240
|
-
- A subclass of a supported
|
|
239
|
+
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested), or a `tuple`/`datetime`/`date` subclass key (`namedtuple` included), accepted and matched against its base-type value the same way — see [Known limitations](#known-limitations)'s subclass bullet. `int`s must fit in `i64`/`u64`, and anything else — a custom object or an arbitrary-precision `int` — raises `TypeError`/`ValueError` naming the exact path it was found at. A non-`str` key's path renders via Python's own `repr()`, except a `tuple` key, which splits into one bracket group per element (`root[1][2]` for `(1, 2)`, never `root[(1, 2)]`, with one deliberate exception for a real DeepDiff bug on the empty tuple, and another on a non-finite-float key — see [`tests/golden/README.md`](tests/golden/README.md)). A nested dict value's own `bool`/`None`/`int`/`float` key stringifies the way `json.dumps` does, but its `datetime`/`date`/`tuple` key renders that same `repr()` text where DeepDiff's own `to_json()` raises `TypeError` on one — a superset, not a difference in the findings (see [`tests/golden/README.md`](tests/golden/README.md)). `1`/`1.0`/`True` match as the same key between two dicts (Python `dict`/`set` equality). The **Datetimes**, **Sets** and **Non-finite floats** bullets below cover the deliberate divergences for those types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
|
|
240
|
+
- A subclass of a supported `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, or `datetime.timedelta` (including `namedtuple` and pandas' `Timestamp`) converts and compares as its base type but carries its own class name into a `type_changes` entry, matching DeepDiff's `type(obj).__name__` reporting, with three exceptions: a calendar-type (`datetime`/`date`/`time`/`timedelta`) subclass held as a `set`/`frozenset` member compares as its base with no `type_changes`, matching DeepDiff, since set membership has no pairwise comparison to report one against; a `tuple`/`frozenset` subclass, including a `namedtuple`, is not accepted as a `set`/`frozenset` member at all and raises `TypeError` where DeepDiff accepts and compares it by value (a documented divergence); and `namedtuple` diffs positionally rather than by field (also documented). A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. See [`tests/golden/README.md`](tests/golden/README.md).
|
|
241
241
|
- **Datetimes** compare by instant, with a naive value read as UTC, matching DeepDiff. A changed pair is reported normalized to UTC (`to_json()` renders `...+00:00`, `to_dict()` returns UTC-aware `datetime`s); everywhere else a datetime keeps its raw value. Three deliberate departures: `to_json()` renders a `date` as `YYYY-MM-DD` where DeepDiff's own `to_json()` raises `TypeError` (a documented superset); a `zoneinfo`/`pytz` tzinfo comes back from `to_dict()` as a fixed-offset `datetime.timezone` carrying the offset it was in force at, not the original zone object; and a set holding both a naive and an aware value at one instant reports both as members, where DeepDiff's own digest cache can report only one (see [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs)). Comparing two datetimes whose UTC form would leave year 1..=9999 raises `ValueError` naming the path, where DeepDiff raises `OverflowError`; under `ignore_order` DeepDiff's hasher normalizes every datetime and so raises for such a value even when it is only added, removed, or shuffled, where onix hashes by instant and reports it normally (see [`tests/golden/README.md`](tests/golden/README.md)). `truncate_datetime` is not supported; the normalized-versus-raw split is documented there too. A `time`/`timedelta`, unlike a datetime, is never normalized for report (DeepDiff compares `time`/`date`/`timedelta` with a plain `!=`), and a naive `time` is never equal to an aware one; `to_json()` renders a `time` as `time.isoformat()`'s bytes and a `timedelta` as `str(timedelta)`'s, both supersets. Under `ignore_order`, `DeepHash` hashes a `time` by whole seconds-of-day only — dropping the microsecond and any offset, a confirmed upstream quirk — while a `timedelta` hashes exactly; see [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
242
242
|
- **Sets** are diffed deterministically, where DeepDiff's own answers depend on the order the running process happens to iterate a set in (hash order, and `PYTHONHASHSEED`-dependent for `str` members) or on how its digest cache/computation handles a tuple, frozenset, or calendar member independently of Python's own `==`. Each consequence — entry order, which member of an equality class is reported, set-versus-sequence coercion, and a tuple/frozenset member's own (positional, not order-/repetition-insensitive) matching rule — is shown with both tools' output in [`tests/golden/README.md`](tests/golden/README.md)'s "Set iteration order" section. A report holding a `frozenset` value also serializes to JSON here, where DeepDiff's own `to_json()` raises `TypeError` — a superset, not a difference in the findings.
|
|
243
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
|
|
|
@@ -177,11 +177,11 @@ The engine's own diff-only time and peak resident memory against pinned `deepdif
|
|
|
177
177
|
| `ignore_order_10k` | 73.475 ms (71.672 ms-73.940 ms) | 12.976 s (12.900 s-13.005 s) | 176.60x | 60.11 MB | 345.19 MB | 5.74x | ✅ |
|
|
178
178
|
| `identical_1m` | 9.254 ms (6.726 ms-9.875 ms) | 15.790 s (15.660 s-15.989 s) | 1706.35x | 315.41 MB | 503.19 MB | 1.60x | ✅ |
|
|
179
179
|
|
|
180
|
-
Both reports carry their full methodology, fairness rules, and the reproduce command. `perf/RESULTS.md` is an upper bound (JSON parsed straight into the engine, no Python-object conversion); the bindings table is the product-surface number. Regenerate them with `perf/run_bench.sh` and `crates/onix-py/benchmarks/bench_bindings.py` (see [CONTRIBUTING.md](CONTRIBUTING.md)).
|
|
180
|
+
Both reports carry their full methodology, fairness rules, and the reproduce command. `perf/RESULTS.md` is an upper bound (JSON parsed straight into the engine, no Python-object conversion); the bindings table is the product-surface number. Regenerate them with `perf/run_bench.sh` and `crates/onix-py/benchmarks/bench_bindings.py` (see [CONTRIBUTING.md](CONTRIBUTING.md)). The Arrow table diff's cost against two hand-rolled baselines — a DuckDB SQL join and a polars anti-join/inner-join diff, on a seeded 5 GB parquet pair and its 1M-row subset — is in [`perf/arrow/RESULTS.md`](perf/arrow/RESULTS.md): onix's single-threaded row diff trails both parallelized baselines at both sizes; see the linked results for the figures; regenerated with `perf/arrow/bench_tables.py`.
|
|
181
181
|
|
|
182
182
|
## Reference
|
|
183
183
|
|
|
184
|
-
**Python API.** The public surface is `DeepDiff`, `diff_json`, `diff_tables` (returning a `TableDiff`), `MaxDepthError`, and `
|
|
184
|
+
**Python API.** The public surface is `DeepDiff`, `diff_json`, `diff_tables` (returning a `TableDiff`), `MaxDepthError`, `MAX_DEPTH_CEILING`, and `__version__` (a `str` matching the installed `deepdiff-rs` distribution version).
|
|
185
185
|
|
|
186
186
|
- `DeepDiff(t1, t2, ignore_order=False, max_depth=None)`: diffs two live Python objects of supported value types — `None`, `bool`, `int`, `float`, `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta` (see [Known limitations](#known-limitations) for the exact restrictions and exclusions, including which types a `dict` key may be); `.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`/`time`/`timedelta` comes back as a real one of those — 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)]`).
|
|
187
187
|
- `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.
|
|
@@ -218,12 +218,12 @@ perf/ # cross-language benchmark harness and RESULTS.md
|
|
|
218
218
|
## Known limitations
|
|
219
219
|
|
|
220
220
|
- Only the core diff is implemented: `exclude_paths`, `significant_digits`, custom operators, `verbose_level != 2`, and delta/patch are not (yet) supported.
|
|
221
|
-
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested). `int`s must fit in `i64`/`u64`, and anything else — a custom object or an arbitrary-precision `int` — raises `TypeError`/`ValueError` naming the exact path it was found at. A non-`str` key's path renders via Python's own `repr()`, except a `tuple` key, which splits into one bracket group per element (`root[1][2]` for `(1, 2)`, never `root[(1, 2)]`, with one deliberate exception for a real DeepDiff bug on the empty tuple, and another on a non-finite-float key — see [`tests/golden/README.md`](tests/golden/README.md)). A nested dict value's own `bool`/`None`/`int`/`float` key stringifies the way `json.dumps` does, but its `datetime`/`date`/`tuple` key renders that same `repr()` text where DeepDiff's own `to_json()` raises `TypeError` on one — a superset, not a difference in the findings (see [`tests/golden/README.md`](tests/golden/README.md)). `1`/`1.0`/`True` match as the same key between two dicts (Python `dict`/`set` equality). The **Datetimes**, **Sets** and **Non-finite floats** bullets below cover the deliberate divergences for those types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
|
|
222
|
-
- A subclass of a supported
|
|
221
|
+
- Supported value types are `None`, `bool`, `int`, `float` (`NaN`/`Infinity`/`-Infinity` included), `str`, `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, and `datetime.timedelta`; a `set`/`frozenset` member may be any of these except a `list`, `dict` or `set`, matching Python's own hashability rule, transitively through whatever the member nests, and a `dict` key may be `str`, `None`, `bool`, `int`, `float`, `datetime.datetime`, `datetime.date`, or a `tuple` of those (never nested), or a `tuple`/`datetime`/`date` subclass key (`namedtuple` included), accepted and matched against its base-type value the same way — see [Known limitations](#known-limitations)'s subclass bullet. `int`s must fit in `i64`/`u64`, and anything else — a custom object or an arbitrary-precision `int` — raises `TypeError`/`ValueError` naming the exact path it was found at. A non-`str` key's path renders via Python's own `repr()`, except a `tuple` key, which splits into one bracket group per element (`root[1][2]` for `(1, 2)`, never `root[(1, 2)]`, with one deliberate exception for a real DeepDiff bug on the empty tuple, and another on a non-finite-float key — see [`tests/golden/README.md`](tests/golden/README.md)). A nested dict value's own `bool`/`None`/`int`/`float` key stringifies the way `json.dumps` does, but its `datetime`/`date`/`tuple` key renders that same `repr()` text where DeepDiff's own `to_json()` raises `TypeError` on one — a superset, not a difference in the findings (see [`tests/golden/README.md`](tests/golden/README.md)). `1`/`1.0`/`True` match as the same key between two dicts (Python `dict`/`set` equality). The **Datetimes**, **Sets** and **Non-finite floats** bullets below cover the deliberate divergences for those types. See [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs) and [`tests/golden/README.md`](tests/golden/README.md).
|
|
222
|
+
- A subclass of a supported `dict`, `list`, `tuple`, `set`, `frozenset`, `datetime.datetime`, `datetime.date`, `datetime.time`, or `datetime.timedelta` (including `namedtuple` and pandas' `Timestamp`) converts and compares as its base type but carries its own class name into a `type_changes` entry, matching DeepDiff's `type(obj).__name__` reporting, with three exceptions: a calendar-type (`datetime`/`date`/`time`/`timedelta`) subclass held as a `set`/`frozenset` member compares as its base with no `type_changes`, matching DeepDiff, since set membership has no pairwise comparison to report one against; a `tuple`/`frozenset` subclass, including a `namedtuple`, is not accepted as a `set`/`frozenset` member at all and raises `TypeError` where DeepDiff accepts and compares it by value (a documented divergence); and `namedtuple` diffs positionally rather than by field (also documented). A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. See [`tests/golden/README.md`](tests/golden/README.md).
|
|
223
223
|
- **Datetimes** compare by instant, with a naive value read as UTC, matching DeepDiff. A changed pair is reported normalized to UTC (`to_json()` renders `...+00:00`, `to_dict()` returns UTC-aware `datetime`s); everywhere else a datetime keeps its raw value. Three deliberate departures: `to_json()` renders a `date` as `YYYY-MM-DD` where DeepDiff's own `to_json()` raises `TypeError` (a documented superset); a `zoneinfo`/`pytz` tzinfo comes back from `to_dict()` as a fixed-offset `datetime.timezone` carrying the offset it was in force at, not the original zone object; and a set holding both a naive and an aware value at one instant reports both as members, where DeepDiff's own digest cache can report only one (see [`crates/onix-py/src/convert.rs`](crates/onix-py/src/convert.rs)). Comparing two datetimes whose UTC form would leave year 1..=9999 raises `ValueError` naming the path, where DeepDiff raises `OverflowError`; under `ignore_order` DeepDiff's hasher normalizes every datetime and so raises for such a value even when it is only added, removed, or shuffled, where onix hashes by instant and reports it normally (see [`tests/golden/README.md`](tests/golden/README.md)). `truncate_datetime` is not supported; the normalized-versus-raw split is documented there too. A `time`/`timedelta`, unlike a datetime, is never normalized for report (DeepDiff compares `time`/`date`/`timedelta` with a plain `!=`), and a naive `time` is never equal to an aware one; `to_json()` renders a `time` as `time.isoformat()`'s bytes and a `timedelta` as `str(timedelta)`'s, both supersets. Under `ignore_order`, `DeepHash` hashes a `time` by whole seconds-of-day only — dropping the microsecond and any offset, a confirmed upstream quirk — while a `timedelta` hashes exactly; see [`tests/golden/README.md`](tests/golden/README.md)'s "Known DeepDiff quirks" section.
|
|
224
224
|
- **Sets** are diffed deterministically, where DeepDiff's own answers depend on the order the running process happens to iterate a set in (hash order, and `PYTHONHASHSEED`-dependent for `str` members) or on how its digest cache/computation handles a tuple, frozenset, or calendar member independently of Python's own `==`. Each consequence — entry order, which member of an equality class is reported, set-versus-sequence coercion, and a tuple/frozenset member's own (positional, not order-/repetition-insensitive) matching rule — is shown with both tools' output in [`tests/golden/README.md`](tests/golden/README.md)'s "Set iteration order" section. A report holding a `frozenset` value also serializes to JSON here, where DeepDiff's own `to_json()` raises `TypeError` — a superset, not a difference in the findings.
|
|
225
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).
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//! the parent `diff` module's "List diffing" doc section for the full,
|
|
4
4
|
//! empirically-verified spec this implements.
|
|
5
5
|
|
|
6
|
-
use crate::value::Value;
|
|
6
|
+
use crate::value::{Value, class_name};
|
|
7
7
|
|
|
8
8
|
use crate::error::Error;
|
|
9
9
|
use crate::ignore_order::{self, IgnoreOrderMemo};
|
|
@@ -12,8 +12,8 @@ use crate::path::PathSegment;
|
|
|
12
12
|
use crate::report::{Report, TypeChangeEntry, ValuesChangedEntry};
|
|
13
13
|
|
|
14
14
|
use super::{
|
|
15
|
-
DiffOptions, check_traversal_depth, check_value_depth, diff_at,
|
|
16
|
-
python_type_name, scoped,
|
|
15
|
+
DiffOptions, check_traversal_depth, check_value_depth, diff_at, effective_type_name,
|
|
16
|
+
normalized_pair, python_type_name, scoped,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
/// Diffs two lists (JSON arrays) at `path`, `depth` levels deep.
|
|
@@ -178,17 +178,36 @@ fn insert_lcs_pair_finding(
|
|
|
178
178
|
new_segments
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
if let (Value::DateTime(
|
|
182
|
-
|
|
181
|
+
if let (Value::DateTime(old_typed), Value::DateTime(new_typed)) = (old_value, new_value) {
|
|
182
|
+
// A `datetime` subclass (e.g. pandas `Timestamp`) still reports
|
|
183
|
+
// `type_changes` against a base `datetime` (or a differently
|
|
184
|
+
// named subclass) even at equal value — see `Typed`'s doc — so
|
|
185
|
+
// this check must run before the normalize-and-compare path
|
|
186
|
+
// below, which only ever produces `values_changed`.
|
|
187
|
+
if old_typed.class_name() != new_typed.class_name() {
|
|
188
|
+
report.insert_type_change(
|
|
189
|
+
path.clone(),
|
|
190
|
+
TypeChangeEntry {
|
|
191
|
+
old_type: effective_type_name(old_value),
|
|
192
|
+
new_type: effective_type_name(new_value),
|
|
193
|
+
old_value: old_value.clone(),
|
|
194
|
+
new_value: new_value.clone(),
|
|
195
|
+
new_path,
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
return Ok(());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let (old_norm, new_norm) = normalized_pair(path, old_typed.value(), new_typed.value())?;
|
|
183
202
|
|
|
184
|
-
if
|
|
203
|
+
if old_norm != new_norm {
|
|
185
204
|
report.insert_values_changed(
|
|
186
205
|
path.clone(),
|
|
187
206
|
ValuesChangedEntry {
|
|
188
207
|
// Datetimes are never strings, so `_diff_str` never runs.
|
|
189
208
|
diff: None,
|
|
190
|
-
old_value: Value::DateTime(
|
|
191
|
-
new_value: Value::DateTime(
|
|
209
|
+
old_value: Value::DateTime(old_norm.into()),
|
|
210
|
+
new_value: Value::DateTime(new_norm.into()),
|
|
192
211
|
new_path,
|
|
193
212
|
},
|
|
194
213
|
);
|
|
@@ -197,7 +216,9 @@ fn insert_lcs_pair_finding(
|
|
|
197
216
|
return Ok(());
|
|
198
217
|
}
|
|
199
218
|
|
|
200
|
-
if python_type_name(old_value) == python_type_name(new_value)
|
|
219
|
+
if python_type_name(old_value) == python_type_name(new_value)
|
|
220
|
+
&& class_name(old_value) == class_name(new_value)
|
|
221
|
+
{
|
|
201
222
|
report.insert_values_changed(
|
|
202
223
|
path.clone(),
|
|
203
224
|
ValuesChangedEntry {
|
|
@@ -211,8 +232,8 @@ fn insert_lcs_pair_finding(
|
|
|
211
232
|
report.insert_type_change(
|
|
212
233
|
path.clone(),
|
|
213
234
|
TypeChangeEntry {
|
|
214
|
-
old_type:
|
|
215
|
-
new_type:
|
|
235
|
+
old_type: effective_type_name(old_value),
|
|
236
|
+
new_type: effective_type_name(new_value),
|
|
216
237
|
old_value: old_value.clone(),
|
|
217
238
|
new_value: new_value.clone(),
|
|
218
239
|
new_path,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//! See the parent `diff` module's doc for the full recursion-depth hardening
|
|
7
7
|
//! (its "Hardening" section) this file implements.
|
|
8
8
|
|
|
9
|
-
use crate::value::{Object, Value};
|
|
9
|
+
use crate::value::{Object, Value, class_name};
|
|
10
10
|
|
|
11
11
|
use crate::error::Error;
|
|
12
12
|
use crate::ignore_order::IgnoreOrderMemo;
|
|
@@ -18,6 +18,18 @@ use super::{
|
|
|
18
18
|
type_change_report,
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/// Whether `a` and `b` — already known to be the same [`Value`] variant —
|
|
22
|
+
/// carry different subclass names, per [`class_name`]. `diff_at` checks
|
|
23
|
+
/// this before recursing into any of the eight variants that can carry one
|
|
24
|
+
/// (`DateTime`, `Date`, `Time`, `TimeDelta`, `Array`, `Tuple`,
|
|
25
|
+
/// `Set`/`FrozenSet`, `Object`): a mismatch means `DeepDiff` would report
|
|
26
|
+
/// `type_changes` here even though the values are the same JSON-ish shape —
|
|
27
|
+
/// see [`crate::value::Typed`]'s doc for why (and why every other matching
|
|
28
|
+
/// identity in the crate stays class-agnostic instead).
|
|
29
|
+
fn same_class(a: &Value, b: &Value) -> bool {
|
|
30
|
+
class_name(a) == class_name(b)
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/// The recursive core of [`diff_with_max_depth()`](super::diff_with_max_depth): identical dispatch, but
|
|
22
34
|
/// carrying the path and depth accumulated so far, so that nested findings
|
|
23
35
|
/// get their full deep path and the recursion-depth bound can be enforced.
|
|
@@ -55,33 +67,64 @@ pub(crate) fn diff_at(
|
|
|
55
67
|
scalar_diff(path, old == new, a, b, depth, opts.max_depth)
|
|
56
68
|
}
|
|
57
69
|
(Value::DateTime(old), Value::DateTime(new)) => {
|
|
58
|
-
|
|
70
|
+
if same_class(a, b) {
|
|
71
|
+
datetime_diff(path, old.value(), new.value(), depth, opts.max_depth)
|
|
72
|
+
} else {
|
|
73
|
+
type_change_report(path, a, b, depth, opts.max_depth)
|
|
74
|
+
}
|
|
59
75
|
}
|
|
60
76
|
(Value::Date(old), Value::Date(new)) => {
|
|
61
|
-
|
|
77
|
+
if same_class(a, b) {
|
|
78
|
+
scalar_diff(path, old == new, a, b, depth, opts.max_depth)
|
|
79
|
+
} else {
|
|
80
|
+
type_change_report(path, a, b, depth, opts.max_depth)
|
|
81
|
+
}
|
|
62
82
|
}
|
|
63
83
|
(Value::Time(old), Value::Time(new)) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
if same_class(a, b) {
|
|
85
|
+
// Plain `_diff_time` equality — no normalization step,
|
|
86
|
+
// unlike `datetime_diff` (see `crate::datetime`'s module
|
|
87
|
+
// doc).
|
|
88
|
+
scalar_diff(
|
|
89
|
+
path,
|
|
90
|
+
crate::datetime::times_equal(old.value(), new.value()),
|
|
91
|
+
a,
|
|
92
|
+
b,
|
|
93
|
+
depth,
|
|
94
|
+
opts.max_depth,
|
|
95
|
+
)
|
|
96
|
+
} else {
|
|
97
|
+
type_change_report(path, a, b, depth, opts.max_depth)
|
|
98
|
+
}
|
|
74
99
|
}
|
|
75
100
|
(Value::TimeDelta(old), Value::TimeDelta(new)) => {
|
|
76
|
-
|
|
101
|
+
if same_class(a, b) {
|
|
102
|
+
scalar_diff(path, old == new, a, b, depth, opts.max_depth)
|
|
103
|
+
} else {
|
|
104
|
+
type_change_report(path, a, b, depth, opts.max_depth)
|
|
105
|
+
}
|
|
77
106
|
}
|
|
78
107
|
(Value::Array(old), Value::Array(new)) | (Value::Tuple(old), Value::Tuple(new)) => {
|
|
79
|
-
|
|
108
|
+
if same_class(a, b) {
|
|
109
|
+
array_diff(path, old, new, depth, opts, memo)
|
|
110
|
+
} else {
|
|
111
|
+
type_change_report(path, a, b, depth, opts.max_depth)
|
|
112
|
+
}
|
|
80
113
|
}
|
|
81
114
|
(Value::Set(old), Value::Set(new)) | (Value::FrozenSet(old), Value::FrozenSet(new)) => {
|
|
82
|
-
|
|
115
|
+
if same_class(a, b) {
|
|
116
|
+
set_diff(path, old, new, depth, opts, memo)
|
|
117
|
+
} else {
|
|
118
|
+
type_change_report(path, a, b, depth, opts.max_depth)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
(Value::Object(old), Value::Object(new)) => {
|
|
122
|
+
if same_class(a, b) {
|
|
123
|
+
object_diff(path, old, new, depth, opts, memo)
|
|
124
|
+
} else {
|
|
125
|
+
type_change_report(path, a, b, depth, opts.max_depth)
|
|
126
|
+
}
|
|
83
127
|
}
|
|
84
|
-
(Value::Object(old), Value::Object(new)) => object_diff(path, old, new, depth, opts, memo),
|
|
85
128
|
_ => type_change_report(path, a, b, depth, opts.max_depth),
|
|
86
129
|
}
|
|
87
130
|
}
|
|
@@ -179,7 +222,7 @@ pub(crate) fn check_value_depth(
|
|
|
179
222
|
) -> Result<(), Error> {
|
|
180
223
|
if deeper_than(value, max_depth.saturating_sub(depth)) {
|
|
181
224
|
return Err(Error::MaxDepthExceeded {
|
|
182
|
-
path: render_path(path),
|
|
225
|
+
path: render_path(path).to_string(),
|
|
183
226
|
max_depth,
|
|
184
227
|
});
|
|
185
228
|
}
|
|
@@ -223,7 +266,7 @@ pub(crate) fn check_map_depth(
|
|
|
223
266
|
) -> Result<(), Error> {
|
|
224
267
|
if map_deeper_than(map, max_depth.saturating_sub(depth)) {
|
|
225
268
|
return Err(Error::MaxDepthExceeded {
|
|
226
|
-
path: render_path(path),
|
|
269
|
+
path: render_path(path).to_string(),
|
|
227
270
|
max_depth,
|
|
228
271
|
});
|
|
229
272
|
}
|
|
@@ -251,7 +294,7 @@ pub(crate) fn check_traversal_depth(
|
|
|
251
294
|
) -> Result<(), Error> {
|
|
252
295
|
if depth > max_depth {
|
|
253
296
|
return Err(Error::MaxDepthExceeded {
|
|
254
|
-
path: render_path(path),
|
|
297
|
+
path: render_path(path).to_string(),
|
|
255
298
|
max_depth,
|
|
256
299
|
});
|
|
257
300
|
}
|
|
@@ -228,7 +228,7 @@ pub(crate) use object::object_diff;
|
|
|
228
228
|
#[cfg(test)]
|
|
229
229
|
pub(crate) use options::diff_with_options_memo;
|
|
230
230
|
pub(crate) use scalar::{
|
|
231
|
-
datetime_diff, normalized_pair, numbers_equal, numeric_diff,
|
|
232
|
-
type_change_report,
|
|
231
|
+
datetime_diff, effective_type_name, normalized_pair, numbers_equal, numeric_diff,
|
|
232
|
+
python_type_name, scalar_diff, type_change_report,
|
|
233
233
|
};
|
|
234
234
|
pub(crate) use set::set_diff;
|
|
@@ -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());
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//! arrays.
|
|
5
5
|
|
|
6
6
|
use crate::datetime::DateTime;
|
|
7
|
-
use crate::value::{Number, Value};
|
|
7
|
+
use crate::value::{Number, Value, class_name};
|
|
8
8
|
|
|
9
9
|
use crate::error::Error;
|
|
10
10
|
use crate::path::{PathSegment, render_path};
|
|
@@ -12,7 +12,9 @@ use crate::report::{Report, TypeChangeEntry, ValuesChangedEntry};
|
|
|
12
12
|
|
|
13
13
|
use super::check_value_depth;
|
|
14
14
|
|
|
15
|
-
/// The
|
|
15
|
+
/// The base-type name for a given [`Value`], ignoring any subclass name it
|
|
16
|
+
/// carries; see [`effective_type_name`] for the name `DeepDiff` actually
|
|
17
|
+
/// reports.
|
|
16
18
|
///
|
|
17
19
|
/// Numbers are split into `"int"` and `"float"` by the compact [`Number`]'s
|
|
18
20
|
/// preserved representation (which carries `serde_json`'s original parse: a
|
|
@@ -37,6 +39,14 @@ pub(crate) fn python_type_name(value: &Value) -> &'static str {
|
|
|
37
39
|
Value::Object(_) => "dict",
|
|
38
40
|
}
|
|
39
41
|
}
|
|
42
|
+
|
|
43
|
+
/// [`python_type_name`], overridden by [`class_name`] when `value` carries a
|
|
44
|
+
/// subclass name — the name `DeepDiff` actually reports for `old_type`/
|
|
45
|
+
/// `new_type` (`type(obj).__name__`, not the base type it structurally
|
|
46
|
+
/// compares as). See [`crate::value::Typed`]'s doc.
|
|
47
|
+
pub(crate) fn effective_type_name(value: &Value) -> String {
|
|
48
|
+
class_name(value).map_or_else(|| python_type_name(value).to_string(), str::to_string)
|
|
49
|
+
}
|
|
40
50
|
/// Builds a single-entry `type_changes` report at `path`, `depth` levels
|
|
41
51
|
/// deep.
|
|
42
52
|
///
|
|
@@ -60,8 +70,8 @@ pub(crate) fn type_change_report(
|
|
|
60
70
|
report.insert_type_change(
|
|
61
71
|
path.to_vec(),
|
|
62
72
|
TypeChangeEntry {
|
|
63
|
-
old_type:
|
|
64
|
-
new_type:
|
|
73
|
+
old_type: effective_type_name(a),
|
|
74
|
+
new_type: effective_type_name(b),
|
|
65
75
|
old_value: a.clone(),
|
|
66
76
|
new_value: b.clone(),
|
|
67
77
|
new_path: None,
|
|
@@ -128,8 +138,8 @@ pub(crate) fn datetime_diff(
|
|
|
128
138
|
scalar_diff(
|
|
129
139
|
path,
|
|
130
140
|
old == new,
|
|
131
|
-
&Value::DateTime(old),
|
|
132
|
-
&Value::DateTime(new),
|
|
141
|
+
&Value::DateTime(old.into()),
|
|
142
|
+
&Value::DateTime(new.into()),
|
|
133
143
|
depth,
|
|
134
144
|
max_depth,
|
|
135
145
|
)
|
|
@@ -149,7 +159,7 @@ pub(crate) fn normalized_pair(
|
|
|
149
159
|
new: DateTime,
|
|
150
160
|
) -> Result<(DateTime, DateTime), Error> {
|
|
151
161
|
let out_of_range = || Error::DateTimeOutOfRange {
|
|
152
|
-
path: render_path(path),
|
|
162
|
+
path: render_path(path).to_string(),
|
|
153
163
|
};
|
|
154
164
|
|
|
155
165
|
Ok((
|