Skip to content

Commit 77d1791

Browse files
Make instance() support all native types (date, time, datetime) (#732)
* Add support for time and date types in instance() * Improve inheritance of pendulum objects Co-Authored-By: Chase Sterling <chase.sterling@gmail.com> --------- Co-authored-by: Chase Sterling <chase.sterling@gmail.com>
1 parent 29c15a6 commit 77d1791

File tree

5 files changed

+142
-67
lines changed

5 files changed

+142
-67
lines changed

pendulum/__init__.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -202,34 +202,47 @@ def time(hour: int, minute: int = 0, second: int = 0, microsecond: int = 0) -> T
202202
return Time(hour, minute, second, microsecond)
203203

204204

205+
@overload
205206
def instance(
206-
dt: _datetime.datetime,
207+
obj: _datetime.datetime,
207208
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
208209
) -> DateTime:
210+
...
211+
212+
213+
@overload
214+
def instance(
215+
obj: _datetime.date,
216+
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
217+
) -> Date:
218+
...
219+
220+
221+
@overload
222+
def instance(
223+
obj: _datetime.time,
224+
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
225+
) -> Time:
226+
...
227+
228+
229+
def instance(
230+
obj: _datetime.datetime | _datetime.date | _datetime.time,
231+
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
232+
) -> DateTime | Date | Time:
209233
"""
210-
Create a DateTime instance from a datetime one.
234+
Create a DateTime/Date/Time instance from a datetime/date/time native one.
211235
"""
212-
if not isinstance(dt, _datetime.datetime):
213-
raise ValueError("instance() only accepts datetime objects.")
214-
215-
if isinstance(dt, DateTime):
216-
return dt
236+
if isinstance(obj, (DateTime, Date, Time)):
237+
return obj
217238

218-
tz = dt.tzinfo or tz
239+
if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime):
240+
return date(obj.year, obj.month, obj.day)
219241

220-
if tz is not None:
221-
tz = _safe_timezone(tz, dt=dt)
242+
if isinstance(obj, _datetime.time):
243+
return Time.instance(obj, tz=tz)
222244

223-
return datetime(
224-
dt.year,
225-
dt.month,
226-
dt.day,
227-
dt.hour,
228-
dt.minute,
229-
dt.second,
230-
dt.microsecond,
231-
tz=cast(Union[str, int, Timezone, FixedTimezone, None], tz),
232-
)
245+
return DateTime.instance(obj, tz=tz)
233246

234247

235248
def now(tz: str | Timezone | None = None) -> DateTime:

pendulum/datetime.py

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,29 @@ def create(
122122
fold=dt.fold,
123123
)
124124

125+
@classmethod
126+
def instance(
127+
cls,
128+
dt: datetime.datetime,
129+
tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC,
130+
) -> Self:
131+
tz = dt.tzinfo or tz
132+
133+
if tz is not None:
134+
tz = pendulum._safe_timezone(tz, dt=dt)
135+
136+
return cls.create(
137+
dt.year,
138+
dt.month,
139+
dt.day,
140+
dt.hour,
141+
dt.minute,
142+
dt.second,
143+
dt.microsecond,
144+
tz=tz,
145+
fold=dt.fold,
146+
)
147+
125148
@overload
126149
@classmethod
127150
def now(cls, tz: datetime.tzinfo | None = None) -> Self:
@@ -172,8 +195,8 @@ def today(cls) -> Self:
172195
return cls.now()
173196

174197
@classmethod
175-
def strptime(cls, time: str, fmt: str) -> DateTime:
176-
return pendulum.instance(datetime.datetime.strptime(time, fmt))
198+
def strptime(cls, time: str, fmt: str) -> Self:
199+
return cls.instance(datetime.datetime.strptime(time, fmt))
177200

178201
# Getters/Setters
179202

@@ -472,19 +495,19 @@ def __repr__(self) -> str:
472495
)
473496

474497
# Comparisons
475-
def closest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[override]
498+
def closest(self, *dts: datetime.datetime) -> Self: # type: ignore[override]
476499
"""
477500
Get the farthest date from the instance.
478501
"""
479-
pdts = [pendulum.instance(x) for x in dts]
502+
pdts = [self.instance(x) for x in dts]
480503

481504
return min((abs(self - dt), dt) for dt in pdts)[1]
482505

483-
def farthest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[override]
506+
def farthest(self, *dts: datetime.datetime) -> Self: # type: ignore[override]
484507
"""
485508
Get the farthest date from the instance.
486509
"""
487-
pdts = [pendulum.instance(x) for x in dts]
510+
pdts = [self.instance(x) for x in dts]
488511

