Repository navigation
_pydecimal loses companion signal flags when a signal is trapped #160945
Description
Bug report
Bug description
The pure-Python decimal implementation (_pydecimal) does not set the
context flags of companion conditions when an operation's result is
delivered through a trapped signal. The C implementation (_decimal,
libmpdec) records the full set of conditions in the context flags before
raising the trapped exception.
Minimal example — an overflowing multiplication signals Overflow,
Inexact and Rounded; Overflow is trapped (as it is in the default
context):
import _decimal as C
import _pydecimal as P
def probe(module):
ctx = module.Context(prec=9, Emin=-999, Emax=999)
ctx.clear_flags()
try:
ctx.multiply(module.Decimal('1E+800'), module.Decimal('1E+800'))
except module.Overflow:
pass
return sorted(s.__name__ for s, v in ctx.flags.items() if v)
print("C _decimal: ", probe(C))
print("P _pydecimal:", probe(P))Output (verified on CPython 3.12 and on a 3.16 build):
C _decimal: ['Inexact', 'Overflow', 'Rounded']
P _pydecimal: ['Overflow'] # Inexact and Rounded are lost
The same loss occurs for every operation that signals several conditions
at once whenever any of them is trapped, e.g.:
| Case (single trap) | _decimal flags after the exception |
_pydecimal flags |
|---|---|---|
divide(1, 3), trap Inexact |
Inexact, Rounded |
Inexact |
divide(1E-999, 1E+10), trap Underflow |
Clamped, Inexact, Rounded, Subnormal, Underflow |
Underflow |
divide(1E-999, 1E+10), trap Subnormal |
Clamped, Inexact, Rounded, Subnormal, Underflow |
Subnormal, Underflow |
divmod(5, 0), trap DivisionByZero |
DivisionByZero, InvalidOperation |
DivisionByZero |
quantize(1.23, 1E+995) with clamp=1, trap Inexact |
Clamped, Inexact, Rounded |
Inexact |
With no traps enabled the two implementations agree exactly.
Expected vs actual
Per the documentation (Doc/library/decimal.rst, "Signals"): "For each
signal there is a flag and a trap enabler. When a signal is encountered,
its flag is set to one, then, if the trap enabler is set to one, an
exception is raised." An overflowing operation encounters Inexact
and Rounded — both implementations set those flags whenever Overflow
is not trapped — so the flags should be set regardless of whether some
other signal is trapped. The two implementations are interchangeable
backends of the same module, so code that catches the exception and then
inspects context.flags (a documented monitoring pattern) must observe
the same state with either backend.
Root cause: in Lib/_pydecimal.py, Context._raise_error() sets one
signal's flag and raises immediately when that signal is trapped.
Operations signal companion conditions through sequential _raise_error
calls (e.g. Decimal._fix signals Overflow, then Inexact, then
Rounded), so a trapped signal makes the remaining calls unreachable.
Note that which exception is raised is correct in both implementations
(the precedence order established by bpo-8567); only the flag state left
behind differs. This is also why the existing test suite does not catch
it: the decTest driver disables the traps of the expected conditions for
its flag comparisons, and the extended error tests assert only the
exception type, never the resulting flags.
CPython versions tested on:
3.12, 3.16
Operating systems tested on:
No response
Activity
pythongh-160945: Fix _pydecimal losing companion signal flags when a …