Skip to content

Commit 52486a9

Browse files
authored
Add PyTime API (#84)
1 parent deb6f40 commit 52486a9

6 files changed

+171
-0
lines changed

docs/api.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,36 @@ Python 3.13
141141
142142
See `Py_HashPointer() documentation <https://docs.python.org/dev/c-api/hash.html#c.Py_HashPointer>`__.
143143
144+
.. c:type:: PyTime_t
145+
146+
A timestamp or duration in nanoseconds, represented as a signed 64-bit
147+
integer.
148+
149+
.. c:var:: PyTime_t PyTime_MIN
150+
151+
Minimum value of :c:type:`PyTime_t`.
152+
153+
.. c:var:: PyTime_t PyTime_MAX
154+
155+
Maximum value of :c:type:`PyTime_t`.
156+
157+
.. c:function:: double PyTime_AsSecondsDouble(PyTime_t t)
158+
159+
See `PyTime_AsSecondsDouble() documentation <https://docs.python.org/dev/c-api/time.html#c.PyTime_AsSecondsDouble>`__.
160+
161+
.. c:function:: int PyTime_Monotonic(PyTime_t *result)
162+
163+
See `PyTime_Monotonic() documentation <https://docs.python.org/dev/c-api/time.html#c.PyTime_Monotonic>`__.
164+
165+
.. c:function:: int PyTime_Time(PyTime_t *result)
166+
167+
See `PyTime_Time() documentation <https://docs.python.org/dev/c-api/time.html#c.PyTime_Time>`__.
168+
169+
.. c:function:: int PyTime_PerfCounter(PyTime_t *result)
170+
171+
See `PyTime_PerfCounter() documentation <https://docs.python.org/dev/c-api/time.html#c.PyTime_PerfCounter>`__.
172+
173+
144174
Not supported:
145175
146176
* ``PySys_Audit()``.

docs/changelog.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Changelog
22
=========
33

4+
* 2024-02-20: Add PyTime API:
5+
6+
* ``PyTime_t`` type
7+
* ``PyTime_MIN`` and ``PyTime_MAX`` constants
8+
* ``PyTime_AsSecondsDouble()``
9+
* ``PyTime_Monotonic()``
10+
* ``PyTime_PerfCounter()``
11+
* ``PyTime_Time()``
12+
413
* 2023-12-15: Add function ``Py_HashPointer()``.
514
* 2023-11-14: Add functions:
615

pythoncapi_compat.h

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,95 @@ static inline Py_hash_t Py_HashPointer(const void *ptr)
11081108
}
11091109
#endif
11101110

1111+
1112+
// Python 3.13a4 added a PyTime API.
1113+
// Use the private API added to Python 3.5.
1114+
#if PY_VERSION_HEX < 0x030D00A4 && PY_VERSION_HEX >= 0x03050000
1115+
typedef _PyTime_t PyTime_t;
1116+
#define PyTime_MIN _PyTime_MIN
1117+
#define PyTime_MAX _PyTime_MAX
1118+
1119+
static inline double PyTime_AsSecondsDouble(PyTime_t t)
1120+
{ return _PyTime_AsSecondsDouble(t); }
1121+
1122+
static inline int PyTime_Monotonic(PyTime_t *result)
1123+
{ return _PyTime_GetMonotonicClockWithInfo(result, NULL); }
1124+
1125+
static inline int PyTime_Time(PyTime_t *result)
1126+
{ return _PyTime_GetSystemClockWithInfo(result, NULL); }
1127+
1128+
static inline int PyTime_PerfCounter(PyTime_t *result)
1129+
{
1130+
#if PY_VERSION_HEX >= 0x03070000 && !defined(PYPY_VERSION)
1131+
return _PyTime_GetPerfCounterWithInfo(result, NULL);
1132+
#elif PY_VERSION_HEX >= 0x03070000
1133+
// Call time.perf_counter_ns() and convert Python int object to PyTime_t.
1134+
// Cache time.perf_counter_ns() function for best performance.
1135+
static PyObject *func = NULL;
1136+
if (func == NULL) {
1137+
PyObject *mod = PyImport_ImportModule("time");
1138+
if (mod == NULL) {
1139+
return -1;
1140+
}
1141+
1142+
func = PyObject_GetAttrString(mod, "perf_counter_ns");
1143+
Py_DECREF(mod);
1144+
if (func == NULL) {
1145+
return -1;
1146+
}
1147+
}
1148+
1149+
PyObject *res = PyObject_CallNoArgs(func);
1150+
if (res == NULL) {
1151+
return -1;
1152+
}
1153+
long long value = PyLong_AsLongLong(res);
1154+
Py_DECREF(res);
1155+
1156+
if (value == -1 && PyErr_Occurred()) {
1157+
return -1;
1158+
}
1159+
1160+
Py_BUILD_ASSERT(sizeof(value) >= sizeof(PyTime_t));
1161+
*result = (PyTime_t)value;
1162+
return 0;
1163+
#else
1164+
// Call time.perf_counter() and convert C double to PyTime_t.
1165+
// Cache time.perf_counter() function for best performance.
1166+
static PyObject *func = NULL;
1167+
if (func == NULL) {
1168+
PyObject *mod = PyImport_ImportModule("time");
1169+
if (mod == NULL) {
1170+
return -1;
1171+
}
1172+
1173+
func = PyObject_GetAttrString(mod, "perf_counter");
1174+
Py_DECREF(mod);
1175+
if (func == NULL) {
1176+
return -1;
1177+
}
1178+
}
1179+
1180+
PyObject *res = PyObject_CallNoArgs(func);
1181+
if (res == NULL) {
1182+
return -1;
1183+
}
1184+
double d = PyFloat_AsDouble(res);
1185+
Py_DECREF(res);
1186+
1187+
if (d == -1.0 && PyErr_Occurred()) {
1188+
return -1;
1189+
}
1190+
1191+
// Avoid floor() to avoid having to link to libm
1192+
*result = (PyTime_t)(d * 1e9);
1193+
return 0;
1194+
#endif
1195+
}
1196+
1197+
#endif
1198+
1199+
11111200
#ifdef __cplusplus
11121201
}
11131202
#endif

