Skip to content

Commit 324e70e

Browse files
gh-160945: Fix _pydecimal losing companion signal flags when a signal is trapped
1 parent 83fe630 commit 324e70e

3 files changed

Lines changed: 184 additions & 65 deletions

File tree

‎Lib/_pydecimal.py‎

Lines changed: 126 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import math as _math
6363
import numbers as _numbers
6464
import sys
65+
from contextlib import contextmanager as _contextmanager
6566

6667
try:
6768
from collections import namedtuple as _namedtuple
@@ -1499,8 +1500,10 @@ def __divmod__(self, other, context=None):
14991500
ans = context._raise_error(DivisionUndefined, 'divmod(0, 0)')
15001501
return ans, ans
15011502
else:
1502-
return (context._raise_error(DivisionByZero, 'x // 0', sign),
1503-
context._raise_error(InvalidOperation, 'x % 0'))
1503+
with context._defer_traps():
1504+
return (context._raise_error(DivisionByZero, 'x // 0',
1505+
sign),
1506+
context._raise_error(InvalidOperation, 'x % 0'))
15041507

15051508
quotient, remainder = self._divide(other, context)
15061509
remainder = remainder._fix(context)
@@ -1746,9 +1749,10 @@ def _fix(self, context):
17461749
exp_min = len(self._int) + self._exp - context.prec
17471750
if exp_min > Etop:
17481751
# overflow: exp_min > Etop iff self.adjusted() > Emax
1749-
ans = context._raise_error(Overflow, 'above Emax', self._sign)
1750-
context._raise_error(Inexact)
1751-
context._raise_error(Rounded)
1752+
with context._defer_traps():
1753+
ans = context._raise_error(Overflow, 'above Emax', self._sign)
1754+
context._raise_error(Inexact)
1755+
context._raise_error(Rounded)
17521756
return ans
17531757

17541758
self_is_subnormal = exp_min < Etiny
@@ -1770,34 +1774,38 @@ def _fix(self, context):
17701774
coeff = coeff[:-1]
17711775
exp_min += 1
17721776

1773-
# check whether the rounding pushed the exponent out of range
1774-
if exp_min > Etop:
1775-
ans = context._raise_error(Overflow, 'above Emax', self._sign)
1776-
else:
1777-
ans = _dec_from_triple(self._sign, coeff, exp_min)
1777+
with context._defer_traps():
1778+
# check whether the rounding pushed the exponent out
1779+
# of range
1780+
if exp_min > Etop:
1781+
ans = context._raise_error(Overflow, 'above Emax',
1782+
self._sign)
1783+
else:
1784+
ans = _dec_from_triple(self._sign, coeff, exp_min)
1785+
1786+
# raise the appropriate signals, taking care to respect
1787+
# the precedence described in the specification
1788+
if changed and self_is_subnormal:
1789+
context._raise_error(Underflow)
1790+
if self_is_subnormal:
1791+
context._raise_error(Subnormal)
1792+
if changed:
1793+
context._raise_error(Inexact)
1794+
context._raise_error(Rounded)
1795+
if not ans:
1796+
# raise Clamped on underflow to 0
1797+
context._raise_error(Clamped)
1798+
return ans
17781799

1779-
# raise the appropriate signals, taking care to respect
1780-
# the precedence described in the specification
1781-
if changed and self_is_subnormal:
1782-
context._raise_error(Underflow)
1800+
with context._defer_traps():
17831801
if self_is_subnormal:
17841802
context._raise_error(Subnormal)
1785-
if changed:
1786-
context._raise_error(Inexact)
1787-
context._raise_error(Rounded)
1788-
if not ans:
1789-
# raise Clamped on underflow to 0
1790-
context._raise_error(Clamped)
1791-
return ans
1792-
1793-
if self_is_subnormal:
1794-
context._raise_error(Subnormal)
17951803

