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
16 changes: 10 additions & 6 deletions src/marshmallow/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,21 @@ 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.
try:
return dt.datetime.fromtimestamp(value, tz=dt.timezone.utc).replace(tzinfo=None)
ret_dt = dt.datetime.fromtimestamp(value, tz=dt.timezone.utc)
except OverflowError as exc:
raise ValueError("Timestamp is too large") from exc
except OSError as exc:
raise ValueError("Error converting value to datetime") from exc
except OSError:
try:
ret_dt = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc) + dt.timedelta(
seconds=value
)
except OverflowError as exc:
raise ValueError("Timestamp is too large") from exc
# Then set timezone to None, to let the Field handle adding timezone info.
return ret_dt.replace(tzinfo=None)


def from_timestamp_ms(value: typing.Any) -> dt.datetime:
Expand Down
18 changes: 14 additions & 4 deletions tests/test_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,8 +569,10 @@ 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)),
("timestamp", -1, dt.datetime(1969, 12, 31, 23, 59, 59)),
("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 Down Expand Up @@ -598,16 +600,14 @@ def test_boolean_timestamp_field_deserialization(self, fmt, in_value):
field.deserialize(in_value)

@pytest.mark.parametrize("fmt", ["timestamp", "timestamp_ms"])
@pytest.mark.parametrize(
"in_value",
["", "!@#", -1],
)
@pytest.mark.parametrize("in_value", ["", "!@#"])
def test_invalid_timestamp_field_deserialization(self, fmt, in_value):
field = fields.DateTime(format=fmt)
with pytest.raises(ValidationError, match="Not a valid datetime."):
field.deserialize(in_value)

# Regression test for https://github.com/marshmallow-code/marshmallow/pull/2102
# fromtimestamp may raise OSError or OverflowError depending on the platform.
@pytest.mark.parametrize("fmt", ["timestamp", "timestamp_ms"])
@pytest.mark.parametrize(
"mock_fromtimestamp", [MockDateTimeOSError, MockDateTimeOverflowError]
Expand All @@ -618,6 +618,16 @@ def test_oversized_timestamp_field_deserialization(self, fmt, mock_fromtimestamp
with pytest.raises(ValidationError, match="Not a valid datetime."):
field.deserialize(99999999999999999)

@pytest.mark.parametrize(
("fmt", "val"), (("timestamp", -1), ("timestamp_ms", -1000))
)
def test_negative_timestamp_field_deserialization(self, fmt, val):
# OSError is raised on Windows for negative timestamps.
# Force it with a mock to test on any platform.
with patch("datetime.datetime", MockDateTimeOSError):
field = fields.DateTime(format=fmt)
assert field.deserialize(val) == dt.datetime(1969, 12, 31, 23, 59, 59)

@pytest.mark.parametrize(
("fmt", "timezone", "value", "expected"),
[
Expand Down
8 changes: 2 additions & 6 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ def test_is_collection():
[
(1676386740, dt.datetime(2023, 2, 14, 14, 59, 00)),
(1676386740.58, dt.datetime(2023, 2, 14, 14, 59, 00, 580000)),
(-627296460, dt.datetime(1950, 2, 14, 14, 59, 00)),
(-627296459.42, dt.datetime(1950, 2, 14, 14, 59, 00, 580000)),
],
)
def test_from_timestamp(value, expected):
Expand All @@ -116,12 +118,6 @@ def test_from_timestamp(value, expected):
assert result == expected


def test_from_timestamp_with_negative_value():
value = -10
with pytest.raises(ValueError, match=r"Not a valid POSIX timestamp"):
utils.from_timestamp(value)


def test_from_timestamp_with_overflow_value():
value = 9223372036854775
with pytest.raises(ValueError, match=r"out of range|year must be in 1\.\.9999"):
Expand Down