numeric.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import decimal
  2. from wtforms import widgets
  3. from wtforms.fields.core import Field
  4. from wtforms.utils import unset_value
  5. __all__ = (
  6. "IntegerField",
  7. "DecimalField",
  8. "FloatField",
  9. "IntegerRangeField",
  10. "DecimalRangeField",
  11. )
  12. class LocaleAwareNumberField(Field):
  13. """
  14. Base class for implementing locale-aware number parsing.
  15. Locale-aware numbers require the 'babel' package to be present.
  16. """
  17. def __init__(
  18. self,
  19. label=None,
  20. validators=None,
  21. use_locale=False,
  22. number_format=None,
  23. **kwargs,
  24. ):
  25. super().__init__(label, validators, **kwargs)
  26. self.use_locale = use_locale
  27. if use_locale:
  28. self.number_format = number_format
  29. self.locale = kwargs["_form"].meta.locales[0]
  30. self._init_babel()
  31. def _init_babel(self):
  32. try:
  33. from babel import numbers
  34. self.babel_numbers = numbers
  35. except ImportError as exc:
  36. raise ImportError(
  37. "Using locale-aware decimals requires the babel library."
  38. ) from exc
  39. def _parse_decimal(self, value):
  40. return self.babel_numbers.parse_decimal(value, self.locale)
  41. def _format_decimal(self, value):
  42. return self.babel_numbers.format_decimal(value, self.number_format, self.locale)
  43. class IntegerField(Field):
  44. """
  45. A text field, except all input is coerced to an integer. Erroneous input
  46. is ignored and will not be accepted as a value.
  47. """
  48. widget = widgets.NumberInput()
  49. def __init__(self, label=None, validators=None, **kwargs):
  50. super().__init__(label, validators, **kwargs)
  51. def _value(self):
  52. if self.raw_data:
  53. return self.raw_data[0]
  54. if self.data is not None:
  55. return str(self.data)
  56. return ""
  57. def process_data(self, value):
  58. if value is None or value is unset_value:
  59. self.data = None
  60. return
  61. try:
  62. self.data = int(value)
  63. except (ValueError, TypeError) as exc:
  64. self.data = None
  65. raise ValueError(self.gettext("Not a valid integer value.")) from exc
  66. def process_formdata(self, valuelist):
  67. if not valuelist:
  68. return
  69. try:
  70. self.data = int(valuelist[0])
  71. except ValueError as exc:
  72. self.data = None
  73. raise ValueError(self.gettext("Not a valid integer value.")) from exc
  74. class DecimalField(LocaleAwareNumberField):
  75. """
  76. A text field which displays and coerces data of the `decimal.Decimal` type.
  77. :param places:
  78. How many decimal places to quantize the value to for display on form.
  79. If unset, use 2 decimal places.
  80. If explicitely set to `None`, does not quantize value.
  81. :param rounding:
  82. How to round the value during quantize, for example
  83. `decimal.ROUND_UP`. If unset, uses the rounding value from the
  84. current thread's context.
  85. :param use_locale:
  86. If True, use locale-based number formatting. Locale-based number
  87. formatting requires the 'babel' package.
  88. :param number_format:
  89. Optional number format for locale. If omitted, use the default decimal
  90. format for the locale.
  91. """
  92. widget = widgets.NumberInput(step="any")
  93. def __init__(
  94. self, label=None, validators=None, places=unset_value, rounding=None, **kwargs
  95. ):
  96. super().__init__(label, validators, **kwargs)
  97. if self.use_locale and (places is not unset_value or rounding is not None):
  98. raise TypeError(
  99. "When using locale-aware numbers, 'places' and 'rounding' are ignored."
  100. )
  101. if places is unset_value:
  102. places = 2
  103. self.places = places
  104. self.rounding = rounding
  105. def _value(self):
  106. if self.raw_data:
  107. return self.raw_data[0]
  108. if self.data is None:
  109. return ""
  110. if self.use_locale:
  111. return str(self._format_decimal(self.data))
  112. if self.places is None:
  113. return str(self.data)
  114. if not hasattr(self.data, "quantize"):
  115. # If for some reason, data is a float or int, then format
  116. # as we would for floats using string formatting.
  117. format = "%%0.%df" % self.places
  118. return format % self.data
  119. exp = decimal.Decimal(".1") ** self.places
  120. if self.rounding is None:
  121. quantized = self.data.quantize(exp)
  122. else:
  123. quantized = self.data.quantize(exp, rounding=self.rounding)
  124. return str(quantized)
  125. def process_formdata(self, valuelist):
  126. if not valuelist:
  127. return
  128. try:
  129. if self.use_locale:
  130. self.data = self._parse_decimal(valuelist[0])
  131. else:
  132. self.data = decimal.Decimal(valuelist[0])
  133. except (decimal.InvalidOperation, ValueError) as exc:
  134. self.data = None
  135. raise ValueError(self.gettext("Not a valid decimal value.")) from exc
  136. class FloatField(Field):
  137. """
  138. A text field, except all input is coerced to an float. Erroneous input
  139. is ignored and will not be accepted as a value.
  140. """
  141. widget = widgets.TextInput()
  142. def __init__(self, label=None, validators=None, **kwargs):
  143. super().__init__(label, validators, **kwargs)
  144. def _value(self):
  145. if self.raw_data:
  146. return self.raw_data[0]
  147. if self.data is not None:
  148. return str(self.data)
  149. return ""
  150. def process_formdata(self, valuelist):
  151. if not valuelist:
  152. return
  153. try:
  154. self.data = float(valuelist[0])
  155. except ValueError as exc:
  156. self.data = None
  157. raise ValueError(self.gettext("Not a valid float value.")) from exc
  158. class IntegerRangeField(IntegerField):
  159. """
  160. Represents an ``<input type="range">``.
  161. """
  162. widget = widgets.RangeInput()
  163. class DecimalRangeField(DecimalField):
  164. """
  165. Represents an ``<input type="range">``.
  166. """
  167. widget = widgets.RangeInput(step="any")