Skip to content

Commit 4929c3a

Browse files
pythongh-102791: allow non-fractional decimal.Decimals to be interpreted as integers
Prior to pythongh-11952, several standard library functions that expected integer arguments would nevertheless silently accept (and truncate) non-integer arguments. This behaviour was deprecated in pythongh-11952, and removed in pythongh-15636. However, it may be possible to interpret some non-integer numeric types (such as `decimal.Decimal`s) as integers if they contain no fractional part. Implement `__index__` for `decimal.Decimal`, returning an integer representation of the value if it does not contain a fractional part or raising a `TypeError` if it does.
1 parent 4f5774f commit 4929c3a

10 files changed

Lines changed: 87 additions & 9 deletions

File tree

Doc/library/decimal.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,11 @@ Decimal objects
410410
compared, sorted, and coerced to another type (such as :class:`float` or
411411
:class:`int`).
412412

413+
.. versionchanged:: 3.12
414+
A :class:`Decimal` instance may be coerced to an integer (i.e. by
415+
:func:`~operator.index`) if it has no fractional part; otherwise, a
416+
:exc:`TypeError` is raised.
417+
413418
There are some small differences between arithmetic on Decimal objects and
414419
arithmetic on integers and floats. When the remainder operator ``%`` is
415420
applied to Decimal objects, the sign of the result is the sign of the

Lib/_pydecimal.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,21 @@ def __int__(self):
16401640
else:
16411641
return s*int(self._int[:self._exp] or '0')
16421642

1643+
def __index__(self):
1644+
"""
1645+
Converts self to an int, if it is possible to do so with no loss of
1646+
precision.
1647+
"""
1648+
if self._is_special:
1649+
if self._isnan():
1650+
raise ValueError("Cannot convert NaN to integer")
1651+
elif self._isinfinity():
1652+
raise OverflowError("Cannot convert infinity to integer")
1653+
elif self._exp != 0:
1654+
raise TypeError("Cannot convert Decimal with fractional part "
1655+
"to integer")
1656+
return (-1)**self._sign*int(self._int)
1657+
16431658
__trunc__ = __int__
16441659

16451660
@property

Lib/test/datetimetester.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5431,7 +5431,7 @@ class Float(float):
54315431
pass
54325432

54335433
for xx in [10.0, Float(10.9),
5434-
decimal.Decimal(10), decimal.Decimal('10.9'),
5434+
decimal.Decimal('10.9'),
54355435
Number(10), Number(10.9),
54365436
'10']:
54375437
self.assertRaises(TypeError, datetime, xx, 10, 10, 10, 10, 10, 10)

Lib/test/test_buffer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import sys, array, io, os
2424
from decimal import Decimal
2525
from fractions import Fraction
26+
import re
2627

2728
try:
2829
from _testbuffer import *
@@ -2532,8 +2533,9 @@ def __index__(self):
25322533

25332534
def f(): return 7
25342535

