"""
pint.definitions
~~~~~~~~~~~~~~~~
Functions and classes related to unit definitions.
:copyright: 2016 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
from __future__ import annotations
from collections import namedtuple
from typing import Callable, Iterable, Optional, Union
from .converters import Converter, LogarithmicConverter, OffsetConverter, ScaleConverter
from .errors import DefinitionSyntaxError
from .util import ParserHelper, UnitsContainer, _is_dim
[docs]class PreprocessedDefinition(
namedtuple("PreprocessedDefinition", "name symbol aliases value rhs_parts")
):
"""Splits a definition into the constitutive parts.
A definition is given as a string with equalities in a single line::
---------------> rhs
a = b = c = d = e
| | | -------> aliases (optional)
| | |
| | -----------> symbol (use "_" for no symbol)
| |
| ---------------> value
|
-------------------> name
Attributes
----------
name : str
value : str
symbol : str or None
aliases : tuple of str
rhs : tuple of str
"""
@classmethod
def from_string(cls, definition: str) -> PreprocessedDefinition:
name, definition = definition.split("=", 1)
name = name.strip()
rhs_parts = tuple(res.strip() for res in definition.split("="))
value, aliases = rhs_parts[0], tuple([x for x in rhs_parts[1:] if x != ""])
symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases)
if symbol == "_":
symbol = None
aliases = tuple([x for x in aliases if x != "_"])
return cls(name, symbol, aliases, value, rhs_parts)
class _NotNumeric(Exception):
"""Internal exception. Do not expose outside Pint"""
def __init__(self, value):
self.value = value
[docs]def numeric_parse(s: str, non_int_type: type = float):
"""Try parse a string into a number (without using eval).
Parameters
----------
s : str
non_int_type : type
Returns
-------
Number
Raises
------
_NotNumeric
If the string cannot be parsed as a number.
"""
ph = ParserHelper.from_string(s, non_int_type)
if len(ph):
raise _NotNumeric(s)
return ph.scale
[docs]class Definition:
"""Base class for definitions.
Parameters
----------
name : str
Canonical name of the unit/prefix/etc.
symbol : str or None
A short name or symbol for the definition.
aliases : iterable of str
Other names for the unit/prefix/etc.
converter : callable or Converter or None
"""
def __init__(
self,
name: str,
symbol: Optional[str],
aliases: Iterable[str],
converter: Optional[Union[Callable, Converter]],
):
if isinstance(converter, str):
raise TypeError(
"The converter parameter cannot be an instance of `str`. Use `from_string` method"
)
self._name = name
self._symbol = symbol
self._aliases = tuple(aliases)
self._converter = converter
@property
def is_multiplicative(self) -> bool:
return self._converter.is_multiplicative
@property
def is_logarithmic(self) -> bool:
return self._converter.is_logarithmic
[docs] @classmethod
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> "Definition":
"""Parse a definition.
Parameters
----------
definition : str or PreprocessedDefinition
non_int_type : type
Returns
-------
Definition or subclass of Definition
"""
if isinstance(definition, str):
definition = PreprocessedDefinition.from_string(definition)
if definition.name.startswith("@alias "):
return AliasDefinition.from_string(definition, non_int_type)
elif definition.name.startswith("["):
return DimensionDefinition.from_string(definition, non_int_type)
elif definition.name.endswith("-"):
return PrefixDefinition.from_string(definition, non_int_type)
else:
return UnitDefinition.from_string(definition, non_int_type)
@property
def name(self) -> str:
return self._name
@property
def symbol(self) -> str:
return self._symbol or self._name
@property
def has_symbol(self) -> bool:
return bool(self._symbol)
@property
def aliases(self) -> Iterable[str]:
return self._aliases
def add_aliases(self, *alias: str) -> None:
alias = tuple(a for a in alias if a not in self._aliases)
self._aliases = self._aliases + alias
@property
def converter(self) -> Converter:
return self._converter
def __str__(self) -> str:
return self.name
[docs]class PrefixDefinition(Definition):
"""Definition of a prefix::
<prefix>- = <amount> [= <symbol>] [= <alias>] [ = <alias> ] [...]
Example::
deca- = 1e+1 = da- = deka-
"""
[docs] @classmethod
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> "PrefixDefinition":
if isinstance(definition, str):
definition = PreprocessedDefinition.from_string(definition)
aliases = tuple(alias.strip("-") for alias in definition.aliases)
if definition.symbol:
symbol = definition.symbol.strip("-")
else:
symbol = definition.symbol
try:
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}"
)
return cls(definition.name.rstrip("-"), symbol, aliases, converter)
[docs]class UnitDefinition(Definition):
"""Definition of a unit::
<canonical name> = <relation to another unit or dimension> [= <symbol>] [= <alias>] [ = <alias> ] [...]
Example::
millennium = 1e3 * year = _ = millennia
Parameters
----------
reference : UnitsContainer
Reference units.
is_base : bool
Indicates if it is a base unit.
"""
def __init__(
self,
name: str,
symbol: Optional[str],
aliases: Iterable[str],
converter: Converter,
reference: Optional[UnitsContainer] = None,
is_base: bool = False,
) -> None:
self.reference = reference
self.is_base = is_base
super().__init__(name, symbol, aliases, converter)
[docs] @classmethod
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> "UnitDefinition":
if isinstance(definition, str):
definition = PreprocessedDefinition.from_string(definition)
if ";" in definition.value:
[converter, modifiers] = definition.value.split(";", 1)
try:
modifiers = dict(
(key.strip(), numeric_parse(value, non_int_type))
for key, value in (part.split(":") for part in modifiers.split(";"))
)
except _NotNumeric as ex:
raise ValueError(
f"Unit definition ('{definition.name}') must contain only numbers in modifier, not {ex.value}"
)
else:
converter = definition.value
modifiers = {}
converter = ParserHelper.from_string(converter, non_int_type)
if not any(_is_dim(key) for key in converter.keys()):
is_base = False
elif all(_is_dim(key) for key in converter.keys()):
is_base = True
else:
raise DefinitionSyntaxError(
"Cannot mix dimensions and units in the same definition. "
"Base units must be referenced only to dimensions. "
"Derived units must be referenced only to units."
)
reference = UnitsContainer(converter)
if not modifiers:
converter = ScaleConverter(converter.scale)
elif "offset" in modifiers:
if modifiers.get("offset", 0.0) != 0.0:
converter = OffsetConverter(converter.scale, modifiers["offset"])
else:
converter = ScaleConverter(converter.scale)
elif "logbase" in modifiers and "logfactor" in modifiers:
converter = LogarithmicConverter(
converter.scale, modifiers["logbase"], modifiers["logfactor"]
)
else:
raise DefinitionSyntaxError("Unable to assign a converter to the unit")
return cls(
definition.name,
definition.symbol,
definition.aliases,
converter,
reference,
is_base,
)
[docs]class DimensionDefinition(Definition):
"""Definition of a dimension::
[dimension name] = <relation to other dimensions>
Example::
[density] = [mass] / [volume]
"""
def __init__(
self,
name: str,
symbol: Optional[str],
aliases: Iterable[str],
converter: Optional[Converter],
reference: Optional[UnitsContainer] = None,
is_base: bool = False,
) -> None:
self.reference = reference
self.is_base = is_base
super().__init__(name, symbol, aliases, converter=None)
[docs] @classmethod
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> "DimensionDefinition":
if isinstance(definition, str):
definition = PreprocessedDefinition.from_string(definition)
converter = ParserHelper.from_string(definition.value, non_int_type)
if not converter:
is_base = True
elif all(_is_dim(key) for key in converter.keys()):
is_base = False
else:
raise DefinitionSyntaxError(
"Base dimensions must be referenced to None. "
"Derived dimensions must only be referenced "
"to dimensions."
)
reference = UnitsContainer(converter, non_int_type=non_int_type)
return cls(
definition.name,
definition.symbol,
definition.aliases,
converter,
reference,
is_base,
)
[docs]class AliasDefinition(Definition):
"""Additional alias(es) for an already existing unit::
@alias <canonical name or previous alias> = <alias> [ = <alias> ] [...]
Example::
@alias meter = my_meter
"""
def __init__(self, name: str, aliases: Iterable[str]) -> None:
super().__init__(name=name, symbol=None, aliases=aliases, converter=None)
[docs] @classmethod
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> AliasDefinition:
if isinstance(definition, str):
definition = PreprocessedDefinition.from_string(definition)
name = definition.name[len("@alias ") :].lstrip()
return AliasDefinition(name, tuple(definition.rhs_parts))