Skip to content
Open
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
16 changes: 11 additions & 5 deletions src/pendulum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,32 +201,38 @@ def time(hour: int, minute: int = 0, second: int = 0, microsecond: int = 0) -> T
@overload
def instance(
obj: _datetime.datetime,
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = None,
) -> DateTime: ...


@overload
def instance(
obj: _datetime.date,
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = None,
) -> Date: ...


@overload
def instance(
obj: _datetime.time,
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = None,
) -> Time: ...


def instance(
obj: _datetime.datetime | _datetime.date | _datetime.time,
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC,
tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = None,
) -> DateTime | Date | Time:
"""
Create a DateTime/Date/Time instance from a datetime/date/time native one.
"""
if isinstance(obj, (DateTime, Date, Time)):
if isinstance(obj, DateTime):
if tz is not None:
return DateTime.instance(obj, tz=tz)

return obj

if isinstance(obj, (Date, Time)):
return obj

if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime):
Expand Down
62 changes: 52 additions & 10 deletions src/pendulum/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,19 @@ def create(
def instance(
cls,
dt: datetime.datetime,
tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC,
tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None,
) -> Self:
tz = dt.tzinfo or tz

if tz is not None:
# An explicitly given timezone takes precedence over the one
# carried by the datetime: aware datetimes are converted to it,
# while naive ones are interpreted in it.
tz = pendulum._safe_timezone(tz, dt=dt)

if dt.tzinfo is not None:
dt = dt.astimezone(tz)
else:
tz = pendulum._safe_timezone(dt.tzinfo or UTC, dt=dt)

return cls.create(
dt.year,
dt.month,
Expand Down Expand Up @@ -1256,16 +1262,26 @@ def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> Self:

@classmethod
def utcfromtimestamp(cls, t: float) -> Self:
return cls.instance(
datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc).replace(
tzinfo=None
),
tz=None,
dt = datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc).replace(
tzinfo=None
)

return cls(
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
dt.microsecond,
fold=dt.fold,
)

@classmethod
def fromordinal(cls, n: int) -> Self:
return cls.instance(datetime.datetime.fromordinal(n), tz=None)
dt = datetime.datetime.fromordinal(n)

return cls(dt.year, dt.month, dt.day)

@classmethod
def combine(
Expand All @@ -1274,7 +1290,33 @@ def combine(
time: datetime.time,
tzinfo: datetime.tzinfo | None = None,
) -> Self:
return cls.instance(datetime.datetime.combine(date, time), tz=tzinfo)
dt = datetime.datetime.combine(date, time)

tz = tzinfo if tzinfo is not None else dt.tzinfo

if tz is None:
return cls(
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
dt.microsecond,
fold=dt.fold,
)

return cls.create(
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
dt.microsecond,
tz=tz,
fold=dt.fold,
)

def astimezone(self, tz: datetime.tzinfo | None = None) -> Self:
# Convert a plain datetime: tzinfo.fromutc() implementations add
Expand Down
8 changes: 4 additions & 4 deletions src/pendulum/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def _parse(
duration = parsed.duration

if parsed.start is not None:
dt = pendulum.instance(parsed.start, tz=options.get("tz", UTC))
dt = pendulum.instance(parsed.start, tz=options.get("tz"))

return pendulum.interval(
dt,
Expand All @@ -92,7 +92,7 @@ def _parse(
)

dt = pendulum.instance(
t.cast("datetime.datetime", parsed.end), tz=options.get("tz", UTC)
t.cast("datetime.datetime", parsed.end), tz=options.get("tz")
)

return pendulum.interval(
Expand All @@ -111,10 +111,10 @@ def _parse(

return pendulum.interval(
pendulum.instance(
t.cast("datetime.datetime", parsed.start), tz=options.get("tz", UTC)
t.cast("datetime.datetime", parsed.start), tz=options.get("tz")
),
pendulum.instance(
t.cast("datetime.datetime", parsed.end), tz=options.get("tz", UTC)
t.cast("datetime.datetime", parsed.end), tz=options.get("tz")
),
)

Expand Down
8 changes: 4 additions & 4 deletions src/pendulum/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ class Time(FormattableMixin, time):

@classmethod
def instance(
cls, t: time, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC
cls, t: time, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None
) -> Self:
tz = t.tzinfo or tz
if tz is None:
tz = t.tzinfo or UTC

if tz is not None:
tz = pendulum._safe_timezone(tz)
tz = pendulum._safe_timezone(tz)

return cls(t.hour, t.minute, t.second, t.microsecond, tzinfo=tz, fold=t.fold)

Expand Down
36 changes: 36 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,42 @@ def test_instance_with_aware_datetime_any_tzinfo() -> None:
assert now.timezone_name == "+02:00"


def test_instance_with_aware_datetime_and_explicit_tz_converts() -> None:
dt = datetime(2024, 9, 3, 13, 56, 35, tzinfo=timezone("Europe/Brussels"))
now = pendulum.instance(dt, tz="EST")

assert now.timezone_name == "EST"
# 13:56 in Brussels (UTC+2) is 06:56 EST (UTC-5)
assert (now.hour, now.minute, now.second) == (6, 56, 35)
assert now == dt


def test_instance_with_pendulum_datetime_and_explicit_tz_converts() -> None:
dt = pendulum.datetime(2024, 9, 3, 13, 56, 35, tz="Europe/Brussels")
now = pendulum.instance(dt, tz="EST")

assert now.timezone_name == "EST"
assert (now.hour, now.minute, now.second) == (6, 56, 35)
assert now == dt


def test_instance_with_naive_datetime_and_explicit_tz() -> None:
now = pendulum.instance(datetime(2024, 9, 3, 13, 56, 35), tz="Europe/Paris")

assert now.timezone_name == "Europe/Paris"
assert (now.hour, now.minute, now.second) == (13, 56, 35)


def test_datetime_instance_with_explicit_tz_converts() -> None:
dt = pendulum.datetime(2024, 9, 3, 13, 56, 35, tz="Europe/Brussels")
converted = pendulum.DateTime.instance(dt, tz="America/New_York")

assert converted.timezone_name == "America/New_York"
# 13:56 in Brussels (UTC+2) is 07:56 in New York (UTC-4)
assert (converted.hour, converted.minute, converted.second) == (7, 56, 35)
assert converted == dt


def test_instance_with_date() -> None:
dt = pendulum.instance(date(2022, 12, 23))

Expand Down