From bdbad71b9def0b86433de12cecca022eee91bd9f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 2 Jun 2019 00:05:48 +0300 Subject: [PATCH] bpo-20092. Use __index__ in constructors of int, float and complex. (GH-13108) --- Doc/c-api/complex.rst | 8 ++- Doc/c-api/float.rst | 4 ++ Doc/library/functions.rst | 21 +++++- Doc/reference/datamodel.rst | 8 +-- Doc/whatsnew/3.8.rst | 11 +++- Lib/test/test_cmath.py | 7 +- Lib/test/test_complex.py | 18 +++++ Lib/test/test_float.py | 15 +++++ Lib/test/test_getargs2.py | 6 ++ Lib/test/test_index.py | 2 +- Lib/test/test_int.py | 66 +++++++++++++++++-- .../2019-05-31-11-55-49.bpo-20092.KIMjBW.rst | 4 ++ Objects/abstract.c | 19 ++++++ Objects/complexobject.c | 6 +- Objects/floatobject.c | 9 +++ 15 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2019-05-31-11-55-49.bpo-20092.KIMjBW.rst diff --git a/Doc/c-api/complex.rst b/Doc/c-api/complex.rst index 675bd013e89..06dbb257272 100644 --- a/Doc/c-api/complex.rst +++ b/Doc/c-api/complex.rst @@ -129,4 +129,10 @@ Complex Numbers as Python Objects If *op* is not a Python complex number object but has a :meth:`__complex__` method, this method will first be called to convert *op* to a Python complex - number object. Upon failure, this method returns ``-1.0`` as a real value. + number object. If ``__complex__()`` is not defined then it falls back to + :meth:`__float__`. If ``__float__()`` is not defined then it falls back + to :meth:`__index__`. Upon failure, this method returns ``-1.0`` as a real + value. + + .. versionchanged:: 3.8 + Use :meth:`__index__` if available. diff --git a/Doc/c-api/float.rst b/Doc/c-api/float.rst index 8a996422ce4..057ff522516 100644 --- a/Doc/c-api/float.rst +++ b/Doc/c-api/float.rst @@ -47,9 +47,13 @@ Floating Point Objects Return a C :c:type:`double` representation of the contents of *pyfloat*. If *pyfloat* is not a Python floating point object but has a :meth:`__float__` method, this method will first be called to convert *pyfloat* into a float. + If ``__float__()`` is not defined then it falls back to :meth:`__index__`. This method returns ``-1.0`` upon failure, so one should call :c:func:`PyErr_Occurred` to check for errors. + .. versionchanged:: 3.8 + Use :meth:`__index__` if available. + .. c:function:: double PyFloat_AS_DOUBLE(PyObject *pyfloat) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 425a985320f..d5c9f18c79b 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -318,6 +318,11 @@ are always available. They are listed here in alphabetical order. :class:`int` and :class:`float`. If both arguments are omitted, returns ``0j``. + For a general Python object ``x``, ``complex(x)`` delegates to + ``x.__complex__()``. If ``__complex__()`` is not defined then it falls back + to :meth:`__float__`. If ``__float__()`` is not defined then it falls back + to :meth:`__index__`. + .. note:: When converting from a string, the string must not contain whitespace @@ -330,6 +335,10 @@ are always available. They are listed here in alphabetical order. .. versionchanged:: 3.6 Grouping digits with underscores as in code literals is allowed. + .. versionchanged:: 3.8 + Falls back to :meth:`__index__` if :meth:`__complex__` and + :meth:`__float__` are not defined. + .. function:: delattr(object, name) @@ -584,7 +593,8 @@ are always available. They are listed here in alphabetical order. float, an :exc:`OverflowError` will be raised. For a general Python object ``x``, ``float(x)`` delegates to - ``x.__float__()``. + ``x.__float__()``. If ``__float__()`` is not defined then it falls back + to :meth:`__index__`. If no argument is given, ``0.0`` is returned. @@ -609,6 +619,9 @@ are always available. They are listed here in alphabetical order. .. versionchanged:: 3.7 *x* is now a positional-only parameter. + .. versionchanged:: 3.8 + Falls back to :meth:`__index__` if :meth:`__float__` is not defined. + .. index:: single: __format__ @@ -780,7 +793,8 @@ are always available. They are listed here in alphabetical order. Return an integer object constructed from a number or string *x*, or return ``0`` if no arguments are given. If *x* defines :meth:`__int__`, - ``int(x)`` returns ``x.__int__()``. If *x* defines :meth:`__trunc__`, + ``int(x)`` returns ``x.__int__()``. If *x* defines :meth:`__index__`, + it returns ``x.__index__()``. If *x* defines :meth:`__trunc__`, it returns ``x.__trunc__()``. For floating point numbers, this truncates towards zero. @@ -812,6 +826,9 @@ are always available. They are listed here in alphabetical order. .. versionchanged:: 3.7 *x* is now a positional-only parameter. + .. versionchanged:: 3.8 + Falls back to :meth:`__index__` if :meth:`__int__` is not defined. + .. function:: isinstance(object, classinfo) diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 44017d8a55d..fa47bf1c161 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -2394,11 +2394,9 @@ left undefined. functions). Presence of this method indicates that the numeric object is an integer type. Must return an integer. - .. note:: - - In order to have a coherent integer type class, when :meth:`__index__` is - defined :meth:`__int__` should also be defined, and both should return - the same value. + If :meth:`__int__`, :meth:`__float__` and :meth:`__complex__` are not + defined then corresponding built-in functions :func:`int`, :func:`float` + and :func:`complex` fall back to :meth:`__index__`. .. method:: object.__round__(self, [,ndigits]) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 4c5a9bb0cdb..591b4548837 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -250,6 +250,12 @@ Other Language Changes compatible with the existing :meth:`float.as_integer_ratio` method. (Contributed by Lisa Roach in :issue:`33073`.) +* Constructors of :class:`int`, :class:`float` and :class:`complex` will now + use the :meth:`~object.__index__` special method, if available and the + corresponding method :meth:`~object.__int__`, :meth:`~object.__float__` + or :meth:`~object.__complex__` is not available. + (Contributed by Serhiy Storchaka in :issue:`20092`.) + * Added support of ``\N{name}`` escapes in :mod:`regular expressions `. (Contributed by Jonathan Eunice and Serhiy Storchaka in :issue:`30688`.) @@ -868,7 +874,10 @@ Build and C API Changes ``__index__()`` method (like :class:`~decimal.Decimal` and :class:`~fractions.Fraction`). :c:func:`PyNumber_Check` will now return ``1`` for objects implementing ``__index__()``. - (Contributed by Serhiy Storchaka in :issue:`36048`.) + :c:func:`PyNumber_Long`, :c:func:`PyNumber_Float` and + :c:func:`PyFloat_AsDouble` also now use the ``__index__()`` method if + available. + (Contributed by Serhiy Storchaka in :issue:`36048` and :issue:`20092`.) * Heap-allocated type objects will now increase their reference count in :c:func:`PyObject_Init` (and its parallel macro ``PyObject_INIT``) diff --git a/Lib/test/test_cmath.py b/Lib/test/test_cmath.py index 43a074b4b66..a00185f43db 100644 --- a/Lib/test/test_cmath.py +++ b/Lib/test/test_cmath.py @@ -220,12 +220,11 @@ class CMathTests(unittest.TestCase): pass class NeitherComplexNorFloatOS: pass - class MyInt(object): + class Index: def __int__(self): return 2 def __index__(self): return 2 - class MyIntOS: + class MyInt: def __int__(self): return 2 - def __index__(self): return 2 # other possible combinations of __float__ and __complex__ # that should work @@ -255,6 +254,7 @@ class CMathTests(unittest.TestCase): self.assertEqual(f(FloatAndComplexOS()), f(cx_arg)) self.assertEqual(f(JustFloat()), f(flt_arg)) self.assertEqual(f(JustFloatOS()), f(flt_arg)) + self.assertEqual(f(Index()), f(int(Index()))) # TypeError should be raised for classes not providing # either __complex__ or __float__, even if they provide # __int__ or __index__. An old-style class @@ -263,7 +263,6 @@ class CMathTests(unittest.TestCase): self.assertRaises(TypeError, f, NeitherComplexNorFloat()) self.assertRaises(TypeError, f, MyInt()) self.assertRaises(Exception, f, NeitherComplexNorFloatOS()) - self.assertRaises(Exception, f, MyIntOS()) # non-complex return value from __complex__ -> TypeError for bad_complex in non_complexes: self.assertRaises(TypeError, f, MyComplex(bad_complex)) diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 21c6eaed605..fe1e566562d 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -368,6 +368,24 @@ class ComplexTest(unittest.TestCase): self.assertAlmostEqual(complex(real=float2(17.), imag=float2(23.)), 17+23j) self.assertRaises(TypeError, complex, float2(None)) + class MyIndex: + def __init__(self, value): + self.value = value + def __index__(self): + return self.value + + self.assertAlmostEqual(complex(MyIndex(42)), 42.0+0.0j) + self.assertAlmostEqual(complex(123, MyIndex(42)), 123.0+42.0j) + self.assertRaises(OverflowError, complex, MyIndex(2**2000)) + self.assertRaises(OverflowError, complex, 123, MyIndex(2**2000)) + + class MyInt: + def __int__(self): + return 42 + + self.assertRaises(TypeError, complex, MyInt()) + self.assertRaises(TypeError, complex, 123, MyInt()) + class complex0(complex): """Test usage of __complex__() when inheriting from 'complex'""" def __complex__(self): diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index 5278d716de2..b656582538e 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -223,6 +223,21 @@ class GeneralFloatCases(unittest.TestCase): with self.assertWarns(DeprecationWarning): self.assertIs(type(FloatSubclass(F())), FloatSubclass) + class MyIndex: + def __init__(self, value): + self.value = value + def __index__(self): + return self.value + + self.assertEqual(float(MyIndex(42)), 42.0) + self.assertRaises(OverflowError, float, MyIndex(2**2000)) + + class MyInt: + def __int__(self): + return 42 + + self.assertRaises(TypeError, float, MyInt()) + def test_keyword_args(self): with self.assertRaisesRegex(TypeError, 'keyword argument'): float(x='3.14') diff --git a/Lib/test/test_getargs2.py b/Lib/test/test_getargs2.py index 07e2d151379..1a73fa46158 100644 --- a/Lib/test/test_getargs2.py +++ b/Lib/test/test_getargs2.py @@ -457,6 +457,8 @@ class Float_TestCase(unittest.TestCase): with self.assertWarns(DeprecationWarning): self.assertEqual(getargs_f(BadFloat2()), 4.25) self.assertEqual(getargs_f(BadFloat3(7.5)), 7.5) + self.assertEqual(getargs_f(Index()), 99.0) + self.assertRaises(TypeError, getargs_f, Int()) for x in (FLT_MIN, -FLT_MIN, FLT_MAX, -FLT_MAX, INF, -INF): self.assertEqual(getargs_f(x), x) @@ -489,6 +491,8 @@ class Float_TestCase(unittest.TestCase): with self.assertWarns(DeprecationWarning): self.assertEqual(getargs_d(BadFloat2()), 4.25) self.assertEqual(getargs_d(BadFloat3(7.5)), 7.5) + self.assertEqual(getargs_d(Index()), 99.0) + self.assertRaises(TypeError, getargs_d, Int()) for x in (DBL_MIN, -DBL_MIN, DBL_MAX, -DBL_MAX, INF, -INF): self.assertEqual(getargs_d(x), x) @@ -511,6 +515,8 @@ class Float_TestCase(unittest.TestCase): with self.assertWarns(DeprecationWarning): self.assertEqual(getargs_D(BadComplex2()), 4.25+0.5j) self.assertEqual(getargs_D(BadComplex3(7.5+0.25j)), 7.5+0.25j) + self.assertEqual(getargs_D(Index()), 99.0+0j) + self.assertRaises(TypeError, getargs_D, Int()) for x in (DBL_MIN, -DBL_MIN, DBL_MAX, -DBL_MAX, INF, -INF): c = complex(x, 1.0) diff --git a/Lib/test/test_index.py b/Lib/test/test_index.py index a2ac32132e2..cbdc56c801a 100644 --- a/Lib/test/test_index.py +++ b/Lib/test/test_index.py @@ -60,7 +60,7 @@ class BaseTestCase(unittest.TestCase): # subclasses. See issue #17576. class MyInt(int): def __index__(self): - return int(self) + 1 + return int(str(self)) + 1 my_int = MyInt(7) direct_index = my_int.__index__() diff --git a/Lib/test/test_int.py b/Lib/test/test_int.py index 307ca36bb4f..6fdf52ef23f 100644 --- a/Lib/test/test_int.py +++ b/Lib/test/test_int.py @@ -378,15 +378,23 @@ class IntTestCases(unittest.TestCase): int(ExceptionalTrunc()) for trunc_result_base in (object, Classic): - class Integral(trunc_result_base): - def __int__(self): + class Index(trunc_result_base): + def __index__(self): return 42 class TruncReturnsNonInt(base): def __trunc__(self): - return Integral() - with self.assertWarns(DeprecationWarning): - self.assertEqual(int(TruncReturnsNonInt()), 42) + return Index() + self.assertEqual(int(TruncReturnsNonInt()), 42) + + class Intable(trunc_result_base): + def __int__(self): + return 42 + + class TruncReturnsNonIndex(base): + def __trunc__(self): + return Intable() + self.assertEqual(int(TruncReturnsNonInt()), 42) class NonIntegral(trunc_result_base): def __trunc__(self): @@ -418,6 +426,21 @@ class IntTestCases(unittest.TestCase): with self.assertRaises(TypeError): int(TruncReturnsBadInt()) + def test_int_subclass_with_index(self): + class MyIndex(int): + def __index__(self): + return 42 + + class BadIndex(int): + def __index__(self): + return 42.0 + + my_int = MyIndex(7) + self.assertEqual(my_int, 7) + self.assertEqual(int(my_int), 7) + + self.assertEqual(int(BadIndex()), 0) + def test_int_subclass_with_int(self): class MyInt(int): def __int__(self): @@ -431,9 +454,19 @@ class IntTestCases(unittest.TestCase): self.assertEqual(my_int, 7) self.assertEqual(int(my_int), 42) - self.assertRaises(TypeError, int, BadInt()) + my_int = BadInt(7) + self.assertEqual(my_int, 7) + self.assertRaises(TypeError, int, my_int) def test_int_returns_int_subclass(self): + class BadIndex: + def __index__(self): + return True + + class BadIndex2(int): + def __index__(self): + return True + class BadInt: def __int__(self): return True @@ -442,6 +475,10 @@ class IntTestCases(unittest.TestCase): def __int__(self): return True + class TruncReturnsBadIndex: + def __trunc__(self): + return BadIndex() + class TruncReturnsBadInt: def __trunc__(self): return BadInt() @@ -450,6 +487,17 @@ class IntTestCases(unittest.TestCase): def __trunc__(self): return True + bad_int = BadIndex() + with self.assertWarns(DeprecationWarning): + n = int(bad_int) + self.assertEqual(n, 1) + self.assertIs(type(n), int) + + bad_int = BadIndex2() + n = int(bad_int) + self.assertEqual(n, 0) + self.assertIs(type(n), int) + bad_int = BadInt() with self.assertWarns(DeprecationWarning): n = int(bad_int) @@ -462,6 +510,12 @@ class IntTestCases(unittest.TestCase): self.assertEqual(n, 1) self.assertIs(type(n), int) + bad_int = TruncReturnsBadIndex() + with self.assertWarns(DeprecationWarning): + n = int(bad_int) + self.assertEqual(n, 1) + self.assertIs(type(n), int) + bad_int = TruncReturnsBadInt() with self.assertWarns(DeprecationWarning): n = int(bad_int) diff --git a/Misc/NEWS.d/next/Core and Builtins/2019-05-31-11-55-49.bpo-20092.KIMjBW.rst b/Misc/NEWS.d/next/Core and Builtins/2019-05-31-11-55-49.bpo-20092.KIMjBW.rst new file mode 100644 index 00000000000..7536dc33c9f --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2019-05-31-11-55-49.bpo-20092.KIMjBW.rst @@ -0,0 +1,4 @@ +Constructors of :class:`int`, :class:`float` and :class:`complex` will now +use the :meth:`~object.__index__` special method, if available and the +corresponding method :meth:`~object.__int__`, :meth:`~object.__float__` +or :meth:`~object.__complex__` is not available. diff --git a/Objects/abstract.c b/Objects/abstract.c index 68d06edfa60..77d09143aa0 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -1373,6 +1373,13 @@ PyNumber_Long(PyObject *o) } return result; } + if (m && m->nb_index) { + result = _PyLong_FromNbIndexOrNbInt(o); + if (result != NULL && !PyLong_CheckExact(result)) { + Py_SETREF(result, _PyLong_Copy((PyLongObject *)result)); + } + return result; + } trunc_func = _PyObject_LookupSpecial(o, &PyId___trunc__); if (trunc_func) { result = _PyObject_CallNoArg(trunc_func); @@ -1480,6 +1487,18 @@ PyNumber_Float(PyObject *o) Py_DECREF(res); return PyFloat_FromDouble(val); } + if (m && m->nb_index) { + PyObject *res = PyNumber_Index(o); + if (!res) { + return NULL; + } + double val = PyLong_AsDouble(res); + Py_DECREF(res); + if (val == -1.0 && PyErr_Occurred()) { + return NULL; + } + return PyFloat_FromDouble(val); + } if (PyFloat_Check(o)) { /* A float subclass with nb_float == NULL */ return PyFloat_FromDouble(PyFloat_AS_DOUBLE(o)); } diff --git a/Objects/complexobject.c b/Objects/complexobject.c index a5f95186d62..f78c0fdf78d 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -984,7 +984,7 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i) } nbr = r->ob_type->tp_as_number; - if (nbr == NULL || nbr->nb_float == NULL) { + if (nbr == NULL || (nbr->nb_float == NULL && nbr->nb_index == NULL)) { PyErr_Format(PyExc_TypeError, "complex() first argument must be a string or a number, " "not '%.200s'", @@ -996,7 +996,7 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i) } if (i != NULL) { nbi = i->ob_type->tp_as_number; - if (nbi == NULL || nbi->nb_float == NULL) { + if (nbi == NULL || (nbi->nb_float == NULL && nbi->nb_index == NULL)) { PyErr_Format(PyExc_TypeError, "complex() second argument must be a number, " "not '%.200s'", @@ -1052,7 +1052,7 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i) /* The "imag" part really is entirely imaginary, and contributes nothing in the real direction. Just treat it as a double. */ - tmp = (*nbi->nb_float)(i); + tmp = PyNumber_Float(i); if (tmp == NULL) return NULL; ci.real = PyFloat_AsDouble(tmp); diff --git a/Objects/floatobject.c b/Objects/floatobject.c index 2bf7061d4f6..15cbe5c9d8b 100644 --- a/Objects/floatobject.c +++ b/Objects/floatobject.c @@ -246,6 +246,15 @@ PyFloat_AsDouble(PyObject *op) nb = Py_TYPE(op)->tp_as_number; if (nb == NULL || nb->nb_float == NULL) { + if (nb && nb->nb_index) { + PyObject *res = PyNumber_Index(op); + if (!res) { + return -1; + } + double val = PyLong_AsDouble(res); + Py_DECREF(res); + return val; + } PyErr_Format(PyExc_TypeError, "must be real number, not %.50s", op->ob_type->tp_name); return -1;