From 0c94f35911a4410ce02227f226be6f4a36bb343f Mon Sep 17 00:00:00 2001 From: Chase Sterling Date: Fri, 5 May 2023 14:35:28 -0400 Subject: [PATCH 1/3] Allow subclassing DateTimes to work as expected --- pendulum/__init__.py | 22 +------------- pendulum/datetime.py | 68 +++++++++++++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/pendulum/__init__.py b/pendulum/__init__.py index 449124424..3ef9d0791 100644 --- a/pendulum/__init__.py +++ b/pendulum/__init__.py @@ -176,27 +176,7 @@ def instance( """ Create a DateTime instance from a datetime one. """ - if not isinstance(dt, _datetime.datetime): - raise ValueError("instance() only accepts datetime objects.") - - if isinstance(dt, DateTime): - return dt - - tz = dt.tzinfo or tz - - if tz is not None: - tz = _safe_timezone(tz, dt=dt) - - return datetime( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tz=cast(Union[str, int, Timezone, FixedTimezone, None], tz), - ) + return DateTime._instance(dt, tz) def now(tz: str | Timezone | None = None) -> DateTime: diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 52ad3cc78..0eb025f39 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -7,6 +7,7 @@ from typing import Any from typing import Callable from typing import Optional +from typing import Union from typing import cast from typing import overload @@ -116,6 +117,37 @@ def create( fold=dt.fold, ) + @classmethod + def _instance( + cls, + dt: datetime.datetime, + tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC, + ) -> DateTime: + """ + Create a DateTime instance from a datetime one. + """ + if not isinstance(dt, datetime.datetime): + raise ValueError("instance() only accepts datetime objects.") + + if isinstance(dt, DateTime): + return dt + + 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=cast(Union[str, int, Timezone, FixedTimezone, None], tz), + ) + @overload @classmethod def now(cls, tz: datetime.tzinfo | None = None) -> DateTime: @@ -167,7 +199,7 @@ def today(cls) -> DateTime: @classmethod def strptime(cls, time: str, fmt: str) -> DateTime: - return pendulum.instance(datetime.datetime.strptime(time, fmt)) + return cls._instance(datetime.datetime.strptime(time, fmt)) # Getters/Setters @@ -199,9 +231,7 @@ def set( if tz is None: tz = self.tz - return DateTime.create( - year, month, day, hour, minute, second, microsecond, tz=tz - ) + return self.create(year, month, day, hour, minute, second, microsecond, tz=tz) @property def float_timestamp(self) -> float: @@ -470,7 +500,7 @@ def closest(self, *dts: datetime.datetime) -> DateTime: # 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] @@ -478,7 +508,7 @@ def farthest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[overrid """ 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] @@ -510,7 +540,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() @@ -524,7 +554,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) @@ -1184,7 +1214,7 @@ def __sub__( if not isinstance(other, self.__class__): if other.tzinfo is None: - other = pendulum.naive( + other = self.__class__( other.year, other.month, other.day, @@ -1194,7 +1224,7 @@ def __sub__( other.microsecond, ) else: - other = pendulum.instance(other) + other = self._instance(other) return other.diff(self, False) @@ -1204,7 +1234,7 @@ def __rsub__(self, other: datetime.datetime) -> Interval: if not isinstance(other, self.__class__): if other.tzinfo is None: - other = pendulum.naive( + other = self.__class__( other.year, other.month, other.day, @@ -1214,7 +1244,7 @@ def __rsub__(self, other: datetime.datetime) -> Interval: other.microsecond, ) else: - other = pendulum.instance(other) + other = self._instance(other) return self.diff(other, False) @@ -1243,17 +1273,15 @@ def __radd__(self, other: datetime.timedelta) -> DateTime: def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> DateTime: 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) + 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) + return cls._instance(datetime.datetime.fromordinal(n), tz=None) @classmethod def combine( @@ -1262,7 +1290,7 @@ def combine( time: datetime.time, tzinfo: datetime.tzinfo | None = None, ) -> DateTime: - return pendulum.instance(datetime.datetime.combine(date, time), tz=tzinfo) + return cls._instance(datetime.datetime.combine(date, time), tz=tzinfo) def astimezone(self, tz: datetime.tzinfo | None = None) -> DateTime: dt = super().astimezone(tz) @@ -1313,7 +1341,7 @@ def replace( if tzinfo is not None: tzinfo = pendulum._safe_timezone(tzinfo) - return DateTime.create( + return self.__class__( year, month, day, @@ -1321,7 +1349,7 @@ def replace( minute, second, microsecond, - tz=tzinfo, + tzinfo=tzinfo, fold=fold, ) From bbb1746fd5bd8849261edc7042a923158e1f9a74 Mon Sep 17 00:00:00 2001 From: Chase Sterling Date: Mon, 19 Jun 2023 10:17:15 -0400 Subject: [PATCH 2/3] Ensure _instance always returns the specific subclass of DateTime --- pendulum/datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 0eb025f39..1cfbdbaa9 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -129,7 +129,7 @@ def _instance( if not isinstance(dt, datetime.datetime): raise ValueError("instance() only accepts datetime objects.") - if isinstance(dt, DateTime): + if isinstance(dt, cls): return dt tz = dt.tzinfo or tz From 7ba888b2849f08815765ab407cf6971dbb0fb6b0 Mon Sep 17 00:00:00 2001 From: Chase Sterling Date: Tue, 8 Aug 2023 12:05:27 -0400 Subject: [PATCH 3/3] Update DateTime type hints to return Self where appropriate --- pendulum/datetime.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 640a0df78..85bceaa7e 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -129,7 +129,7 @@ def _instance( cls, dt: datetime.datetime, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC, - ) -> DateTime: + ) -> Self: """ Create a DateTime instance from a datetime one. """ @@ -205,7 +205,7 @@ def today(cls) -> Self: return cls.now() @classmethod - def strptime(cls, time: str, fmt: str) -> DateTime: + def strptime(cls, time: str, fmt: str) -> Self: return cls._instance(datetime.datetime.strptime(time, fmt)) # Getters/Setters @@ -505,7 +505,7 @@ 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. """ @@ -513,7 +513,7 @@ def closest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[override 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. """ @@ -540,7 +540,7 @@ def is_long_year(self) -> bool: See link `https://en.wikipedia.org/wiki/ISO_8601#Week_dates`_ """ return ( - DateTime.create(self.year, 12, 28, 0, 0, 0, tz=self.tz).isocalendar()[1] + self.create(self.year, 12, 28, 0, 0, 0, tz=self.tz).isocalendar()[1] == 53 ) @@ -1263,17 +1263,17 @@ 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 cls._instance(datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo) @classmethod - def utcfromtimestamp(cls, t: float) -> DateTime: + def utcfromtimestamp(cls, t: float) -> Self: return cls._instance(datetime.datetime.utcfromtimestamp(t), tz=None) @classmethod - def fromordinal(cls, n: int) -> DateTime: + def fromordinal(cls, n: int) -> Self: return cls._instance(datetime.datetime.fromordinal(n), tz=None) @classmethod @@ -1282,7 +1282,7 @@ def combine( date: datetime.date, time: datetime.time, tzinfo: datetime.tzinfo | None = None, - ) -> DateTime: + ) -> Self: return cls._instance(datetime.datetime.combine(date, time), tz=tzinfo) def astimezone(self, tz: datetime.tzinfo | None = None) -> Self: @@ -1346,7 +1346,7 @@ def replace( fold=fold, ) - def __getnewargs__(self) -> tuple[DateTime]: + def __getnewargs__(self) -> tuple[Self]: return (self,) def _getstate( @@ -1366,14 +1366,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)