Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make instance() support all native types (date, time, datetime) #732

Merged
merged 2 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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