Django

Code

Ticket #2365: DecimalField-FloatField.2.diff

File DecimalField-FloatField.2.diff, 143.4 kB (added by adurdin@gmail.com, 2 years ago)

Patch -- implements database FloatField? and DecimalField?, and supporting fields in oldforms and newforms, with tests.

  • django/oldforms/__init__.py

    old new  
    745745            raise validators.CriticalValidationError, gettext("Enter a whole number between 0 and 32,767.") 
    746746 
    747747class FloatField(TextField): 
     748    def __init__(self, field_name, is_required=False, validator_list=None):  
     749        if validator_list is None: validator_list = []  
     750        validator_list = [validators.isValidFloat] + validator_list  
     751        TextField.__init__(self, field_name, is_required=is_required, validator_list=validator_list)  
     752  
     753    def html2python(data):  
     754        if data == '' or data is None:  
     755            return None  
     756        return float(data)  
     757    html2python = staticmethod(html2python)  
     758  
     759class DecimalField(TextField):  
    748760    def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None): 
    749761        if validator_list is None: validator_list = [] 
    750762        self.max_digits, self.decimal_places = max_digits, decimal_places 
    751         validator_list = [self.isValidFloat] + validator_list 
    752         TextField.__init__(self, field_name, max_digits+2, max_digits+2, is_required, validator_list) 
     763        validator_list = [self.isValidDecimal] + validator_list  
     764        # Initialise the TextField, making sure it's large enough to fit the number with a - sign and a decimal point.  
     765        super(DecimalField, self).__init__(field_name, max_digits+2, max_digits+2, is_required, validator_list)  
    753766 
    754     def isValidFloat(self, field_data, all_data): 
    755         v = validators.IsValidFloat(self.max_digits, self.decimal_places) 
     767    def isValidDecimal(self, field_data, all_data):  
     768        v = validators.IsValidDecimal(self.max_digits, self.decimal_places)  
    756769        try: 
    757770            v(field_data, all_data) 
    758771        except validators.ValidationError, e: 
     
    761774    def html2python(data): 
    762775        if data == '' or data is None: 
    763776            return None 
    764         return float(data) 
     777        try:  
     778            import decimal  
     779        except ImportError: 
     780            from django.utils import decimal 
     781        try:  
     782            return decimal.Decimal(data)  
     783        except decimal.InvalidOperation, e:  
     784            raise ValueError, e  
    765785    html2python = staticmethod(html2python) 
    766786 
    767787#################### 
  • django/db/models/fields/__init__.py

    old new  
    668668    def get_manipulator_field_objs(self): 
    669669        return [curry(oldforms.FilePathField, path=self.path, match=self.match, recursive=self.recursive)] 
    670670 
    671 class FloatField(Field): 
     671class DecimalField(Field): 
    672672    empty_strings_allowed = False 
    673673    def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs): 
    674674        self.max_digits, self.decimal_places = max_digits, decimal_places 
    675675        Field.__init__(self, verbose_name, name, **kwargs) 
    676676 
     677    def to_python(self, value): 
     678        try: 
     679            import decimal 
     680        except ImportError: 
     681            from django.utils import decimal 
     682        try: 
     683            return decimal.Decimal(value) 
     684        except (decimal.InvalidOperation): 
     685            raise validators.ValidationError, gettext("This value must be a decimal number.") 
     686 
     687    def _format(self, value): 
     688        if isinstance(value, basestring): 
     689            return value 
     690        else: 
     691            return self.format_number(value) 
     692     
     693    def format_number(self, value): 
     694        """Formats a number into a string with the requisite number of digits and decimal places.""" 
     695         
     696        num_chars = self.max_digits 
     697        # Allow for a decimal point  
     698        if self.decimal_places > 0: 
     699            num_chars += 1 
     700        # Allow for a minus sign 
     701        if value < 0: 
     702            num_chars += 1 
     703         
     704        return "%.*f" % (self.decimal_places, value) 
     705 
     706    def get_db_prep_save(self, value): 
     707        value = value # self._format(value) 
     708        return super(DecimalField, self).get_db_prep_save(value) 
     709 
     710    def get_db_prep_lookup(self, lookup_type, value): 
     711        if lookup_type == 'range': 
     712            value = [self._format(v) for v in value] 
     713        else: 
     714            value = self._format(value) 
     715        return super(DecimalField, self).get_db_prep_lookup(lookup_type, value) 
     716 
    677717    def get_manipulator_field_objs(self): 
    678         return [curry(oldforms.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)] 
     718        return [curry(oldforms.DecimalField, max_digits=self.max_digits, decimal_places=self.decimal_places)] 
    679719 
     720    def formfield(self, **kwargs): 
     721        "Returns a django.newforms.Field instance for this database Field." 
     722        defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 
     723            'max_digits': self.max_digits, 'decimal_places': self.decimal_places} 
     724        defaults.update(kwargs) 
     725        return forms.DecimalField(**defaults) 
     726 
     727class FloatField(Field): 
     728    empty_strings_allowed = False 
     729    def __init__(self, verbose_name=None, name=None, **kwargs): 
     730        Field.__init__(self, verbose_name, name, **kwargs) 
     731 
     732    def get_manipulator_field_objs(self): 
     733        return [oldforms.FloatField] 
     734 
     735    def formfield(self, **kwargs): 
     736        "Returns a django.newforms.FloatField instance for this database Field." 
     737        defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name)} 
     738        defaults.update(kwargs) 
     739        return forms.FloatField(**defaults) 
     740 
    680741class ImageField(FileField): 
    681742    def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs): 
    682743        self.width_field, self.height_field = width_field, height_field 
  • django/db/backends/ado_mssql/creation.py

    old new  
    55    'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', 
    66    'DateField':         'smalldatetime', 
    77    'DateTimeField':     'smalldatetime', 
     8    'DecimalField':      'numeric(%(max_digits)s, %(decimal_places)s)', 
    89    'FileField':         'varchar(100)', 
    910    'FilePathField':     'varchar(100)', 
    10     'FloatField':        'numeric(%(max_digits)s, %(decimal_places)s)', 
     11    'FloatField':        'double precision', 
    1112    'ImageField':        'varchar(100)', 
    1213    'IntegerField':      'int', 
    1314    'IPAddressField':    'char(15)', 
  • django/db/backends/postgresql/introspection.py

    old new  
    7979    1114: 'DateTimeField', 
    8080    1184: 'DateTimeField', 
    8181    1266: 'TimeField', 
    82     1700: 'FloatField', 
     82    1700: 'DecimalField', 
    8383} 
  • django/db/backends/postgresql/creation.py

    old new  
    99    'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', 
    1010    'DateField':         'date', 
    1111    'DateTimeField':     'timestamp with time zone', 
     12    'DecimalField':      'numeric(%(max_digits)s, %(decimal_places)s)', 
    1213    'FileField':         'varchar(100)', 
    1314    'FilePathField':     'varchar(100)', 
    14     'FloatField':        'numeric(%(max_digits)s, %(decimal_places)s)', 
     15    'FloatField':        'double precision', 
    1516    'ImageField':        'varchar(100)', 
    1617    'IntegerField':      'integer', 
    1718    'IPAddressField':    'inet', 
  • django/db/backends/sqlite3/base.py

    old new  
    1717        module = 'sqlite3' 
    1818    raise ImproperlyConfigured, "Error loading %s module: %s" % (module, e) 
    1919 
     20try: 
     21    # Only exists in Python 2.4+ 
     22    import decimal 
     23except ImportError: 
     24    # Import copy of decimal.py from Python 2.4 
     25    from django.utils import decimal 
     26 
    2027DatabaseError = Database.DatabaseError 
    2128 
    2229Database.register_converter("bool", lambda s: str(s) == '1') 
     
    2532Database.register_converter("datetime", util.typecast_timestamp) 
    2633Database.register_converter("timestamp", util.typecast_timestamp) 
    2734Database.register_converter("TIMESTAMP", util.typecast_timestamp) 
     35Database.register_converter("decimal", util.typecast_decimal) 
     36Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal) 
    2837 
    2938def utf8rowFactory(cursor, row): 
    3039    def utf8(s): 
  • django/db/backends/sqlite3/creation.py

    old new  
    88    'CommaSeparatedIntegerField':   'varchar(%(maxlength)s)', 
    99    'DateField':                    'date', 
    1010    'DateTimeField':                'datetime', 
     11    'DecimalField':                 'decimal', 
    1112    'FileField':                    'varchar(100)', 
    1213    'FilePathField':                'varchar(100)', 
    13     'FloatField':                   'numeric(%(max_digits)s, %(decimal_places)s)', 
     14    'FloatField':                   'real', 
    1415    'ImageField':                   'varchar(100)', 
    1516    'IntegerField':                 'integer', 
    1617    'IPAddressField':               'char(15)', 
  • django/db/backends/util.py

    old new  
    11import datetime 
    22from time import time 
    33 
     4try: 
     5    # Only exists in Python 2.4+ 
     6    import decimal 
     7except ImportError: 
     8    # Import copy of decimal.py from Python 2.4 
     9    from django.utils import decimal 
     10 
    411class CursorDebugWrapper(object): 
    512    def __init__(self, cursor, db): 
    613        self.cursor = cursor 
     
    8592    if not s: return False 
    8693    return str(s)[0].lower() == 't' 
    8794 
     95def typecast_decimal(s): 
     96    if s is None: return None 
     97    return decimal.Decimal(s) 
     98 
    8899############################################### 
    89100# Converters from Python to database (string) # 
    90101############################################### 
     
    92103def rev_typecast_boolean(obj, d): 
    93104    return obj and '1' or '0' 
    94105 
     106def rev_typecast_decimal(d): 
     107    if d is None: return None 
     108    return str(d) 
     109 
    95110################################################################################## 
    96111# Helper functions for dictfetch* for databases that don't natively support them # 
    97112################################################################################## 
  • django/db/backends/mysql/base.py

    old new  
    2323    FIELD_TYPE.DATETIME: util.typecast_timestamp, 
    2424    FIELD_TYPE.DATE: util.typecast_date, 
    2525    FIELD_TYPE.TIME: util.typecast_time, 
     26    FIELD_TYPE.DECIMAL: util.typecast_decimal, 
    2627}) 
    2728 
    2829# This should match the numerical portion of the version numbers (we can treat 
  • django/db/backends/mysql/introspection.py

    old new  
    7676DATA_TYPES_REVERSE = { 
    7777    FIELD_TYPE.BLOB: 'TextField', 
    7878    FIELD_TYPE.CHAR: 'CharField', 
    79     FIELD_TYPE.DECIMAL: 'FloatField', 
     79    FIELD_TYPE.DECIMAL: 'DecimalField', 
    8080    FIELD_TYPE.DATE: 'DateField', 
    8181    FIELD_TYPE.DATETIME: 'DateTimeField', 
    8282    FIELD_TYPE.DOUBLE: 'FloatField', 
  • django/db/backends/mysql/creation.py

    old new  
    99    'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', 
    1010    'DateField':         'date', 
    1111    'DateTimeField':     'datetime', 
     12    'DecimalField':      'decimal(%(max_digits)s, %(decimal_places)s)', 
    1213    'FileField':         'varchar(100)', 
    1314    'FilePathField':     'varchar(100)', 
    14     'FloatField':        'numeric(%(max_digits)s, %(decimal_places)s)', 
     15    'FloatField':        'double precision', 
    1516    'ImageField':        'varchar(100)', 
    1617    'IntegerField':      'integer', 
    1718    'IPAddressField':    'char(15)', 
  • django/db/backends/oracle/introspection.py

    old new  
    4646    1114: 'DateTimeField', 
    4747    1184: 'DateTimeField', 
    4848    1266: 'TimeField', 
    49     1700: 'FloatField', 
     49    1700: 'DecimalField', 
    5050} 
  • django/db/backends/oracle/creation.py

    old new  
    55    'CommaSeparatedIntegerField': 'varchar2(%(maxlength)s)', 
    66    'DateField':         'date', 
    77    'DateTimeField':     'date', 
     8    'DecimalField':      'number(%(max_digits)s, %(decimal_places)s)', 
    89    'FileField':         'varchar2(100)', 
    910    'FilePathField':     'varchar2(100)', 
    10     'FloatField':        'number(%(max_digits)s, %(decimal_places)s)', 
     11    'FloatField':        'double precision', 
    1112    'ImageField':        'varchar2(100)', 
    1213    'IntegerField':      'integer', 
    1314    'IPAddressField':    'char(15)', 
  • django/db/backends/postgresql_psycopg2/introspection.py

    old new  
    7979    1114: 'DateTimeField', 
    8080    1184: 'DateTimeField', 
    8181    1266: 'TimeField', 
    82     1700: 'FloatField', 
     82    1700: 'DecimalField', 
    8383} 
  • django/core/validators.py

    old new  
    2525    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom 
    2626    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string 
    2727    r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain 
     28decimal_re = re.compile(r'^-?(?P<digits>\d+)(\.(?P<decimals>\d+))?$') 
    2829integer_re = re.compile(r'^-?\d+$') 
    2930ip4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') 
    3031phone_re = re.compile(r'^[A-PR-Y0-9]{3}-[A-PR-Y0-9]{3}-[A-PR-Y0-9]{4}$', re.IGNORECASE) 
     
    403404        if val != int(val): 
    404405            raise ValidationError, gettext("This value must be a power of %s.") % self.power_of 
    405406 
    406 class IsValidFloat(object): 
     407class IsValidDecimal(object): 
    407408    def __init__(self, max_digits, decimal_places): 
    408409        self.max_digits, self.decimal_places = max_digits, decimal_places 
    409410 
    410411    def __call__(self, field_data, all_data): 
    411         data = str(field_data) 
    412         try: 
    413             float(data) 
    414         except ValueError: 
     412        match = decimal_re.search(str(field_data)) 
     413        if not match: 
    415414            raise ValidationError, gettext("Please enter a valid decimal number.") 
    416         # Negative floats require more space to input. 
    417         max_allowed_length = data.startswith('-') and (self.max_digits + 2) or (self.max_digits + 1) 
    418         if len(data) > max_allowed_length: 
     415         
     416        digits = len(match.group('digits') or '') 
     417        decimals = len(match.group('decimals') or '') 
     418         
     419        if digits + decimals > self.max_digits: 
    419420            raise ValidationError, ngettext("Please enter a valid decimal number with at most %s total digit.", 
    420421                "Please enter a valid decimal number with at most %s total digits.", self.max_digits) % self.max_digits 
    421         if (not '.' in data and len(data) > (max_allowed_length - self.decimal_places - 1)) or ('.' in data and len(data) > (max_allowed_length - (self.decimal_places - len(data.split('.')[1])))): 
     422        if digits > (self.max_digits - self.decimal_places): 
    422423            raise ValidationError, ngettext( "Please enter a valid decimal number with a whole part of at most %s digit.", 
    423424                "Please enter a valid decimal number with a whole part of at most %s digits.", str(self.max_digits-self.decimal_places)) % str(self.max_digits-self.decimal_places) 
    424         if '.' in data and len(data.split('.')[1]) > self.decimal_places: 
     425        if decimals > self.decimal_places: 
    425426            raise ValidationError, ngettext("Please enter a valid decimal number with at most %s decimal place.", 
    426427                "Please enter a valid decimal number with at most %s decimal places.", self.decimal_places) % self.decimal_places 
    427428 
     429def isValidFloat(field_data, all_data): 
     430    data = str(field_data) 
     431    try: 
     432        float(data) 
     433    except ValueError: 
     434        raise ValidationError, gettext("Please enter a valid floating point number.") 
     435 
    428436class HasAllowableSize(object): 
    429437    """ 
    430438    Checks that the file-upload field data is a certain size. min_size and 
  • django/core/management.py

    old new  
    804804                if field_type == 'CharField' and row[3]: 
    805805                    extra_params['maxlength'] = row[3] 
    806806 
    807                 if field_type == 'FloatField': 
     807                if field_type == 'DecimalField': 
    808808                    extra_params['max_digits'] = row[4] 
    809809                    extra_params['decimal_places'] = row[5] 
    810810 
     
    879879                e.add(opts, '"%s": You can\'t use "id" as a field name, because each model automatically gets an "id" field if none of the fields have primary_key=True. You need to either remove/rename your "id" field or add primary_key=True to a field.' % f.name) 
    880880            if isinstance(f, models.CharField) and f.maxlength in (None, 0): 
    881881                e.add(opts, '"%s": CharFields require a "maxlength" attribute.' % f.name) 
    882             if isinstance(f, models.FloatField): 
     882            if isinstance(f, models.DecimalField): 
    883883                if f.decimal_places is None: 
    884                     e.add(opts, '"%s": FloatFields require a "decimal_places" attribute.' % f.name) 
     884                    e.add(opts, '"%s": DecimalFields require a "decimal_places" attribute.' % f.name) 
    885885                if f.max_digits is None: 
    886                     e.add(opts, '"%s": FloatFields require a "max_digits" attribute.' % f.name) 
     886                    e.add(opts, '"%s": DecimalFields require a "max_digits" attribute.' % f.name) 
    887887            if isinstance(f, models.FileField) and not f.upload_to: 
    888888                e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name) 
    889889            if isinstance(f, models.ImageField): 
  • django/newforms/fields.py

    old new  
    1717    'RegexField', 'EmailField', 'URLField', 'BooleanField', 
    1818    'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', 
    1919    'ComboField', 'MultiValueField', 
     20    'FloatField', 'DecimalField', 
    2021    'SplitDateTimeField', 
    2122) 
    2223 
     
    2829except NameError: 
    2930    from sets import Set as set # Python 2.3 fallback 
    3031 
     32try: 
     33    from decimal import Decimal # Only available in Python 2.4+ 
     34except ImportError: 
     35    from django.utils.decimal import Decimal # Use bundled version for Python 2.3 
     36 
    3137class Field(object): 
    3238    widget = TextInput # Default widget to use when rendering this type of Field. 
    3339    hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". 
     
    128134            raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value) 
    129135        return value 
    130136 
     137class FloatField(Field): 
     138    def __init__(self, max_value=None, min_value=None, required=True, widget=None, label=None, initial=None): 
     139        self.max_value, self.min_value = max_value, min_value 
     140        Field.__init__(self, required, widget, label, initial) 
     141         
     142    def clean(self, value): 
     143        """ 
     144        Validates that float() can be called on the input. Returns a float. 
     145        Returns None for empty values. 
     146        """ 
     147        super(FloatField, self).clean(value) 
     148        if not self.required and value in EMPTY_VALUES: 
     149            return None 
     150        try: 
     151            value = float(value) 
     152        except (ValueError, TypeError): 
     153            raise ValidationError(gettext(u'Enter a number.')) 
     154        if self.max_value is not None and value > self.max_value: 
     155            raise ValidationError(gettext(u'Ensure this value is less than or equal to %s.') % self.max_value) 
     156        if self.min_value is not None and value < self.min_value: 
     157            raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value) 
     158        return value 
     159 
     160decimal_re = re.compile(r'^-?(?P<digits>\d+)(\.(?P<decimals>\d+))?$') 
     161 
     162class DecimalField(Field): 
     163    def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, required=True, widget=None, label=None, initial=None): 
     164        self.max_value, self.min_value = max_value, min_value 
     165        self.max_digits, self.decimal_places = max_digits, decimal_places 
     166        Field.__init__(self, required, widget, label, initial) 
     167         
     168    def clean(self, value): 
     169        """ 
     170        Validates that the input is a decimal number. Returns a decimal.Decimal 
     171        instance. Returns None for empty values. Ensures that there are no more 
     172        than max_digits in the number, and no more than decimal_places digits 
     173        after the decimal point. 
     174        """ 
     175        super(DecimalField, self).clean(value) 
     176        if not self.required and value in EMPTY_VALUES: 
     177            return None 
     178        value = value.strip() 
     179        match = decimal_re.search(value) 
     180        if not match: 
     181            raise ValidationError(gettext(u'Enter a number.')) 
     182        else: 
     183            value = Decimal(value) 
     184        digits = len(match.group('digits') or '') 
     185        decimals = len(match.group('decimals') or '') 
     186        if self.max_value is not None and value > self.max_value: 
     187            raise ValidationError(gettext(u'Ensure this value is less than or equal to %s.') % self.max_value) 
     188        if self.min_value is not None and value < self.min_value: 
     189            raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value) 
     190        if self.max_digits is not None and (digits + decimals) > self.max_digits: 
     191            raise ValidationError(gettext(u'Ensure that there are no more than %s digits in total.') % self.max_digits) 
     192        if self.decimal_places is not None and decimals > self.decimal_places: 
     193            raise ValidationError(gettext(u'Ensure that there are no more than %s decimal places.') % self.decimal_places) 
     194        if self.max_digits is not None and self.decimal_places is not None and digits > (self.max_digits - self.decimal_places): 
     195            raise ValidationError(gettext(u'Ensure that there are no more than %s digits before the decimal point.') % (self.max_digits - self.decimal_places)) 
     196        return value 
     197 
    131198DEFAULT_DATE_INPUT_FORMATS = ( 
    132199    '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' 
    133200    '%b %d %Y', '%b %d, %Y',            # 'Oct 25 2006', 'Oct 25, 2006' 
  • django/contrib/admin/templatetags/admin_list.py

    old new  
    157157            # Booleans are special: We use images. 
    158158            elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField): 
    159159                result_repr = _boolean_icon(field_val) 
    160             # FloatFields are special: Zero-pad the decimals. 
    161             elif isinstance(f, models.FloatField): 
     160            # DecimalFields are special: Zero-pad the decimals. 
     161            elif isinstance(f, models.DecimalField): 
    162162                if field_val is not None: 
    163163                    result_repr = ('%%.%sf' % f.decimal_places) % field_val 
    164164                else: 
  • django/contrib/admin/views/doc.py

    old new  
    294294    'CommaSeparatedIntegerField': _('Comma-separated integers'), 
    295295    'DateField'                 : _('Date (without time)'), 
    296296    'DateTimeField'             : _('Date (with time)'), 
     297    'DecimalField'              : _('Decimal number'), 
    297298    'EmailField'                : _('E-mail address'), 
    298299    'FileField'                 : _('File path'), 
    299300    'FilePathField'             : _('File path'), 
    300     'FloatField'                : _('Decimal number'), 
     301    'FloatField'                : _('Floating point number'), 
    301302    'ForeignKey'                : _('Integer'), 
    302303    'ImageField'                : _('File path'), 
    303304    'IntegerField'              : _('Integer'), 
  • django/utils/decimal.py

    old new  
     1# Copyright (c) 2004 Python Software Foundation. 
     2# All rights reserved. 
     3 
     4# Written by Eric Price <eprice at tjhsst.edu> 
     5#    and Facundo Batista <facundo at taniquetil.com.ar> 
     6#    and Raymond Hettinger <python at rcn.com> 
     7#    and Aahz <aahz at pobox.com> 
     8#    and Tim Peters 
     9 
     10# This module is currently Py2.3 compatible and should be kept that way 
     11# unless a major compelling advantage arises.  IOW, 2.3 compatibility is 
     12# strongly preferred, but not guaranteed. 
     13 
     14# Also, this module should be kept in sync with the latest updates of 
     15# the IBM specification as it evolves.  Those updates will be treated 
     16# as bug fixes (deviation from the spec is a compatibility, usability 
     17# bug) and will be backported.  At this point the spec is stabilizing 
     18# and the updates are becoming fewer, smaller, and less significant. 
     19 
     20""" 
     21This is a Py2.3 implementation of decimal floating point arithmetic based on 
     22the General Decimal Arithmetic Specification: 
     23 
     24    www2.hursley.ibm.com/decimal/decarith.html 
     25 
     26and IEEE standard 854-1987: 
     27 
     28    www.cs.berkeley.edu/~ejr/projects/754/private/drafts/854-1987/dir.html 
     29 
     30Decimal floating point has finite precision with arbitrarily large bounds. 
     31 
     32The purpose of the module is to support arithmetic using familiar 
     33"schoolhouse" rules and to avoid the some of tricky representation 
     34issues associated with binary floating point.  The package is especially 
     35useful for financial applications or for contexts where users have 
     36expectations that are at odds with binary floating point (for instance, 
     37in binary floating point, 1.00 % 0.1 gives 0.09999999999999995 instead 
     38of the expected Decimal("0.00") returned by decimal floating point). 
     39 
     40Here are some examples of using the decimal module: 
     41 
     42>>> from decimal import * 
     43>>> setcontext(ExtendedContext) 
     44>>> Decimal(0) 
     45Decimal("0") 
     46>>> Decimal("1") 
     47Decimal("1") 
     48>>> Decimal("-.0123") 
     49Decimal("-0.0123") 
     50>>> Decimal(123456) 
     51Decimal("123456") 
     52>>> Decimal("123.45e12345678901234567890") 
     53Decimal("1.2345E+12345678901234567892") 
     54>>> Decimal("1.33") + Decimal("1.27") 
     55Decimal("2.60") 
     56>>> Decimal("12.34") + Decimal("3.87") - Decimal("18.41") 
     57Decimal("-2.20") 
     58>>> dig = Decimal(1) 
     59>>> print dig / Decimal(3) 
     600.333333333 
     61>>> getcontext().prec = 18 
     62>>> print dig / Decimal(3) 
     630.333333333333333333 
     64>>> print dig.sqrt() 
     651 
     66>>> print Decimal(3).sqrt() 
     671.73205080756887729 
     68>>> print Decimal(3) ** 123 
     694.85192780976896427E+58 
     70>>> inf = Decimal(1) / Decimal(0) 
     71>>> print inf 
     72Infinity 
     73>>> neginf = Decimal(-1) / Decimal(0) 
     74>>> print neginf 
     75-Infinity 
     76>>> print neginf + inf 
     77NaN 
     78>>> print neginf * inf 
     79-Infinity 
     80>>> print dig / 0 
     81Infinity 
     82>>> getcontext().traps[DivisionByZero] = 1 
     83>>> print dig / 0 
     84Traceback (most recent call last): 
     85  ... 
     86  ... 
     87  ... 
     88DivisionByZero: x / 0 
     89>>> c = Context() 
     90>>> c.traps[InvalidOperation] = 0 
     91>>> print c.flags[InvalidOperation] 
     920 
     93>>> c.divide(Decimal(0), Decimal(0)) 
     94Decimal("NaN") 
     95>>> c.traps[InvalidOperation] = 1 
     96>>> print c.flags[InvalidOperation] 
     971 
     98>>> c.flags[InvalidOperation] = 0 
     99>>> print c.flags[InvalidOperation] 
     1000 
     101>>> print c.divide(Decimal(0), Decimal(0)) 
     102Traceback (most recent call last): 
     103  ... 
     104  ... 
     105  ... 
     106InvalidOperation: 0 / 0 
     107>>> print c.flags[InvalidOperation] 
     1081 
     109>>> c.flags[InvalidOperation] = 0 
     110>>> c.traps[InvalidOperation] = 0 
     111>>> print c.divide(Decimal(0), Decimal(0)) 
     112NaN 
     113>>> print c.flags[InvalidOperation] 
     1141 
     115>>> 
     116""" 
     117 
     118__all__ = [ 
     119    # Two major classes 
     120    'Decimal', 'Context', 
     121 
     122    # Contexts 
     123    'DefaultContext', 'BasicContext', 'ExtendedContext', 
     124 
     125    # Exceptions 
     126    'DecimalException', 'Clamped', 'InvalidOperation', 'DivisionByZero', 
     127    'Inexact', 'Rounded', 'Subnormal', 'Overflow', 'Underflow', 
     128 
     129    # Constants for use in setting up contexts 
     130    'ROUND_DOWN', 'ROUND_HALF_UP', 'ROUND_HALF_EVEN', 'ROUND_CEILING', 
     131    'ROUND_FLOOR', 'ROUND_UP', 'ROUND_HALF_DOWN', 
     132 
     133    # Functions for manipulating contexts 
     134    'setcontext', 'getcontext' 
     135] 
     136 
     137import copy 
     138 
     139#Rounding 
     140ROUND_DOWN = 'ROUND_DOWN' 
     141ROUND_HALF_UP = 'ROUND_HALF_UP' 
     142ROUND_HALF_EVEN = 'ROUND_HALF_EVEN' 
     143ROUND_CEILING = 'ROUND_CEILING' 
     144ROUND_FLOOR = 'ROUND_FLOOR' 
     145ROUND_UP = 'ROUND_UP' 
     146ROUND_HALF_DOWN = 'ROUND_HALF_DOWN' 
     147 
     148#Rounding decision (not part of the public API) 
     149NEVER_ROUND = 'NEVER_ROUND'    # Round in division (non-divmod), sqrt ONLY 
     150ALWAYS_ROUND = 'ALWAYS_ROUND'  # Every operation rounds at end. 
     151 
     152#Errors 
     153 
     154class DecimalException(ArithmeticError): 
     155    """Base exception class. 
     156 
     157    Used exceptions derive from this. 
     158    If an exception derives from another exception besides this (such as 
     159    Underflow (Inexact, Rounded, Subnormal) that indicates that it is only 
     160    called if the others are present.  This isn't actually used for 
     161    anything, though. 
     162 
     163    handle  -- Called when context._raise_error is called and the 
     164               trap_enabler is set.  First argument is self, second is the 
     165               context.  More arguments can be given, those being after 
     166               the explanation in _raise_error (For example, 
     167               context._raise_error(NewError, '(-x)!', self._sign) would 
     168               call NewError().handle(context, self._sign).) 
     169 
     170    To define a new exception, it should be sufficient to have it derive 
     171    from DecimalException. 
     172    """ 
     173    def handle(self, context, *args): 
     174        pass 
     175 
     176 
     177class Clamped(DecimalException): 
     178    """Exponent of a 0 changed to fit bounds. 
     179 
     180    This occurs and signals clamped if the exponent of a result has been 
     181    altered in order to fit the constraints of a specific concrete 
     182    representation. This may occur when the exponent of a zero result would 
     183    be outside the bounds of a representation, or  when a large normal 
     184    number would have an encoded exponent that cannot be represented. In 
     185    this latter case, the exponent is reduced to fit and the corresponding 
     186    number of zero digits are appended to the coefficient ("fold-down"). 
     187    """ 
     188 
     189 
     190class InvalidOperation(DecimalException): 
     191    """An invalid operation was performed. 
     192 
     193    Various bad things cause this: 
     194 
     195    Something creates a signaling NaN 
     196    -INF + INF 
     197     0 * (+-)INF 
     198     (+-)INF / (+-)INF 
     199    x % 0 
     200    (+-)INF % x 
     201    x._rescale( non-integer ) 
     202    sqrt(-x) , x > 0 
     203    0 ** 0 
     204    x ** (non-integer) 
     205    x ** (+-)INF 
     206    An operand is invalid 
     207    """ 
     208    def handle(self, context, *args): 
     209        if args: 
     210            if args[0] == 1: #sNaN, must drop 's' but keep diagnostics 
     211                return Decimal( (args[1]._sign, args[1]._int, 'n') ) 
     212        return NaN 
     213 
     214class ConversionSyntax(InvalidOperation): 
     215    """Trying to convert badly formed string. 
     216 
     217    This occurs and signals invalid-operation if an string is being 
     218    converted to a number and it does not conform to the numeric string 
     219    syntax. The result is [0,qNaN]. 
     220    """ 
     221 
     222    def handle(self, context, *args): 
     223        return (0, (0,), 'n') #Passed to something which uses a tuple. 
     224 
     225class DivisionByZero(DecimalException, ZeroDivisionError): 
     226    """Division by 0. 
     227 
     228    This occurs and signals division-by-zero if division of a finite number 
     229    by zero was attempted (during a divide-integer or divide operation, or a 
     230    power operation with negative right-hand operand), and the dividend was 
     231    not zero. 
     232 
     233    The result of the operation is [sign,inf], where sign is the exclusive 
     234    or of the signs of the operands for divide, or is 1 for an odd power of 
     235    -0, for power. 
     236    """ 
     237 
     238    def handle(self, context, sign, double = None, *args): 
     239        if double is not None: 
     240            return (Infsign[sign],)*2 
     241        return Infsign[sign] 
     242 
     243class DivisionImpossible(InvalidOperation): 
     244    """Cannot perform the division adequately. 
     245 
     246    This occurs and signals invalid-operation if the integer result of a 
     247    divide-integer or remainder operation had too many digits (would be 
     248    longer than precision). The result is [0,qNaN]. 
     249    """ 
     250 
     251    def handle(self, context, *args): 
     252        return (NaN, NaN) 
     253 
     254class DivisionUndefined(InvalidOperation, ZeroDivisionError): 
     255    """Undefined result of division. 
     256 
     257    This occurs and signals invalid-operation if division by zero was 
     258    attempted (during a divide-integer, divide, or remainder operation), and 
     259    the dividend is also zero. The result is [0,qNaN]. 
     260    """ 
     261 
     262    def handle(self, context, tup=None, *args): 
     263        if tup is not None: 
     264            return (NaN, NaN) #for 0 %0, 0 // 0 
     265        return NaN 
     266 
     267class Inexact(DecimalException): 
     268    """Had to round, losing information. 
     269 
     270    This occurs and signals inexact whenever the result of an operation is 
     271    not exact (that is, it needed to be rounded and any discarded digits 
     272    were non-zero), or if an overflow or underflow condition occurs. The 
     273    result in all cases is unchanged. 
     274 
     275    The inexact signal may be tested (or trapped) to determine if a given 
     276    operation (or sequence of operations) was inexact. 
     277    """ 
     278    pass 
     279 
     280class InvalidContext(InvalidOperation): 
     281    """Invalid context.  Unknown rounding, for example. 
     282 
     283    This occurs and signals invalid-operation if an invalid context was 
     284    detected during an operation. This can occur if contexts are not checked 
     285    on creation and either the precision exceeds the capability of the 
     286    underlying concrete representation or an unknown or unsupported rounding 
     287    was specified. These aspects of the context need only be checked when 
     288    the values are required to be used. The result is [0,qNaN]. 
     289    """ 
     290 
     291    def handle(self, context, *args): 
     292        return NaN 
     293 
     294class Rounded(DecimalException): 
     295    """Number got rounded (not  necessarily changed during rounding). 
     296 
     297    This occurs and signals rounded whenever the result of an operation is 
     298    rounded (that is, some zero or non-zero digits were discarded from the 
     299    coefficient), or if an overflow or underflow condition occurs. The 
     300    result in all cases is unchanged. 
     301 
     302    The rounded signal may be tested (or trapped) to determine if a given 
     303    operation (or sequence of operations) caused a loss of precision. 
     304    """ 
     305    pass 
     306 
     307class Subnormal(DecimalException): 
     308    """Exponent < Emin before rounding. 
     309 
     310    This occurs and signals subnormal whenever the result of a conversion or 
     311    operation is subnormal (that is, its adjusted exponent is less than 
     312    Emin, before any rounding). The result in all cases is unchanged. 
     313 
     314    The subnormal signal may be tested (or trapped) to determine if a given 
     315    or operation (or sequence of operations) yielded a subnormal result. 
     316    """ 
     317    pass 
     318 
     319class Overflow(Inexact, Rounded): 
     320    """Numerical overflow. 
     321 
     322    This occurs and signals overflow if the adjusted exponent of a result 
     323    (from a conversion or from an operation that is not an attempt to divide 
     324    by zero), after rounding, would be greater than the largest value that 
     325    can be handled by the implementation (the value Emax). 
     326 
     327    The result depends on the rounding mode: 
     328 
     329    For round-half-up and round-half-even (and for round-half-down and 
     330    round-up, if implemented), the result of the operation is [sign,inf], 
     331    where sign is the sign of the intermediate result. For round-down, the 
     332    result is the largest finite number that can be represented in the 
     333    current precision, with the sign of the intermediate result. For 
     334    round-ceiling, the result is the same as for round-down if the sign of 
     335    the intermediate result is 1, or is [0,inf] otherwise. For round-floor, 
     336    the result is the same as for round-down if the sign of the intermediate 
     337    result is 0, or is [1,inf] otherwise. In all cases, Inexact and Rounded 
     338    will also be raised. 
     339   """ 
     340 
     341    def handle(self, context, sign, *args): 
     342        if context.rounding in (ROUND_HALF_UP, ROUND_HALF_EVEN, 
     343                                     ROUND_HALF_DOWN, ROUND_UP): 
     344            return Infsign[sign] 
     345        if sign == 0: 
     346            if context.rounding == ROUND_CEILING: 
     347                return Infsign[sign] 
     348            return Decimal((sign, (9,)*context.prec, 
     349                            context.Emax-context.prec+1)) 
     350        if sign == 1: 
     351            if context.rounding == ROUND_FLOOR: 
     352                return Infsign[sign] 
     353            return Decimal( (sign, (9,)*context.prec, 
     354                             context.Emax-context.prec+1)) 
     355 
     356 
     357class Underflow(Inexact, Rounded, Subnormal): 
     358    """Numerical underflow with result rounded to 0. 
     359 
     360    This occurs and signals underflow if a result is inexact and the 
     361    adjusted exponent of the result would be smaller (more negative) than 
     362    the smallest value that can be handled by the implementation (the value 
     363    Emin). That is, the result is both inexact and subnormal. 
     364 
     365    The result after an underflow will be a subnormal number rounded, if 
     366    necessary, so that its exponent is not less than Etiny. This may result 
     367    in 0 with the sign of the intermediate result and an exponent of Etiny. 
     368 
     369    In all cases, Inexact, Rounded, and Subnormal will also be raised. 
     370    """ 
     371 
     372# List of public traps and flags 
     373_signals = [Clamped, DivisionByZero, Inexact, Overflow, Rounded, 
     374           Underflow, InvalidOperation, Subnormal] 
     375 
     376# Map conditions (per the spec) to signals 
     377_condition_map = {ConversionSyntax:InvalidOperation, 
     378                  DivisionImpossible:InvalidOperation, 
     379                  DivisionUndefined:InvalidOperation, 
     380                  InvalidContext:InvalidOperation} 
     381 
     382##### Context Functions ####################################### 
     383 
     384# The getcontext() and setcontext() function manage access to a thread-local 
     385# current context.  Py2.4 offers direct support for thread locals.  If that 
     386# is not available, use threading.currentThread() which is slower but will 
     387# work for older Pythons.  If threads are not part of the build, create a 
     388# mock threading object with threading.local() returning the module namespace. 
     389 
     390try: 
     391    import threading 
     392except ImportError: 
     393    # Python was compiled without threads; create a mock object instead 
     394    import sys 
     395    class MockThreading: 
     396        def local(self, sys=sys): 
     397            return sys.modules[__name__] 
     398    threading = MockThreading() 
     399    del sys, MockThreading 
     400 
     401try: 
     402    threading.local 
     403 
     404except AttributeError: 
     405 
     406    #To fix reloading, force it to create a new context 
     407    #Old contexts have different exceptions in their dicts, making problems. 
     408    if hasattr(threading.currentThread(), '__decimal_context__'): 
     409        del threading.currentThread().__decimal_context__ 
     410 
     411    def setcontext(context): 
     412        """Set this thread's context to context.""" 
     413        if context in (DefaultContext, BasicContext, ExtendedContext): 
     414            context = context.copy() 
     415            context.clear_flags() 
     416        threading.currentThread().__decimal_context__ = context 
     417 
     418    def getcontext(): 
     419        """Returns this thread's context. 
     420 
     421        If this thread does not yet have a context, returns 
     422        a new context and sets this thread's context. 
     423        New contexts are copies of DefaultContext. 
     424        """ 
     425        try: 
     426            return threading.currentThread().__decimal_context__ 
     427        except AttributeError: 
     428            context = Context() 
     429            threading.currentThread().__decimal_context__ = context 
     430            return context 
     431 
     432else: 
     433 
     434    local = threading.local() 
     435    if hasattr(local, '__decimal_context__'): 
     436        del local.__decimal_context__ 
     437 
     438    def getcontext(_local=local): 
     439        """Returns this thread's context. 
     440 
     441        If this thread does not yet have a context, returns 
     442        a new context and sets this thread's context. 
     443