Skip to content

Commit bdbad71

Browse files
bpo-20092. Use __index__ in constructors of int, float and complex. (GH-13108)
1 parent 1a4d9ff commit bdbad71

File tree

15 files changed

+181
-23
lines changed

15 files changed

+181
-23
lines changed

Doc/c-api/complex.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,10 @@ Complex Numbers as Python Objects
129129
130130
If *op* is not a Python complex number object but has a :meth:`__complex__`
131131
method, this method will first be called to convert *op* to a Python complex
132-
number object. Upon failure, this method returns ``-1.0`` as a real value.
132+
number object. If ``__complex__()`` is not defined then it falls back to
133+
:meth:`__float__`. If ``__float__()`` is not defined then it falls back
134+
to :meth:`__index__`. Upon failure, this method returns ``-1.0`` as a real
135+
value.
136+
137+
.. versionchanged:: 3.8
138+
Use :meth:`__index__` if available.

Doc/c-api/float.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ Floating Point Objects
4747
Return a C :c:type:`double` representation of the contents of *pyfloat*. If
4848
*pyfloat* is not a Python floating point object but has a :meth:`__float__`
4949
method, this method will first be called to convert *pyfloat* into a float.
50+
If ``__float__()`` is not defined then it falls back to :meth:`__index__`.
5051
This method returns ``-1.0`` upon failure, so one should call
5152
:c:func:`PyErr_Occurred` to check for errors.
5253
54+
.. versionchanged:: 3.8
55+
Use :meth:`__index__` if available.
56+
5357
5458
.. c:function:: double PyFloat_AS_DOUBLE(PyObject *pyfloat)
5559

Doc/library/functions.rst

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,11 @@ are always available. They are listed here in alphabetical order.
318318
:class:`int` and :class:`float`. If both arguments are omitted, returns
319319
``0j``.
320320

321+
For a general Python object ``x``, ``complex(x)`` delegates to
322+
``x.__complex__()``. If ``__complex__()`` is not defined then it falls back
323+
to :meth:`__float__`. If ``__float__()`` is not defined then it falls back
324+
to :meth:`__index__`.
325+
321326
.. note::
322327

323328
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.
330335
.. versionchanged:: 3.6
331336
Grouping digits with underscores as in code literals is allowed.
332337

338+
.. versionchanged:: 3.8
339+
Falls back to :meth:`__index__` if :meth:`__complex__` and
340+
:meth:`__float__` are not defined.
341+
333342

334343
.. function:: delattr(object, name)
335344

@@ -584,7 +593,8 @@ are always available. They are listed here in alphabetical order.
584593
float, an :exc:`OverflowError` will be raised.
585594

586595
For a general Python object ``x``, ``float(x)`` delegates to
587-
``x.__float__()``.
596+
``x.__float__()``. If ``__float__()`` is not defined then it falls back
597+
to :meth:`__index__`.
588598

589599
If no argument is given, ``0.0`` is returned.
590600

@@ -609,6 +619,9 @@ are always available. They are listed here in alphabetical order.
609619
.. versionchanged:: 3.7
610620
*x* is now a positional-only parameter.
611621

622+
.. versionchanged:: 3.8
623+
Falls back to :meth:`__index__` if :meth:`__float__` is not defined.
624+
612625

613626
.. index::
614627
single: __format__
@@ -780,7 +793,8 @@ are always available. They are listed here in alphabetical order.
780793

781794
Return an integer object constructed from a number or string *x*, or return
782795
``0`` if no arguments are given. If *x* defines :meth:`__int__`,
783-
``int(x)`` returns ``x.__int__()``. If *x* defines :meth:`__trunc__`,
796+
``int(x)`` returns ``x.__int__()``. If *x* defines :meth:`__index__`,
797+
it returns ``x.__index__()``. If *x* defines :meth:`__trunc__`,
784798
it returns ``x.__trunc__()``.
785799
For floating point numbers, this truncates towards zero.
786800

@@ -812,6 +826,9 @@ are always available. They are listed here in alphabetical order.
812826
.. versionchanged:: 3.7
813827
*x* is now a positional-only parameter.
814828

829+
.. versionchanged:: 3.8
830+
Falls back to :meth:`__index__` if :meth:`__int__` is not defined.
831+
815832

816833
.. function:: isinstance(object, classinfo)
817834

Doc/reference/datamodel.rst

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2394,11 +2394,9 @@ left undefined.
23942394
functions). Presence of this method indicates that the numeric object is
23952395
an integer type. Must return an integer.
23962396