runtests.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
TEST_UPGRADE = os.path.join(TEST_DIR, "test_upgrade_pythoncapi.py")
3131

3232
PYTHONS = (
33+
# CPython
3334
"python3-debug",
3435
"python3",
3536
"python2.7",
@@ -43,6 +44,8 @@
4344
"python3.11",
4445
"python3.12",
4546
"python3.13",
47+
48+
# PyPy
4649
"pypy",
4750
"pypy2",
4851
"pypy2.7",

tests/test_pythoncapi_compat.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ def main():
188188
global VERBOSE
189189
VERBOSE = ("-v" in sys.argv[1:] or "--verbose" in sys.argv[1:])
190190

191+
if (3, 13) <= sys.version_info <= (3, 13, 0, 'alpha', 4):
192+
print("SKIP Python 3.13 alpha 1..4: not supported!")
193+
return
194+
191195
if faulthandler is not None:
192196
faulthandler.enable()
193197

tests/test_pythoncapi_compat_cext.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,39 @@ test_hash(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
15261526
}
15271527

15281528

1529+
#if PY_VERSION_HEX >= 0x03050000
1530+
#define TEST_PYTIME
1531+
1532+
static PyObject *
1533+
test_time(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
1534+
{
1535+
PyTime_t t;
1536+
#define UNINITIALIZED_TIME ((PyTime_t)-483884113929936179)
1537+
1538+
t = UNINITIALIZED_TIME;
1539+
assert(PyTime_Time(&t) == 0);
1540+
assert(t != UNINITIALIZED_TIME);
1541+
1542+
t = UNINITIALIZED_TIME;
1543+
assert(PyTime_Monotonic(&t) == 0);
1544+
assert(t != UNINITIALIZED_TIME);
1545+
1546+
// Test multiple times since an implementation uses a cache
1547+
for (int i=0; i < 5; i++) {
1548+
t = UNINITIALIZED_TIME;
1549+
assert(PyTime_PerfCounter(&t) == 0);
1550+
assert(t != UNINITIALIZED_TIME);
1551+
}
1552+
1553+
assert(PyTime_AsSecondsDouble(1) == 1e-9);
1554+
assert(PyTime_AsSecondsDouble(1500 * 1000 * 1000) == 1.5);
1555+
assert(PyTime_AsSecondsDouble(-500 * 1000 * 1000) == -0.5);
1556+
1557+
Py_RETURN_NONE;
1558+
}
1559+
#endif
1560+
1561+
15291562
static struct PyMethodDef methods[] = {
15301563
{"test_object", test_object, METH_NOARGS, _Py_NULL},
15311564
{"test_py_is", test_py_is, METH_NOARGS, _Py_NULL},
@@ -1559,6 +1592,9 @@ static struct PyMethodDef methods[] = {
15591592
{"test_unicode", test_unicode, METH_NOARGS, _Py_NULL},
15601593
{"test_list", test_list, METH_NOARGS, _Py_NULL},
15611594
{"test_hash", test_hash, METH_NOARGS, _Py_NULL},
1595+
#ifdef TEST_PYTIME
1596+
{"test_time", test_time, METH_NOARGS, _Py_NULL},
1597+
#endif
15621598
{_Py_NULL, _Py_NULL, 0, _Py_NULL}
15631599
};
15641600

0 commit comments

Comments
 (0)