Skip to content

Commit

Permalink
Add support for other numerical types at the registry level
Browse files Browse the repository at this point in the history
Until this commit, `int` and `float` were special types in Pint
(just as they are in Python). Numbers were parsed from strings
as int/float from the definition files and from user provided
strings; and exponents of units were also stored as int/float.

This commit change this by adding a new argument (`non_int_type`)
to classes and methods. It indicates how numeric values will be parsed
and defaulted. Any numerical class can be used such as `float` (default),
Decimal, Fraction.

This argument will be found in the following places
1. UnitRegistry: used for parsing the definition files and any
  value provided as a string.
2. UnitsContainer: used to compare equality with strings, multiply and
  divide by strings (which is equivalent to parse the string)
3. All methods OUTSIDE the UnitRegistry/Quantity that can parse
   strings have a `non_int_type` argument. (e.g. Definition.from_string)

Tests have been added for by duplicating most cases in `test_quantity.py`.
(Some tests have been deleted such as those dealing with numpy.)
The new file `test_non_int.py` run the tests for `Decimal`, `Fraction` and
`float` (which is redundant but is kept as a crosscheck for the implementation
of this testsuite)

BREAKING CHANGE: `use_decimal` is deprecated.
Use `non_int_type` keyword argument when instantiating the registry.
 >>> from decimal import Decimal
 >>> ureg = UnitRegistry(non_int_type=Decimal)
  • Loading branch information
hgrecco committed Jan 22, 2020
1 parent 98a8463 commit d43c56a
Show file tree
Hide file tree
Showing 9 changed files with 1,284 additions and 99 deletions.
4 changes: 4 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Pint Changelog
0.11 (unreleased)
-----------------

- Add full support for Decimal and Fraction at the registry level.
**BREAKING CHANGE**:
`use_decimal` is deprecated. Use `non_int_type=Decimal` when instantiating
the registry.
- Moved Pi to defintions files.
- Use ints (not floats) a defaults at many points in the codebase as in Python 3
the true division is the default one.
Expand Down
12 changes: 9 additions & 3 deletions pint/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def from_context(cls, context, **defaults):
return context

@classmethod
def from_lines(cls, lines, to_base_func=None):
def from_lines(cls, lines, to_base_func=None, non_int_type=float):
lines = SourceIterator(lines)

lineno, header = next(lines)
Expand Down Expand Up @@ -186,14 +186,20 @@ def to_num(val):
func = _expression_to_function(eq)

if "<->" in rel:
src, dst = (ParserHelper.from_string(s) for s in rel.split("<->"))
src, dst = (
ParserHelper.from_string(s, non_int_type)
for s in rel.split("<->")
)
if to_base_func:
src = to_base_func(src)
dst = to_base_func(dst)
ctx.add_transformation(src, dst, func)
ctx.add_transformation(dst, src, func)
elif "->" in rel:
src, dst = (ParserHelper.from_string(s) for s in rel.split("->"))
src, dst = (
ParserHelper.from_string(s, non_int_type)
for s in rel.split("->")
)
if to_base_func:
src = to_base_func(src)
dst = to_base_func(dst)
Expand Down
34 changes: 18 additions & 16 deletions pint/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,13 @@ def __init__(self, value):
self.value = value


def numeric_parse(s):
def numeric_parse(s, non_int_type=float):
"""Try parse a string into a number (without using eval).
Parameters
----------
s : str
non_int_type : type
Returns
-------
Expand All @@ -81,7 +82,7 @@ def numeric_parse(s):
_NotNumeric
If the string cannot be parsed as a number.
"""
ph = ParserHelper.from_string(s)
ph = ParserHelper.from_string(s, non_int_type)

if len(ph):
raise _NotNumeric(s)
Expand Down Expand Up @@ -120,12 +121,13 @@ def is_multiplicative(self):
return self._converter.is_multiplicative

@classmethod
def from_string(cls, definition):
def from_string(cls, definition, non_int_type=float):
"""Parse a definition.
Parameters
----------
definition : str or ParsedDefinition
non_int_type : type
Returns
-------
Expand All @@ -136,13 +138,13 @@ def from_string(cls, definition):
definition = ParsedDefinition.from_string(definition)

if definition.name.startswith("@alias "):
return AliasDefinition.from_string(definition)
return AliasDefinition.from_string(definition, non_int_type)
elif definition.name.startswith("["):
return DimensionDefinition.from_string(definition)
return DimensionDefinition.from_string(definition, non_int_type)
elif definition.name.endswith("-"):
return PrefixDefinition.from_string(definition)
return PrefixDefinition.from_string(definition, non_int_type)
else:
return UnitDefinition.from_string(definition)
return UnitDefinition.from_string(definition, non_int_type)

@property
def name(self):
Expand Down Expand Up @@ -182,7 +184,7 @@ class PrefixDefinition(Definition):
"""

