From 1879016eef985665fc67062db2736ad60e304d2f Mon Sep 17 00:00:00 2001 From: uttam12331 Date: Sun, 5 Jul 2026 17:22:56 +0530 Subject: [PATCH] Fix full-day Durations collapsing to zero on PyPy On PyPy, `Duration.total_seconds()` is overridden to compute the total from `_days`/`_seconds`/`_microseconds`/etc. But `Duration.__new__` called `self.total_seconds()` to derive `_total` *before* those attributes were populated (they are still at their class defaults of 0 at that point), so the computed total was 0 and the whole duration collapsed to `Duration()`. As a result `duration(days=1).total_seconds()` returned 0 on PyPy. Derive the total from `timedelta.total_seconds(self)` instead, which reads the already-initialized underlying `timedelta` value and is independent of the PyPy override. Behavior on CPython is unchanged (there the method is not overridden, so both calls are equivalent). Closes #876 --- src/pendulum/duration.py | 9 ++++++++- tests/duration/test_construct.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/pendulum/duration.py b/src/pendulum/duration.py index d6cc0657..7741a860 100644 --- a/src/pendulum/duration.py +++ b/src/pendulum/duration.py @@ -95,7 +95,14 @@ def __new__( ) # Intuitive normalization - total = self.total_seconds() - (years * 365 + months * 30) * SECONDS_PER_DAY + # Use timedelta's own total_seconds() rather than self.total_seconds(): + # on PyPy the latter is overridden to read _days/_seconds/... which are + # not populated yet at this point, which would zero out the duration + # (see issue #876). + total = ( + timedelta.total_seconds(self) + - (years * 365 + months * 30) * SECONDS_PER_DAY + ) self._total = total m = 1 diff --git a/tests/duration/test_construct.py b/tests/duration/test_construct.py index aaa53909..1d5abe5e 100644 --- a/tests/duration/test_construct.py +++ b/tests/duration/test_construct.py @@ -97,3 +97,16 @@ def test_float_years_and_months(): with pytest.raises(ValueError): pendulum.duration(months=1.5) + + +def test_new_does_not_depend_on_total_seconds_override(monkeypatch): + # __new__ must derive the duration's total from the underlying timedelta, + # not from a (possibly overridden) total_seconds(). On PyPy, total_seconds() + # is overridden to read _days/_seconds/... which are not populated yet during + # __new__, which zeroed out full-day durations (issue #876). + from pendulum.duration import Duration + + monkeypatch.setattr(Duration, "total_seconds", lambda self: 0.0) + + assert pendulum.duration(days=1)._total == 86400.0 + assert pendulum.duration(hours=23)._total == 82800.0