8000 bpo-35707: support __index__ and __float__ in time functions · python/cpython@1ad2f3d · GitHub
[go: up one dir, main page]

Skip to content

Commit 1ad2f3d

Browse files
committed
bpo-35707: support __index__ and __float__ in time functions
1 parent 808180c commit 1ad2f3d

File tree

3 files changed

+173
-52
lines changed

3 files changed

+173
-52
lines changed

Lib/test/test_time.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ class _PyTime(enum.IntEnum):
4646
)
4747

4848

49+
class IndexLike:
50+
def __init__(self, value):
51+
self.value = int(value)
52+
def __index__(self):
53+
return self.value
54+
55+
class FloatLike:
56+
def __init__(self, value):
57+
self.value = float(value)
58+
def __float__(self):
59+
return self.value
60+
61+
4962
class TimeTestCase(unittest.TestCase):
5063

5164
def setUp(self):
@@ -160,6 +173,8 @@ def test_sleep(self):
160173
self.assertRaises(ValueError, time.sleep, -2)
161174
self.assertRaises(ValueError, time.sleep, -1)
162175
time.sleep(1.2)
176+
time.sleep(IndexLike(0))
177+
time.sleep(FloatLike(0))
163178

164179
def test_strftime(self):
165180
tt = time.gmtime(self.t)
@@ -851,14 +866,23 @@ def convert_values(ns_timestamps):
851866
pytime_converter(value, time_rnd)
852 8000 867

853868
def check_int_rounding(self, pytime_converter, expected_func,
854-
unit_to_sec=1, value_filter=None):
869+
unit_to_sec=1, value_filter=None, *, index=True):
855870
self._check_rounding(pytime_converter, expected_func,
856871
False, unit_to_sec, value_filter)
872+
if index:
873+
def convert_IndexLike(x, rnd):
874+
return pytime_converter(IndexLike(x), rnd)
875+
self._check_rounding(convert_IndexLike, expected_func,
876+
False, unit_to_sec, value_filter)
857877

858878
def check_float_rounding(self, pytime_converter, expected_func,
859879
unit_to_sec=1, value_filter=None):
860880
self._check_rounding(pytime_converter, expected_func,
861881
True, unit_to_sec, value_filter)
882+
def convert_FloatLike(x, rnd):
883+
return pytime_converter(FloatLike(x), rnd)
884+
self._check_rounding(convert_FloatLike, expected_func,
885+
True, unit_to_sec, value_filter)
862886

863887
def decimal_round(self, x):
864888
d = decimal.Decimal(x)
@@ -882,7 +906,7 @@ def c_int_filter(secs):
882906

883907
self.check_int_rounding(lambda secs, rnd: PyTime_FromSeconds(secs),
884908
lambda secs: secs * SEC_TO_NS,
885-
value_filter=c_int_filter)
909+
value_filter=c_int_filter, index=False)
886910

887911
# test nan
888912
for time_rnd, _ in ROUNDING_MODES:
@@ -916,7 +940,7 @@ def float_converter(ns):
916940

917941
self.check_int_rounding(lambda ns, rnd: PyTime_AsSecondsDouble(ns),
918942
float_converter,
919-
NS_TO_SEC)
943+
NS_TO_SEC, index=False)
920944

921945
# test nan
922946
for time_rnd, _ in ROUNDING_MODES:
@@ -953,7 +977,7 @@ def seconds_filter(secs):
953977
self.check_int_rounding(PyTime_AsTimeval,
954978
timeval_converter,
955979
NS_TO_SEC,
956-
value_filter=seconds_filter)
980+
value_filter=seconds_filter, index=False)
957981

958982
@unittest.skipUnless(hasattr(_testcapi, 'PyTime_AsTimespec'),
959983
'need _testcapi.PyTime_AsTimespec')
@@ -966,21 +990,21 @@ def timespec_converter(ns):
966990
self.check_int_rounding(lambda ns, rnd: PyTime_AsTimespec(ns),
967991
timespec_converter,
968992
NS_TO_SEC,
969-
value_filter=self.time_t_filter)
993+
value_filter=self.time_t_filter, index=False)
970994

971995
def test_AsMilliseconds(self):
972996
from _testcapi import PyTime_AsMilliseconds
973997

974998
self.check_int_rounding(PyTime_AsMilliseconds,
975999
self.create_decimal_converter(MS_TO_NS),
976-
NS_TO_SEC)
1000+
NS_TO_SEC, index=False)
9771001

9781002
def test_AsMicroseconds(self):
9791003
from _testcapi import PyTime_AsMicroseconds
9801004

