diff --git a/src/pendulum/__init__.py b/src/pendulum/__init__.py index 16ae0865..73a71c34 100644 --- a/src/pendulum/__init__.py +++ b/src/pendulum/__init__.py @@ -201,32 +201,38 @@ def time(hour: int, minute: int = 0, second: int = 0, microsecond: int = 0) -> T @overload def instance( obj: _datetime.datetime, - tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = None, ) -> DateTime: ... @overload def instance( obj: _datetime.date, - tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = None, ) -> Date: ... @overload def instance( obj: _datetime.time, - tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = None, ) -> Time: ... def instance( obj: _datetime.datetime | _datetime.date | _datetime.time, - tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = None, ) -> DateTime | Date | Time: """ Create a DateTime/Date/Time instance from a datetime/date/time native one. """ - if isinstance(obj, (DateTime, Date, Time)): + if isinstance(obj, DateTime): + if tz is not None: + return DateTime.instance(obj, tz=tz) + + return obj + + if isinstance(obj, (Date, Time)): return obj if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime): diff --git a/src/pendulum/datetime.py b/src/pendulum/datetime.py index 96aff25b..a6c1951c 100644 --- a/src/pendulum/datetime.py +++ b/src/pendulum/datetime.py @@ -128,13 +128,19 @@ def create( def instance( cls, dt: datetime.datetime, - tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC, + tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None, ) -> Self: - tz = dt.tzinfo or tz - if tz is not None: + # An explicitly given timezone takes precedence over the one + # carried by the datetime: aware datetimes are converted to it, + # while naive ones are interpreted in it. tz = pendulum._safe_timezone(tz, dt=dt) + if dt.tzinfo is not None: + dt = dt.astimezone(tz) + else: + tz = pendulum._safe_timezone(dt.tzinfo or UTC, dt=dt) + return cls.create( dt.year, dt.month, @@ -1256,16 +1262,26 @@ def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> Self: @classmethod def utcfromtimestamp(cls, t: float) -> Self: - return cls.instance( - datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc).replace( - tzinfo=None - ), - tz=None, + dt = datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc).replace( + tzinfo=None + ) + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + fold=dt.fold, ) @classmethod def fromordinal(cls, n: int) -> Self: - return cls.instance(datetime.datetime.fromordinal(n), tz=None) + dt = datetime.datetime.fromordinal(n) + + return cls(dt.year, dt.month, dt.day) @classmethod def combine( @@ -1274,7 +1290,33 @@ def combine( time: datetime.time, tzinfo: datetime.tzinfo | None = None, ) -> Self: - return cls.instance(datetime.datetime.combine(date, time), tz=tzinfo) + dt = datetime.datetime.combine(date, time) + + tz = tzinfo if tzinfo is not None else dt.tzinfo + + if tz is None: + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + fold=dt.fold, + ) + + return cls.create( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tz=tz, + fold=dt.fold, + ) def astimezone(self, tz: datetime.tzinfo | None = None) -> Self: # Convert a plain datetime: tzinfo.fromutc() implementations add diff --git a/src/pendulum/parser.py b/src/pendulum/parser.py index 833bae3c..0f498035 100644 --- a/src/pendulum/parser.py +++ b/src/pendulum/parser.py @@ -75,7 +75,7 @@ def _parse( duration = parsed.duration if parsed.start is not None: - dt = pendulum.instance(parsed.start, tz=options.get("tz", UTC)) + dt = pendulum.instance(parsed.start, tz=options.get("tz")) return pendulum.interval( dt, @@ -92,7 +92,7 @@ def _parse( ) dt = pendulum.instance( - t.cast("datetime.datetime", parsed.end), tz=options.get("tz", UTC) + t.cast("datetime.datetime", parsed.end), tz=options.get("tz") ) return pendulum.interval( @@ -111,10 +111,10 @@ def _parse( return pendulum.interval( pendulum.instance( - t.cast("datetime.datetime", parsed.start), tz=options.get("tz", UTC) + t.cast("datetime.datetime", parsed.start), tz=options.get("tz") ), pendulum.instance( - t.cast("datetime.datetime", parsed.end), tz=options.get("tz", UTC) + t.cast("datetime.datetime", parsed.end), tz=options.get("tz") ), ) diff --git a/src/pendulum/time.py b/src/pendulum/time.py index 58ca7e81..a38f741d 100644 --- a/src/pendulum/time.py +++ b/src/pendulum/time.py @@ -36,12 +36,12 @@ class Time(FormattableMixin, time): @classmethod def instance( - cls, t: time, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC + cls, t: time, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None ) -> Self: - tz = t.tzinfo or tz + if tz is None: + tz = t.tzinfo or UTC - if tz is not None: - tz = pendulum._safe_timezone(tz) + tz = pendulum._safe_timezone(tz) return cls(t.hour, t.minute, t.second, t.microsecond, tzinfo=tz, fold=t.fold) diff --git a/tests/test_main.py b/tests/test_main.py index 76033450..a86f0228 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -36,6 +36,42 @@ def test_instance_with_aware_datetime_any_tzinfo() -> None: assert now.timezone_name == "+02:00" +def test_instance_with_aware_datetime_and_explicit_tz_converts() -> None: + dt = datetime(2024, 9, 3, 13, 56, 35, tzinfo=timezone("Europe/Brussels")) + now = pendulum.instance(dt, tz="EST") + + assert now.timezone_name == "EST" + # 13:56 in Brussels (UTC+2) is 06:56 EST (UTC-5) + assert (now.hour, now.minute, now.second) == (6, 56, 35) + assert now == dt + + +def test_instance_with_pendulum_datetime_and_explicit_tz_converts() -> None: + dt = pendulum.datetime(2024, 9, 3, 13, 56, 35, tz="Europe/Brussels") + now = pendulum.instance(dt, tz="EST") + + assert now.timezone_name == "EST" + assert (now.hour, now.minute, now.second) == (6, 56, 35) + assert now == dt + + +def test_instance_with_naive_datetime_and_explicit_tz() -> None: + now = pendulum.instance(datetime(2024, 9, 3, 13, 56, 35), tz="Europe/Paris") + + assert now.timezone_name == "Europe/Paris" + assert (now.hour, now.minute, now.second) == (13, 56, 35) + + +def test_datetime_instance_with_explicit_tz_converts() -> None: + dt = pendulum.datetime(2024, 9, 3, 13, 56, 35, tz="Europe/Brussels") + converted = pendulum.DateTime.instance(dt, tz="America/New_York") + + assert converted.timezone_name == "America/New_York" + # 13:56 in Brussels (UTC+2) is 07:56 in New York (UTC-4) + assert (converted.hour, converted.minute, converted.second) == (7, 56, 35) + assert converted == dt + + def test_instance_with_date() -> None: dt = pendulum.instance(date(2022, 12, 23))