diff --git a/Lib/decimal.py b/Lib/decimal.py index 727aee2f108..64808806d37 100644 --- a/Lib/decimal.py +++ b/Lib/decimal.py @@ -1655,47 +1655,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) @@ -2322,6 +2328,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)) @@ -2346,6 +2353,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: @@ -2368,24 +2376,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): @@ -2479,14 +2518,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 @@ -2585,10 +2625,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): @@ -3469,13 +3509,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: diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index d6e170d1e41..21590880b08 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -41,6 +41,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(): @@ -351,6 +357,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 as 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: diff --git a/Misc/NEWS b/Misc/NEWS index 6b4f316b5f0..e847c01e5be 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -348,6 +348,12 @@ C-API 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.