1796-
# fold down if clamp == 1 and self has too few digits
1797-
if context.clamp == 1 and self._exp > Etop:
1798-
context._raise_error(Clamped)
1799-
self_padded = self._int + '0'*(self._exp - Etop)
1800-
return _dec_from_triple(self._sign, self_padded, Etop)
1804+
# fold down if clamp == 1 and self has too few digits
1805+
if context.clamp == 1 and self._exp > Etop:
1806+
context._raise_error(Clamped)
1807+
self_padded = self._int + '0'*(self._exp - Etop)
1808+
return _dec_from_triple(self._sign, self_padded, Etop)
18011809

18021810
# here self was representable to begin with; return unchanged
18031811
return Decimal(self)
@@ -2445,8 +2453,9 @@ def __pow__(self, other, modulo=None, context=None):
24452453
exp = 1-context.prec
24462454
context._raise_error(Rounded)
24472455
else:
2448-
context._raise_error(Inexact)
2449-
context._raise_error(Rounded)
2456+
with context._defer_traps():
2457+
context._raise_error(Inexact)
2458+
context._raise_error(Rounded)
24502459
exp = 1-context.prec
24512460

24522461
return _dec_from_triple(result_sign, '1'+'0'*-exp, exp)
@@ -2554,11 +2563,13 @@ def __pow__(self, other, modulo=None, context=None):
25542563
# Inexact, Rounded, Clamped. Overflow needs the correct
25552564
# arguments. Note that the order of the exceptions is
25562565
# important here.
2557-
if newcontext.flags[Overflow]:
2558-
context._raise_error(Overflow, 'above Emax', ans._sign)
2559-
for exception in Underflow, Subnormal, Inexact, Rounded, Clamped:
2560-
if newcontext.flags[exception]:
2561-
context._raise_error(exception)
2566+
with context._defer_traps():
2567+
if newcontext.flags[Overflow]:
2568+
context._raise_error(Overflow, 'above Emax', ans._sign)
2569+
for exception in (Underflow, Subnormal, Inexact, Rounded,
2570+
Clamped):
2571+
if newcontext.flags[exception]:
2572+
context._raise_error(exception)
25622573

25632574
else:
25642575
ans = ans._fix(context)
@@ -2645,17 +2656,18 @@ def quantize(self, exp, rounding=None, context=None):
26452656
return context._raise_error(InvalidOperation,
26462657
'quantize result has too many digits for current context')
26472658

2648-
# raise appropriate flags
2649-
if ans and ans.adjusted() < context.Emin:
2650-
context._raise_error(Subnormal)
2651-
if ans._exp > self._exp:
2652-
if ans != self:
2653-
context._raise_error(Inexact)
2654-
context._raise_error(Rounded)
2659+
with context._defer_traps():
2660+
# raise appropriate flags
2661+
if ans and ans.adjusted() < context.Emin:
2662+
context._raise_error(Subnormal)
2663+
if ans._exp > self._exp:
2664+
if ans != self:
2665+
context._raise_error(Inexact)
2666+
context._raise_error(Rounded)
26552667

2656-
# call to fix takes care of any necessary folddown, and
2657-
# signals Clamped if necessary
2658-
ans = ans._fix(context)
2668+
# call to fix takes care of any necessary folddown, and
2669+
# signals Clamped if necessary
2670+
ans = ans._fix(context)
26592671
return ans
26602672

26612673
def same_quantum(self, other, context=None):
@@ -2754,9 +2766,10 @@ def to_integral_exact(self, rounding=None, context=None):
27542766
if rounding is None:
27552767
rounding = context.rounding
27562768
ans = self._rescale(0, rounding)
2757-
if ans != self:
2758-
context._raise_error(Inexact)
2759-
context._raise_error(Rounded)
2769+
with context._defer_traps():
2770+
if ans != self:
2771+
context._raise_error(Inexact)
2772+
context._raise_error(Rounded)
27602773
return ans
27612774

27622775
def to_integral_value(self, rounding=None, context=None):
@@ -3646,21 +3659,22 @@ def next_toward(self, other, context=None):
36463659
ans = self.next_minus(context)
36473660

