Skip to content
Closed
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
2 changes: 0 additions & 2 deletions src/marshmallow/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ def from_timestamp(value: typing.Any) -> dt.datetime:
if value is True or value is False:
raise ValueError("Not a valid POSIX timestamp")
value = float(value)
if value < 0:
raise ValueError("Not a valid POSIX timestamp")

# Load a timestamp with utc as timezone to prevent using system timezone.
# Then set timezone to None, to let the Field handle adding timezone info.
Expand Down
32 changes: 31 additions & 1 deletion tests/test_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,8 +569,14 @@ def test_iso_datetime_field_deserialization(self, fmt, value, expected, aware):
dt.datetime(2013, 11, 10, 0, 23, 45, 123456),
),
("timestamp", 1, dt.datetime(1970, 1, 1, 0, 0, 1)),
# Negative timestamps (pre-1970-01-01 dates) are valid POSIX
# timestamps and must be accepted; see
# test_timestamp_field_deserialization_round_trip_negative below.
("timestamp", -1, dt.datetime(1969, 12, 31, 23, 59, 59)),
("timestamp", -86400, dt.datetime(1969, 12, 31, 0, 0, 0)),
("timestamp_ms", 1384043025000, dt.datetime(2013, 11, 10, 0, 23, 45)),
("timestamp_ms", 1000, dt.datetime(1970, 1, 1, 0, 0, 1)),
("timestamp_ms", -1000, dt.datetime(1969, 12, 31, 23, 59, 59)),
],
)
def test_timestamp_field_deserialization(self, fmt, value, expected):
Expand All @@ -590,6 +596,30 @@ def test_timestamp_field_deserialization(self, fmt, value, expected):
expected_aware = expected.replace(tzinfo=central)
assert field.deserialize(value) == expected_aware

# Regression test: serializing a datetime before 1970-01-01 with
# format="timestamp"/"timestamp_ms" legitimately produces a negative
# float (datetime.timestamp() handles pre-epoch dates fine), so
# from_timestamp() rejecting negative values broke the serialize/
# deserialize round trip for any date before the Unix epoch. This does
# not use mocks: it serializes a real pre-1970 datetime and asserts that
# deserializing the result reproduces the original value exactly.
@pytest.mark.parametrize("fmt", ["timestamp", "timestamp_ms"])
@pytest.mark.parametrize(
"value",
[
dt.datetime(1969, 12, 31, 23, 59, 59, tzinfo=dt.timezone.utc),
dt.datetime(1950, 5, 3, 12, 0, 0, tzinfo=dt.timezone.utc),
dt.datetime(1900, 1, 1, 0, 0, 0, tzinfo=dt.timezone.utc),
],
)
def test_timestamp_field_deserialization_round_trip_negative(self, fmt, value):
field = fields.DateTime(format=fmt)
serialized = field.serialize("value", {"value": value})
assert serialized < 0

roundtripped = field.deserialize(serialized)
assert roundtripped == value.replace(tzinfo=None)

@pytest.mark.parametrize("fmt", ["timestamp", "timestamp_ms"])
@pytest.mark.parametrize("in_value", [True, False])
def test_boolean_timestamp_field_deserialization(self, fmt, in_value):
Expand All @@ -600,7 +630,7 @@ def test_boolean_timestamp_field_deserialization(self, fmt, in_value):
@pytest.mark.parametrize("fmt", ["timestamp", "timestamp_ms"])
@pytest.mark.parametrize(
"in_value",
["", "!@#", -1],
["", "!@#"],
)
def test_invalid_timestamp_field_deserialization(self, fmt, in_value):
field = fields.DateTime(format=fmt)
Expand Down
16 changes: 14 additions & 2 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ def test_is_collection():
[
(1676386740, dt.datetime(2023, 2, 14, 14, 59, 00)),
(1676386740.58, dt.datetime(2023, 2, 14, 14, 59, 00, 580000)),
# Negative timestamps (dates before 1970-01-01) are valid POSIX
# timestamps and must round-trip correctly; see
# test_from_timestamp_with_negative_value below for the regression.
(-10, dt.datetime(1969, 12, 31, 23, 59, 50)),
(0, dt.datetime(1970, 1, 1, 0, 0, 0)),
],
)
def test_from_timestamp(value, expected):
Expand All @@ -117,9 +122,16 @@ def test_from_timestamp(value, expected):


def test_from_timestamp_with_negative_value():
"""Regression test: negative POSIX timestamps (dates before 1970-01-01)
are valid and must be accepted. Serializing an aware pre-1970 datetime
with `fields.DateTime(format="timestamp")` legitimately produces a
negative float (via `datetime.timestamp()`), so rejecting negative
values here breaks the serialize/deserialize round trip for any date
before the Unix epoch.
"""
value = -10
with pytest.raises(ValueError, match=r"Not a valid POSIX timestamp"):
utils.from_timestamp(value)
result = utils.from_timestamp(value)
assert result == dt.datetime(1969, 12, 31, 23, 59, 50)


def test_from_timestamp_with_overflow_value():
Expand Down