2397-
.. note::
2398-
2399-
In order to have a coherent integer type class, when :meth:`__index__` is
2400-
defined :meth:`__int__` should also be defined, and both should return
2401-
the same value.
2397+
If :meth:`__int__`, :meth:`__float__` and :meth:`__complex__` are not
2398+
defined then corresponding built-in functions :func:`int`, :func:`float`
2399+
and :func:`complex` fall back to :meth:`__index__`.
24022400

24032401

24042402
.. method:: object.__round__(self, [,ndigits])

Doc/whatsnew/3.8.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ Other Language Changes
250250
compatible with the existing :meth:`float.as_integer_ratio` method.
251251
(Contributed by Lisa Roach in :issue:`33073`.)
252252

253+
* Constructors of :class:`int`, :class:`float` and :class:`complex` will now
254+
use the :meth:`~object.__index__` special method, if available and the
255+
corresponding method :meth:`~object.__int__`, :meth:`~object.__float__`
256+
or :meth:`~object.__complex__` is not available.
257+
(Contributed by Serhiy Storchaka in :issue:`20092`.)
258+
253259
* Added support of ``\N{name}`` escapes in :mod:`regular expressions <re>`.
254260
(Contributed by Jonathan Eunice and Serhiy Storchaka in :issue:`30688`.)
255261

@@ -868,7 +874,10 @@ Build and C API Changes
868874
``__index__()`` method (like :class:`~decimal.Decimal` and
869875
:class:`~fractions.Fraction`). :c:func:`PyNumber_Check` will now return
870876
``1`` for objects implementing ``__index__()``.
871-
(Contributed by Serhiy Storchaka in :issue:`36048`.)
877+
:c:func:`PyNumber_Long`, :c:func:`PyNumber_Float` and
878+
:c:func:`PyFloat_AsDouble` also now use the ``__index__()`` method if
879+
available.
880+
(Contributed by Serhiy Storchaka in :issue:`36048` and :issue:`20092`.)
872881

873882
* Heap-allocated type objects will now increase their reference count
874883
in :c:func:`PyObject_Init` (and its parallel macro ``PyObject_INIT``)

Lib/test/test_cmath.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,11 @@ class NeitherComplexNorFloat(object):
220220
pass
221221
class NeitherComplexNorFloatOS:
222222
pass
223-
class MyInt(object):
223+
class Index:
224224
def __int__(self): return 2
225225
def __index__(self): return 2
226-
class MyIntOS:
226+
class MyInt:
227227
def __int__(self): return 2
228-
def __index__(self): return 2
229228

230229
# other possible combinations of __float__ and __complex__
231230
# that should work
@@ -255,6 +254,7 @@ def __float__(self):
255254
self.assertEqual(f(FloatAndComplexOS()), f(cx_arg))
256255
self.assertEqual(f(JustFloat()), f(flt_arg))
257256
self.assertEqual(f(JustFloatOS()), f(flt_arg))
257+
self.assertEqual(f(Index()), f(int(Index())))
258258
# TypeError should be raised for classes not providing
259259
# either __complex__ or __float__, even if they provide
260260
# __int__ or __index__. An old-style class
@@ -263,7 +263,6 @@ def __float__(self):
263263
self.assertRaises(TypeError, f, NeitherComplexNorFloat())
264264
self.assertRaises(TypeError, f, MyInt())
265265
self.assertRaises(Exception, f, NeitherComplexNorFloatOS())
266-
self.assertRaises(Exception, f, MyIntOS())
267266
# non-complex return value from __complex__ -> TypeError
268267
for bad_complex in non_complexes:
269268
self.assertRaises(TypeError, f, MyComplex(bad_complex))

Lib/test/test_complex.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,24 @@ def __float__(self):
368368
self.assertAlmostEqual(complex(real=float2(17.), imag=float2(23.)), 17+23j)
369369
self.assertRaises(TypeError, complex, float2(None))
370370

371+
class MyIndex:
372+
def __init__(self, value):
373+
self.value = value
374+
def __index__(self):
375+
return self.value
376+
377+
self.assertAlmostEqual(complex(MyIndex(42)), 42.0+0.0j)
378+
self.assertAlmostEqual(complex(123, MyIndex(42)), 123.0+42.0j)
379+
self.assertRaises(OverflowError, complex, MyIndex(2**2000))
380+
self.assertRaises(OverflowError, complex, 123, MyIndex(2**2000))
381+
382+
class MyInt:
383+
def __int__(self):
384+
return 42
385+
386+
self.assertRaises(TypeError, complex, MyInt())
387+
self.assertRaises(TypeError, complex, 123, MyInt())
388+
371389
class complex0(complex):
372390
"""Test usage of __complex__() when inheriting from 'complex'"""
373391
def __complex__(self):

