From ca777d71036eb5ab5ff5763573834acecde4928d Mon Sep 17 00:00:00 2001 From: Muneeb Sheikh Date: Sun, 11 Oct 2026 11:23:38 -0400 Subject: [PATCH] Support native timedelta operands in duration arithmetic --- src/pendulum/duration.py | 24 +++++++++++++----------- tests/duration/test_arithmetic.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/pendulum/duration.py b/src/pendulum/duration.py index d6cc0657d..1d1802839 100644 --- a/src/pendulum/duration.py +++ b/src/pendulum/duration.py @@ -2,7 +2,6 @@ from datetime import timedelta from typing import TYPE_CHECKING -from typing import cast from typing import overload import pendulum @@ -43,6 +42,15 @@ def _divide_and_round(a: float, b: float) -> int: return q +def _timedelta_to_microseconds(delta: timedelta) -> int: + if isinstance(delta, Duration): + return delta._to_microseconds() + + return ( + delta.days * SECONDS_PER_DAY + delta.seconds + ) * US_PER_SECOND + delta.microseconds + + class Duration(timedelta): """ Replacement for the standard timedelta class. @@ -386,10 +394,7 @@ def __floordiv__(self, other: int | timedelta) -> int | Duration: usec = self._to_microseconds() if isinstance(other, timedelta): - return cast( - "int", - usec // other._to_microseconds(), # type: ignore[attr-defined] - ) + return usec // _timedelta_to_microseconds(other) if isinstance(other, int): return self.__class__( @@ -412,10 +417,7 @@ def __truediv__(self, other: int | float | timedelta) -> Self | float: usec = self._to_microseconds() if isinstance(other, timedelta): - return cast( - "float", - usec / other._to_microseconds(), # type: ignore[attr-defined] - ) + return usec / _timedelta_to_microseconds(other) if isinstance(other, int): return self.__class__( @@ -441,7 +443,7 @@ def __truediv__(self, other: int | float | timedelta) -> Self | float: def __mod__(self, other: timedelta) -> Self: if isinstance(other, timedelta): - r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined] + r = self._to_microseconds() % _timedelta_to_microseconds(other) return self.__class__(0, 0, r) @@ -451,7 +453,7 @@ def __divmod__(self, other: timedelta) -> tuple[int, Duration]: if isinstance(other, timedelta): q, r = divmod( self._to_microseconds(), - other._to_microseconds(), # type: ignore[attr-defined] + _timedelta_to_microseconds(other), ) return q, self.__class__(0, 0, r) diff --git a/tests/duration/test_arithmetic.py b/tests/duration/test_arithmetic.py index cba4d39d0..8327f13ab 100644 --- a/tests/duration/test_arithmetic.py +++ b/tests/duration/test_arithmetic.py @@ -1,5 +1,11 @@ from __future__ import annotations +import operator + +from datetime import timedelta + +import pytest + import pendulum from tests.conftest import assert_duration @@ -83,3 +89,28 @@ def test_floor_divide(): assert isinstance(mul, pendulum.Duration) assert_duration(mul, 0, 1, 0, 0, 16, 0, 11, 840740) + + +@pytest.mark.parametrize( + "operation", [operator.truediv, operator.floordiv, operator.mod, divmod] +) +@pytest.mark.parametrize( + "divisor", + [ + timedelta(seconds=2, microseconds=1), + timedelta(seconds=-2, microseconds=1), + timedelta(days=500_000, microseconds=1), + ], +) +def test_arithmetic_with_native_timedelta(operation, divisor): + native = timedelta(days=1, seconds=1, microseconds=3) + duration = pendulum.duration(days=1, seconds=1, microseconds=3) + assert operation(duration, divisor) == operation(native, divisor) + + +@pytest.mark.parametrize( + "operation", [operator.truediv, operator.floordiv, operator.mod, divmod] +) +def test_arithmetic_with_zero_native_timedelta(operation): + with pytest.raises(ZeroDivisionError): + operation(pendulum.duration(seconds=1), timedelta())