diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index f790d43b6f4746..b3828160aa6a54 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -46,6 +46,19 @@ class _PyTime(enum.IntEnum): ) +class IndexLike: + def __init__(self, value): + self.value = int(value) + def __index__(self): + return self.value + +class FloatLike: + def __init__(self, value): + self.value = float(value) + def __float__(self): + return self.value + + class TimeTestCase(unittest.TestCase): def setUp(self): @@ -150,7 +163,7 @@ def test_conversions(self): def test_sleep(self): self.assertRaises(ValueError, time.sleep, -2) self.assertRaises(ValueError, time.sleep, -1) - time.sleep(1.2) + time.sleep(0.2) def test_strftime(self): tt = time.gmtime(self.t) @@ -453,21 +466,24 @@ def test_monotonic(self): self.assertGreaterEqual(t2, t1, "times=%s" % times) t1 = t2 - # monotonic() includes time elapsed during a sleep - t1 = time.monotonic() - time.sleep(0.5) - t2 = time.monotonic() - dt = t2 - t1 - self.assertGreater(t2, t1) - # bpo-20101: tolerate a difference of 50 ms because of bad timer - # resolution on Windows - self.assertTrue(0.450 <= dt) - # monotonic() is a monotonic but non adjustable clock info = time.get_clock_info('monotonic') self.assertTrue(info.monotonic) self.assertFalse(info.adjustable) + def test_monotic_sleep(self): + # This tests both time.sleep() and time.monotonic(): we test various + # types of input to time.sleep() and check using time.monotonic() that + # we sleep sufficiently long. + for T in (0.5, decimal.Decimal('0.5'), FloatLike(0.5), IndexLike(1)): + t1 = time.monotonic() + time.sleep(T) + t2 = time.monotonic() + dt = t2 - t1 + # bpo-20101: tolerate a difference of 50 ms because of bad timer + # resolution on Windows + self.assertGreaterEqual(dt, float(T) - 0.05) + def test_perf_counter(self): time.perf_counter() @@ -839,14 +855,23 @@ def convert_values(ns_timestamps): pytime_converter(value, time_rnd) def check_int_rounding(self, pytime_converter, expected_func, - unit_to_sec=1, value_filter=None): + unit_to_sec=1, value_filter=None, *, index=True): self._check_rounding(pytime_converter, expected_func, False, unit_to_sec, value_filter) + if index: + def convert_IndexLike(x, rnd): + return pytime_converter(IndexLike(x), rnd) + self._check_rounding(convert_IndexLike, expected_func, + False, unit_to_sec, value_filter) def check_float_rounding(self, pytime_converter, expected_func, unit_to_sec=1, value_filter=None): self._check_rounding(pytime_converter, expected_func, True, unit_to_sec, value_filter) + def convert_FloatLike(x, rnd): + return pytime_converter(FloatLike(x), rnd) + self._check_rounding(convert_FloatLike, expected_func, + True, unit_to_sec, value_filter) def decimal_round(self, x): d = decimal.Decimal(x) @@ -870,7 +895,7 @@ def c_int_filter(secs): self.check_int_rounding(lambda secs, rnd: PyTime_FromSeconds(secs), lambda secs: secs * SEC_TO_NS, - value_filter=c_int_filter) + value_filter=c_int_filter, index=False) # test nan for time_rnd, _ in ROUNDING_MODES: @@ -904,7 +929,7 @@ def float_converter(ns): self.check_int_rounding(lambda ns, rnd: PyTime_AsSecondsDouble(ns), float_converter, - NS_TO_SEC) + NS_TO_SEC, index=False) # test nan for time_rnd, _ in ROUNDING_MODES: @@ -941,7 +966,7 @@ def seconds_filter(secs): self.check_int_rounding(PyTime_AsTimeval, timeval_converter, NS_TO_SEC, - value_filter=seconds_filter) + value_filter=seconds_filter, index=False) @unittest.skipUnless(hasattr(_testcapi, 'PyTime_AsTimespec'), 'need _testcapi.PyTime_AsTimespec') @@ -954,21 +979,21 @@ def timespec_converter(ns): self.check_int_rounding(lambda ns, rnd: PyTime_AsTimespec(ns), timespec_converter, NS_TO_SEC, - value_filter=self.time_t_filter) + value_filter=self.time_t_filter, index=False) def test_AsMilliseconds(self): from _testcapi import PyTime_AsMilliseconds self.check_int_rounding(PyTime_AsMilliseconds, self.create_decimal_converter(MS_TO_NS), - NS_TO_SEC) + NS_TO_SEC, index=False) def test_AsMicroseconds(self): from _testcapi import PyTime_AsMicroseconds self.check_int_rounding(PyTime_AsMicroseconds, self.create_decimal_converter(US_TO_NS), - NS_TO_SEC) + NS_TO_SEC, index=False) class TestOldPyTime(CPyTimeTestCase, unittest.TestCase): @@ -1016,7 +1041,7 @@ def test_object_to_timeval(self): self.create_converter(SEC_TO_US), value_filter=self.time_t_filter) - # test nan + # test nan for time_rnd, _ in ROUNDING_MODES: with self.assertRaises(ValueError): pytime_object_to_timeval(float('nan'), time_rnd) diff --git a/Misc/NEWS.d/next/C API/2019-01-21-14-44-42.bpo-35707.3wB7XC.rst b/Misc/NEWS.d/next/C API/2019-01-21-14-44-42.bpo-35707.3wB7XC.rst new file mode 100644 index 00000000000000..7c254736434285 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2019-01-21-14-44-42.bpo-35707.3wB7XC.rst @@ -0,0 +1,3 @@ +Various time-related functions now use the ``__index__`` and ``__float__`` +methods to convert objects to time. This affects in particular +``time.sleep()``. diff --git a/Python/pytime.c b/Python/pytime.c index 9ff300699f04af..563ad027beb4b4 100644 --- a/Python/pytime.c +++ b/Python/pytime.c @@ -137,6 +137,9 @@ _PyTime_DoubleToDenominator(double d, time_t *sec, long *numerator, /* volatile avoids optimization changing how numbers are rounded */ volatile double floatpart; + /* For correctly rounding ROUND_HALF_EVEN, the denominator must be even */ + assert(idenominator % 2 == 0); + floatpart = modf(d, &intpart); floatpart *= denominator; @@ -167,57 +170,117 @@ _PyTime_ObjectToDenominator(PyObject *obj, time_t *sec, long *numerator, { assert(denominator >= 1); - if (PyFloat_Check(obj)) { - double d = PyFloat_AsDouble(obj); + /* Try the methods __index__, __float__, __int__ in this order + * to ensure best possible precision. See the discussion at + * https://bugs.python.org/issue35707 */ + + PyObject *intobj = NULL; + if (PyIndex_Check(obj)) { + intobj = PyNumber_Index(obj); + if (intobj != NULL) { + /* Success with __index__: skip the __float__ check */ + obj = intobj; + goto convert_from_int; + } + /* If __index__ raises TypeError, try __float__ */ + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + } + else { + return -1; + } + } + + /* If we get here, then intobj is NULL */ + double d = PyFloat_AsDouble(obj); + if (d == -1.0 && PyErr_Occurred()) { + /* If __float__ raises TypeError, try __int__ */ + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + } + else { + return -1; + } + } + else { + /* Success with __float__ */ if (Py_IS_NAN(d)) { - *numerator = 0; PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a number)"); return -1; } return _PyTime_DoubleToDenominator(d, sec, numerator, denominator, round); } - else { - *sec = _PyLong_AsTime_t(obj); - *numerator = 0; - if (*sec == (time_t)-1 && PyErr_Occurred()) { - return -1; - } - return 0; + +convert_from_int: + *sec = _PyLong_AsTime_t(obj); + *numerator = 0; + Py_XDECREF(intobj); + if (*sec == (time_t)-1 && PyErr_Occurred()) { + return -1; } + return 0; } + int _PyTime_ObjectToTime_t(PyObject *obj, time_t *sec, _PyTime_round_t round) { - if (PyFloat_Check(obj)) { - double intpart; - /* volatile avoids optimization changing how numbers are rounded */ - volatile double d; + /* Try the methods __index__, __float__, __int__ in this order + * to ensure best possible precision. See the discussion at + * https://bugs.python.org/issue35707 */ + + PyObject *intobj = NULL; + if (PyIndex_Check(obj)) { + intobj = PyNumber_Index(obj); + if (intobj != NULL) { + /* Success with __index__: skip the __float__ check */ + obj = intobj; + goto convert_from_int; + } + /* If __index__ raises TypeError, try __float__ */ + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + } + else { + return -1; + } + } - d = PyFloat_AsDouble(obj); + /* If we get here, then intobj is NULL */ + double d = PyFloat_AsDouble(obj); + if (d == -1.0 && PyErr_Occurred()) { + /* If __float__ raises TypeError, try __int__ */ + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + } + else { + return -1; + } + } + else { + /* Success with __float__ */ if (Py_IS_NAN(d)) { PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a number)"); return -1; } d = _PyTime_Round(d, round); - (void)modf(d, &intpart); - - if (!_Py_InIntegralTypeRange(time_t, intpart)) { + if (!_Py_InIntegralTypeRange(time_t, d)) { error_time_t_overflow(); return -1; } - *sec = (time_t)intpart; + *sec = (time_t)d; return 0; } - else { - *sec = _PyLong_AsTime_t(obj); - if (*sec == (time_t)-1 && PyErr_Occurred()) { - return -1; - } - return 0; + +convert_from_int: + *sec = _PyLong_AsTime_t(obj); + Py_XDECREF(intobj); + if (*sec == (time_t)-1 && PyErr_Occurred()) { + return -1; } + return 0; } int @@ -401,34 +464,65 @@ static int _PyTime_FromObject(_PyTime_t *t, PyObject *obj, _PyTime_round_t round, long unit_to_ns) { - if (PyFloat_Check(obj)) { - double d; - d = PyFloat_AsDouble(obj); + /* Try the methods __index__, __float__, __int__ in this order + * to ensure best possible precision. See the discussion at + * https://bugs.python.org/issue35707 */ + + PyObject *intobj = NULL; + if (PyIndex_Check(obj)) { + intobj = PyNumber_Index(obj); + if (intobj != NULL) { + /* Success with __index__: skip the __float__ check */ + obj = intobj; + goto convert_from_int; + } + /* If __index__ raises TypeError, try __float__ */ + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + } + else { + return -1; + } + } + + /* If we get here, then intobj is NULL */ + double d = PyFloat_AsDouble(obj); + if (d == -1.0 && PyErr_Occurred()) { + /* If __float__ raises TypeError, try __int__ */ + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + } + else { + return -1; + } + } + else { + /* Success with __float__ */ if (Py_IS_NAN(d)) { PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a number)"); return -1; } return _PyTime_FromDouble(t, d, round, unit_to_ns); } - else { - long long sec; - Py_BUILD_ASSERT(sizeof(long long) <= sizeof(_PyTime_t)); - sec = PyLong_AsLongLong(obj); - if (sec == -1 && PyErr_Occurred()) { - if (PyErr_ExceptionMatches(PyExc_OverflowError)) { - _PyTime_overflow(); - } - return -1; - } +convert_from_int: + Py_BUILD_ASSERT(sizeof(long long) <= sizeof(_PyTime_t)); - if (_PyTime_check_mul_overflow(sec, unit_to_ns)) { + long long sec = PyLong_AsLongLong(obj); + Py_XDECREF(intobj); + if (sec == -1 && PyErr_Occurred()) { + if (PyErr_ExceptionMatches(PyExc_OverflowError)) { _PyTime_overflow(); - return -1; } - *t = sec * unit_to_ns; - return 0; + return -1; + } + + if (_PyTime_check_mul_overflow(sec, unit_to_ns)) { + _PyTime_overflow(); + return -1; } + *t = sec * unit_to_ns; + return 0; } int @@ -451,8 +545,8 @@ _PyTime_AsSecondsDouble(_PyTime_t t) if (t % SEC_TO_NS == 0) { _PyTime_t secs; - /* Divide using integers to avoid rounding issues on the integer part. - 1e-9 cannot be stored exactly in IEEE 64-bit. */ + /* Divide using integers to avoid rounding issues when + * converting t to double */ secs = t / SEC_TO_NS; d = (double)secs; }