489512
return max((abs(self - dt), dt) for dt in pdts)[1]
490513

@@ -516,7 +539,7 @@ def is_same_day(self, dt: datetime.datetime) -> bool: # type: ignore[override]
516539
Checks if the passed in date is the same day
517540
as the instance current day.
518541
"""
519-
dt = pendulum.instance(dt)
542+
dt = self.instance(dt)
520543

521544
return self.to_date_string() == dt.to_date_string()
522545

@@ -530,7 +553,7 @@ def is_anniversary( # type: ignore[override]
530553
if dt is None:
531554
dt = self.now(self.tz)
532555

533-
instance = pendulum.instance(dt)
556+
instance = self.instance(dt)
534557

535558
return (self.month, self.day) == (instance.month, instance.day)
536559

@@ -1192,7 +1215,7 @@ def __sub__(self, other: datetime.datetime | datetime.timedelta) -> Self | Inter
11921215
other.microsecond,
11931216
)
11941217
else:
1195-
other = pendulum.instance(other)
1218+
other = self.instance(other)
11961219

11971220
return other.diff(self, False)
11981221

@@ -1212,7 +1235,7 @@ def __rsub__(self, other: datetime.datetime) -> Interval:
12121235
other.microsecond,
12131236
)
12141237
else:
1215-
other = pendulum.instance(other)
1238+
other = self.instance(other)
12161239

12171240
return self.diff(other, False)
12181241

@@ -1236,29 +1259,27 @@ def __radd__(self, other: datetime.timedelta) -> Self:
12361259
# Native methods override
12371260

12381261
@classmethod
1239-
def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> DateTime:
1262+
def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> Self:
12401263
tzinfo = pendulum._safe_timezone(tz)
12411264

1242-
return pendulum.instance(
1243-
datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo
1244-
)
1265+
return cls.instance(datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo)
12451266

12461267
@classmethod
1247-
def utcfromtimestamp(cls, t: float) -> DateTime:
1248-
return pendulum.instance(datetime.datetime.utcfromtimestamp(t), tz=None)
1268+
def utcfromtimestamp(cls, t: float) -> Self:
1269+
return cls.instance(datetime.datetime.utcfromtimestamp(t), tz=None)
12491270

12501271
@classmethod
1251-
def fromordinal(cls, n: int) -> DateTime:
1252-
return pendulum.instance(datetime.datetime.fromordinal(n), tz=None)
1272+
def fromordinal(cls, n: int) -> Self:
1273+
return cls.instance(datetime.datetime.fromordinal(n), tz=None)
12531274

12541275
@classmethod
12551276
def combine(
12561277
cls,
12571278
date: datetime.date,
12581279
time: datetime.time,
12591280
tzinfo: datetime.tzinfo | None = None,
1260-
) -> DateTime:
1261-
return pendulum.instance(datetime.datetime.combine(date, time), tz=tzinfo)
1281+
) -> Self:
1282+
return cls.instance(datetime.datetime.combine(date, time), tz=tzinfo)
12621283

12631284
def astimezone(self, tz: datetime.tzinfo | None = None) -> Self:
12641285
dt = super().astimezone(tz)
@@ -1321,7 +1342,7 @@ def replace(
13211342
fold=fold,
13221343
)
13231344

1324-
def __getnewargs__(self) -> tuple[DateTime]:
1345+
def __getnewargs__(self) -> tuple[Self]:
13251346
return (self,)
13261347

13271348
def _getstate(
@@ -1341,14 +1362,14 @@ def _getstate(
13411362
def __reduce__(
13421363
self,
13431364
) -> tuple[
1344-
type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]
1365+
type[Self], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]
13451366
]:
13461367
return self.__reduce_ex__(2)
13471368

13481369
def __reduce_ex__(
13491370
self, protocol: SupportsIndex
13501371
) -> tuple[
1351-
type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]
1372+
type[Self], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]
13521373
]:
13531374
return self.__class__, self._getstate(protocol)
13541375

pendulum/time.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,34 @@
1717
from pendulum.duration import AbsoluteDuration
1818
from pendulum.duration import Duration
1919
from pendulum.mixins.default import FormattableMixin
20+
from pendulum.tz.timezone import UTC
2021

2122

2223
if TYPE_CHECKING:
2324
from typing_extensions import Literal
2425
from typing_extensions import Self
2526
from typing_extensions import SupportsIndex
2627

28+
from pendulum.tz.timezone import FixedTimezone
29+
from pendulum.tz.timezone import Timezone
30+
2731

2832
class Time(FormattableMixin, time):
2933
"""
3034
Represents a time instance as hour, minute, second, microsecond.
3135
"""
3236

37+
@classmethod
38+
def instance(
39+
cls, t: time, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC
40+
) -> Self:
41+
tz = t.tzinfo or tz
42+
43+
if tz is not None:
44+
tz = pendulum._safe_timezone(tz)
45+
46+
return cls(t.hour, t.minute, t.second, t.microsecond, tzinfo=tz, fold=t.fold)
47+
3348
# String formatting
3449
def __repr__(self) -> str:
3550
us = ""

tests/datetime/test_construct.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
from datetime import datetime
66

77
import pytest
8-
import pytz
9-
10-
from dateutil import tz
118

129
import pendulum
1310

@@ -83,27 +80,6 @@ def test_yesterday():
8380
assert now.diff(yesterday, False).in_days() == -1
8481

8582

86-
def test_instance_naive_datetime_defaults_to_utc():
87-
now = pendulum.instance(datetime.now())
88-
assert now.timezone_name == "UTC"
89-
90-
91-
def test_instance_timezone_aware_datetime():
92-
now = pendulum.instance(datetime.now(timezone("Europe/Paris")))
93-
assert now.timezone_name == "Europe/Paris"
94-
95-
96-
def test_instance_timezone_aware_datetime_pytz():
97-
now = pendulum.instance(datetime.now(pytz.timezone("Europe/Paris")))
98-
assert now.timezone_name == "Europe/Paris"
99-
100-
101-
def test_instance_timezone_aware_datetime_any_tzinfo():
102-
dt = datetime(2016, 8, 7, 12, 34, 56, tzinfo=tz.gettz("Europe/Paris"))
103-
now = pendulum.instance(dt)
104-
assert now.timezone_name == "+02:00"
105-
106-
10783
def test_now():
10884
now = pendulum.now("America/Toronto")
10985
in_paris = pendulum.now("Europe/Paris")

tests/test_main.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,61 @@
11
from __future__ import annotations
22

3+
from datetime import date
4+
from datetime import datetime
5+
from datetime import time
6+
37
import pytz
48

9+
from dateutil import tz
10+
11+
import pendulum
12+
513
from pendulum import _safe_timezone
14+
from pendulum import timezone
615
from pendulum.tz.timezone import Timezone
716

817

18+
def test_instance_with_naive_datetime_defaults_to_utc() -> None:
19+
now = pendulum.instance(datetime.now())
20+
assert now.timezone_name == "UTC"
21+
22+
23+
def test_instance_with_aware_datetime() -> None:
24+
now = pendulum.instance(datetime.now(timezone("Europe/Paris")))
25+
assert now.timezone_name == "Europe/Paris"
26+
27+
28+
def test_instance_with_aware_datetime_pytz() -> None:
29+
now = pendulum.instance(datetime.now(pytz.timezone("Europe/Paris")))
30+
assert now.timezone_name == "Europe/Paris"
31+
32+
33+
def test_instance_with_aware_datetime_any_tzinfo() -> None:
34+
dt = datetime(2016, 8, 7, 12, 34, 56, tzinfo=tz.gettz("Europe/Paris"))
35+
now = pendulum.instance(dt)
36+
assert now.timezone_name == "+02:00"
37+
38+
39+
def test_instance_with_date() -> None:
40+
dt = pendulum.instance(date(2022, 12, 23))
41+
42+
assert isinstance(dt, pendulum.Date)
43+
44+
45+
def test_instance_with_naive_time() -> None:
46+
dt = pendulum.instance(time(12, 34, 56, 123456))
47+
48+
assert isinstance(dt, pendulum.Time)
49+
50+
51+
def test_instance_with_aware_time() -> None:
52+
dt = pendulum.instance(time(12, 34, 56, 123456, tzinfo=timezone("Europe/Paris")))
53+
54+
assert isinstance(dt, pendulum.Time)
55+
assert isinstance(dt.tzinfo, Timezone)
56+
assert dt.tzinfo.name == "Europe/Paris"
57+
58+
959
def test_safe_timezone_with_tzinfo_objects() -> None:
1060
tz = _safe_timezone(pytz.timezone("Europe/Paris"))
1161

0 commit comments

Comments
 (0)