Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 112 additions & 65 deletions Lib/_pydecimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import math as _math
import numbers as _numbers
import sys
from contextlib import contextmanager as _contextmanager

try:
from collections import namedtuple as _namedtuple
Expand Down Expand Up @@ -1499,8 +1500,9 @@ def __divmod__(self, other, context=None):
ans = context._raise_error(DivisionUndefined, 'divmod(0, 0)')
return ans, ans
else:
return (context._raise_error(DivisionByZero, 'x // 0', sign),
context._raise_error(InvalidOperation, 'x % 0'))
with context._defer_traps():
return (context._raise_error(DivisionByZero, 'x // 0', sign),
context._raise_error(InvalidOperation, 'x % 0'))

quotient, remainder = self._divide(other, context)
remainder = remainder._fix(context)
Expand Down Expand Up @@ -1746,9 +1748,10 @@ def _fix(self, context):
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)
with context._defer_traps():
ans = context._raise_error(Overflow, 'above Emax', self._sign)
context._raise_error(Inexact)
context._raise_error(Rounded)
return ans

self_is_subnormal = exp_min < Etiny
Expand All @@ -1770,34 +1773,36 @@ def _fix(self, context):
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)
# check if rounding pushed the exponent out of range
with context._defer_traps():
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)
context._raise_error(Rounded)
if not ans:
# raise Clamped on underflow to 0
context._raise_error(Clamped)
return ans

# raise the appropriate signals, taking care to respect
# the precedence described in the specification
if changed and self_is_subnormal:
context._raise_error(Underflow)
with context._defer_traps():
if self_is_subnormal:
context._raise_error(Subnormal)
if changed:
context._raise_error(Inexact)
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)
self_padded = self._int + '0'*(self._exp - Etop)
return _dec_from_triple(self._sign, self_padded, Etop)
# fold down if clamp == 1 and self has too few digits
if context.clamp == 1 and self._exp > Etop:
context._raise_error(Clamped)
self_padded = self._int + '0'*(self._exp - Etop)
return _dec_from_triple(self._sign, self_padded, Etop)

# here self was representable to begin with; return unchanged
return Decimal(self)
Expand Down Expand Up @@ -2445,8 +2450,9 @@ def __pow__(self, other, modulo=None, context=None):
exp = 1-context.prec
context._raise_error(Rounded)
else:
context._raise_error(Inexact)
context._raise_error(Rounded)
with context._defer_traps():
context._raise_error(Inexact)
context._raise_error(Rounded)
exp = 1-context.prec

return _dec_from_triple(result_sign, '1'+'0'*-exp, exp)
Expand Down Expand Up @@ -2554,11 +2560,12 @@ def __pow__(self, other, modulo=None, context=None):
# 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)
with context._defer_traps():
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)
Expand Down Expand Up @@ -2645,17 +2652,18 @@ def quantize(self, exp, rounding=None, context=None):
return context._raise_error(InvalidOperation,
'quantize result has too many digits for current context')

# raise appropriate flags
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)
with context._defer_traps():
# raise appropriate flags
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, and
# signals Clamped if necessary
ans = ans._fix(context)
# call to fix takes care of any necessary folddown, and
# signals Clamped if necessary
ans = ans._fix(context)
return ans

def same_quantum(self, other, context=None):
Expand Down Expand Up @@ -2754,9 +2762,10 @@ def to_integral_exact(self, rounding=None, context=None):
if rounding is None:
rounding = context.rounding
ans = self._rescale(0, rounding)
if ans != self:
context._raise_error(Inexact)
context._raise_error(Rounded)
with context._defer_traps():
if ans != self:
context._raise_error(Inexact)
context._raise_error(Rounded)
return ans

def to_integral_value(self, rounding=None, context=None):
Expand Down Expand Up @@ -3646,21 +3655,22 @@ def next_toward(self, other, context=None):
ans = self.next_minus(context)