36483661
# decide which flags to raise using value of ans
3649-
if ans._isinfinity():
3650-
context._raise_error(Overflow,
3651-
'Infinite result from next_toward',
3652-
ans._sign)
3653-
context._raise_error(Inexact)
3654-
context._raise_error(Rounded)
3655-
elif ans.adjusted() < context.Emin:
3656-
context._raise_error(Underflow)
3657-
context._raise_error(Subnormal)
3658-
context._raise_error(Inexact)
3659-
context._raise_error(Rounded)
3660-
# if precision == 1 then we don't raise Clamped for a
3661-
# result 0E-Etiny.
3662-
if not ans:
3663-
context._raise_error(Clamped)
3662+
with context._defer_traps():
3663+
if ans._isinfinity():
3664+
context._raise_error(Overflow,
3665+
'Infinite result from next_toward',
3666+
ans._sign)
3667+
context._raise_error(Inexact)
3668+
context._raise_error(Rounded)
3669+
elif ans.adjusted() < context.Emin:
3670+
context._raise_error(Underflow)
3671+
context._raise_error(Subnormal)
3672+
context._raise_error(Inexact)
3673+
context._raise_error(Rounded)
3674+
# if precision == 1 then we don't raise Clamped for a
3675+
# result 0E-Etiny.
3676+
if not ans:
3677+
context._raise_error(Clamped)
36643678

36653679
return ans
36663680