@classmethod
def from_string(cls, definition):
def from_string(cls, definition, non_int_type=float):
if isinstance(definition, str):
definition = ParsedDefinition.from_string(definition)

Expand All @@ -193,7 +195,7 @@ def from_string(cls, definition):
symbol = definition.symbol

try:
converter = ScaleConverter(numeric_parse(definition.value))
converter = ScaleConverter(numeric_parse(definition.value, non_int_type))
except _NotNumeric as ex:
raise ValueError(
f"Prefix definition ('{definition.name}') must contain only numbers, not {ex.value}"
Expand Down Expand Up @@ -226,7 +228,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal
super().__init__(name, symbol, aliases, converter)

@classmethod
def from_string(cls, definition):
def from_string(cls, definition, non_int_type=float):
if isinstance(definition, str):
definition = ParsedDefinition.from_string(definition)

Expand All @@ -235,7 +237,7 @@ def from_string(cls, definition):

try:
modifiers = dict(
(key.strip(), numeric_parse(value))
(key.strip(), numeric_parse(value, non_int_type))
for key, value in (part.split(":") for part in modifiers.split(";"))
)
except _NotNumeric as ex:
Expand All @@ -247,7 +249,7 @@ def from_string(cls, definition):
converter = definition.value
modifiers = {}

converter = ParserHelper.from_string(converter)
converter = ParserHelper.from_string(converter, non_int_type)

if not any(_is_dim(key) for key in converter.keys()):
is_base = False
Expand Down Expand Up @@ -292,11 +294,11 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal
super().__init__(name, symbol, aliases, converter=None)

@classmethod
def from_string(cls, definition):
def from_string(cls, definition, non_int_type=float):
if isinstance(definition, str):
definition = ParsedDefinition.from_string(definition)

converter = ParserHelper.from_string(definition.value)
converter = ParserHelper.from_string(definition.value, non_int_type)

if not converter:
is_base = True
Expand All @@ -308,7 +310,7 @@ def from_string(cls, definition):
"Derived dimensions must only be referenced "
"to dimensions."
)
reference = UnitsContainer(converter)
reference = UnitsContainer(converter, non_int_type=non_int_type)