2536+
fd = Decimal("-21.1")
25352537
values = [INT(9), IDX(9),
2536-
2.2+3j, Decimal("-21.1"), 12.2, Fraction(5, 2),
2538+
2.2+3j, Decimal("2"), fd, 12.2, Fraction(5, 2),
25372539
[1,2,3], {4,5,6}, {7:8}, (), (9,),
25382540
True, False, None, Ellipsis,
25392541
b'a', b'abc', bytearray(b'a'), bytearray(b'abc'),
@@ -2559,6 +2561,10 @@ def f(): return 7
25592561
struct.pack_into(fmt, nd, itemsize, v)
25602562
except struct.error:
25612563
struct_err = struct.error
2564+
except TypeError as e:
2565+
# This can't be represented as an integer:
2566+
if v == fd and re.search('[bBhHiIlLqQnN]', fmt):
2567+
struct_err = e
25622568

25632569
mv_err = None
25642570
try:

Lib/test/test_decimal.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2613,6 +2613,27 @@ def test_int(self):
26132613
self.assertRaises(OverflowError, int, Decimal('inf'))
26142614
self.assertRaises(OverflowError, int, Decimal('-inf'))
26152615

2616+
def test_index(self):
2617+
Decimal = self.decimal.Decimal
2618+
2619+
for x in range(-250, 250):
2620+
self.assertEqual(operator.index(Decimal(x)), x)
2621+
2622+
self.assertRaises(TypeError, operator.index, Decimal('2.5'))
2623+
2624+
HAVE_CONFIG_64 = (C.MAX_PREC > 425000000)
2625+
2626+
# Corner cases
2627+
int_max = 2**63-1 if HAVE_CONFIG_64 else 2**31-1
2628+
2629+
self.assertEqual(operator.index(Decimal(int_max-1)), int_max-1)
2630+
self.assertEqual(operator.index(Decimal(-int_max)), -int_max)
2631+
2632+
self.assertRaises(ValueError, operator.index, Decimal('-nan'))
2633+
self.assertRaises(ValueError, operator.index, Decimal('snan'))
2634+
self.assertRaises(OverflowError, operator.index, Decimal('inf'))
2635+
self.assertRaises(OverflowError, operator.index, Decimal('-inf'))
2636+
26162637
@cpython_only
26172638
def test_small_ints(self):
26182639
Decimal = self.decimal.Decimal

Lib/test/test_math.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,6 @@ def testFactorialNonIntegers(self):
534534
self.assertRaises(TypeError, math.factorial, 5.2)
535535
self.assertRaises(TypeError, math.factorial, -1.0)
536536
self.assertRaises(TypeError, math.factorial, -1e100)
537-
self.assertRaises(TypeError, math.factorial, decimal.Decimal('5'))
538537
self.assertRaises(TypeError, math.factorial, decimal.Decimal('5.2'))
539538
self.assertRaises(TypeError, math.factorial, "5")
540539

@@ -2173,10 +2172,10 @@ def testPerm(self):
21732172
# Raises TypeError if any argument is non-integer or argument count is
21742173
# not 1 or 2
21752174
self.assertRaises(TypeError, perm, 10, 1.0)
2176-
self.assertRaises(TypeError, perm, 10, decimal.Decimal(1.0))
2175+
self.assertRaises(TypeError, perm, 10, decimal.Decimal(1.1))
21772176
self.assertRaises(TypeError, perm, 10, "1")
21782177
self.assertRaises(TypeError, perm, 10.0, 1)
2179-
self.assertRaises(TypeError, perm, decimal.Decimal(10.0), 1)
2178+
self.assertRaises(TypeError, perm, decimal.Decimal(10.1), 1)
21802179
self.assertRaises(TypeError, perm, "10", 1)
21812180

21822181
self.assertRaises(TypeError, perm)
@@ -2240,10 +2239,10 @@ def testComb(self):
22402239
# Raises TypeError if any argument is non-integer or argument count is
22412240
# not 2
22422241
self.assertRaises(TypeError, comb, 10, 1.0)
2243-
self.assertRaises(TypeError, comb, 10, decimal.Decimal(1.0))
2242+
self.assertRaises(TypeError, comb, 10, decimal.Decimal(1.1))
22442243
self.assertRaises(TypeError, comb, 10, "1")
22452244
self.assertRaises(TypeError, comb, 10.0, 1)
2246-
self.assertRaises(TypeError, comb, decimal.Decimal(10.0), 1)
2245+
self.assertRaises(TypeError, comb, decimal.Decimal(10.1), 1)
22472246
self.assertRaises(TypeError, comb, "10", 1)
22482247

22492248
self.assertRaises(TypeError, comb, 10)

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,7 @@ Stefan Norberg
12931293
Tim Northover
12941294
Joe Norton
12951295
Neal Norwitz
1296+
Chris Novakovic
12961297
Mikhail Novikov
12971298
Michal Nowikowski
12981299
Steffen Daode Nurpmeso
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`decimal.Decimal` instances may now be interpreted as integers (i.e.
2+
by :func:`~operator.index`) if they have no fractional part. Patch by Chris
3+
Novakovic.

Modules/_decimal/_decimal.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3773,6 +3773,33 @@ PyDec_AsFloat(PyObject *dec)
37733773
return f;
37743774
}
37753775

3776+
static PyObject *
3777+
PyDec_AsInt(PyObject *dec)
3778+
{
3779+
PyObject *context;
3780+
3781+
if (mpd_isspecial(MPD(dec))) {
3782+
if (mpd_isnan(MPD(dec))) {
3783+
PyErr_SetString(PyExc_ValueError,
3784+
"NaN cannot be interpreted as an integer");
3785+
}
3786+
else {
3787+
PyErr_SetString(PyExc_OverflowError,
3788+
"Infinity cannot be interpreted as an integer");
3789+
}
3790+
return NULL;
3791+
}
3792+
3793+
if (!mpd_isinteger(MPD(dec))) {
3794+
PyErr_SetString(PyExc_TypeError,
3795+
"Decimal with fractional part cannot be interpreted as an integer");
3796+
return NULL;
3797+
}
3798+
3799+
CURRENT_CONTEXT(context);
3800+
return dec_as_long(dec, context, MPD_ROUND_TRUNC);
3801+
}
3802+
37763803
static PyObject *
37773804
PyDec_Round(PyObject *dec, PyObject *args)
37783805
{
@@ -4877,6 +4904,7 @@ static PyNumberMethods dec_number_methods =
48774904
(binaryfunc) nm_mpd_qdiv, /* binaryfunc nb_true_divide; */
48784905
0, /* binaryfunc nb_inplace_floor_divide; */
48794906
0, /* binaryfunc nb_inplace_true_divide; */
4907+
(unaryfunc) PyDec_AsInt, /* unaryfunc nb_index; */
48804908
};
48814909

48824910
static PyMethodDef dec_methods [] =

Modules/_decimal/tests/deccheck.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@
6363
# Plain unary:
6464
'unary': (
6565
'__abs__', '__bool__', '__ceil__', '__complex__', '__copy__',
66-
'__floor__', '__float__', '__hash__', '__int__', '__neg__',
67-
'__pos__', '__reduce__', '__repr__', '__str__', '__trunc__',
66+
'__floor__', '__float__', '__hash__', '__index__', '__int__',
67+
'__neg__', '__pos__', '__reduce__', '__repr__', '__str__', '__trunc__',
6868
'adjusted', 'as_integer_ratio', 'as_tuple', 'canonical', 'conjugate',
6969
'copy_abs', 'copy_negate', 'is_canonical', 'is_finite', 'is_infinite',
7070
'is_nan', 'is_qnan', 'is_signed', 'is_snan', 'is_zero', 'radix'

0 commit comments

Comments
 (0)