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

This commit is contained in:
Serhiy Storchaka 2019-06-02 00:05:48 +03:00 committed by GitHub
parent 1a4d9ffa1a
commit bdbad71b9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 181 additions and 23 deletions

View File

@ -129,4 +129,10 @@ Complex Numbers as Python Objects
If *op* is not a Python complex number object but has a :meth:`__complex__` 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 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.

View File

@ -47,9 +47,13 @@ Floating Point Objects
Return a C :c:type:`double` representation of the contents of *pyfloat*. If 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__` *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. 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 This method returns ``-1.0`` upon failure, so one should call
:c:func:`PyErr_Occurred` to check for errors. :c:func:`PyErr_Occurred` to check for errors.
.. versionchanged:: 3.8
Use :meth:`__index__` if available.
.. c:function:: double PyFloat_AS_DOUBLE(PyObject *pyfloat) .. c:function:: double PyFloat_AS_DOUBLE(PyObject *pyfloat)

View File

@ -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 :class:`int` and :class:`float`. If both arguments are omitted, returns
``0j``. ``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:: .. note::
When converting from a string, the string must not contain whitespace 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 .. versionchanged:: 3.6
Grouping digits with underscores as in code literals is allowed. 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) .. 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. float, an :exc:`OverflowError` will be raised.
For a general Python object ``x``, ``float(x)`` delegates to 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. 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 .. versionchanged:: 3.7
*x* is now a positional-only parameter. *x* is now a positional-only parameter.
.. versionchanged:: 3.8
Falls back to :meth:`__index__` if :meth:`__float__` is not defined.
.. index:: .. index::
single: __format__ 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 Return an integer object constructed from a number or string *x*, or return
``0`` if no arguments are given. If *x* defines :meth:`__int__`, ``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__()``. it returns ``x.__trunc__()``.
For floating point numbers, this truncates towards zero. 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 .. versionchanged:: 3.7
*x* is now a positional-only parameter. *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) .. function:: isinstance(object, classinfo)

View File

@ -2394,11 +2394,9 @@ left undefined.
functions). Presence of this method indicates that the numeric object is functions). Presence of this method indicates that the numeric object is
an integer type. Must return an integer. an integer type. Must return an integer.
.. note:: If :meth:`__int__`, :meth:`__float__` and :meth:`__complex__` are not
defined then corresponding built-in functions :func:`int`, :func:`float`
In order to have a coherent integer type class, when :meth:`__index__` is and :func:`complex` fall back to :meth:`__index__`.
defined :meth:`__int__` should also be defined, and both should return
the same value.
.. method:: object.__round__(self, [,ndigits]) .. method:: object.__round__(self, [,ndigits])

View File

@ -250,6 +250,12 @@ Other Language Changes
compatible with the existing :meth:`float.as_integer_ratio` method. compatible with the existing :meth:`float.as_integer_ratio` method.
(Contributed by Lisa Roach in :issue:`33073`.) (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>`. * Added support of ``\N{name}`` escapes in :mod:`regular expressions <re>`.
(Contributed by Jonathan Eunice and Serhiy Storchaka in :issue:`30688`.) (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 ``__index__()`` method (like :class:`~decimal.Decimal` and
:class:`~fractions.Fraction`). :c:func:`PyNumber_Check` will now return :class:`~fractions.Fraction`). :c:func:`PyNumber_Check` will now return
``1`` for objects implementing ``__index__()``. ``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 * Heap-allocated type objects will now increase their reference count
in :c:func:`PyObject_Init` (and its parallel macro ``PyObject_INIT``) in :c:func:`PyObject_Init` (and its parallel macro ``PyObject_INIT``)

View File

