Skip to content

Commit 4d8c8c0

Browse files
pganssleberkerpeksag
authored andcommitted
bpo-36025: Fix PyDate_FromTimestamp API (GH-11922)
In the process of converting the date.fromtimestamp function to use argument clinic in GH-8535, the C API for PyDate_FromTimestamp was inadvertently changed to expect a timestamp object rather than an argument tuple. This PR fixes this backwards-incompatible change by adding a new wrapper function for the C API function that unwraps the argument tuple and passes it to the underlying function. This PR also adds tests for both PyDate_FromTimestamp and PyDateTime_FromTimestamp to prevent any further regressions.
1 parent 5c403b2 commit 4d8c8c0

File tree

4 files changed

+126
-2
lines changed

4 files changed

+126
-2
lines changed

Lib/test/datetimetester.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5942,6 +5942,41 @@ class TZInfoSubclass(tzinfo):
59425942
with self.subTest(arg=arg, exact=exact):
59435943
self.assertFalse(is_tzinfo(arg, exact))
59445944

5945+
def test_date_from_timestamp(self):
5946+
ts = datetime(1995, 4, 12).timestamp()
5947+
5948+
for macro in [0, 1]:
5949+
with self.subTest(macro=macro):
5950+
d = _testcapi.get_date_fromtimestamp(int(ts), macro)
5951+
5952+
self.assertEqual(d, date(1995, 4, 12))
5953+
5954+
def test_datetime_from_timestamp(self):
5955+
ts0 = datetime(1995, 4, 12).timestamp()
5956+
ts1 = datetime(1995, 4, 12, 12, 30).timestamp()
5957+
5958+
cases = [
5959+
((1995, 4, 12), None, False),
5960+
((1995, 4, 12), None, True),
5961+
((1995, 4, 12), timezone(timedelta(hours=1)), True),
5962+
((1995, 4, 12, 14, 30), None, False),
5963+
((1995, 4, 12, 14, 30), None, True),
5964+
((1995, 4, 12, 14, 30), timezone(timedelta(hours=1)), True),
5965+
]
5966+
5967+
from_timestamp = _testcapi.get_datetime_fromtimestamp
5968+
for case in cases:
5969+
for macro in [0, 1]:
5970+
with self.subTest(case=case, macro=macro):
5971+
dtup, tzinfo, usetz = case
5972+
dt_orig = datetime(*dtup, tzinfo=tzinfo)
5973+
ts = int(dt_orig.timestamp())
5974+
5975+
dt_rt = from_timestamp(ts, tzinfo, usetz, macro)
5976+
5977+
self.assertEqual(dt_orig, dt_rt)
5978+
5979+
59455980
def load_tests(loader, standard_tests, pattern):
59465981
standard_tests.addTest(ZoneInfoCompleteTest())
59475982
return standard_tests
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed an accidental change to the datetime C API where the arguments to the
2+
:c:func:`PyDate_FromTimestamp` function were incorrectly interpreted as a
3+
single timestamp rather than an arguments tuple, which causes existing code to
4+
start raising :exc:`TypeError`. The backwards-incompatible change was only
5+
present in alpha releases of Python 3.8. Patch by Paul Ganssle.

Modules/_datetimemodule.c

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2921,6 +2921,23 @@ datetime_date_fromtimestamp(PyTypeObject *type, PyObject *timestamp)
29212921
return date_fromtimestamp((PyObject *) type, timestamp);
29222922
}
29232923

2924+
/* bpo-36025: This is a wrapper for API compatibility with the public C API,
2925+
* which expects a function that takes an *args tuple, whereas the argument
2926+
* clinic generates code that takes METH_O.
2927+
*/
2928+
static PyObject *
2929+
datetime_date_fromtimestamp_capi(PyObject *cls, PyObject *args)
2930+
{
2931+
PyObject *timestamp;
2932+
PyObject *result = NULL;
2933+
2934+
if (PyArg_UnpackTuple(args, "fromtimestamp", 1, 1, &timestamp)) {
2935+
result = date_fromtimestamp(cls, timestamp);
2936+
}
2937+
2938+
return result;
2939+
}
2940+
29242941
/* Return new date from proleptic Gregorian ordinal. Raises ValueError if
29252942
* the ordinal is out of range.
29262943
*/
@@ -6275,7 +6292,7 @@ static PyDateTime_CAPI CAPI = {
62756292
new_delta_ex,
62766293
new_timezone,
62776294
datetime_fromtimestamp,
6278-
date_fromtimestamp,
6295+
datetime_date_fromtimestamp_capi,
62796296
new_datetime_ex2,
62806297
new_time_ex2
62816298
};

