From 6f8e424ea7670b9cbef1e67d0e4d768926aa6f44 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:35:45 -0700 Subject: [PATCH] fix: skip truncate_datetime for non-truncatable types Replace the has_time check with a positive isinstance check so truncate_datetime only applies to datetime and time objects. This fixes the AttributeError when diffing timedelta values (timedelta has no .replace()) and avoids the same crash for np.datetime64 in DeepHash. Adds regression tests for timedelta and np.datetime64. Targets dev, no CHANGELOG edit. --- deepdiff/helper.py | 7 ++++--- tests/test_diff_datetime.py | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/deepdiff/helper.py b/deepdiff/helper.py index 5143a3b7..8cb6cfa2 100644 --- a/deepdiff/helper.py +++ b/deepdiff/helper.py @@ -691,9 +691,10 @@ def datetime_normalize( datetime.timezone, "BaseTzInfo" ] = datetime.timezone.utc, ) -> Any: - # A plain date has no time component, so truncation does not apply to it. - has_time = not (isinstance(obj, datetime.date) and not isinstance(obj, datetime.datetime)) - if truncate_datetime and has_time: + # Only datetime and time objects carry truncatable time fields. A plain + # date has no time component, and other types (e.g. timedelta) do not + # support .replace() with time fields, so truncation skips them. + if truncate_datetime and isinstance(obj, (datetime.datetime, datetime.time)): if truncate_datetime == 'second': obj = obj.replace(microsecond=0) elif truncate_datetime == 'minute': diff --git a/tests/test_diff_datetime.py b/tests/test_diff_datetime.py index c3905291..880e458e 100644 --- a/tests/test_diff_datetime.py +++ b/tests/test_diff_datetime.py @@ -1,5 +1,6 @@ +import pytest import pytz -from datetime import date, datetime, time, timezone +from datetime import date, datetime, time, timedelta, timezone from deepdiff import DeepDiff @@ -123,3 +124,23 @@ def test_datetime_within_array_with_timezone_diff(self): assert not DeepDiff(d1, d2) assert not DeepDiff(d1, d2, ignore_order=True) assert not DeepDiff(d1, d2, truncate_datetime='second') + + def test_truncate_datetime_with_timedelta(self): + """truncate_datetime must not crash on timedelta (no .replace()).""" + for truncate_datetime in ('second', 'minute', 'hour', 'day'): + d1 = {"a": timedelta(days=1)} + d2 = {"a": timedelta(days=1)} + assert not DeepDiff(d1, d2, truncate_datetime=truncate_datetime) + d2 = {"a": timedelta(days=2)} + assert DeepDiff(d1, d2, truncate_datetime=truncate_datetime) + + def test_truncate_datetime_with_np_datetime64(self): + """truncate_datetime must not crash on np.datetime64 (no .replace()).""" + np = pytest.importorskip("numpy") + from deepdiff import DeepHash + for truncate_datetime in ('second', 'minute', 'hour', 'day'): + h1 = DeepHash(np.datetime64('2020-05-17'), truncate_datetime=truncate_datetime) + h2 = DeepHash(np.datetime64('2020-05-17'), truncate_datetime=truncate_datetime) + assert h1 == h2 + h3 = DeepHash(np.datetime64('2020-05-18'), truncate_datetime=truncate_datetime) + assert h1 != h3