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
26 changes: 26 additions & 0 deletions Lib/test/test_zoneinfo/test_zoneinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,32 @@ def test_unambiguous(self):
self.assertEqual(dt.utcoffset(), offset.utcoffset, dt)
self.assertEqual(dt.dst(), offset.dst, dt)

def test_datetime_subclass_negative_components(self):
# Regression test for gh-154892.
class MinusOneDateTime(datetime):
@property
def hour(self):
return -1

@property
def minute(self):
return -1

@property
def second(self):
return -1

zi = self.zone_from_key("UTC")
dt = MinusOneDateTime(2024, 1, 1, tzinfo=zi)

self.assertEqual(dt.utcoffset(), ZERO)
self.assertEqual(dt.dst(), ZERO)
self.assertEqual(dt.tzname(), "UTC")
self.assertEqual(
zi.fromutc(dt),
datetime(2024, 1, 1, tzinfo=zi),
)

def test_folds_and_gaps(self):
test_cases = []
for key in self.zones():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a bug in the C accelerator for :mod:`zoneinfo` where
:class:`datetime.datetime` subclasses returning ``-1`` for ``hour``,
``minute``, or ``second`` could incorrectly raise a :exc:`SystemError`.
6 changes: 3 additions & 3 deletions Modules/_zoneinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -2311,7 +2311,7 @@ get_local_timestamp(PyObject *dt, int64_t *local_ts)
}
hour = PyLong_AsLong(num);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly the issue, but related. What if a subclass returns 2**40? Maybe PyLong_AsInt can be used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I hadn't considered oversized integer values like 2**40. Since this PR is focused on fixing the -1 sentinel handling from PyLong_AsLong(), I'd prefer to keep that as a separate change if we decide PyLong_AsInt() is the better choice.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Could you open an issue for this so we do not forget?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll investigate this further and open a separate issue to track it so it doesn't get lost.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out! I investigated it further.

The C implementation currently uses PyObject_GetAttrString() + PyLong_AsLong() for datetime subclasses, but the pure Python implementation follows the same approach:

(dt.toordinal() - EPOCHORDINAL) * 86400 + dt.hour * 3600 + dt.minute * 60 + dt.second

So both implementations intentionally consult the subclass-visible hour, minute, and second attributes.

I also tested subclasses returning very large values (e.g. 10**12), and both the C and pure Python implementations behave the same way. This means switching to PyLong_AsInt() in the C implementation would only narrow the accepted range there, while the pure Python implementation would continue to accept arbitrarily large values, resulting in inconsistent behavior between the two implementations.

It seems the underlying question is whether ZoneInfo should be consulting overridden subclass attributes at all, rather than whether PyLong_AsLong() should be replaced with PyLong_AsInt().

Py_DECREF(num);
if (hour == -1) {
if (hour == -1 && PyErr_Occurred()) {
return -1;
}

Expand All @@ -2321,7 +2321,7 @@ get_local_timestamp(PyObject *dt, int64_t *local_ts)
}
minute = PyLong_AsLong(num);
Py_DECREF(num);
if (minute == -1) {
if (minute == -1 && PyErr_Occurred()) {
return -1;
}

Expand All @@ -2331,7 +2331,7 @@ get_local_timestamp(PyObject *dt, int64_t *local_ts)
}
second = PyLong_AsLong(num);
Py_DECREF(num);
if (second == -1) {
if (second == -1 && PyErr_Occurred()) {
return -1;
}
}
Expand Down
Loading