Modules/_testcapimodule.c

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2340,6 +2340,71 @@ get_timezone_utc_capi(PyObject* self, PyObject *args) {
23402340
}
23412341
}
23422342

2343+
static PyObject *
2344+
get_date_fromtimestamp(PyObject* self, PyObject *args)
2345+
{
2346+
PyObject *tsargs = NULL, *ts = NULL, *rv = NULL;
2347+
int macro = 0;
2348+
2349+
if (!PyArg_ParseTuple(args, "O|p", &ts, &macro)) {
2350+
return NULL;
2351+
}
2352+
2353+
// Construct the argument tuple
2354+
if ((tsargs = PyTuple_Pack(1, ts)) == NULL) {
2355+
return NULL;
2356+
}
2357+
2358+
// Pass along to the API function
2359+
if (macro) {
2360+
rv = PyDate_FromTimestamp(tsargs);
2361+
}
2362+
else {
2363+
rv = PyDateTimeAPI->Date_FromTimestamp(
2364+
(PyObject *)PyDateTimeAPI->DateType, tsargs
2365+
);
2366+
}
2367+
2368+
Py_DECREF(tsargs);
2369+
return rv;
2370+
}
2371+
2372+
static PyObject *
2373+
get_datetime_fromtimestamp(PyObject* self, PyObject *args)
2374+
{
2375+
int macro = 0;
2376+
int usetz = 0;
2377+
PyObject *tsargs = NULL, *ts = NULL, *tzinfo = Py_None, *rv = NULL;
2378+
if (!PyArg_ParseTuple(args, "OO|pp", &ts, &tzinfo, &usetz, &macro)) {
2379+
return NULL;
2380+
}
2381+
2382+
// Construct the argument tuple
2383+
if (usetz) {
2384+
tsargs = PyTuple_Pack(2, ts, tzinfo);
2385+
}
2386+
else {
2387+
tsargs = PyTuple_Pack(1, ts);
2388+
}
2389+
2390+
if (tsargs == NULL) {
2391+
return NULL;
2392+
}
2393+
2394+
// Pass along to the API function
2395+
if (macro) {
2396+
rv = PyDateTime_FromTimestamp(tsargs);
2397+
}
2398+
else {
2399+
rv = PyDateTimeAPI->DateTime_FromTimestamp(
2400+
(PyObject *)PyDateTimeAPI->DateTimeType, tsargs, NULL
2401+
);
2402+
}
2403+
2404+
Py_DECREF(tsargs);
2405+
return rv;
2406+
}
2407+
23432408

23442409
/* test_thread_state spawns a thread of its own, and that thread releases
23452410
* `thread_done` when it's finished. The driver code has to know when the
@@ -4769,7 +4834,9 @@ static PyMethodDef TestMethods[] = {
47694834
{"datetime_check_tzinfo", datetime_check_tzinfo, METH_VARARGS},
47704835
{"make_timezones_capi", make_timezones_capi, METH_NOARGS},
47714836
{"get_timezones_offset_zero", get_timezones_offset_zero, METH_NOARGS},
4772-
{"get_timezone_utc_capi", get_timezone_utc_capi, METH_VARARGS},
4837+
{"get_timezone_utc_capi", get_timezone_utc_capi, METH_VARARGS},
4838+
{"get_date_fromtimestamp", get_date_fromtimestamp, METH_VARARGS},
4839+
{"get_datetime_fromtimestamp", get_datetime_fromtimestamp, METH_VARARGS},
47734840
{"test_list_api", test_list_api, METH_NOARGS},
47744841
{"test_dict_iteration", test_dict_iteration, METH_NOARGS},
47754842
{"dict_getitem_knownhash", dict_getitem_knownhash, METH_VARARGS},

0 commit comments

Comments
 (0)