Skip to content

Commit

Permalink
Make instance() support all native types (date, time, datetime) (#732)
Browse files Browse the repository at this point in the history
* Add support for time and date types in instance()

* Improve inheritance of pendulum objects

Co-Authored-By: Chase Sterling <[email protected]>

---------

Co-authored-by: Chase Sterling <[email protected]>
  • Loading branch information
sdispater and gazpachoking authored Aug 15, 2023
1 parent 29c15a6 commit 77d1791
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 67 deletions.
53 changes: 33 additions & 20 deletions pendulum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,34 +202,47 @@ 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(dt, DateTime):
return dt
if isinstance(obj, (DateTime, Date, Time)):
return obj

tz = dt.tzinfo or tz
if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime):
return date(obj.year, obj.month, obj.day)

if tz is not None:
tz = _safe_timezone(tz, dt=dt)
if isinstance(obj, _datetime.time):
return Time.instance(obj, tz=tz)

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(obj, tz=tz)


def now(tz: str | Timezone | None = None) -> DateTime:
Expand Down
67 changes: 44 additions & 23 deletions pendulum/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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()

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -1236,29 +1259,27 @@ 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(
cls,
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)
Expand Down Expand Up @@ -1321,7 +1342,7 @@ def replace(
fold=fold,
)

def __getnewargs__(self) -> tuple[DateTime]:
def __getnewargs__(self) -> tuple[Self]:
return (self,)

def _getstate(
Expand All @@ -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)

Expand Down
15 changes: 15 additions & 0 deletions pendulum/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,34 @@
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:
from typing_extensions import Literal
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 = ""
Expand Down
24 changes: 0 additions & 24 deletions tests/datetime/test_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
from datetime import datetime

import pytest
import pytz

from dateutil import tz

import pendulum

Expand Down Expand Up @@ -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")
Expand Down
50 changes: 50 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -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"))

Expand Down

0 comments on commit 77d1791

Please sign in to comment.