Skip to content

bpo-36048: Use __index__() instead of __int__() for implicit conversion if available. #11952

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 54 additions & 12 deletions Doc/c-api/long.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,28 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.
single: OverflowError (built-in exception)

Return a C :c:type:`long` representation of *obj*. If *obj* is not an
instance of :c:type:`PyLongObject`, first call its :meth:`__int__` method
(if present) to convert it to a :c:type:`PyLongObject`.
instance of :c:type:`PyLongObject`, first call its :meth:`__index__` or
:meth:`__int__` method (if present) to convert it to a
:c:type:`PyLongObject`.

Raise :exc:`OverflowError` if the value of *obj* is out of range for a
:c:type:`long`.

Returns ``-1`` on error. Use :c:func:`PyErr_Occurred` to disambiguate.

.. versionchanged:: 3.8
Use :meth:`__index__` if available.

.. deprecated:: 3.8
Using :meth:`__int__` is deprecated.


.. c:function:: long PyLong_AsLongAndOverflow(PyObject *obj, int *overflow)

Return a C :c:type:`long` representation of *obj*. If *obj* is not an
instance of :c:type:`PyLongObject`, first call its :meth:`__int__` method
(if present) to convert it to a :c:type:`PyLongObject`.
instance of :c:type:`PyLongObject`, first call its :meth:`__index__` or
:meth:`__int__` method (if present) to convert it to a
:c:type:`PyLongObject`.

If the value of *obj* is greater than :const:`LONG_MAX` or less than
:const:`LONG_MIN`, set *\*overflow* to ``1`` or ``-1``, respectively, and
Expand All @@ -153,27 +161,41 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.

Returns ``-1`` on error. Use :c:func:`PyErr_Occurred` to disambiguate.

.. versionchanged:: 3.8
Use :meth:`__index__` if available.

.. deprecated:: 3.8
Using :meth:`__int__` is deprecated.


.. c:function:: long long PyLong_AsLongLong(PyObject *obj)

.. index::
single: OverflowError (built-in exception)

Return a C :c:type:`long long` representation of *obj*. If *obj* is not an
instance of :c:type:`PyLongObject`, first call its :meth:`__int__` method
(if present) to convert it to a :c:type:`PyLongObject`.
instance of :c:type:`PyLongObject`, first call its :meth:`__index__` or
:meth:`__int__` method (if present) to convert it to a
:c:type:`PyLongObject`.

Raise :exc:`OverflowError` if the value of *obj* is out of range for a
:c:type:`long`.

Returns ``-1`` on error. Use :c:func:`PyErr_Occurred` to disambiguate.

.. versionchanged:: 3.8
Use :meth:`__index__` if available.

.. deprecated:: 3.8
Using :meth:`__int__` is deprecated.


.. c:function:: long long PyLong_AsLongLongAndOverflow(PyObject *obj, int *overflow)

Return a C :c:type:`long long` representation of *obj*. If *obj* is not an
instance of :c:type:`PyLongObject`, first call its :meth:`__int__` method
(if present) to convert it to a :c:type:`PyLongObject`.
instance of :c:type:`PyLongObject`, first call its :meth:`__index__` or
:meth:`__int__` method (if present) to convert it to a
:c:type:`PyLongObject`.

If the value of *obj* is greater than :const:`PY_LLONG_MAX` or less than
:const:`PY_LLONG_MIN`, set *\*overflow* to ``1`` or ``-1``, respectively,
Expand All @@ -184,6 +206,12 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.

.. versionadded:: 3.2

.. versionchanged:: 3.8
Use :meth:`__index__` if available.

.. deprecated:: 3.8
Using :meth:`__int__` is deprecated.


.. c:function:: Py_ssize_t PyLong_AsSsize_t(PyObject *pylong)

Expand Down Expand Up @@ -253,26 +281,40 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.
.. c:function:: unsigned long PyLong_AsUnsignedLongMask(PyObject *obj)

Return a C :c:type:`unsigned long` representation of *obj*. If *obj*
is not an instance of :c:type:`PyLongObject`, first call its :meth:`__int__`
method (if present) to convert it to a :c:type:`PyLongObject`.
is not an instance of :c:type:`PyLongObject`, first call its
:meth:`__index__` or :meth:`__int__` method (if present) to convert
it to a :c:type:`PyLongObject`.

If the value of *obj* is out of range for an :c:type:`unsigned long`,
return the reduction of that value modulo ``ULONG_MAX + 1``.

Returns ``-1`` on error. Use :c:func:`PyErr_Occurred` to disambiguate.

.. versionchanged:: 3.8
Use :meth:`__index__` if available.

.. deprecated:: 3.8
Using :meth:`__int__` is deprecated.


.. c:function:: unsigned long long PyLong_AsUnsignedLongLongMask(PyObject *obj)

Return a C :c:type:`unsigned long long` representation of *obj*. If *obj*
is not an instance of :c:type:`PyLongObject`, first call its :meth:`__int__`
method (if present) to convert it to a :c:type:`PyLongObject`.
is not an instance of :c:type:`PyLongObject`, first call its
:meth:`__index__` or :meth:`__int__` method (if present) to convert
it to a :c:type:`PyLongObject`.

If the value of *obj* is out of range for an :c:type:`unsigned long long`,
return the reduction of that value modulo ``PY_ULLONG_MAX + 1``.

Returns ``-1`` on error. Use :c:func:`PyErr_Occurred` to disambiguate.

.. versionchanged:: 3.8
Use :meth:`__index__` if available.

.. deprecated:: 3.8
Using :meth:`__int__` is deprecated.


.. c:function:: double PyLong_AsDouble(PyObject *pylong)