9811005
self.check_int_rounding(PyTime_AsMicroseconds,
9821006
self.create_decimal_converter(US_TO_NS),
983-
NS_TO_SEC)
1007+
NS_TO_SEC, index=False)
9841008

9851009

9861010
class TestOldPyTime(CPyTimeTestCase, unittest.TestCase):
@@ -1028,7 +1052,7 @@ def test_object_to_timeval(self):
10281052
self.create_converter(SEC_TO_US),
10291053
value_filter=self.time_t_filter)
10301054

1031-
# test nan
1055+
# test nan
10321056
for time_rnd, _ in ROUNDING_MODES:
10331057
with self.assertRaises(ValueError):
10341058
pytime_object_to_timeval(float('nan'), time_rnd)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Various time-related functions now use the ``__index__`` and ``__float__``
2+
methods to convert objects to time. This affects in particular
3+
``time.sleep()``.

Python/pytime.c

Lines changed: 138 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ _PyTime_DoubleToDenominator(double d, time_t *sec, long *numerator,
137137
/* volatile avoids optimization changing how numbers are rounded */
138138
volatile double floatpart;
139139

140+
/* For correctly rounding ROUND_HALF_EVEN, the denominator must be even */
141+
assert(idenominator % 2 == 0);
142+
140143
floatpart = modf(d, &intpart);
141144

142145
floatpart *= denominator;
@@ -167,57 +170,117 @@ _PyTime_ObjectToDenominator(PyObject *obj, time_t *sec, long *numerator,
167170
{
168171
assert(denominator >= 1);
169172

170-
if (PyFloat_Check(obj)) {
171-
double d = PyFloat_AsDouble(obj);
173+
/* Try the methods __index__, __float__, __int__ in this order
174+
* to ensure best possible precision. See the discussion at
175+
* https://bugs.python.org/issue35707 */
176+
177+
PyObject *intobj = NULL;
178+
if (PyIndex_Check(obj)) {
179+
intobj = PyNumber_Index(obj);
180+
if (intobj != NULL) {
181+
/* Success with __index__: skip the __float__ check */
182+
obj = intobj;
183+
goto convert_from_int;
184+
}
185+
/* If __index__ raises TypeError, try __float__ */
186+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
187+
PyErr_Clear();
188+
}
189+
else {
190+
return -1;
191+
}
192+
}
193+
194+
/* If we get here, then intobj is NULL */
195+
double d = PyFloat_AsDouble(obj);
196+
if (d == -1.0 && PyErr_Occurred()) {
197+
/* If __float__ raises TypeError, try __int__ */
198+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
199+
PyErr_Clear();
200+
}
201+
else {
202+
return -1;
203+
}
204+
}
205+
else {
206+
/* Success with __float__ */
172207
if (Py_IS_NAN(d)) {
173-
*numerator = 0;
174208
PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a number)");
175209
return -1;
176210
}
177211
return _PyTime_DoubleToDenominator(d, sec, numerator,
178212
denominator, round);
179213
}
180-
else {
181-
*sec = _PyLong_AsTime_t(obj);
182-
*numerator = 0;
183-
if (*sec == (time_t)-1 && PyErr_Occurred()) {
184-
return -1;
185-
}
186-
return 0;
214+
215+
convert_from_int:
216+
*sec = _PyLong_AsTime_t(obj);
217+
*numerator = 0;
218+
Py_XDECREF(intobj);
219+
if (*sec == (time_t)-1 && PyErr_Occurred()) {
220+
return -1;
187221
}
222+
return 0;
188223
}
189224

225+
190226
int
191227
_PyTime_ObjectToTime_t(PyObject *obj, time_t *sec, _PyTime_round_t round)
192228
{
193-
if (PyFloat_Check(obj)) {
194-
double intpart;
195-
/* volatile avoids optimization changing how numbers are rounded */
196-
volatile double d;
229+
/* Try the methods __index__, __float__, __int__ in this order
230+
* to ensure best possible precision. See the discussion at
231+
* https://bugs.python.org/issue35707 */
232+
233+
PyObject *intobj = NULL;
234+
if (PyIndex_Check(obj)) {
235+
intobj = PyNumber_Index(obj);
236+
if (intobj != NULL) {
237+
/* Success with __index__: skip the __float__ check */
238+
obj = intobj;
239+
goto convert_from_int;
240+
}
241+
/* If __index__ raises TypeError, try __float__ */
242+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
243+
PyErr_Clear();
244+
}
245+
else {
246+
return -1;
247+
}
248+
}
197249

198-
d = PyFloat_AsDouble(obj);
250+
/* If we get here, then intobj is NULL */
251+
double d = PyFloat_AsDouble(obj);
252+
if (d == -1.0 && PyErr_Occurred()) {
253+
/* If __float__ raises TypeError, try __int__ */
254+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
255+
PyErr_Clear();
256+
}
257+
else {
258+
return -1;
259+
}
260+
}
261+
else {
262+
/* Success with __float__ */
199263
if (Py_IS_NAN(d)) {
200264
PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a number)");
201265
return -1;
202266
}
203267

204268
d = _PyTime_Round(d, round);
205-
(void)modf(d, &intpart);
206-
207-
if (!_Py_InIntegralTypeRange(time_t, intpart)) {
269+
if (!_Py_InIntegralTypeRange(time_t, d)) {
208270
error_time_t_overflow();
209271
return -1;
210272
}
211-
*sec = (time_t)intpart;
273+
*sec = (time_t)d;
212274
return 0;
213275
}
214-
else {
215-
*sec = _PyLong_AsTime_t(obj);
216-
if (*sec == (time_t)-1 && PyErr_Occurred()) {
217-
return -1;
218-
}
219-
return 0;
276+
277+
convert_from_int:
278+
*sec = _PyLong_AsTime_t(obj);
279+
Py_XDECREF(intobj);
280+
if (*sec == (time_t)-1 && PyErr_Occurred()) {
281+
return -1;
220282
}
283+
return 0;
221284
}
222285

223286
int
@@ -401,34 +464,65 @@ static int
401464
_PyTime_FromObject(_PyTime_t *t, PyObject *obj, _PyTime_round_t round,
402465
long unit_to_ns)
403466
{
404-
if (PyFloat_Check(obj)) {
405-
double d;
406-
d = PyFloat_AsDouble(obj);
467+
/* Try the methods __index__, __float__, __int__ in this order
468+
* to ensure best possible precision. See the discussion at
469+
* https://bugs.python.org/issue35707 */
470+
471+
PyObject *intobj = NULL;
472+
if (PyIndex_Check(obj)) {
473+
intobj = PyNumber_Index(obj);
474+
if (intobj != NULL) {
475+
/* Success with __index__: skip the __float__ check */
476+
obj = intobj;
477+
goto convert_from_int;
478+
}
479+
/* If __index__ raises TypeError, try __float__ */
480+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
481+
PyErr_Clear();
482+
}
483+
else {
484+
return -1;
485+
}
486+
}
487+
488+
/* If we get here, then intobj is NULL */
489+
double d = PyFloat_AsDouble(obj);
490+
if (d == -1.0 && PyErr_Occurred()) {
491+
/* If __float__ raises TypeError, try __int__ */
492+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
493+
PyErr_Clear();
494+
}
495+
else {
496+
return -1;
497+
}
498+
}
499+
else {
500+
/* Success with __float__ */
407501
if (Py_IS_NAN(d)) {
408502
PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a number)");
409503
return -1;
410504
}
411505
return _PyTime_FromDouble(t, d, round, unit_to_ns);
412506
}
413-
else {
414-
long long sec;
415-
Py_BUILD_ASSERT(sizeof(long long) <= sizeof(_PyTime_t));
416507

417-
sec = PyLong_AsLongLong(obj);
418-
if (sec == -1 && PyErr_Occurred()) {
419-
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
420-
_PyTime_overflow();
421-
}
422-
return -1;
423-
}
508+
convert_from_int:
509+
Py_BUILD_ASSERT(sizeof(long long) <= sizeof(_PyTime_t));
424510

425-
if (_PyTime_check_mul_overflow(sec, unit_to_ns)) {
511+
long long sec = PyLong_AsLongLong(obj);
512+
Py_XDECREF(intobj);
513+
if (sec == -1 && PyErr_Occurred()) {
514+
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
426515
_PyTime_overflow();
427-
return -1;
428516
}
429-
*t = sec * unit_to_ns;
430-
return 0;
517+
return -1;
518+
}
519+
520+
if (_PyTime_check_mul_overflow(sec, unit_to_ns)) {
521+
_PyTime_overflow();
522+
return -1;
431523
}
524+
*t = sec * unit_to_ns;
525+
return 0;
432526
}
433527

434528
int
@@ -451,8 +545,8 @@ _PyTime_AsSecondsDouble(_PyTime_t t)
451545

452546
if (t % SEC_TO_NS == 0) {
453547
_PyTime_t secs;
454-
/* Divide using integers to avoid rounding issues on the integer part.
455-
1e-9 cannot be stored exactly in IEEE 64-bit. */
548+
/* Divide using integers to avoid rounding issues when
549+
* converting t to double */
456550
secs = t / SEC_TO_NS;
457551
d = (double)secs;
458552
}

0 commit comments

Comments
 (0)
0