Lib/test/test_float.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,21 @@ def __float__(self):
223223
with self.assertWarns(DeprecationWarning):
224224
self.assertIs(type(FloatSubclass(F())), FloatSubclass)
225225

226+
class MyIndex:
227+
def __init__(self, value):
228+
self.value = value
229+
def __index__(self):
230+
return self.value
231+
232+
self.assertEqual(float(MyIndex(42)), 42.0)
233+
self.assertRaises(OverflowError, float, MyIndex(2**2000))
234+
235+
class MyInt:
236+
def __int__(self):
237+
return 42
238+
239+
self.assertRaises(TypeError, float, MyInt())
240+
226241
def test_keyword_args(self):
227242
with self.assertRaisesRegex(TypeError, 'keyword argument'):
228243
float(x='3.14')

Lib/test/test_getargs2.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,8 @@ def test_f(self):
457457
with self.assertWarns(DeprecationWarning):
458458
self.assertEqual(getargs_f(BadFloat2()), 4.25)
459459
self.assertEqual(getargs_f(BadFloat3(7.5)), 7.5)
460+
self.assertEqual(getargs_f(Index()), 99.0)
461+
self.assertRaises(TypeError, getargs_f, Int())
460462

461463
for x in (FLT_MIN, -FLT_MIN, FLT_MAX, -FLT_MAX, INF, -INF):
462464
self.assertEqual(getargs_f(x), x)
@@ -489,6 +491,8 @@ def test_d(self):
489491
with self.assertWarns(DeprecationWarning):
490492
self.assertEqual(getargs_d(BadFloat2()), 4.25)
491493
self.assertEqual(getargs_d(BadFloat3(7.5)), 7.5)
494+
self.assertEqual(getargs_d(Index()), 99.0)
495+
self.assertRaises(TypeError, getargs_d, Int())
492496

493497
for x in (DBL_MIN, -DBL_MIN, DBL_MAX, -DBL_MAX, INF, -INF):
494498
self.assertEqual(getargs_d(x), x)
@@ -511,6 +515,8 @@ def test_D(self):
511515
with self.assertWarns(DeprecationWarning):
512516
self.assertEqual(getargs_D(BadComplex2()), 4.25+0.5j)
513517
self.assertEqual(getargs_D(BadComplex3(7.5+0.25j)), 7.5+0.25j)
518+
self.assertEqual(getargs_D(Index()), 99.0+0j)
519+
self.assertRaises(TypeError, getargs_D, Int())
514520

515521
for x in (DBL_MIN, -DBL_MIN, DBL_MAX, -DBL_MAX, INF, -INF):
516522
c = complex(x, 1.0)

Lib/test/test_index.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def test_int_subclass_with_index(self):
6060
# subclasses. See issue #17576.
6161
class MyInt(int):
6262
def __index__(self):
63-
return int(self) + 1
63+
return int(str(self)) + 1
6464

6565
my_int = MyInt(7)
6666
direct_index = my_int.__index__()

Lib/test/test_int.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -378,15 +378,23 @@ def __trunc__(self):
378378
int(ExceptionalTrunc())
379379

380380
for trunc_result_base in (object, Classic):
381-
class Integral(trunc_result_base):
382-
def __int__(self):
381+
class Index(trunc_result_base):
382+
def __index__(self):
383383
return 42
384384

385385
class TruncReturnsNonInt(base):
386386
def __trunc__(self):
387-
return Integral()
388-
with self.assertWarns(DeprecationWarning):
389-
self.assertEqual(int(TruncReturnsNonInt()), 42)
387+
return Index()
388+
self.assertEqual(int(TruncReturnsNonInt()), 42)
389+
390+
class Intable(trunc_result_base):
391+
def __int__(self):
392+
return 42
393+
394+
class TruncReturnsNonIndex(base):
395+
def __trunc__(self):
396+
return Intable()
397+
self.assertEqual(int(TruncReturnsNonInt()), 42)
390398

391399
class NonIntegral(trunc_result_base):
392400
def __trunc__(self):
@@ -418,6 +426,21 @@ def __trunc__(self):
418426
with self.assertRaises(TypeError):
419427
int(TruncReturnsBadInt())
420428