# decide which flags to raise using value of ans
if ans._isinfinity():
context._raise_error(Overflow,
'Infinite result from next_toward',
ans._sign)
context._raise_error(Inexact)
context._raise_error(Rounded)
elif ans.adjusted() < context.Emin:
context._raise_error(Underflow)
context._raise_error(Subnormal)
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:
context._raise_error(Clamped)
with context._defer_traps():
if ans._isinfinity():
context._raise_error(Overflow,
'Infinite result from next_toward',
ans._sign)
context._raise_error(Inexact)
context._raise_error(Rounded)
elif ans.adjusted() < context.Emin:
context._raise_error(Underflow)
context._raise_error(Subnormal)
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:
context._raise_error(Clamped)

return ans

Expand Down Expand Up @@ -3988,6 +3998,11 @@ def __init__(self, prec=None, rounding=None, Emin=None, Emax=None,
else:
self._ignored_flags = _ignored_flags

# _defer_traps state: nesting level and the deferred exception.
# Transient: not copied or pickled.
self._deferred_traps = 0
self._deferred_error = None

if traps is None:
self.traps = dc.traps.copy()
elif not isinstance(traps, dict):
Expand Down Expand Up @@ -4046,7 +4061,7 @@ def __setattr__(self, name, value):
return object.__setattr__(self, name, value)
elif name == 'flags' or name == 'traps':
return self._set_signal_dict(name, value)
elif name == '_ignored_flags':
elif name in {'_ignored_flags', '_deferred_traps', '_deferred_error'}:
return object.__setattr__(self, name, value)
else:
raise AttributeError(
Expand Down Expand Up @@ -4134,10 +4149,42 @@ def _raise_error(self, condition, explanation = None, *args):
# The errors define how to handle themselves.
return condition().handle(self, *args)

if self._deferred_traps:
# Inside a _defer_traps block: record the first trapped
# exception instead of raising it, and return the default
# response as if the signal had not been trapped.
if self._deferred_error is None:
self._deferred_error = error(explanation)
return condition().handle(self, *args)

# Errors should only be risked on copies of the context
# self._ignored_flags = []
raise error(explanation)

@_contextmanager
def _defer_traps(self):
"""Defer trapped signals until the end of the with block.

_raise_error() sets the flag of every signal in the block and
raises the first trapped exception on exit, so that a
multi-signal operation sets the same flags as _decimal.
"""
self._deferred_traps += 1
try:
yield
except BaseException:
# The block is aborted, so the deferred error is
# discarded: the propagating exception wins, and the
# context does not retain an exception object.
self._deferred_error = None
raise
finally:
self._deferred_traps -= 1
if self._deferred_traps == 0 and self._deferred_error is not None:
error = self._deferred_error
self._deferred_error = None
raise error

def _ignore_all_flags(self):
"""Ignore all flags, if they are raised"""
return self._ignore_flags(*_signals)
Expand Down
103 changes: 103 additions & 0 deletions Lib/test/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4014,6 +4014,59 @@ def raise_error(context, flag):
"operation raises different flags depending on flags set: " +
"expected %s, got %s" % (expected_flags, new_flags))

def test_trapped_signal_sets_companion_flags(self):
# If an operation signals several conditions and one of them is
# trapped, the exception is raised, but the flags of all the
# other conditions encountered must be set as well.
Decimal = self.decimal.Decimal
Context = self.decimal.Context
Clamped = self.decimal.Clamped
DivisionByZero = self.decimal.DivisionByZero
Inexact = self.decimal.Inexact
InvalidOperation = self.decimal.InvalidOperation
Overflow = self.decimal.Overflow
Rounded = self.decimal.Rounded
Subnormal = self.decimal.Subnormal
Underflow = self.decimal.Underflow

def check(trap, operation, expected_flags):
context = Context(prec=9, Emin=-999, Emax=999, traps=[])
context.traps[trap] = 1
with self.assertRaises(trap):
operation(context)
flags = {signal for signal, v in context.flags.items() if v}
self.assertEqual(flags, expected_flags)

# overflow from _fix: Inexact and Rounded are also signaled
check(Overflow,
lambda c: c.multiply(Decimal('1E+800'), Decimal('1E+800')),
{Inexact, Overflow, Rounded})

# 1/3 is rounded: Rounded accompanies the trapped Inexact
check(Inexact,
lambda c: c.divide(Decimal(1), Decimal(3)),
{Inexact, Rounded})

# underflow to zero signals the full set of conditions; a trap
# on any of them must not suppress the flags of the others
check(Underflow,
lambda c: c.divide(Decimal('1E-999'), Decimal('1E+10')),
{Clamped, Inexact, Rounded, Subnormal, Underflow})
check(Subnormal,
lambda c: c.divide(Decimal('1E-999'), Decimal('1E+10')),
{Clamped, Inexact, Rounded, Subnormal, Underflow})

# divmod by zero signals DivisionByZero (for the quotient) and
# InvalidOperation (for the remainder)
check(DivisionByZero,
lambda c: c.divmod(Decimal(5), Decimal(0)),
{DivisionByZero, InvalidOperation})

# to_integral_exact signals Inexact and Rounded together
check(Inexact,
lambda c: c.to_integral_exact(Decimal('2.5')),
{Inexact, Rounded})

def test_flag_comparisons(self):
Context = self.decimal.Context
Inexact = self.decimal.Inexact
Expand Down Expand Up @@ -4234,6 +4287,56 @@ def test_signaldict_repr(self):
class PyContextFlags(ContextFlags, unittest.TestCase):
decimal = P

def test_deferred_traps_attributes(self):
# The _defer_traps state lives in the _deferred_traps and
# _deferred_error attributes, which __setattr__ must accept.
Context = self.decimal.Context
Decimal = self.decimal.Decimal
Overflow = self.decimal.Overflow
context = Context(prec=9, Emin=-999, Emax=999, traps=[])
self.assertEqual(context._deferred_traps, 0)
self.assertIsNone(context._deferred_error)
context._deferred_traps = 1
context._deferred_traps = 0
context._deferred_error = Overflow('test')
context._deferred_error = None
with self.assertRaises(AttributeError):
context._no_such_attribute = 1

# A trapped operation resets the state, so the context
# remains usable afterwards.
context.traps[Overflow] = 1
with self.assertRaises(Overflow):
context.multiply(Decimal('1E+800'), Decimal('1E+800'))
self.assertEqual(context._deferred_traps, 0)
self.assertIsNone(context._deferred_error)

def test_deferred_traps_exception_discards_error(self):
# If an exception propagates through a _defer_traps block, the
# deferred error is discarded at every nesting level and the
# state is reset, so the propagating exception wins and the
# context stays usable.
Context = self.decimal.Context
Decimal = self.decimal.Decimal
Overflow = self.decimal.Overflow
context = Context(prec=9, Emin=-999, Emax=999, traps=[Overflow])
with self.assertRaises(RuntimeError):
with context._defer_traps():
with context._defer_traps():
context._raise_error(Overflow, 'test', 1)
raise RuntimeError('boom')
self.assertEqual(context._deferred_traps, 0)
self.assertIsNone(context._deferred_error)

# The flag was still set, and a later trapped operation
# raises its own error rather than the discarded one.
self.assertTrue(context.flags[Overflow])
context.clear_flags()
with self.assertRaises(Overflow):
context.multiply(Decimal('1E+800'), Decimal('1E+800'))
self.assertEqual(context._deferred_traps, 0)
self.assertIsNone(context._deferred_error)

class SpecialContexts:
"""Test the context templates."""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fixed :mod:`!_pydecimal` losing the context flags of companion conditions
when a signal was trapped. After an operation that signals several
conditions at once (for example an overflowing multiplication), the flags
for *all* encountered conditions are now set, matching :mod:`!_decimal`;
the exception raised is unchanged.
Loading