diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst index 7b23f6a3b2c..ff9bd91e8ce 100644 --- a/Doc/library/decimal.rst +++ b/Doc/library/decimal.rst @@ -122,7 +122,7 @@ precision, rounding, or enabled traps:: >>> from decimal import * >>> getcontext() Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999, - capitals=1, flags=[], traps=[Overflow, DivisionByZero, + capitals=1, clamp=0, flags=[], traps=[Overflow, DivisionByZero, InvalidOperation]) >>> getcontext().prec = 7 # Set a new precision @@ -244,7 +244,7 @@ enabled: >>> ExtendedContext Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999, - capitals=1, flags=[], traps=[]) + capitals=1, clamp=0, flags=[], traps=[]) >>> setcontext(ExtendedContext) >>> Decimal(1) / Decimal(7) Decimal('0.142857143') @@ -269,7 +269,7 @@ using the :meth:`clear_flags` method. :: Decimal('3.14159292') >>> getcontext() Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999, - capitals=1, flags=[Inexact, Rounded], traps=[]) + capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[]) The *flags* entry shows that the rational approximation to :const:`Pi` was rounded (digits beyond the context precision were thrown away) and that the @@ -891,7 +891,7 @@ In addition to the three supplied contexts, new contexts can be created with the :class:`Context` constructor. -.. class:: Context(prec=None, rounding=None, traps=None, flags=None, Emin=None, Emax=None, capitals=1) +.. class:: Context(prec=None, rounding=None, traps=None, flags=None, Emin=None, Emax=None, capitals=None, clamp=None) Creates a new context. If a field is not specified or is :const:`None`, the default values are copied from the :const:`DefaultContext`. If the *flags* @@ -922,6 +922,23 @@ In addition to the three supplied contexts, new contexts can be created with the :const:`1`, exponents are printed with a capital :const:`E`; otherwise, a lowercase :const:`e` is used: :const:`Decimal('6.02e+23')`. + The *clamp* field is either :const:`0` (the default) or :const:`1`. + If set to :const:`1`, the exponent ``e`` of a :class:`Decimal` + instance representable in this context is strictly limited to the + range ``Emin - prec + 1 <= e <= Emax - prec + 1``. If *clamp* is + :const:`0` then a weaker condition holds: the adjusted exponent of + the :class:`Decimal` instance is at most ``Emax``. When *clamp* is + :const:`1`, a large normal number will, where possible, have its + exponent reduced and a corresponding number of zeros added to its + coefficient, in order to fit the exponent constraints; this + preserves the value of the number but loses information about + significant trailing zeros. For example:: + + >>> Context(prec=6, Emax=999, clamp=1).create_decimal('1.23e999') + Decimal('1.23000E+999') + + A *clamp* value of :const:`1` allows compatibility with the + fixed-width decimal interchange formats specified in IEEE 754. The :class:`Context` class defines several general purpose methods as well as a large number of methods for doing arithmetic directly in a given context. diff --git a/Lib/decimal.py b/Lib/decimal.py index c9edf9c86a7..cc71cd83403 100644 --- a/Lib/decimal.py +++ b/Lib/decimal.py @@ -1611,9 +1611,9 @@ class Decimal(object): """Decapitate the payload of a NaN to fit the context""" payload = self._int - # maximum length of payload is precision if _clamp=0, - # precision-1 if _clamp=1. - max_payload_len = context.prec - context._clamp + # maximum length of payload is precision if clamp=0, + # precision-1 if clamp=1. + max_payload_len = context.prec - context.clamp if len(payload) > max_payload_len: payload = payload[len(payload)-max_payload_len:].lstrip('0') return _dec_from_triple(self._sign, payload, self._exp, True) @@ -1638,11 +1638,11 @@ class Decimal(object): return Decimal(self) # if self is zero then exponent should be between Etiny and - # Emax if _clamp==0, and between Etiny and Etop if _clamp==1. + # Emax if clamp==0, and between Etiny and Etop if clamp==1. Etiny = context.Etiny() Etop = context.Etop() if not self: - exp_max = [context.Emax, Etop][context._clamp] + exp_max = [context.Emax, Etop][context.clamp] new_exp = min(max(self._exp, Etiny), exp_max) if new_exp != self._exp: context._raise_error(Clamped) @@ -1702,8 +1702,8 @@ class Decimal(object): 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: + # 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) @@ -2451,7 +2451,7 @@ class Decimal(object): if not dup: return _dec_from_triple(dup._sign, '0', 0) - exp_max = [context.Emax, context.Etop()][context._clamp] + exp_max = [context.Emax, context.Etop()][context.clamp] end = len(dup._int) exp = dup._exp while dup._int[end-1] == '0' and exp < exp_max: @@ -3828,13 +3828,13 @@ class Context(object): Emax - Maximum exponent capitals - If 1, 1*10^1 is printed as 1E+1. If 0, printed as 1e1 - _clamp - If 1, change exponents if too high (Default 0) + clamp - If 1, change exponents if too high (Default 0) """ def __init__(self, prec=None, rounding=None, traps=None, flags=None, Emin=None, Emax=None, - capitals=None, _clamp=0, + capitals=None, clamp=None, _ignored_flags=None): if flags is None: flags = [] @@ -3855,7 +3855,8 @@ class Context(object): """Show the current context.""" s = [] s.append('Context(prec=%(prec)d, rounding=%(rounding)s, ' - 'Emin=%(Emin)d, Emax=%(Emax)d, capitals=%(capitals)d' + 'Emin=%(Emin)d, Emax=%(Emax)d, capitals=%(capitals)d, ' + 'clamp=%(clamp)d' % vars(self)) names = [f.__name__ for f, v in self.flags.items() if v] s.append('flags=[' + ', '.join(names) + ']') @@ -3872,17 +3873,39 @@ class Context(object): """Returns a shallow copy from self.""" nc = Context(self.prec, self.rounding, self.traps, self.flags, self.Emin, self.Emax, - self.capitals, self._clamp, self._ignored_flags) + self.capitals, self.clamp, self._ignored_flags) return nc def copy(self): """Returns a deep copy from self.""" nc = Context(self.prec, self.rounding, self.traps.copy(), self.flags.copy(), self.Emin, self.Emax, - self.capitals, self._clamp, self._ignored_flags) + self.capitals, self.clamp, self._ignored_flags) return nc __copy__ = copy + # _clamp is provided for backwards compatibility with third-party + # code. May be removed in Python >= 3.3. + def _get_clamp(self): + "_clamp mirrors the clamp attribute. Its use is deprecated." + import warnings + warnings.warn('Use of the _clamp attribute is deprecated. ' + 'Please use clamp instead.', + DeprecationWarning) + return self.clamp + + def _set_clamp(self, clamp): + "_clamp mirrors the clamp attribute. Its use is deprecated." + import warnings + warnings.warn('Use of the _clamp attribute is deprecated. ' + 'Please use clamp instead.', + DeprecationWarning) + self.clamp = clamp + + # don't bother with _del_clamp; no sane 3rd party code should + # be deleting the _clamp attribute + _clamp = property(_get_clamp, _set_clamp) + def _raise_error(self, condition, explanation = None, *args): """Handles an error @@ -3965,7 +3988,7 @@ class Context(object): "permitted.") d = Decimal(num, context=self) - if d._isnan() and len(d._int) > self.prec - self._clamp: + if d._isnan() and len(d._int) > self.prec - self.clamp: return self._raise_error(ConversionSyntax, "diagnostic info too long in NaN") return d._fix(self) @@ -5875,7 +5898,8 @@ DefaultContext = Context( flags=[], Emax=999999999, Emin=-999999999, - capitals=1 + capitals=1, + clamp=0 ) # Pre-made alternate contexts offered by the specification diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 21590880b08..50b660b3413 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -27,11 +27,13 @@ with the corresponding argument. import math import os, sys import operator +import warnings import pickle, copy import unittest from decimal import * import numbers from test.support import run_unittest, run_doctest, is_resource_enabled +from test.support import check_warnings import random try: import threading @@ -412,7 +414,7 @@ class DecimalTest(unittest.TestCase): def change_max_exponent(self, exp): self.context.Emax = exp def change_clamp(self, clamp): - self.context._clamp = clamp + self.context.clamp = clamp @@ -1815,6 +1817,26 @@ class ContextAPItests(unittest.TestCase): self.assertNotEqual(id(c.flags), id(d.flags)) self.assertNotEqual(id(c.traps), id(d.traps)) + def test__clamp(self): + # In Python 3.2, the private attribute `_clamp` was made + # public (issue 8540), with the old `_clamp` becoming a + # property wrapping `clamp`. For the duration of Python 3.2 + # only, the attribute should be gettable/settable via both + # `clamp` and `_clamp`; in Python 3.3, `_clamp` should be + # removed. + c = Context(clamp = 0) + self.assertEqual(c.clamp, 0) + + with check_warnings(("", DeprecationWarning)): + c._clamp = 1 + self.assertEqual(c.clamp, 1) + with check_warnings(("", DeprecationWarning)): + self.assertEqual(c._clamp, 1) + c.clamp = 0 + self.assertEqual(c.clamp, 0) + with check_warnings(("", DeprecationWarning)): + self.assertEqual(c._clamp, 0) + def test_abs(self): c = Context() d = c.abs(Decimal(-1)) diff --git a/Misc/NEWS b/Misc/NEWS index 1799e73b31c..22df937a136 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -393,6 +393,11 @@ C-API Library ------- +- Issue #8540: Decimal module: rename the Context._clamp attribute to + Context.clamp and make it public. This is useful in creating + contexts that correspond to the decimal interchange formats + specified in IEEE 754. + - Issue #6268: Fix seek() method of codecs.open(), don't read or write the BOM twice after seek(0). Fix also reset() method of codecs, UTF-16, UTF-32 and StreamWriter classes.