@@ -3988,6 +4002,12 @@ def __init__(self, prec=None, rounding=None, Emin=None, Emax=None,
39884002
else:
39894003
self._ignored_flags = _ignored_flags
39904004

4005+
# State for _defer_traps: the nesting level of deferral blocks
4006+
# and the first trapped exception waiting to be raised by the
4007+
# outermost block. Transient: not copied or pickled.
4008+
self._deferred_traps = 0
4009+
self._deferred_error = None
4010+
39914011
if traps is None:
39924012
self.traps = dc.traps.copy()
39934013
elif not isinstance(traps, dict):
@@ -4046,7 +4066,7 @@ def __setattr__(self, name, value):
40464066
return object.__setattr__(self, name, value)
40474067
elif name == 'flags' or name == 'traps':
40484068
return self._set_signal_dict(name, value)
4049-
elif name == '_ignored_flags':
4069+
elif name in ('_ignored_flags', '_deferred_traps', '_deferred_error'):
40504070
return object.__setattr__(self, name, value)
40514071
else:
40524072
raise AttributeError(
@@ -4134,10 +4154,51 @@ def _raise_error(self, condition, explanation = None, *args):
41344154
# The errors define how to handle themselves.
41354155
return condition().handle(self, *args)
41364156

4157+
if self._deferred_traps:
4158+
# A _defer_traps block is recording the signals of a
4159+
# multi-signal operation: remember the first trapped
4160+
# exception instead of raising it now, so that the
4161+
# remaining signals can still set their flags. The block
4162+
# raises it on exit. Return the default response, exactly
4163+
# as if the signal had not been trapped.
4164+
if self._deferred_error is None:
4165+
self._deferred_error = error(explanation)
4166+
return condition().handle(self, *args)
4167+
41374168
# Errors should only be risked on copies of the context
41384169
# self._ignored_flags = []
41394170
raise error(explanation)
41404171

4172+
@_contextmanager
4173+
def _defer_traps(self):
4174+
"""Defer trapped signals until the end of the with block.
4175+
4176+
Some operations signal several conditions at once (for
4177+
example, Overflow together with Inexact and Rounded). The
4178+
specification requires the flag of every condition encountered
4179+
to be set, and the C implementation records the full set of
4180+
flags before raising a trapped exception. Within this block,
4181+
_raise_error does the same: it sets the flag of each signal
4182+
and returns its default response, and the first trapped
4183+
exception (in the order the signals are raised, which is the
4184+
precedence order of the specification) is raised when the
4185+
outermost block exits. If the block raises another exception,
4186+
any deferred exception is discarded.
4187+
"""
4188+
self._deferred_traps += 1
4189+
try:
4190+
yield
4191+
except BaseException:
4192+
if self._deferred_traps == 1:
4193+
self._deferred_error = None
4194+
raise
4195+
finally:
4196+
self._deferred_traps -= 1
4197+
if self._deferred_traps == 0 and self._deferred_error is not None:
4198+
error = self._deferred_error
4199+
self._deferred_error = None
4200+
raise error
4201+
41414202
def _ignore_all_flags(self):
41424203
"""Ignore all flags, if they are raised"""
41434204
return self._ignore_flags(*_signals)

‎Lib/test/test_decimal.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4014,6 +4014,59 @@ def raise_error(context, flag):
40144014
"operation raises different flags depending on flags set: " +
40154015
"expected %s, got %s" % (expected_flags, new_flags))
40164016

4017+
def test_trapped_signal_sets_companion_flags(self):
4018+
# If an operation signals several conditions and one of them is
4019+
# trapped, the exception is raised, but the flags of all the
4020+
# other conditions encountered must be set as well.
4021+
Decimal = self.decimal.Decimal
4022+
Context = self.decimal.Context
4023+
Clamped = self.decimal.Clamped
4024+
DivisionByZero = self.decimal.DivisionByZero
4025+
Inexact = self.decimal.Inexact
4026+
InvalidOperation = self.decimal.InvalidOperation
4027+
Overflow = self.decimal.Overflow
4028+
Rounded = self.decimal.Rounded
4029+
Subnormal = self.decimal.Subnormal
4030+
Underflow = self.decimal.Underflow
4031+
4032+
def check(trap, operation, expected_flags):
4033+
context = Context(prec=9, Emin=-999, Emax=999, traps=[])
4034+
context.traps[trap] = 1
4035+
with self.assertRaises(trap):
4036+
operation(context)
4037+
flags = {signal for signal, v in context.flags.items() if v}
4038+
self.assertEqual(flags, expected_flags)
4039+
4040+
# overflow from _fix: Inexact and Rounded are also signaled
4041+
check(Overflow,
4042+
lambda c: c.multiply(Decimal('1E+800'), Decimal('1E+800')),
4043+
{Inexact, Overflow, Rounded})
4044+
4045+
# 1/3 is rounded: Rounded accompanies the trapped Inexact
4046+
check(Inexact,
4047+
lambda c: c.divide(Decimal(1), Decimal(3)),
4048+
{Inexact, Rounded})
4049+
4050+
# underflow to zero signals the full set of conditions; a trap
4051+
# on any of them must not suppress the flags of the others
4052+
check(Underflow,
4053+
lambda c: c.divide(Decimal('1E-999'), Decimal('1E+10')),
4054+
{Clamped, Inexact, Rounded, Subnormal, Underflow})
4055+
check(Subnormal,
4056+
lambda c: c.divide(Decimal('1E-999'), Decimal('1E+10')),
4057+
{Clamped, Inexact, Rounded, Subnormal, Underflow})
4058+
4059+
# divmod by zero signals DivisionByZero (for the quotient) and
4060+
# InvalidOperation (for the remainder)
4061+
check(DivisionByZero,
4062+
lambda c: c.divmod(Decimal(5), Decimal(0)),
4063+
{DivisionByZero, InvalidOperation})
4064+
4065+
# to_integral_exact signals Inexact and Rounded together
4066+
check(Inexact,
4067+
lambda c: c.to_integral_exact(Decimal('2.5')),
4068+
{Inexact, Rounded})
4069+
40174070
def test_flag_comparisons(self):
40184071
Context = self.decimal.Context
40194072
Inexact = self.decimal.Inexact
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed :mod:`!_pydecimal` losing the context flags of companion conditions
2+
when a signal was trapped. After an operation that signals several
3+
conditions at once (for example an overflowing multiplication), the flags
4+
for *all* encountered conditions are now set, matching :mod:`!_decimal`;
5+
the exception raised is unchanged.

0 commit comments

Comments
 (0)