From e7e8ea4752505f6c90a1fc747782b164c65be76b Mon Sep 17 00:00:00 2001 From: Arie Bovenberg Date: Mon, 5 Oct 2026 20:34:58 +0200 Subject: [PATCH] Remove call-stack inspection from DateTime.__add__ --- src/pendulum/datetime.py | 20 ++++++++++++++------ tests/datetime/test_add.py | 9 +++++++++ tests/datetime/test_timezone.py | 25 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/pendulum/datetime.py b/src/pendulum/datetime.py index 0d7e4f9a0..4a363a500 100644 --- a/src/pendulum/datetime.py +++ b/src/pendulum/datetime.py @@ -2,7 +2,6 @@ import calendar import datetime -import traceback from typing import TYPE_CHECKING from typing import Any @@ -1238,10 +1237,6 @@ def __add__(self, other: datetime.timedelta) -> Self: if not isinstance(other, datetime.timedelta): return NotImplemented - caller = traceback.extract_stack(limit=2)[0].name - if caller == "astimezone": - return super().__add__(other) - return self._add_timedelta_(other) def __radd__(self, other: datetime.timedelta) -> Self: @@ -1278,7 +1273,20 @@ def combine( return cls.instance(datetime.datetime.combine(date, time), tz=tzinfo) def astimezone(self, tz: datetime.tzinfo | None = None) -> Self: - dt = super().astimezone(tz) + # Convert a plain datetime: tzinfo.fromutc() implementations add + # timedeltas to the value they're given and expect the standard + # library's arithmetic, not the overridden __add__. + dt = datetime.datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + tzinfo=self.tzinfo, + fold=self.fold, + ).astimezone(tz) return self.__class__( dt.year, diff --git a/tests/datetime/test_add.py b/tests/datetime/test_add.py index 409f5bd40..94ac26df4 100644 --- a/tests/datetime/test_add.py +++ b/tests/datetime/test_add.py @@ -108,6 +108,15 @@ def test_add_timedelta(): assert d.microsecond == 777777 +def test_add_timedelta_does_not_depend_on_caller_name(): + def astimezone(d): + return d + timedelta(hours=24) + + d = pendulum.datetime(2013, 3, 30, 12, tz="Europe/Paris") + + assert_datetime(astimezone(d), 2013, 3, 31, 13) + + def test_add_duration(): duration = pendulum.duration( years=2, months=3, days=6, seconds=45, microseconds=123456 diff --git a/tests/datetime/test_timezone.py b/tests/datetime/test_timezone.py index 343435d86..af757ef62 100644 --- a/tests/datetime/test_timezone.py +++ b/tests/datetime/test_timezone.py @@ -1,5 +1,11 @@ from __future__ import annotations +from datetime import datetime + +import pytest + +from dateutil import tz + import pendulum from tests.conftest import assert_datetime @@ -36,3 +42,22 @@ def test_astimezone(): d = d.astimezone(pendulum.timezone("Europe/Paris")) assert d.timezone_name == "Europe/Paris" assert_datetime(d, now.year, now.month, now.day, now.hour + 1, now.minute) + + +def test_astimezone_with_tzinfo_doing_arithmetic_in_fromutc(): + # dateutil's tzoffset.fromutc() adds its offset to the datetime it's given + cest = tz.tzoffset("CEST", 7200) + d = pendulum.datetime(2024, 7, 1, 12) + + d = d.astimezone(cest) + assert isinstance(d, pendulum.DateTime) + assert d.tzinfo is cest + assert_datetime(d, 2024, 7, 1, 14) + + +@pytest.mark.parametrize("fold, hour", [(0, 0), (1, 1)]) +def test_astimezone_respects_fold(fold, hour): + d = pendulum.datetime(2024, 10, 27, 2, 30, tz="Europe/Paris", fold=fold) + + d = d.astimezone(pendulum.UTC) + assert d == datetime(2024, 10, 27, hour, 30, tzinfo=pendulum.UTC)