return cls(
definition.name,
Expand All @@ -333,7 +335,7 @@ def __init__(self, name, aliases):
super().__init__(name=name, symbol=None, aliases=aliases, converter=None)

@classmethod
def from_string(cls, definition):
def from_string(cls, definition, non_int_type=float):

if isinstance(definition, str):
definition = ParsedDefinition.from_string(definition)
Expand Down
49 changes: 29 additions & 20 deletions pint/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ def force_ndarray(self):
def force_ndarray_like(self):
return self._REGISTRY.force_ndarray_like

@property
def UnitsContainer(self):
return self._REGISTRY.UnitsContainer

def __reduce__(self):
"""Allow pickling quantities. Since UnitRegistries are not pickled, upon
unpickling the new object is always attached to the application registry.
Expand Down Expand Up @@ -181,7 +185,7 @@ def __new__(cls, value, units=None):
inst._magnitude = _to_magnitude(
value, inst.force_ndarray, inst.force_ndarray_like
)
inst._units = UnitsContainer()
inst._units = inst.UnitsContainer()
elif isinstance(units, (UnitsContainer, UnitDefinition)):
inst = SharedRegistryObject.__new__(cls)
inst._magnitude = _to_magnitude(
Expand Down Expand Up @@ -492,7 +496,7 @@ def from_sequence(cls, seq, units=None):

@classmethod
def from_tuple(cls, tup):
return cls(tup[0], UnitsContainer(tup[1]))
return cls(tup[0], cls._REGISTRY.UnitsContainer(tup[1]))

def to_tuple(self):
return self.m, tuple(self._units.items())
Expand Down Expand Up @@ -760,7 +764,7 @@ def _iadd_sub(self, other, op):
# the operation.
self._magnitude = op(self._magnitude, other_magnitude)
elif self.dimensionless:
self.ito(UnitsContainer())
self.ito(self.UnitsContainer())
self._magnitude = op(self._magnitude, other_magnitude)
else:
raise DimensionalityError(self._units, "dimensionless")
Expand Down Expand Up @@ -870,7 +874,7 @@ def _add_sub(self, other, op):
_to_magnitude(other, self.force_ndarray, self.force_ndarray_like),
)
elif self.dimensionless:
units = UnitsContainer()
units = self.UnitsContainer()
magnitude = op(
self.to(units)._magnitude,
_to_magnitude(other, self.force_ndarray, self.force_ndarray_like),
Expand Down Expand Up @@ -1035,7 +1039,7 @@ def _imul_div(self, other, magnitude_op, units_op=None):
except TypeError:
return NotImplemented
self._magnitude = magnitude_op(self._magnitude, other_magnitude)
self._units = units_op(self._units, UnitsContainer())
self._units = units_op(self._units, self.UnitsContainer())
return self

if isinstance(other, self._REGISTRY.Unit):
Expand Down Expand Up @@ -1106,7 +1110,7 @@ def _mul_div(self, other, magnitude_op, units_op=None):
return NotImplemented

magnitude = magnitude_op(self._magnitude, other_magnitude)
units = units_op(self._units, UnitsContainer())
units = units_op(self._units, self.UnitsContainer())

return self.__class__(magnitude, units)

Expand Down Expand Up @@ -1190,7 +1194,7 @@ def __ifloordiv__(self, other):
self._magnitude = self.to("")._magnitude // other
else:
raise DimensionalityError(self._units, "dimensionless")
self._units = UnitsContainer({})
self._units = self.UnitsContainer({})
return self

@check_implemented
Expand All @@ -1201,7 +1205,7 @@ def __floordiv__(self, other):
magnitude = self.to("")._magnitude // other
else:
raise DimensionalityError(self._units, "dimensionless")
return self.__class__(magnitude, UnitsContainer({}))
return self.__class__(magnitude, self.UnitsContainer({}))

@check_implemented
def __rfloordiv__(self, other):
Expand All @@ -1211,19 +1215,19 @@ def __rfloordiv__(self, other):
magnitude = other // self.to("")._magnitude
else:
raise DimensionalityError(self._units, "dimensionless")
return self.__class__(magnitude, UnitsContainer({}))
return self.__class__(magnitude, self.UnitsContainer({}))

@check_implemented
def __imod__(self, other):
if not self._check(other):
other = self.__class__(other, UnitsContainer({}))
other = self.__class__(other, self.UnitsContainer({}))
self._magnitude %= other.to(self._units)._magnitude
return self

@check_implemented
def __mod__(self, other):
if not self._check(other):
other = self.__class__(other, UnitsContainer({}))
other = self.__class__(other, self.UnitsContainer({}))
magnitude = self._magnitude % other.to(self._units)._magnitude
return self.__class__(magnitude, self._units)

Expand All @@ -1234,16 +1238,19 @@ def __rmod__(self, other):
return self.__class__(magnitude, other._units)
elif self.dimensionless:
magnitude = other % self.to("")._magnitude
return self.__class__(magnitude, UnitsContainer({}))
return self.__class__(magnitude, self.UnitsContainer({}))
else:
raise DimensionalityError(self._units, "dimensionless")

@check_implemented
def __divmod__(self, other):
if not self._check(other):
other = self.__class__(other, UnitsContainer({}))
other = self.__class__(other, self.UnitsContainer({}))
q, r = divmod(self._magnitude, other.to(self._units)._magnitude)
return (self.__class__(q, UnitsContainer({})), self.__class__(r, self._units))
return (
self.__class__(q, self.UnitsContainer({})),
self.__class__(r, self._units),
)

@check_implemented
def __rdivmod__(self, other):
Expand All @@ -1252,10 +1259,10 @@ def __rdivmod__(self, other):
unit = other._units
elif self.dimensionless:
q, r = divmod(other, self.to("")._magnitude)
unit = UnitsContainer({})
unit = self.UnitsContainer({})
else:
raise DimensionalityError(self._units, "dimensionless")
return (self.__class__(q, UnitsContainer({})), self.__class__(r, unit))
return (self.__class__(q, self.UnitsContainer({})), self.__class__(r, unit))

@check_implemented
def __ipow__(self, other):
Expand Down Expand Up @@ -1296,7 +1303,7 @@ def __ipow__(self, other):
if other == 1:
return self
elif other == 0:
self._units = UnitsContainer()
self._units = self.UnitsContainer()
else:
if not self._is_multiplicative:
if self._REGISTRY.autoconvert_offset_to_baseunit:
Expand Down Expand Up @@ -1353,7 +1360,7 @@ def __pow__(self, other):
return self
elif other == 0:
exponent = 0
units = UnitsContainer()
units = self.UnitsContainer()
else:
if not self._is_multiplicative:
if self._REGISTRY.autoconvert_offset_to_baseunit:
Expand Down Expand Up @@ -1424,7 +1431,7 @@ def __eq__(self, other):
raise OffsetUnitCalculusError(self._units)

return self.dimensionless and eq(
self._convert_magnitude(UnitsContainer()), other, False
self._convert_magnitude(self.UnitsContainer()), other, False
)

if eq(self._magnitude, 0, True) and eq(other._magnitude, 0, True):
Expand Down Expand Up @@ -1453,7 +1460,9 @@ def __ne__(self, other):
def compare(self, other, op):
if not isinstance(other, self.__class__):
if self.dimensionless:
return op(self._convert_magnitude_not_inplace(UnitsContainer()), other)
return op(
self._convert_magnitude_not_inplace(self.UnitsContainer()), other
)
elif eq(other, 0, True):
# Handle the special case in which we compare to zero
# (or an array of zeros)
Expand Down

0 comments on commit d43c56a

Please sign in to comment.