Merged revisions 80753 via svnmerge from

svn+ssh://pythondev@svn.python.org/python/trunk

........
  r80753 | mark.dickinson | 2010-05-04 15:25:50 +0100 (Tue, 04 May 2010) | 10 lines

  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:
Mark Dickinson 2010-05-04 14:35:33 +00:00
parent a714257a66
commit c69160e808
3 changed files with 115 additions and 44 deletions

View File

@ -1655,47 +1655,53 @@ class Decimal(object):
exp_min = len(self._int) + self._exp - context.prec exp_min = len(self._int) + self._exp - context.prec
if exp_min > Etop: if exp_min > Etop:
# overflow: exp_min > Etop iff self.adjusted() > Emax # overflow: exp_min > Etop iff self.adjusted() > Emax
ans = context._raise_error(Overflow, 'above Emax', self._sign)
context._raise_error(Inexact) context._raise_error(Inexact)
context._raise_error(Rounded) context._raise_error(Rounded)
return context._raise_error(Overflow, 'above Emax', self._sign) return ans
self_is_subnormal = exp_min < Etiny self_is_subnormal = exp_min < Etiny
if self_is_subnormal: if self_is_subnormal:
context._raise_error(Subnormal)
exp_min = Etiny exp_min = Etiny
# round if self has too many digits # round if self has too many digits
if self._exp < exp_min: if self._exp < exp_min:
context._raise_error(Rounded)
digits = len(self._int) + self._exp - exp_min digits = len(self._int) + self._exp - exp_min
if digits < 0: if digits < 0:
self = _dec_from_triple(self._sign, '1', exp_min-1) self = _dec_from_triple(self._sign, '1', exp_min-1)
digits = 0 digits = 0
this_function = getattr(self, self._pick_rounding_function[context.rounding]) rounding_method = self._pick_rounding_function[context.rounding]
changed = this_function(digits) changed = getattr(self, rounding_method)(digits)
coeff = self._int[:digits] or '0' coeff = self._int[:digits] or '0'
if changed == 1: if changed > 0:
coeff = str(int(coeff)+1) coeff = str(int(coeff)+1)
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) 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: if changed:
context._raise_error(Inexact) context._raise_error(Inexact)
if self_is_subnormal: context._raise_error(Rounded)
context._raise_error(Underflow)
if not ans: if not ans:
# raise Clamped on underflow to 0 # raise Clamped on underflow to 0
context._raise_error(Clamped) 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)
return ans return ans
if self_is_subnormal:
context._raise_error(Subnormal)
# fold down if _clamp == 1 and self has too few digits # fold down if _clamp == 1 and self has too few digits
if context._clamp == 1 and self._exp > Etop: if context._clamp == 1 and self._exp > Etop:
context._raise_error(Clamped) context._raise_error(Clamped)
@ -2322,6 +2328,7 @@ class Decimal(object):
# from here on, the result always goes through the call # from here on, the result always goes through the call
# to _fix at the end of this function. # to _fix at the end of this function.
ans = None ans = None
exact = False
# crude test to catch cases of extreme overflow/underflow. If # crude test to catch cases of extreme overflow/underflow. If
# log10(self)*other >= 10**bound and bound >= len(str(Emax)) # 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) ans = self._power_exact(other, context.prec + 1)
if ans is not None and result_sign == 1: if ans is not None and result_sign == 1:
ans = _dec_from_triple(1, ans._int, ans._exp) ans = _dec_from_triple(1, ans._int, ans._exp)
exact = True
# usual case: inexact result, x**y computed directly as exp(y*log(x)) # usual case: inexact result, x**y computed directly as exp(y*log(x))
if ans is None: if ans is None:
@ -2368,24 +2376,55 @@ class Decimal(object):
ans = _dec_from_triple(result_sign, str(coeff), exp) ans = _dec_from_triple(result_sign, str(coeff), exp)
# the specification says that for non-integer other we need to # unlike exp, ln and log10, the power function respects the
# raise Inexact, even when the result is actually exact. In # rounding mode; no need to switch to ROUND_HALF_EVEN here
# the same way, we need to raise Underflow here if the result
# is subnormal. (The call to _fix will take care of raising # There's a difficulty here when 'other' is not an integer and
# Rounded and Subnormal, as usual.) # the result is exact. In this case, the specification
if not other._isinteger(): # requires that the Inexact flag be raised (in spite of
context._raise_error(Inexact) # exactness), but since the result is exact _fix won't do this
# pad with zeros up to length context.prec+1 if necessary # 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: 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 = _dec_from_triple(ans._sign, ans._int+'0'*expdiff,
ans._exp-expdiff) ans._exp-expdiff)
if ans.adjusted() < context.Emin:
context._raise_error(Underflow)
# unlike exp, ln and log10, the power function respects the # create a copy of the current context, with cleared flags/traps
# rounding mode; no need to use ROUND_HALF_EVEN here 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) ans = ans._fix(context)
return ans return ans
def __rpow__(self, other, context=None): def __rpow__(self, other, context=None):
@ -2479,14 +2518,15 @@ class Decimal(object):
'quantize result has too many digits for current context') 'quantize result has too many digits for current context')
# raise appropriate flags # 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: if ans and ans.adjusted() < context.Emin:
context._raise_error(Subnormal) 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) ans = ans._fix(context)
return ans return ans
@ -2585,10 +2625,10 @@ class Decimal(object):
context = getcontext() context = getcontext()
if rounding is None: if rounding is None:
rounding = context.rounding rounding = context.rounding
context._raise_error(Rounded)
ans = self._rescale(0, rounding) ans = self._rescale(0, rounding)
if ans != self: if ans != self:
context._raise_error(Inexact) context._raise_error(Inexact)
context._raise_error(Rounded)
return ans return ans
def to_integral_value(self, rounding=None, context=None): def to_integral_value(self, rounding=None, context=None):
@ -3469,13 +3509,13 @@ class Decimal(object):
context._raise_error(Overflow, context._raise_error(Overflow,
'Infinite result from next_toward', 'Infinite result from next_toward',
ans._sign) ans._sign)
context._raise_error(Rounded)
context._raise_error(Inexact) context._raise_error(Inexact)
context._raise_error(Rounded)
elif ans.adjusted() < context.Emin: elif ans.adjusted() < context.Emin:
context._raise_error(Underflow) context._raise_error(Underflow)
context._raise_error(Subnormal) context._raise_error(Subnormal)
context._raise_error(Rounded)
context._raise_error(Inexact) context._raise_error(Inexact)
context._raise_error(Rounded)
# if precision == 1 then we don't raise Clamped for a # if precision == 1 then we don't raise Clamped for a
# result 0E-Etiny. # result 0E-Etiny.
if not ans: if not ans:

View File

@ -41,6 +41,12 @@ except ImportError:
# Useful Test Constant # Useful Test Constant
Signals = tuple(getcontext().flags.keys()) 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. # Tests are built around these assumed context defaults.
# test_main() restores the original context. # test_main() restores the original context.
def init(): def init():
@ -351,6 +357,25 @@ class DecimalTest(unittest.TestCase):
else: else:
self.fail("Did not raise %s in %s" % (error, s)) self.fail("Did not raise %s in %s" % (error, s))
self.context.traps[error] = 0 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: if DEBUG:
print("--", self.context) print("--", self.context)
try: try:

View File

@ -348,6 +348,12 @@ C-API
Library 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 - Issue #7865: The close() method of :mod:`io` objects should not swallow
exceptions raised by the implicit flush(). Also ensure that calling exceptions raised by the implicit flush(). Also ensure that calling
close() several times is supported. Patch by Pascal Chambon. close() several times is supported. Patch by Pascal Chambon.