Expand Down
3 changes: 3 additions & 0 deletions Doc/c-api/number.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Number Protocol
Returns ``1`` if the object *o* provides numeric protocols, and false otherwise.
This function always succeeds.

.. versionchanged:: 3.8
Returns ``1`` if *o* is an index integer.


.. c:function:: PyObject* PyNumber_Add(PyObject *o1, PyObject *o2)

Expand Down
20 changes: 20 additions & 0 deletions Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,17 @@ Build and C API Changes

(Contributed by Antoine Pitrou in :issue:`32430`.)

* Functions that convert Python number to C integer like
:c:func:`PyLong_AsLong` and argument parsing functions like
:c:func:`PyArg_ParseTuple` with integer converting format units like ``'i'``
will now use the :meth:`~object.__index__` special method instead of
:meth:`~object.__int__`, if available. The deprecation warning will be
emitted for objects with the ``__int__()`` method but without the
``__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`.)


Deprecated
==========
Expand Down Expand Up @@ -452,6 +463,15 @@ Deprecated
* The :meth:`~threading.Thread.isAlive()` method of :class:`threading.Thread` has been deprecated.
(Contributed by Dong-hee Na in :issue:`35283`.)

* Many builtin and extension functions that take integer arguments will
now emit a deprecation warning for :class:`~decimal.Decimal`\ s,
:class:`~fractions.Fraction`\ s and any other objects that can be converted
to integers only with a loss (e.g. that have the :meth:`~object.__int__`
method but do not have the :meth:`~object.__index__` method). In future
version they will be errors.
(Contributed by Serhiy Storchaka in :issue:`36048`.)


API and Feature Removals
========================

Expand Down
12 changes: 11 additions & 1 deletion Include/longobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,17 @@ PyAPI_FUNC(int) _PyLong_AsByteArray(PyLongObject* v,
nb_int slot is not available or the result of the call to nb_int
returns something not of type int.
*/
PyAPI_FUNC(PyLongObject *)_PyLong_FromNbInt(PyObject *);
PyAPI_FUNC(PyObject *) _PyLong_FromNbInt(PyObject *);

/* Convert the given object to a PyLongObject using the nb_index or
nb_int slots, if available (the latter is deprecated).
Raise TypeError if either nb_index and nb_int slots are not
available or the result of the call to nb_index or nb_int
returns something not of type int.
Should be replaced with PyNumber_Index after the end of the
deprecation period.
*/
PyAPI_FUNC(PyObject *) _PyLong_FromNbIndexOrNbInt(PyObject *);

/* _PyLong_Format: Convert the long to a string object with given base,
appending a base prefix of 0[box] if base is 2, 8 or 16. */
Expand Down
8 changes: 7 additions & 1 deletion Lib/ctypes/test/test_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,18 @@ def __float__(self):
class IntLike(object):
def __int__(self):
return 2
i = IntLike()
d = IntLike()
class IndexLike(object):
def __index__(self):
return 2
i = IndexLike()
# integers cannot be constructed from floats,
# but from integer-like objects
for t in signed_types + unsigned_types:
self.assertRaises(TypeError, t, 3.14)
self.assertRaises(TypeError, t, f)
with self.assertWarns(DeprecationWarning):
self.assertEqual(t(d).value, 2)
self.assertEqual(t(i).value, 2)

def test_sizes(self):
Expand Down
37 changes: 26 additions & 11 deletions Lib/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,19 +379,34 @@ def _check_utc_offset(name, offset):
def _check_int_field(value):
if isinstance(value, int):
return value
if not isinstance(value, float):
try:
value = value.__int__()
except AttributeError:
pass
else:
if isinstance(value, int):
return value
if isinstance(value, float):
raise TypeError('integer argument expected, got float')
try:
value = value.__index__()
except AttributeError:
pass
else:
if not isinstance(value, int):
raise TypeError('__index__ returned non-int (type %s)' %
type(value).__name__)
return value
orig = value
try:
value = value.__int__()
except AttributeError:
pass
else:
if not isinstance(value, int):
raise TypeError('__int__ returned non-int (type %s)' %
type(value).__name__)
raise TypeError('an integer is required (got type %s)' %
type(value).__name__)
raise TypeError('integer argument expected, got float')
import warnings
warnings.warn("an integer is required (got type %s)" %
type(orig).__name__,
DeprecationWarning,
stacklevel=2)
return value
raise TypeError('an integer is required (got type %s)' %
type(value).__name__)

def _check_date_fields(year, month, day):
year = _check_int_field(year)
Expand Down
7 changes: 4 additions & 3 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -4918,16 +4918,17 @@ def __int__(self):
for xx in [decimal.Decimal(10),
decimal.Decimal('10.9'),
Number(10)]:
self.assertEqual(datetime(10, 10, 10, 10, 10, 10, 10),
datetime(xx, xx, xx, xx, xx, xx, xx))
with self.assertWarns(DeprecationWarning):
self.assertEqual(datetime(10, 10, 10, 10, 10, 10, 10),
datetime(xx, xx, xx, xx, xx, xx, xx))

with self.assertRaisesRegex(TypeError, '^an integer is required '
r'\(got type str\)$'):
datetime(10, 10, '10')

f10 = Number(10.9)
with self.assertRaisesRegex(TypeError, '^__int__ returned non-int '
r'\(type float\)$'):
r'\(type float\)$'):
datetime(10, 10, f10)

class Float(float):
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,8 @@ def test_type_error(self):
class Intable:
def __init__(self, num):
self._num = num
def __index__(self):
return self._num
def __int__(self):
return self._num
def __sub__(self, other):
Expand Down
Loading