429+
def test_int_subclass_with_index(self):
430+
class MyIndex(int):
431+
def __index__(self):
432+
return 42
433+
434+
class BadIndex(int):
435+
def __index__(self):
436+
return 42.0
437+
438+
my_int = MyIndex(7)
439+
self.assertEqual(my_int, 7)
440+
self.assertEqual(int(my_int), 7)
441+
442+
self.assertEqual(int(BadIndex()), 0)
443+
421444
def test_int_subclass_with_int(self):
422445
class MyInt(int):
423446
def __int__(self):
@@ -431,9 +454,19 @@ def __int__(self):
431454
self.assertEqual(my_int, 7)
432455
self.assertEqual(int(my_int), 42)
433456

434-
self.assertRaises(TypeError, int, BadInt())
457+
my_int = BadInt(7)
458+
self.assertEqual(my_int, 7)
459+
self.assertRaises(TypeError, int, my_int)
435460

436461
def test_int_returns_int_subclass(self):
462+
class BadIndex:
463+
def __index__(self):
464+
return True
465+
466+
class BadIndex2(int):
467+
def __index__(self):
468+
return True
469+
437470
class BadInt:
438471
def __int__(self):
439472
return True
@@ -442,6 +475,10 @@ class BadInt2(int):
442475
def __int__(self):
443476
return True
444477

478+
class TruncReturnsBadIndex:
479+
def __trunc__(self):
480+
return BadIndex()
481+
445482
class TruncReturnsBadInt:
446483
def __trunc__(self):
447484
return BadInt()
@@ -450,6 +487,17 @@ class TruncReturnsIntSubclass:
450487
def __trunc__(self):
451488
return True
452489

490+
bad_int = BadIndex()
491+
with self.assertWarns(DeprecationWarning):
492+
n = int(bad_int)
493+
self.assertEqual(n, 1)
494+
self.assertIs(type(n), int)
495+
496+
bad_int = BadIndex2()
497+
n = int(bad_int)
498+
self.assertEqual(n, 0)
499+
self.assertIs(type(n), int)
500+
453501
bad_int = BadInt()
454502
with self.assertWarns(DeprecationWarning):
455503
n = int(bad_int)
@@ -462,6 +510,12 @@ def __trunc__(self):
462510
self.assertEqual(n, 1)
463511
self.assertIs(type(n), int)
464512

513+
bad_int = TruncReturnsBadIndex()
514+
with self.assertWarns(DeprecationWarning):
515+
n = int(bad_int)
516+
self.assertEqual(n, 1)
517+
self.assertIs(type(n), int)
518+
465519
bad_int = TruncReturnsBadInt()
466520
with self.assertWarns(DeprecationWarning):
467521
n = int(bad_int)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Constructors of :class:`int`, :class:`float` and :class:`complex` will now
2+
use the :meth:`~object.__index__` special method, if available and the
3+
corresponding method :meth:`~object.__int__`, :meth:`~object.__float__`
4+
or :meth:`~object.__complex__` is not available.

Objects/abstract.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,6 +1373,13 @@ PyNumber_Long(PyObject *o)
13731373
}
13741374
return result;
13751375
}
1376+
if (m && m->nb_index) {
1377+
result = _PyLong_FromNbIndexOrNbInt(o);
1378+
if (result != NULL && !PyLong_CheckExact(result)) {
1379+
Py_SETREF(result, _PyLong_Copy((PyLongObject *)result));
1380+
}
1381+
return result;
1382+
}
13761383
trunc_func = _PyObject_LookupSpecial(o, &PyId___trunc__);
13771384
if (trunc_func) {
13781385
result = _PyObject_CallNoArg(trunc_func);
@@ -1480,6 +1487,18 @@ PyNumber_Float(PyObject *o)
14801487
Py_DECREF(res);
14811488
return PyFloat_FromDouble(val);
14821489
}
1490+
if (m && m->nb_index) {
1491+
PyObject *res = PyNumber_Index(o);
1492+
if (!res) {
1493+
return NULL;
1494+
}
1495+
double val = PyLong_AsDouble(res);
1496+
Py_DECREF(res);
1497+
if (val == -1.0 && PyErr_Occurred()) {
1498+
return NULL;
1499+
}
1500+
return PyFloat_FromDouble(val);
1501+
}
14831502
if (PyFloat_Check(o)) { /* A float subclass with nb_float == NULL */
14841503
return PyFloat_FromDouble(PyFloat_AS_DOUBLE(o));
14851504
}

0 commit comments

Comments
 (0)