[Python-checkins] bpo-20092. Use __index__ in constructors of int, float and complex. (GH-13108)

Serhiy Storchaka webhook-mailer at python.org
Sat Jun 1 17:05:51 EDT 2019


https://github.com/python/cpython/commit/bdbad71b9def0b86433de12cecca022eee91bd9f
commit: bdbad71b9def0b86433de12cecca022eee91bd9f
branch: master
author: Serhiy Storchaka <storchaka at gmail.com>
committer: GitHub <noreply at github.com>
date: 2019-06-02T00:05:48+03:00
summary:

bpo-20092. Use __index__ in constructors of int, float and complex. (GH-13108)

files:
A Misc/NEWS.d/next/Core and Builtins/2019-05-31-11-55-49.bpo-20092.KIMjBW.rst
M Doc/c-api/complex.rst
M Doc/c-api/float.rst
M Doc/library/functions.rst
M Doc/reference/datamodel.rst
M Doc/whatsnew/3.8.rst
M Lib/test/test_cmath.py
M Lib/test/test_complex.py
M Lib/test/test_float.py
M Lib/test/test_getargs2.py
M Lib/test/test_index.py
M Lib/test/test_int.py
M Objects/abstract.c
M Objects/complexobject.c
M Objects/floatobject.c

diff --git a/Doc/c-api/complex.rst b/Doc/c-api/complex.rst
index 675bd013e892..06dbb2572725 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 8a996422ce48..057ff522516a 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 425a985320fc..d5c9f18c79b7 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 44017d8a55df..fa47bf1c1619 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 4c5a9bb0cdb9..591b45488372 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 <re>`.
   (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 43a074b4b663..a00185f43dbf 100644
--- a/Lib/test/test_cmath.py
+++ b/Lib/test/test_cmath.py
@@ -220,12 +220,11 @@ class NeitherComplexNorFloat(object):
             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 @@ def __float__(self):
             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 @@ def __float__(self):
             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 21c6eaed6054..fe1e566562de 100644
--- a/Lib/test/test_complex.py
+++ b/Lib/test/test_complex.py
@@ -368,6 +368,24 @@ def __float__(self):
         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 5278d716de23..b656582538e8 100644
--- a/Lib/test/test_float.py
+++ b/Lib/test/test_float.py
@@ -223,6 +223,21 @@ def __float__(self):
         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 07e2d1513791..1a73fa461580 100644
--- a/Lib/test/test_getargs2.py
+++ b/Lib/test/test_getargs2.py
@@ -457,6 +457,8 @@ def test_f(self):
         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 @@ def test_d(self):
         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 @@ def test_D(self):
         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 a2ac32132e23..cbdc56c801a4 100644
--- a/Lib/test/test_index.py
+++ b/Lib/test/test_index.py
@@ -60,7 +60,7 @@ def test_int_subclass_with_index(self):
         # 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 307ca36bb4fa..6fdf52ef23f6 100644
--- a/Lib/test/test_int.py
+++ b/Lib/test/test_int.py
@@ -378,15 +378,23 @@ def __trunc__(self):
                 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 @@ def __trunc__(self):
                 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 @@ def __int__(self):
         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 BadInt2(int):
             def __int__(self):
                 return True
 
+        class TruncReturnsBadIndex:
+            def __trunc__(self):
+                return BadIndex()
+
         class TruncReturnsBadInt:
             def __trunc__(self):
                 return BadInt()
@@ -450,6 +487,17 @@ class TruncReturnsIntSubclass:
             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 @@ def __trunc__(self):
         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 000000000000..7536dc33c9f1
--- /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 68d06edfa600..77d09143aa07 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 a5f95186d625..f78c0fdf78de 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 2bf7061d4f62..15cbe5c9d8ba 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;



More information about the Python-checkins mailing list