deepdiff-rs 0.9.1__tar.gz → 0.9.2__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 (82) hide show
  1. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/Cargo.lock +4 -4
  2. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/Cargo.toml +1 -1
  3. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/PKG-INFO +5 -5
  4. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/README.md +4 -4
  5. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/diff/array.rs +32 -11
  6. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/diff/dispatch.rs +60 -17
  7. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/diff/mod.rs +2 -2
  8. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/diff/scalar.rs +16 -6
  9. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/diff/tests.rs +251 -59
  10. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/ignore_order/distance.rs +3 -1
  11. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/ignore_order/hash.rs +2 -2
  12. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/ignore_order/tests.rs +51 -85
  13. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/lcs.rs +1 -1
  14. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/path.rs +36 -28
  15. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/report.rs +4 -1
  16. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/test_support.rs +27 -5
  17. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/value.rs +237 -23
  18. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/value_tests.rs +17 -11
  19. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/tests/golden.rs +9 -6
  20. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/tests/ignore_order_memory.rs +5 -4
  21. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/deepdiff_rs.pyi +7 -1
  22. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/src/convert.rs +312 -121
  23. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/src/deepdiff.rs +20 -16
  24. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/src/lib.rs +1 -0
  25. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_conversions.py +143 -36
  26. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_datetimes.py +76 -0
  27. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_differential_fuzz.py +152 -9
  28. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_sets.py +49 -11
  29. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_stub_signatures.py +20 -2
  30. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_tuples.py +41 -22
  31. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/pyproject.toml +9 -6
  32. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/Cargo.toml +0 -0
  33. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/examples/row_diff_rss.rs +0 -0
  34. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/examples/type_stack_cost.rs +0 -0
  35. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/src/error.rs +0 -0
  36. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/src/json_rows.rs +0 -0
  37. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/src/lib.rs +0 -0
  38. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/src/options.rs +0 -0
  39. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/src/row_diff.rs +0 -0
  40. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/src/schema.rs +0 -0
  41. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-arrow/src/table_diff.rs +0 -0
  42. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/Cargo.toml +0 -0
  43. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
  44. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/datetime.rs +0 -0
  45. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/datetime_tests.rs +0 -0
  46. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/diff/object.rs +0 -0
  47. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/diff/options.rs +0 -0
  48. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/diff/set.rs +0 -0
  49. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/error.rs +0 -0
  50. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/ignore_order/fxhash.rs +0 -0
  51. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/ignore_order/memo.rs +0 -0
  52. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/ignore_order/mod.rs +0 -0
  53. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/ignore_order/pairing.rs +0 -0
  54. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/lcs_tests.rs +0 -0
  55. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/lib.rs +0 -0
  56. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/report_tests.rs +0 -0
  57. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/unified_diff.rs +0 -0
  58. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/src/unified_diff_tests.rs +0 -0
  59. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/tests/memory_footprint.rs +0 -0
  60. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/tests/proptest_diff.rs +0 -0
  61. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
  62. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/.python-version +0 -0
  63. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/Cargo.toml +0 -0
  64. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/benchmarks/bench_bindings.py +0 -0
  65. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/src/arrow.rs +0 -0
  66. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/src/errors.rs +0 -0
  67. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/src/fast_path.rs +0 -0
  68. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/src/guard.rs +0 -0
  69. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/conftest.py +0 -0
  70. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_bindings_memory.py +0 -0
  71. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_depth_guard.py +0 -0
  72. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_golden_parity.py +0 -0
  73. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_non_finite.py +0 -0
  74. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_signed_zero.py +0 -0
  75. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_smoke.py +0 -0
  76. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_stub_mypy.py +0 -0
  77. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_suite_hygiene.py +0 -0
  78. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_table_diff.py +0 -0
  79. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_table_row_diff.py +0 -0
  80. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_timedeltas.py +0 -0
  81. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/crates/onix-py/tests/test_times.py +0 -0
  82. {deepdiff_rs-0.9.1 → deepdiff_rs-0.9.2}/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.1"
632
+ version = "0.9.2"
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.1"
649
+ version = "0.9.2"
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.1"
657
+ version = "0.9.2"
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.1"
668
+ version = "0.9.2"
669
669
  dependencies = [
670
670
  "arrow-array",
671
671
  "arrow-ipc",
@@ -3,7 +3,7 @@ resolver = "3"
3
3
  members = ["crates/onix-core", "crates/onix-py", "crates/onix-arrow"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.9.1"
6
+ version = "0.9.2"
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.9.1
3
+ Version: 0.9.2
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -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 `MAX_DEPTH_CEILING`.
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,8 +236,8 @@ 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 type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date`/`time`/`timedelta` subclass such as pandas' `Timestamp`) raises `TypeError` rather than being diffed as its base type, because DeepDiff reports each value's own type name. A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. Both are described in [`tests/golden/README.md`](tests/golden/README.md).
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.
@@ -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 `MAX_DEPTH_CEILING`.
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,8 +218,8 @@ 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 type (a `tuple`, `set` or `frozenset` subclass including `namedtuple`, a `datetime`/`date`/`time`/`timedelta` subclass such as pandas' `Timestamp`) raises `TypeError` rather than being diffed as its base type, because DeepDiff reports each value's own type name. A `type_changes` entry's `old_type`/`new_type` are type *names* in `to_dict()`, where DeepDiff returns the type objects. Both are described in [`tests/golden/README.md`](tests/golden/README.md).
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.
@@ -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, normalized_pair,
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(old_value), Value::DateTime(new_value)) = (old_value, new_value) {
182
- let (old_value, new_value) = normalized_pair(path, *old_value, *new_value)?;
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 old_value != new_value {
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(old_value),
191
- new_value: Value::DateTime(new_value),
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: python_type_name(old_value).to_string(),
215
- new_type: python_type_name(new_value).to_string(),
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
- datetime_diff(path, *old, *new, depth, opts.max_depth)
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
- scalar_diff(path, old == new, a, b, depth, opts.max_depth)
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
- // Plain `_diff_time` equality — no normalization step, unlike
65
- // `datetime_diff` (see `crate::datetime`'s module doc).
66
- scalar_diff(
67
- path,
68
- crate::datetime::times_equal(*old, *new),
69
- a,
70
- b,
71
- depth,
72
- opts.max_depth,
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
- scalar_diff(path, old == new, a, b, depth, opts.max_depth)
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
- array_diff(path, old, new, depth, opts, memo)
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
- set_diff(path, old, new, depth, opts, memo)
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
  }
@@ -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, python_type_name, scalar_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;
@@ -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 Python type name `DeepDiff` would report for a given [`Value`].
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: python_type_name(a).to_string(),
64
- new_type: python_type_name(b).to_string(),
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
  )