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