Skip to content

Commit 01e1128

Browse files
gh-156263: Add the duration and timestamp converters to Argument Clinic
The duration converter converts a number of seconds or milliseconds to PyTime_t or struct timeval. The timestamp converter converts a number of seconds since the epoch to time_t or PyTime_t. They are used for the timeout parameters in the select, signal, faulthandler, _queue, _thread, _multiprocessing, socket and _sqlite3 modules and for the timestamp parameter of date.fromtimestamp(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 43a1869 commit 01e1128

25 files changed

Lines changed: 607 additions & 375 deletions

Include/internal/pycore_time.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ PyAPI_FUNC(time_t) _PyLong_AsTime_t(PyObject *obj);
102102

103103
// Convert a number of seconds, int or float, to time_t.
104104
// Export for '_datetime' shared extension.
105+
// Argument Clinic converters for durations, see the "duration" converter.
106+
// Export for shared extensions (Argument Clinic code).
107+
PyAPI_FUNC(int) _PyTime_Duration_Seconds_Converter(PyObject *, void *);
108+
PyAPI_FUNC(int) _PyTime_DurationOrNone_Seconds_Converter(PyObject *, void *);
109+
PyAPI_FUNC(int) _PyTime_Duration_SecondsCeil_Converter(PyObject *, void *);
110+
PyAPI_FUNC(int) _PyTime_DurationOrNone_SecondsCeil_Converter(PyObject *, void *);
111+
PyAPI_FUNC(int) _PyTime_Duration_Milliseconds_Converter(PyObject *, void *);
112+
PyAPI_FUNC(int) _PyTime_DurationOrNone_Milliseconds_Converter(PyObject *, void *);
113+
114+
#ifndef MS_WINDOWS
115+
PyAPI_FUNC(int) _PyTime_Duration_Timeval_Converter(PyObject *, void *);
116+
PyAPI_FUNC(int) _PyTime_Duration_TimevalCeil_Converter(PyObject *, void *);
117+
#endif
118+
119+
// Argument Clinic converters for timestamps, see the "timestamp" converter.
120+
// Export for shared extensions (Argument Clinic code).
121+
PyAPI_FUNC(int) _PyTime_Timestamp_Time_t_Converter(PyObject *, void *);
122+
PyAPI_FUNC(int) _PyTime_Timestamp_Converter(PyObject *, void *);
123+
105124
PyAPI_FUNC(int) _PyTime_ObjectToTime_t(
106125
PyObject *obj,
107126
time_t *sec,

Lib/test/test_clinic.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3150,10 +3150,11 @@ def test_var_keyword_with_pos_or_kw_and_kw_only(self):
31503150
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
31513151
self.expect_failure(block, err)
31523152

3153-
def test_allow_negative_accepted_by_py_ssize_t_converter_only(self):
3153+
def test_allow_negative_accepted_by_few_converters_only(self):
31543154
errmsg = re.escape("converter_init() got an unexpected keyword argument 'allow_negative'")
3155+
supported = {"Py_ssize_t", "duration"}
31553156
unsupported_converters = [converter_name for converter_name in converters.keys()
3156-
if converter_name != "Py_ssize_t"]
3157+
if converter_name not in supported]
31573158
for converter in unsupported_converters:
31583159
with self.subTest(converter=converter):
31593160
block = f"""
@@ -3565,6 +3566,7 @@ def test_cli_converters(self):
35653566
"char",
35663567
"defining_class",
35673568
"double",
3569+
"duration",
35683570
"fildes",
35693571
"float",
35703572
"int",
@@ -3582,6 +3584,7 @@ def test_cli_converters(self):
35823584
"size_t",
35833585
"slice_index",
35843586
"str",
3587+
"timestamp",
35853588
"uint16",
35863589
"uint32",
35873590
"uint64",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
The timeout arguments in the :mod:`select`, :mod:`signal`, :mod:`faulthandler`,
2+
:mod:`queue`, :mod:`_thread`, :mod:`multiprocessing`, :mod:`socket` and
3+
:mod:`sqlite3` modules are now converted by Argument Clinic. As a result,
4+
they are validated even if blocking is false,
5+
:meth:`socket.socket.settimeout` and :func:`socket.setdefaulttimeout` raise
6+
:exc:`ValueError` with a different message for a negative timeout, and
7+
:func:`sqlite3.connect` raises :exc:`OverflowError` for a too large timeout.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Argument Clinic: add the ``duration`` converter, which converts a number of
2+
seconds or milliseconds to :c:type:`PyTime_t` or ``struct timeval``, and the
3+
``timestamp`` converter, which converts a number of seconds since the epoch
4+
to :c:type:`time_t` or :c:type:`PyTime_t`.

Modules/_datetimemodule.c

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3286,13 +3286,9 @@ datetime_date_impl(PyTypeObject *type, int year, int month, int day)
32863286
}
32873287

32883288
static PyObject *
3289-
date_fromtimestamp(PyTypeObject *cls, PyObject *obj)
3289+
date_fromtimet(PyTypeObject *cls, time_t t)
32903290
{
32913291
struct tm tm;
3292-
time_t t;
3293-
3294-
if (_PyTime_ObjectToTime_t(obj, &t, _PyTime_ROUND_FLOOR) == -1)
3295-
return NULL;
32963292

32973293
if (_PyTime_localtime(t, &tm) != 0)
32983294
return NULL;
@@ -3354,7 +3350,7 @@ datetime_date_today_impl(PyTypeObject *type)
33543350
@classmethod
33553351
datetime.date.fromtimestamp
33563352
3357-
timestamp: object
3353+
timestamp: timestamp
33583354
/
33593355
33603356
Create a date from a POSIX timestamp.
@@ -3364,10 +3360,10 @@ interpreted as local time.
33643360
[clinic start generated code]*/
33653361

33663362
static PyObject *
3367-
datetime_date_fromtimestamp_impl(PyTypeObject *type, PyObject *timestamp)
3368-
/*[clinic end generated code: output=59def4e32c028fb6 input=15720eef43b169a1]*/
3363+
datetime_date_fromtimestamp_impl(PyTypeObject *type, time_t timestamp)
3364+
/*[clinic end generated code: output=a4240b6ce153c150 input=74a7bdf0575c89a8]*/
33693365
{
3370-
return date_fromtimestamp(type, timestamp);
3366+
return date_fromtimet(type, timestamp);
33713367
}
33723368

33733369
/* bpo-36025: This is a wrapper for API compatibility with the public C API,
@@ -3381,7 +3377,11 @@ datetime_date_fromtimestamp_capi(PyObject *cls, PyObject *args)
33813377
PyObject *result = NULL;
33823378

33833379
if (PyArg_UnpackTuple(args, "fromtimestamp", 1, 1, &timestamp)) {
3384-
result = date_fromtimestamp((PyTypeObject *)cls, timestamp);
3380+
time_t t;
3381+
if (_PyTime_ObjectToTime_t(timestamp, &t, _PyTime_ROUND_FLOOR) == -1) {
3382+
return NULL;
3383+
}
3384+
result = date_fromtimet((PyTypeObject *)cls, t);
33853385
}
33863386

33873387
return result;

Modules/_multiprocessing/clinic/semaphore.c.h

Lines changed: 18 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_multiprocessing/semaphore.c

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -89,38 +89,35 @@ _GetSemaphoreValue(HANDLE handle, int *value)
8989
_multiprocessing.SemLock.acquire
9090
9191
block as blocking: bool = True
92-
timeout as timeout_obj: object = None
92+
timeout: duration(accept={float, NoneType}, c_default='PyTime_MIN') = None
9393
9494
Acquire the semaphore/lock.
9595
[clinic start generated code]*/
9696

9797
static PyObject *
9898
_multiprocessing_SemLock_acquire_impl(SemLockObject *self, int blocking,
99-
PyObject *timeout_obj)
100-
/*[clinic end generated code: output=f9998f0b6b0b0872 input=079ca779975f3ad6]*/
99+
PyTime_t timeout)
100+
/*[clinic end generated code: output=38d34f4b0b2f918e input=b76d85c9695dc3a4]*/
101101
{
102-
double timeout;
103102
DWORD res, full_msecs, nhandles;
104103
HANDLE handles[2], sigint_event;
105104

106105
/* calculate timeout */
107106
if (!blocking) {
108107
full_msecs = 0;
109-
} else if (timeout_obj == Py_None) {
108+
} else if (timeout == PyTime_MIN) { /* timeout=None: wait forever */
110109
full_msecs = INFINITE;
111110
} else {
112-
timeout = PyFloat_AsDouble(timeout_obj);
113-
if (PyErr_Occurred())
114-
return NULL;
115-
timeout *= 1000.0; /* convert to millisecs */
116-
if (timeout < 0.0) {
117-
timeout = 0.0;
118-
} else if (timeout >= 0.5 * INFINITE) { /* 25 days */
111+
if (timeout < 0) {
112+
timeout = 0;
113+
}
114+
PyTime_t msecs = _PyTime_AsMilliseconds(timeout, _PyTime_ROUND_TIMEOUT);
115+
if (msecs >= INFINITE / 2) { /* 25 days */
119116
PyErr_SetString(PyExc_OverflowError,
120117
"timeout is too large");
121118
return NULL;
122119
}
123-
full_msecs = (DWORD)(timeout + 0.5);
120+
full_msecs = (DWORD)msecs;
124121
}
125122

126123
/* check whether we already own the lock */
@@ -307,15 +304,15 @@ sem_timedwait_save(sem_t *sem, struct timespec *deadline, PyThreadState *_save)
307304
_multiprocessing.SemLock.acquire
308305
309306
block as blocking: bool = True
310-
timeout as timeout_obj: object = None
307+
timeout: duration(accept={float, NoneType}, c_default='PyTime_MIN') = None
311308
312309
Acquire the semaphore/lock.
313310
[clinic start generated code]*/
314311

315312
static PyObject *
316313
_multiprocessing_SemLock_acquire_impl(SemLockObject *self, int blocking,
317-
PyObject *timeout_obj)
318-
/*[clinic end generated code: output=f9998f0b6b0b0872 input=079ca779975f3ad6]*/
314+
PyTime_t timeout)
315+
/*[clinic end generated code: output=38d34f4b0b2f918e input=b76d85c9695dc3a4]*/
319316
{
320317
int res, err = 0;
321318
struct timespec deadline = {0};
@@ -325,25 +322,19 @@ _multiprocessing_SemLock_acquire_impl(SemLockObject *self, int blocking,
325322
Py_RETURN_TRUE;
326323
}
327324

328-
int use_deadline = (timeout_obj != Py_None);
325+
int use_deadline = (timeout != PyTime_MIN); /* timeout=None: wait forever */
329326
if (use_deadline) {
330-
double timeout = PyFloat_AsDouble(timeout_obj);
331-
if (PyErr_Occurred()) {
332-
return NULL;
333-
}
334-
if (timeout < 0.0) {
335-
timeout = 0.0;
327+
if (timeout < 0) {
328+
timeout = 0;
336329
}
337330

338331
struct timeval now;
339332
if (gettimeofday(&now, NULL) < 0) {
340333
PyErr_SetFromErrno(PyExc_OSError);
341334
return NULL;
342335
}
343-
long sec = (long) timeout;
344-
long nsec = (long) (1e9 * (timeout - sec) + 0.5);
345-
deadline.tv_sec = now.tv_sec + sec;
346-
deadline.tv_nsec = now.tv_usec * 1000 + nsec;
336+
deadline.tv_sec = now.tv_sec + (time_t)(timeout / 1000000000);
337+
deadline.tv_nsec = now.tv_usec * 1000 + (long)(timeout % 1000000000);
347338
deadline.tv_sec += (deadline.tv_nsec / 1000000000);
348339
deadline.tv_nsec %= 1000000000;
349340
}
@@ -697,7 +688,7 @@ static PyObject *
697688
_multiprocessing_SemLock___enter___impl(SemLockObject *self)
698689
/*[clinic end generated code: output=beeb2f07c858511f input=d35c9860992ee790]*/
699690
{
700-
return _multiprocessing_SemLock_acquire_impl(self, 1, Py_None);
691+
return _multiprocessing_SemLock_acquire_impl(self, 1, PyTime_MIN);
701692
}
702693

703694
/*[clinic input]

Modules/_queuemodule.c

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ _queue.SimpleQueue.get
357357
cls: defining_class
358358
/
359359
block: bool = True
360-
timeout as timeout_obj: object = None
360+
timeout: duration(round='ceiling', accept={float, NoneType}, allow_negative=False) = None
361361
362362
Remove and return an item from the queue.
363363
@@ -374,25 +374,13 @@ in that case).
374374

375375
static PyObject *
376376
_queue_SimpleQueue_get_impl(simplequeueobject *self, PyTypeObject *cls,
377-
int block, PyObject *timeout_obj)
378-
/*[clinic end generated code: output=5c2cca914cd1e55b input=afa0889bbc6b4761]*/
377+
int block, PyTime_t timeout)
378+
/*[clinic end generated code: output=08940a9800530258 input=4274501b8112609f]*/
379379
{
380380
PyTime_t endtime = 0;
381381

382-
// XXX Use PyThread_ParseTimeoutArg().
383-
384-
if (block != 0 && !Py_IsNone(timeout_obj)) {
382+
if (block != 0 && timeout >= 0) {
385383
/* With timeout */
386-
PyTime_t timeout;
387-
if (_PyTime_FromSecondsObject(&timeout,
388-
timeout_obj, _PyTime_ROUND_CEILING) < 0) {
389-
return NULL;
390-
}
391-
if (timeout < 0) {
392-
PyErr_SetString(PyExc_ValueError,
393-
"'timeout' must be a non-negative number");
394-
return NULL;
395-
}
396384
endtime = _PyDeadline_Init(timeout);
397385
}
398386

@@ -467,7 +455,7 @@ _queue_SimpleQueue_get_nowait_impl(simplequeueobject *self,
467455
PyTypeObject *cls)
468456
/*[clinic end generated code: output=620c58e2750f8b8a input=d48be63633fefae9]*/
469457
{
470-
return _queue_SimpleQueue_get_impl(self, cls, 0, Py_None);
458+
return _queue_SimpleQueue_get_impl(self, cls, 0, -1);
471459
}
472460

473461
/*[clinic input]

Modules/_sqlite/clinic/_sqlite3.connect.c.h

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)