From 91fa82ded191ceb14083f9473f5a54736c2b298c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Fri, 23 Dec 2022 11:06:41 +0100 Subject: [PATCH 1/2] Add support for time and date types in instance() --- pendulum/__init__.py | 57 +++++++++++++++++++++++--------- tests/datetime/test_construct.py | 24 -------------- tests/test_main.py | 50 ++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 39 deletions(-) diff --git a/pendulum/__init__.py b/pendulum/__init__.py index 76763ce9..adca19b1 100644 --- a/pendulum/__init__.py +++ b/pendulum/__init__.py @@ -202,32 +202,59 @@ def time(hour: int, minute: int = 0, second: int = 0, microsecond: int = 0) -> T return Time(hour, minute, second, microsecond) +@overload def instance( - dt: _datetime.datetime, + obj: _datetime.datetime, tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, ) -> DateTime: + ... + + +@overload +def instance( + obj: _datetime.date, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> Date: + ... + + +@overload +def instance( + obj: _datetime.time, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> Time: + ... + + +def instance( + obj: _datetime.datetime | _datetime.date | _datetime.time, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> DateTime | Date | Time: """ - Create a DateTime instance from a datetime one. + Create a DateTime/Date/Time instance from a datetime/date/time native one. """ - if not isinstance(dt, _datetime.datetime): - raise ValueError("instance() only accepts datetime objects.") + if isinstance(obj, (DateTime, Date, Time)): + return obj - if isinstance(dt, DateTime): - return dt + if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime): + return date(obj.year, obj.month, obj.day) - tz = dt.tzinfo or tz + tz = obj.tzinfo or tz if tz is not None: - tz = _safe_timezone(tz, dt=dt) + tz = _safe_timezone(tz, dt=obj if isinstance(obj, _datetime.datetime) else None) + + if isinstance(obj, _datetime.time): + return Time(obj.hour, obj.minute, obj.second, obj.microsecond, tzinfo=tz) return datetime( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, + obj.year, + obj.month, + obj.day, + obj.hour, + obj.minute, + obj.second, + obj.microsecond, tz=cast(Union[str, int, Timezone, FixedTimezone, None], tz), ) diff --git a/tests/datetime/test_construct.py b/tests/datetime/test_construct.py index d0e659d5..b083cbef 100644 --- a/tests/datetime/test_construct.py +++ b/tests/datetime/test_construct.py @@ -5,9 +5,6 @@ from datetime import datetime import pytest -import pytz - -from dateutil import tz import pendulum @@ -83,27 +80,6 @@ def test_yesterday(): assert now.diff(yesterday, False).in_days() == -1 -def test_instance_naive_datetime_defaults_to_utc(): - now = pendulum.instance(datetime.now()) - assert now.timezone_name == "UTC" - - -def test_instance_timezone_aware_datetime(): - now = pendulum.instance(datetime.now(timezone("Europe/Paris"))) - assert now.timezone_name == "Europe/Paris" - - -def test_instance_timezone_aware_datetime_pytz(): - now = pendulum.instance(datetime.now(pytz.timezone("Europe/Paris"))) - assert now.timezone_name == "Europe/Paris" - - -def test_instance_timezone_aware_datetime_any_tzinfo(): - dt = datetime(2016, 8, 7, 12, 34, 56, tzinfo=tz.gettz("Europe/Paris")) - now = pendulum.instance(dt) - assert now.timezone_name == "+02:00" - - def test_now(): now = pendulum.now("America/Toronto") in_paris = pendulum.now("Europe/Paris") diff --git a/tests/test_main.py b/tests/test_main.py index 7d6c46b0..011a6b1d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,11 +1,61 @@ from __future__ import annotations +from datetime import date +from datetime import datetime +from datetime import time + import pytz +from dateutil import tz + +import pendulum + from pendulum import _safe_timezone +from pendulum import timezone from pendulum.tz.timezone import Timezone +def test_instance_with_naive_datetime_defaults_to_utc() -> None: + now = pendulum.instance(datetime.now()) + assert now.timezone_name == "UTC" + + +def test_instance_with_aware_datetime() -> None: + now = pendulum.instance(datetime.now(timezone("Europe/Paris"))) + assert now.timezone_name == "Europe/Paris" + + +def test_instance_with_aware_datetime_pytz() -> None: + now = pendulum.instance(datetime.now(pytz.timezone("Europe/Paris"))) + assert now.timezone_name == "Europe/Paris" + + +def test_instance_with_aware_datetime_any_tzinfo() -> None: + dt = datetime(2016, 8, 7, 12, 34, 56, tzinfo=tz.gettz("Europe/Paris")) + now = pendulum.instance(dt) + assert now.timezone_name == "+02:00" + + +def test_instance_with_date() -> None: + dt = pendulum.instance(date(2022, 12, 23)) + + assert isinstance(dt, pendulum.Date) + + +def test_instance_with_naive_time() -> None: + dt = pendulum.instance(time(12, 34, 56, 123456)) + + assert isinstance(dt, pendulum.Time) + + +def test_instance_with_aware_time() -> None: + dt = pendulum.instance(time(12, 34, 56, 123456, tzinfo=timezone("Europe/Paris"))) + + assert isinstance(dt, pendulum.Time) + assert isinstance(dt.tzinfo, Timezone) + assert dt.tzinfo.name == "Europe/Paris" + + def test_safe_timezone_with_tzinfo_objects() -> None: tz = _safe_timezone(pytz.timezone("Europe/Paris")) From e807ca091e584aae8fa559bca8fcd4eb06a9a295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 15 Aug 2023 00:19:27 +0200 Subject: [PATCH 2/2] Improve inheritance of pendulum objects Co-Authored-By: Chase Sterling --- pendulum/__init__.py | 18 ++---------- pendulum/datetime.py | 67 +++++++++++++++++++++++++++++--------------- pendulum/time.py | 15 ++++++++++ 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/pendulum/__init__.py b/pendulum/__init__.py index adca19b1..1f70f14f 100644 --- a/pendulum/__init__.py +++ b/pendulum/__init__.py @@ -239,24 +239,10 @@ def instance( if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime): return date(obj.year, obj.month, obj.day) - tz = obj.tzinfo or tz - - if tz is not None: - tz = _safe_timezone(tz, dt=obj if isinstance(obj, _datetime.datetime) else None) - if isinstance(obj, _datetime.time): - return Time(obj.hour, obj.minute, obj.second, obj.microsecond, tzinfo=tz) + return Time.instance(obj, tz=tz) - return datetime( - obj.year, - obj.month, - obj.day, - obj.hour, - obj.minute, - obj.second, - obj.microsecond, - tz=cast(Union[str, int, Timezone, FixedTimezone, None], tz), - ) + return DateTime.instance(obj, tz=tz) def now(tz: str | Timezone | None = None) -> DateTime: diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 5f369100..0bb837fe 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -122,6 +122,29 @@ def create( fold=dt.fold, ) + @classmethod + def instance( + cls, + dt: datetime.datetime, + tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC, + ) -> Self: + tz = dt.tzinfo or tz + + if tz is not None: + tz = pendulum._safe_timezone(tz, dt=dt) + + return cls.create( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tz=tz, + fold=dt.fold, + ) + @overload @classmethod def now(cls, tz: datetime.tzinfo | None = None) -> Self: @@ -172,8 +195,8 @@ def today(cls) -> Self: return cls.now() @classmethod - def strptime(cls, time: str, fmt: str) -> DateTime: - return pendulum.instance(datetime.datetime.strptime(time, fmt)) + def strptime(cls, time: str, fmt: str) -> Self: + return cls.instance(datetime.datetime.strptime(time, fmt)) # Getters/Setters @@ -472,19 +495,19 @@ def __repr__(self) -> str: ) # Comparisons - def closest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[override] + def closest(self, *dts: datetime.datetime) -> Self: # type: ignore[override] """ Get the farthest date from the instance. """ - pdts = [pendulum.instance(x) for x in dts] + pdts = [self.instance(x) for x in dts] return min((abs(self - dt), dt) for dt in pdts)[1] - def farthest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[override] + def farthest(self, *dts: datetime.datetime) -> Self: # type: ignore[override] """ Get the farthest date from the instance. """ - pdts = [pendulum.instance(x) for x in dts] + pdts = [self.instance(x) for x in dts] return max((abs(self - dt), dt) for dt in pdts)[1] @@ -516,7 +539,7 @@ def is_same_day(self, dt: datetime.datetime) -> bool: # type: ignore[override] Checks if the passed in date is the same day as the instance current day. """ - dt = pendulum.instance(dt) + dt = self.instance(dt) return self.to_date_string() == dt.to_date_string() @@ -530,7 +553,7 @@ def is_anniversary( # type: ignore[override] if dt is None: dt = self.now(self.tz) - instance = pendulum.instance(dt) + instance = self.instance(dt) return (self.month, self.day) == (instance.month, instance.day) @@ -1192,7 +1215,7 @@ def __sub__(self, other: datetime.datetime | datetime.timedelta) -> Self | Inter other.microsecond, ) else: - other = pendulum.instance(other) + other = self.instance(other) return other.diff(self, False) @@ -1212,7 +1235,7 @@ def __rsub__(self, other: datetime.datetime) -> Interval: other.microsecond, ) else: - other = pendulum.instance(other) + other = self.instance(other) return self.diff(other, False) @@ -1236,20 +1259,18 @@ def __radd__(self, other: datetime.timedelta) -> Self: # Native methods override @classmethod - def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> DateTime: + def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> Self: tzinfo = pendulum._safe_timezone(tz) - return pendulum.instance( - datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo - ) + return cls.instance(datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo) @classmethod - def utcfromtimestamp(cls, t: float) -> DateTime: - return pendulum.instance(datetime.datetime.utcfromtimestamp(t), tz=None) + def utcfromtimestamp(cls, t: float) -> Self: + return cls.instance(datetime.datetime.utcfromtimestamp(t), tz=None) @classmethod - def fromordinal(cls, n: int) -> DateTime: - return pendulum.instance(datetime.datetime.fromordinal(n), tz=None) + def fromordinal(cls, n: int) -> Self: + return cls.instance(datetime.datetime.fromordinal(n), tz=None) @classmethod def combine( @@ -1257,8 +1278,8 @@ def combine( date: datetime.date, time: datetime.time, tzinfo: datetime.tzinfo | None = None, - ) -> DateTime: - return pendulum.instance(datetime.datetime.combine(date, time), tz=tzinfo) + ) -> Self: + return cls.instance(datetime.datetime.combine(date, time), tz=tzinfo) def astimezone(self, tz: datetime.tzinfo | None = None) -> Self: dt = super().astimezone(tz) @@ -1321,7 +1342,7 @@ def replace( fold=fold, ) - def __getnewargs__(self) -> tuple[DateTime]: + def __getnewargs__(self) -> tuple[Self]: return (self,) def _getstate( @@ -1341,14 +1362,14 @@ def _getstate( def __reduce__( self, ) -> tuple[ - type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None] + type[Self], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None] ]: return self.__reduce_ex__(2) def __reduce_ex__( self, protocol: SupportsIndex ) -> tuple[ - type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None] + type[Self], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None] ]: return self.__class__, self._getstate(protocol) diff --git a/pendulum/time.py b/pendulum/time.py index f1edb107..23c79c00 100644 --- a/pendulum/time.py +++ b/pendulum/time.py @@ -17,6 +17,7 @@ from pendulum.duration import AbsoluteDuration from pendulum.duration import Duration from pendulum.mixins.default import FormattableMixin +from pendulum.tz.timezone import UTC if TYPE_CHECKING: @@ -24,12 +25,26 @@ from typing_extensions import Self from typing_extensions import SupportsIndex + from pendulum.tz.timezone import FixedTimezone + from pendulum.tz.timezone import Timezone + class Time(FormattableMixin, time): """ Represents a time instance as hour, minute, second, microsecond. """ + @classmethod + def instance( + cls, t: time, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC + ) -> Self: + tz = t.tzinfo or tz + + if tz is not None: + tz = pendulum._safe_timezone(tz) + + return cls(t.hour, t.minute, t.second, t.microsecond, tzinfo=tz, fold=t.fold) + # String formatting def __repr__(self) -> str: us = ""