Skip to content
Draft
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
6 changes: 5 additions & 1 deletion src/pendulum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,11 @@ def from_timestamp(timestamp: int | float, tz: str | Timezone = UTC) -> DateTime
"""
Create a DateTime instance from a timestamp.
"""
dt = _datetime.datetime.fromtimestamp(timestamp, tz=UTC)
try:
dt = _datetime.datetime.fromtimestamp(timestamp, tz=UTC)
except (OSError, OverflowError):
epoch = _datetime.datetime(1970, 1, 1, tzinfo=UTC)
dt = epoch + _datetime.timedelta(seconds=timestamp)

dt = datetime(
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond
Expand Down
16 changes: 14 additions & 2 deletions src/pendulum/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1253,11 +1253,23 @@ def __radd__(self, other: datetime.timedelta) -> Self:
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)
try:
dt = datetime.datetime.fromtimestamp(t, tz=tzinfo)
except (OSError, OverflowError):
dt = (cls._EPOCH + datetime.timedelta(seconds=t)).astimezone(tzinfo)

return cls.instance(dt, tz=tzinfo)

@classmethod
def utcfromtimestamp(cls, t: float) -> Self:
return cls.instance(datetime.datetime.utcfromtimestamp(t), tz=None)
try:
dt = datetime.datetime.utcfromtimestamp(t)
except (OSError, OverflowError):
# Match datetime.datetime.utcfromtimestamp(), which returns a
# naive datetime representing UTC.
dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=t)

return cls.instance(dt, tz=None)

@classmethod
def fromordinal(cls, n: int) -> Self:
Expand Down
53 changes: 53 additions & 0 deletions tests/datetime/test_behavior.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from __future__ import annotations

import datetime as datetime_
import importlib
import pickle
import types
import zoneinfo

import pytest

from copy import deepcopy
from datetime import date
from datetime import datetime
Expand Down Expand Up @@ -107,6 +112,54 @@ def test_utcfromtimestamp():
assert p == dt


@pytest.mark.parametrize("exception_type", [OSError, OverflowError])
def test_fromtimestamp_falls_back_for_negative_timestamp(monkeypatch, exception_type):
pendulum_datetime_module = importlib.import_module("pendulum.datetime")

class FakeDateTime(datetime_.datetime):
@classmethod
def fromtimestamp(cls, t: float, tz: datetime_.tzinfo | None = None):
if t == -43201:
raise exception_type("Invalid argument")

return super().fromtimestamp(t, tz=tz)

monkeypatch.setattr(
pendulum_datetime_module,
"datetime",
types.SimpleNamespace(datetime=FakeDateTime, timedelta=datetime_.timedelta),
)

p = pendulum.DateTime.fromtimestamp(-43201, pendulum.UTC)

assert p == pendulum.datetime(1969, 12, 31, 11, 59, 59)
assert p.timezone_name == "UTC"


@pytest.mark.parametrize("exception_type", [OSError, OverflowError])
def test_utcfromtimestamp_falls_back_for_negative_timestamp(monkeypatch, exception_type):
pendulum_datetime_module = importlib.import_module("pendulum.datetime")

class FakeDateTime(datetime_.datetime):
@classmethod
def utcfromtimestamp(cls, t: float):
if t == -43201:
raise exception_type("Invalid argument")

return super().utcfromtimestamp(t)

monkeypatch.setattr(
pendulum_datetime_module,
"datetime",
types.SimpleNamespace(datetime=FakeDateTime, timedelta=datetime_.timedelta),
)

p = pendulum.DateTime.utcfromtimestamp(-43201)

assert p == pendulum.naive(1969, 12, 31, 11, 59, 59)
assert p.tzinfo is None


def test_fromordinal():
assert datetime.fromordinal(730120) == pendulum.DateTime.fromordinal(730120)

Expand Down
41 changes: 41 additions & 0 deletions tests/datetime/test_create_from_timestamp.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

import datetime as datetime_

import pendulum
import pytest

from pendulum import timezone
from tests.conftest import assert_datetime
Expand All @@ -22,3 +25,41 @@ def test_create_from_timestamp_with_timezone():
d = pendulum.from_timestamp(0, timezone("America/Toronto"))
assert d.timezone_name == "America/Toronto"
assert_datetime(d, 1969, 12, 31, 19, 0, 0)


def test_create_from_timestamp_negative():
d = pendulum.from_timestamp(-43201)
assert_datetime(d, 1969, 12, 31, 11, 59, 59)
assert d.timezone_name == "UTC"


def test_create_from_timestamp_negative_with_timezone():
d = pendulum.from_timestamp(-43201, "America/Toronto")
assert d.timezone_name == "America/Toronto"
assert_datetime(d, 1969, 12, 31, 6, 59, 59)


def test_create_from_timestamp_negative_with_microseconds():
d = pendulum.from_timestamp(-43201.5)
assert_datetime(d, 1969, 12, 31, 11, 59, 58, 500000)
assert d.timezone_name == "UTC"


@pytest.mark.parametrize("exception_type", [OSError, OverflowError])
def test_create_from_timestamp_falls_back_for_negative_timestamp(
monkeypatch, exception_type
):
class FakeDateTime(datetime_.datetime):
@classmethod
def fromtimestamp(cls, timestamp: float, tz: datetime_.tzinfo | None = None):
if timestamp == -43201:
raise exception_type("Invalid argument")

return super().fromtimestamp(timestamp, tz=tz)

monkeypatch.setattr(pendulum._datetime, "datetime", FakeDateTime)

d = pendulum.from_timestamp(-43201)

assert_datetime(d, 1969, 12, 31, 11, 59, 59)
assert d.timezone_name == "UTC"