mirror of https://github.com/python/cpython
Issue #8567: Fix incorrect precedence of signals in Decimal module.
When a Decimal operation raises multiple signals and more than one of those signals is trapped, the specification determines the order in which the signals should be handled. In many cases this order wasn't being followed, leading to the wrong Python exception being raised. This commit fixes those cases, and adds extra tests. The tests are only enabled when EXTENDEDERRORTESTS is True, since they involve rerunning each Decimal testcase several times.
This commit is contained in:
parent
7120219918
commit
4f96f5ffc6
128
Lib/decimal.py
128
Lib/decimal.py
|
@ -1664,47 +1664,53 @@ class Decimal(object):
|
|||
exp_min = len(self._int) + self._exp - context.prec
|
||||
if exp_min > Etop:
|
||||
# overflow: exp_min > Etop iff self.adjusted() > Emax
|
||||
ans = context._raise_error(Overflow, 'above Emax', self._sign)
|
||||
context._raise_error(Inexact)
|
||||
context._raise_error(Rounded)
|
||||
return context._raise_error(Overflow, 'above Emax', self._sign)
|
||||
return ans
|
||||
|
||||
self_is_subnormal = exp_min < Etiny
|
||||
if self_is_subnormal:
|
||||
context._raise_error(Subnormal)
|
||||
exp_min = Etiny
|
||||
|
||||
# round if self has too many digits
|
||||
if self._exp < exp_min:
|
||||
context._raise_error(Rounded)
|
||||
digits = len(self._int) + self._exp - exp_min
|
||||
if digits < 0:
|
||||
self = _dec_from_triple(self._sign, '1', exp_min-1)
|
||||
digits = 0
|
||||
this_function = getattr(self, self._pick_rounding_function[context.rounding])
|
||||
changed = this_function(digits)
|
||||
rounding_method = self._pick_rounding_function[context.rounding]
|
||||
changed = getattr(self, rounding_method)(digits)
|
||||
coeff = self._int[:digits] or '0'
|
||||
if changed == 1:
|
||||
if changed > 0:
|
||||
coeff = str(int(coeff)+1)
|
||||
ans = _dec_from_triple(self._sign, coeff, exp_min)
|
||||
if len(coeff) > context.prec:
|
||||
coeff = coeff[:-1]
|
||||
exp_min += 1
|
||||
|
||||
# check whether the rounding pushed the exponent out of range
|
||||
if exp_min > Etop:
|
||||
ans = context._raise_error(Overflow, 'above Emax', self._sign)
|
||||
else:
|
||||
ans = _dec_from_triple(self._sign, coeff, exp_min)
|
||||
|
||||
# raise the appropriate signals, taking care to respect
|
||||
# the precedence described in the specification
|
||||
if changed and self_is_subnormal:
|
||||
context._raise_error(Underflow)
|
||||
if self_is_subnormal:
|
||||
context._raise_error(Subnormal)
|
||||
if changed:
|
||||
context._raise_error(Inexact)
|
||||
if self_is_subnormal:
|
||||
context._raise_error(Underflow)
|
||||
if not ans:
|
||||
# raise Clamped on underflow to 0
|
||||
context._raise_error(Clamped)
|
||||
elif len(ans._int) == context.prec+1:
|
||||
# we get here only if rescaling rounds the
|
||||
# cofficient up to exactly 10**context.prec
|
||||
if ans._exp < Etop:
|
||||
ans = _dec_from_triple(ans._sign,
|
||||
ans._int[:-1], ans._exp+1)
|
||||
else:
|
||||
# Inexact and Rounded have already been raised
|
||||
ans = context._raise_error(Overflow, 'above Emax',
|
||||
self._sign)
|
||||
context._raise_error(Rounded)
|
||||
if not ans:
|
||||
# raise Clamped on underflow to 0
|
||||
context._raise_error(Clamped)
|
||||
return ans
|
||||
|
||||
if self_is_subnormal:
|
||||
context._raise_error(Subnormal)
|
||||
|
||||
# fold down if _clamp == 1 and self has too few digits
|
||||
if context._clamp == 1 and self._exp > Etop:
|
||||
context._raise_error(Clamped)
|
||||
|
@ -2239,6 +2245,7 @@ class Decimal(object):
|
|||
# from here on, the result always goes through the call
|
||||
# to _fix at the end of this function.
|
||||
ans = None
|
||||
exact = False
|
||||
|
||||
# crude test to catch cases of extreme overflow/underflow. If
|
||||
# log10(self)*other >= 10**bound and bound >= len(str(Emax))
|
||||
|
@ -2263,6 +2270,7 @@ class Decimal(object):
|
|||
ans = self._power_exact(other, context.prec + 1)
|
||||
if ans is not None and result_sign == 1:
|
||||
ans = _dec_from_triple(1, ans._int, ans._exp)
|
||||
exact = True
|
||||
|
||||
# usual case: inexact result, x**y computed directly as exp(y*log(x))
|
||||
if ans is None:
|
||||
|
@ -2285,24 +2293,55 @@ class Decimal(object):
|
|||
|
||||
ans = _dec_from_triple(result_sign, str(coeff), exp)
|
||||
|
||||
# the specification says that for non-integer other we need to
|
||||
# raise Inexact, even when the result is actually exact. In
|
||||
# the same way, we need to raise Underflow here if the result
|
||||
# is subnormal. (The call to _fix will take care of raising
|
||||
# Rounded and Subnormal, as usual.)
|
||||
if not other._isinteger():
|
||||
context._raise_error(Inexact)
|
||||
# pad with zeros up to length context.prec+1 if necessary
|
||||
# unlike exp, ln and log10, the power function respects the
|
||||
# rounding mode; no need to switch to ROUND_HALF_EVEN here
|
||||
|
||||
# There's a difficulty here when 'other' is not an integer and
|
||||
# the result is exact. In this case, the specification
|
||||
# requires that the Inexact flag be raised (in spite of
|
||||
# exactness), but since the result is exact _fix won't do this
|
||||
# for us. (Correspondingly, the Underflow signal should also
|
||||
# be raised for subnormal results.) We can't directly raise
|
||||
# these signals either before or after calling _fix, since
|
||||
# that would violate the precedence for signals. So we wrap
|
||||
# the ._fix call in a temporary context, and reraise
|
||||
# afterwards.
|
||||
if exact and not other._isinteger():
|
||||
# pad with zeros up to length context.prec+1 if necessary; this
|
||||
# ensures that the Rounded signal will be raised.
|
||||
if len(ans._int) <= context.prec:
|
||||
expdiff = context.prec+1 - len(ans._int)
|
||||
expdiff = context.prec + 1 - len(ans._int)
|
||||
ans = _dec_from_triple(ans._sign, ans._int+'0'*expdiff,
|
||||
ans._exp-expdiff)
|
||||
if ans.adjusted() < context.Emin:
|
||||
context._raise_error(Underflow)
|
||||
|
||||
# unlike exp, ln and log10, the power function respects the
|
||||
# rounding mode; no need to use ROUND_HALF_EVEN here
|
||||
ans = ans._fix(context)
|
||||
# create a copy of the current context, with cleared flags/traps
|
||||
newcontext = context.copy()
|
||||
newcontext.clear_flags()
|
||||
for exception in _signals:
|
||||
newcontext.traps[exception] = 0
|
||||
|
||||
# round in the new context
|
||||
ans = ans._fix(newcontext)
|
||||
|
||||
# raise Inexact, and if necessary, Underflow
|
||||
newcontext._raise_error(Inexact)
|
||||
if newcontext.flags[Subnormal]:
|
||||
newcontext._raise_error(Underflow)
|
||||
|
||||
# propagate signals to the original context; _fix could
|
||||
# have raised any of Overflow, Underflow, Subnormal,
|
||||
# Inexact, Rounded, Clamped. Overflow needs the correct
|
||||
# arguments. Note that the order of the exceptions is
|
||||
# important here.
|
||||
if newcontext.flags[Overflow]:
|
||||
context._raise_error(Overflow, 'above Emax', ans._sign)
|
||||
for exception in Underflow, Subnormal, Inexact, Rounded, Clamped:
|
||||
if newcontext.flags[exception]:
|
||||
context._raise_error(exception)
|
||||
|
||||
else:
|
||||
ans = ans._fix(context)
|
||||
|
||||
return ans
|
||||
|
||||
def __rpow__(self, other, context=None):
|
||||
|
@ -2396,14 +2435,15 @@ class Decimal(object):
|
|||
'quantize result has too many digits for current context')
|
||||
|
||||
# raise appropriate flags
|
||||
if ans._exp > self._exp:
|
||||
context._raise_error(Rounded)
|
||||
if ans != self:
|
||||
context._raise_error(Inexact)
|
||||
if ans and ans.adjusted() < context.Emin:
|
||||
context._raise_error(Subnormal)
|
||||
if ans._exp > self._exp:
|
||||
if ans != self:
|
||||
context._raise_error(Inexact)
|
||||
context._raise_error(Rounded)
|
||||
|
||||
# call to fix takes care of any necessary folddown
|
||||
# call to fix takes care of any necessary folddown, and
|
||||
# signals Clamped if necessary
|
||||
ans = ans._fix(context)
|
||||
return ans
|
||||
|
||||
|
@ -2502,10 +2542,10 @@ class Decimal(object):
|
|||
context = getcontext()
|
||||
if rounding is None:
|
||||
rounding = context.rounding
|
||||
context._raise_error(Rounded)
|
||||
ans = self._rescale(0, rounding)
|
||||
if ans != self:
|
||||
context._raise_error(Inexact)
|
||||
context._raise_error(Rounded)
|
||||
return ans
|
||||
|
||||
def to_integral_value(self, rounding=None, context=None):
|
||||
|
@ -3386,13 +3426,13 @@ class Decimal(object):
|
|||
context._raise_error(Overflow,
|
||||
'Infinite result from next_toward',
|
||||
ans._sign)
|
||||
context._raise_error(Rounded)
|
||||
context._raise_error(Inexact)
|
||||
context._raise_error(Rounded)
|
||||
elif ans.adjusted() < context.Emin:
|
||||
context._raise_error(Underflow)
|
||||
context._raise_error(Subnormal)
|
||||
context._raise_error(Rounded)
|
||||
context._raise_error(Inexact)
|
||||
context._raise_error(Rounded)
|
||||
# if precision == 1 then we don't raise Clamped for a
|
||||
# result 0E-Etiny.
|
||||
if not ans:
|
||||
|
|
|
@ -42,6 +42,12 @@ except ImportError:
|
|||
# Useful Test Constant
|
||||
Signals = tuple(getcontext().flags.keys())
|
||||
|
||||
# Signals ordered with respect to precedence: when an operation
|
||||
# produces multiple signals, signals occurring later in the list
|
||||
# should be handled before those occurring earlier in the list.
|
||||
OrderedSignals = (Clamped, Rounded, Inexact, Subnormal,
|
||||
Underflow, Overflow, DivisionByZero, InvalidOperation)
|
||||
|
||||
# Tests are built around these assumed context defaults.
|
||||
# test_main() restores the original context.
|
||||
def init():
|
||||
|
@ -352,6 +358,25 @@ class DecimalTest(unittest.TestCase):
|
|||
else:
|
||||
self.fail("Did not raise %s in %s" % (error, s))
|
||||
self.context.traps[error] = 0
|
||||
|
||||
# as above, but add traps cumulatively, to check precedence
|
||||
ordered_errors = [e for e in OrderedSignals if e in theirexceptions]
|
||||
for error in ordered_errors:
|
||||
self.context.traps[error] = 1
|
||||
try:
|
||||
funct(*vals)
|
||||
except error:
|
||||
pass
|
||||
except Signals, e:
|
||||
self.fail("Raised %s in %s; expected %s" %
|
||||
(type(e), s, error))
|
||||
else:
|
||||
self.fail("Did not raise %s in %s" % (error, s))
|
||||
# reset traps
|
||||
for error in ordered_errors:
|
||||
self.context.traps[error] = 0
|
||||
|
||||
|
||||
if DEBUG:
|
||||
print "--", self.context
|
||||
try:
|
||||
|
|
|
@ -33,6 +33,12 @@ Core and Builtins
|
|||
Library
|
||||
-------
|
||||
|
||||
- Issue #8567: Fix precedence of signals in Decimal module: when a
|
||||
Decimal operation raises multiple signals and more than one of those
|
||||
signals is trapped, the specification determines the order in which
|
||||
the signals should be handled. In many cases this order wasn't
|
||||
being followed, leading to the wrong Python exception being raised.
|
||||
|
||||
- Issue #7865: The close() method of :mod:`io` objects should not swallow
|
||||
exceptions raised by the implicit flush(). Also ensure that calling
|
||||
close() several times is supported. Patch by Pascal Chambon.
|
||||
|
|
Loading…
Reference in New Issue