@ -220,12 +220,11 @@ class CMathTests(unittest.TestCase):
pass pass
class NeitherComplexNorFloatOS: class NeitherComplexNorFloatOS:
pass pass
class MyInt(object): class Index:
def __int__(self): return 2 def __int__(self): return 2
def __index__(self): return 2 def __index__(self): return 2
class MyIntOS: class MyInt:
def __int__(self): return 2 def __int__(self): return 2
def __index__(self): return 2
# other possible combinations of __float__ and __complex__ # other possible combinations of __float__ and __complex__
# that should work # that should work
@ -255,6 +254,7 @@ class CMathTests(unittest.TestCase):
self.assertEqual(f(FloatAndComplexOS()), f(cx_arg)) self.assertEqual(f(FloatAndComplexOS()), f(cx_arg))
self.assertEqual(f(JustFloat()), f(flt_arg)) self.assertEqual(f(JustFloat()), f(flt_arg))
self.assertEqual(f(JustFloatOS()), 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 # TypeError should be raised for classes not providing
# either __complex__ or __float__, even if they provide # either __complex__ or __float__, even if they provide
# __int__ or __index__. An old-style class # __int__ or __index__. An old-style class
@ -263,7 +263,6 @@ class CMathTests(unittest.TestCase):
self.assertRaises(TypeError, f, NeitherComplexNorFloat()) self.assertRaises(TypeError, f, NeitherComplexNorFloat())
self.assertRaises(TypeError, f, MyInt()) self.assertRaises(TypeError, f, MyInt())
self.assertRaises(Exception, f, NeitherComplexNorFloatOS()) self.assertRaises(Exception, f, NeitherComplexNorFloatOS())
self.assertRaises(Exception, f, MyIntOS())
# non-complex return value from __complex__ -> TypeError # non-complex return value from __complex__ -> TypeError
for bad_complex in non_complexes: for bad_complex in non_complexes:
self.assertRaises(TypeError, f, MyComplex(bad_complex)) self.assertRaises(TypeError, f, MyComplex(bad_complex))

View File

@ -368,6 +368,24 @@ class ComplexTest(unittest.TestCase):
self.assertAlmostEqual(complex(real=float2(17.), imag=float2(23.)), 17+23j) self.assertAlmostEqual(complex(real=float2(17.), imag=float2(23.)), 17+23j)
self.assertRaises(TypeError, complex, float2(None)) 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): class complex0(complex):
"""Test usage of __complex__() when inheriting from 'complex'""" """Test usage of __complex__() when inheriting from 'complex'"""
def __complex__(self): def __complex__(self):

View File

@ -223,6 +223,21 @@ class GeneralFloatCases(unittest.TestCase):
with self.assertWarns(DeprecationWarning): with self.assertWarns(DeprecationWarning):
self.assertIs(type(FloatSubclass(F())), FloatSubclass) 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): def test_keyword_args(self):
with self.assertRaisesRegex(TypeError, 'keyword argument'): with self.assertRaisesRegex(TypeError, 'keyword argument'):
float(x='3.14') float(x='3.14')

View File

@ -457,6 +457,8 @@ class Float_TestCase(unittest.TestCase):
with self.assertWarns(DeprecationWarning): with self.assertWarns(DeprecationWarning):
self.assertEqual(getargs_f(BadFloat2()), 4.25) self.assertEqual(getargs_f(BadFloat2()), 4.25)
self.assertEqual(getargs_f(BadFloat3(7.5)), 7.5) 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): for x in (FLT_MIN, -FLT_MIN, FLT_MAX, -FLT_MAX, INF, -INF):
self.assertEqual(getargs_f(x), x) self.assertEqual(getargs_f(x), x)
@ -489,6 +491,8 @@ class Float_TestCase(unittest.TestCase):
with self.assertWarns(DeprecationWarning): with self.assertWarns(DeprecationWarning):
self.assertEqual(getargs_d(BadFloat2()), 4.25) self.assertEqual(getargs_d(BadFloat2()), 4.25)
self.assertEqual(getargs_d(BadFloat3(7.5)), 7.5) 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): for x in (DBL_MIN, -DBL_MIN, DBL_MAX, -DBL_MAX, INF, -INF):
self.assertEqual(getargs_d(x), x) self.assertEqual(getargs_d(x), x)
@ -511,6 +515,8 @@ class Float_TestCase(unittest.TestCase):
with self.assertWarns(DeprecationWarning): with self.assertWarns(DeprecationWarning):
self.assertEqual(getargs_D(BadComplex2()), 4.25+0.5j) 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(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): for x in (DBL_MIN, -DBL_MIN, DBL_MAX, -DBL_MAX, INF, -INF):
c = complex(x, 1.0) c = complex(x, 1.0)

View File

@ -60,7 +60,7 @@ class BaseTestCase(unittest.TestCase):
# subclasses. See issue #17576. # subclasses. See issue #17576.
class MyInt(int): class MyInt(int):
def __index__(self): def __index__(self):
return int(self) + 1 return int(str(self)) + 1
my_int = MyInt(7) my_int = MyInt(7)
direct_index = my_int.__index__() direct_index = my_int.__index__()

View File

