Skip to content

Commit e07b164

Browse files
committed
fix: handle OverflowError in timestamp fallbacks
1 parent 4365af0 commit e07b164

4 files changed

Lines changed: 36 additions & 21 deletions

File tree

src/pendulum/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,7 @@ def from_timestamp(timestamp: int | float, tz: str | Timezone = UTC) -> DateTime
288288
"""
289289
try:
290290
dt = _datetime.datetime.fromtimestamp(timestamp, tz=UTC)
291-
except OSError:
292-
# On some platforms (notably Windows), datetime.fromtimestamp
293-
# raises OSError for negative timestamps that are too far from
294-
# the Unix epoch. Fall back to computing the result from the
295-
# epoch and applying the offset manually.
291+
except (OSError, OverflowError):
296292
epoch = _datetime.datetime(1970, 1, 1, tzinfo=UTC)
297293
dt = epoch + _datetime.timedelta(seconds=timestamp)
298294

src/pendulum/datetime.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,7 @@ def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> Self:
12551255

12561256
try:
12571257
dt = datetime.datetime.fromtimestamp(t, tz=tzinfo)
1258-
except OSError:
1258+
except (OSError, OverflowError):
12591259
dt = (cls._EPOCH + datetime.timedelta(seconds=t)).astimezone(tzinfo)
12601260

12611261
return cls.instance(dt, tz=tzinfo)
@@ -1264,7 +1264,9 @@ def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> Self:
12641264
def utcfromtimestamp(cls, t: float) -> Self:
12651265
try:
12661266
dt = datetime.datetime.utcfromtimestamp(t)
1267-
except OSError:
1267+
except (OSError, OverflowError):
1268+
# Match datetime.datetime.utcfromtimestamp(), which returns a
1269+
# naive datetime representing UTC.
12681270
dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=t)
12691271

12701272
return cls.instance(dt, tz=None)

tests/datetime/test_behavior.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import types
77
import zoneinfo
88

9+
import pytest
10+
911
from copy import deepcopy
1012
from datetime import date
1113
from datetime import datetime
@@ -110,14 +112,15 @@ def test_utcfromtimestamp():
110112
assert p == dt
111113

112114

113-
def test_fromtimestamp_falls_back_for_negative_timestamp(monkeypatch):
115+
@pytest.mark.parametrize("exception_type", [OSError, OverflowError])
116+
def test_fromtimestamp_falls_back_for_negative_timestamp(monkeypatch, exception_type):
114117
pendulum_datetime_module = importlib.import_module("pendulum.datetime")
115118

116119
class FakeDateTime(datetime_.datetime):
117120
@classmethod
118121
def fromtimestamp(cls, t: float, tz: datetime_.tzinfo | None = None):
119122
if t == -43201:
120-
raise OSError("Invalid argument")
123+
raise exception_type("Invalid argument")
121124

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

@@ -133,14 +136,15 @@ def fromtimestamp(cls, t: float, tz: datetime_.tzinfo | None = None):
133136
assert p.timezone_name == "UTC"
134137

135138

136-
def test_utcfromtimestamp_falls_back_for_negative_timestamp(monkeypatch):
139+
@pytest.mark.parametrize("exception_type", [OSError, OverflowError])
140+
def test_utcfromtimestamp_falls_back_for_negative_timestamp(monkeypatch, exception_type):
137141
pendulum_datetime_module = importlib.import_module("pendulum.datetime")
138142

139143
class FakeDateTime(datetime_.datetime):
140144
@classmethod
141145
def utcfromtimestamp(cls, t: float):
142146
if t == -43201:
143-
raise OSError("Invalid argument")
147+
raise exception_type("Invalid argument")
144148

145149
return super().utcfromtimestamp(t)
146150

tests/datetime/test_create_from_timestamp.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import annotations
22

3+
import datetime as datetime_
4+
35
import pendulum
6+
import pytest
47

58
from pendulum import timezone
69
from tests.conftest import assert_datetime
@@ -25,28 +28,38 @@ def test_create_from_timestamp_with_timezone():
2528

2629

2730
def test_create_from_timestamp_negative():
28-
"""Negative timestamps earlier than 12h before the Unix epoch should work.
29-
30-
Regression test for issue 956: on Windows (and potentially other
31-
platforms), datetime.fromtimestamp raises OSError for timestamps
32-
below a platform-specific minimum. pendulum should still return a
33-
correct DateTime.
34-
"""
35-
# -43201 is 1 second past the 12h-before-epoch boundary reported in the issue
3631
d = pendulum.from_timestamp(-43201)
3732
assert_datetime(d, 1969, 12, 31, 11, 59, 59)
3833
assert d.timezone_name == "UTC"
3934

4035

4136
def test_create_from_timestamp_negative_with_timezone():
42-
"""Negative timestamps with an explicit timezone should also work."""
4337
d = pendulum.from_timestamp(-43201, "America/Toronto")
4438
assert d.timezone_name == "America/Toronto"
4539
assert_datetime(d, 1969, 12, 31, 6, 59, 59)
4640

4741

4842
def test_create_from_timestamp_negative_with_microseconds():
49-
"""Negative float timestamps preserving microseconds."""
5043
d = pendulum.from_timestamp(-43201.5)
5144
assert_datetime(d, 1969, 12, 31, 11, 59, 58, 500000)
5245
assert d.timezone_name == "UTC"
46+
47+
48+
@pytest.mark.parametrize("exception_type", [OSError, OverflowError])
49+
def test_create_from_timestamp_falls_back_for_negative_timestamp(
50+
monkeypatch, exception_type
51+
):
52+
class FakeDateTime(datetime_.datetime):
53+
@classmethod
54+
def fromtimestamp(cls, timestamp: float, tz: datetime_.tzinfo | None = None):
55+
if timestamp == -43201:
56+
raise exception_type("Invalid argument")
57+
58+
return super().fromtimestamp(timestamp, tz=tz)
59+
60+
monkeypatch.setattr(pendulum._datetime, "datetime", FakeDateTime)
61+
62+
d = pendulum.from_timestamp(-43201)
63+
64+
assert_datetime(d, 1969, 12, 31, 11, 59, 59)
65+
assert d.timezone_name == "UTC"

0 commit comments

Comments
 (0)