@ -378,15 +378,23 @@ class IntTestCases(unittest.TestCase):
int(ExceptionalTrunc()) int(ExceptionalTrunc())
for trunc_result_base in (object, Classic): for trunc_result_base in (object, Classic):
class Integral(trunc_result_base): class Index(trunc_result_base):
def __int__(self): def __index__(self):
return 42 return 42
class TruncReturnsNonInt(base): class TruncReturnsNonInt(base):
def __trunc__(self): def __trunc__(self):
return Integral() return Index()
with self.assertWarns(DeprecationWarning): self.assertEqual(int(TruncReturnsNonInt()), 42)
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): class NonIntegral(trunc_result_base):
def __trunc__(self): def __trunc__(self):
@ -418,6 +426,21 @@ class IntTestCases(unittest.TestCase):
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
int(TruncReturnsBadInt()) 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): def test_int_subclass_with_int(self):
class MyInt(int): class MyInt(int):
def __int__(self): def __int__(self):
@ -431,9 +454,19 @@ class IntTestCases(unittest.TestCase):
self.assertEqual(my_int, 7) self.assertEqual(my_int, 7)
self.assertEqual(int(my_int), 42) 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): def test_int_returns_int_subclass(self):
class BadIndex:
def __index__(self):
return True
class BadIndex2(int):
def __index__(self):
return True
class BadInt: class BadInt:
def __int__(self): def __int__(self):
return True return True
@ -442,6 +475,10 @@ class IntTestCases(unittest.TestCase):
def __int__(self): def __int__(self):
return True return True
class TruncReturnsBadIndex:
def __trunc__(self):
return BadIndex()
class TruncReturnsBadInt: class TruncReturnsBadInt:
def __trunc__(self): def __trunc__(self):
return BadInt() return BadInt()
@ -450,6 +487,17 @@ class IntTestCases(unittest.TestCase):
def __trunc__(self): def __trunc__(self):
return True 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() bad_int = BadInt()
with self.assertWarns(DeprecationWarning): with self.assertWarns(DeprecationWarning):
n = int(bad_int) n = int(bad_int)
@ -462,6 +510,12 @@ class IntTestCases(unittest.TestCase):
self.assertEqual(n, 1) self.assertEqual(n, 1)
self.assertIs(type(n), int) 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() bad_int = TruncReturnsBadInt()
with self.assertWarns(DeprecationWarning): with self.assertWarns(DeprecationWarning):
n = int(bad_int) n = int(bad_int)

View File

@ -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.

View File

@ -1373,6 +1373,13 @@ PyNumber_Long(PyObject *o)
} }
return result; 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__); trunc_func = _PyObject_LookupSpecial(o, &PyId___trunc__);
if (trunc_func) { if (trunc_func) {
result = _PyObject_CallNoArg(trunc_func); result = _PyObject_CallNoArg(trunc_func);
@ -1480,6 +1487,18 @@ PyNumber_Float(PyObject *o)
Py_DECREF(res); Py_DECREF(res);
return PyFloat_FromDouble(val); 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 */ if (PyFloat_Check(o)) { /* A float subclass with nb_float == NULL */
return PyFloat_FromDouble(PyFloat_AS_DOUBLE(o)); return PyFloat_FromDouble(PyFloat_AS_DOUBLE(o));
} }

View File

@ -984,7 +984,7 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
} }
nbr = r->ob_type->tp_as_number; 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, PyErr_Format(PyExc_TypeError,
"complex() first argument must be a string or a number, " "complex() first argument must be a string or a number, "
"not '%.200s'", "not '%.200s'",
@ -996,7 +996,7 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
} }
if (i != NULL) { if (i != NULL) {
nbi = i->ob_type->tp_as_number; 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, PyErr_Format(PyExc_TypeError,
"complex() second argument must be a number, " "complex() second argument must be a number, "
"not '%.200s'", "not '%.200s'",
@ -1052,7 +1052,7 @@ complex_new_impl(PyTypeObject *type, PyObject *r, PyObject *i)
/* The "imag" part really is entirely imaginary, and /* The "imag" part really is entirely imaginary, and
contributes nothing in the real direction. contributes nothing in the real direction.
Just treat it as a double. */ Just treat it as a double. */
tmp = (*nbi->nb_float)(i); tmp = PyNumber_Float(i);
if (tmp == NULL) if (tmp == NULL)
return NULL; return NULL;
ci.real = PyFloat_AsDouble(tmp); ci.real = PyFloat_AsDouble(tmp);

View File

@ -246,6 +246,15 @@ PyFloat_AsDouble(PyObject *op)
nb = Py_TYPE(op)->tp_as_number; nb = Py_TYPE(op)->tp_as_number;
if (nb == NULL || nb->nb_float == NULL) { 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", PyErr_Format(PyExc_TypeError, "must be real number, not %.50s",
op->ob_type->tp_name); op->ob_type->tp_name);
return -1; return -1;