"""
pint.facets.context.definitions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
from __future__ import annotations
import itertools
import numbers
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable
from collections.abc import Iterable
from ... import errors
from ..plain import UnitDefinition
if TYPE_CHECKING:
from ..._typing import Quantity, UnitsContainer
@dataclass(frozen=True)
class Relation:
"""Base class for a relation between different dimensionalities."""
_varname_re = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
#: Source dimensionality
src: UnitsContainer
#: Destination dimensionality
dst: UnitsContainer
#: Equation connecting both dimensionalities from which the tranformation
#: will be built.
equation: str
# Instead of defining __post_init__ here,
# it will be added to the container class
# so that the name and a meaningfull class
# could be used.
@property
def variables(self) -> set[str]:
"""Find all variables names in the equation."""
return set(self._varname_re.findall(self.equation))
@property
def transformation(self) -> Callable[..., Quantity]:
"""Return a transformation callable that uses the registry
to parse the transformation equation.
"""
return lambda ureg, value, **kwargs: ureg.parse_expression(
self.equation, value=value, **kwargs
)
@property
def bidirectional(self) -> bool:
raise NotImplementedError
@dataclass(frozen=True)
class ForwardRelation(Relation):
"""A relation connecting a dimension to another via a transformation function.
<source dimension> -> <target dimension>: <transformation function>
"""
@property
def bidirectional(self) -> bool:
return False
@dataclass(frozen=True)
class BidirectionalRelation(Relation):
"""A bidirectional relation connecting a dimension to another
via a simple transformation function.
<source dimension> <-> <target dimension>: <transformation function>
"""
@property
def bidirectional(self) -> bool:
return True
[docs]@dataclass(frozen=True)
class ContextDefinition(errors.WithDefErr):
"""Definition of a Context"""
#: name of the context
name: str
#: other na
aliases: tuple[str, ...]
defaults: dict[str, numbers.Number]
relations: tuple[Relation, ...]
redefinitions: tuple[UnitDefinition, ...]
@property
def variables(self) -> set[str]:
"""Return all variable names in all transformations."""
return set().union(*(r.variables for r in self.relations))
@classmethod
def from_lines(cls, lines: Iterable[str], non_int_type: type):
# TODO: this is to keep it backwards compatible
from ...delegates import ParserConfig, txt_defparser
cfg = ParserConfig(non_int_type)
parser = txt_defparser.DefParser(cfg, None)
pp = parser.parse_string("\n".join(lines) + "\n@end")
for definition in parser.iter_parsed_project(pp):
if isinstance(definition, cls):
return definition
def __post_init__(self):
if not errors.is_valid_context_name(self.name):
raise self.def_err(errors.MSG_INVALID_GROUP_NAME)
for k in self.aliases:
if not errors.is_valid_context_name(k):
raise self.def_err(
f"refers to '{k}' that " + errors.MSG_INVALID_CONTEXT_NAME
)
for relation in self.relations:
invalid = tuple(
itertools.filterfalse(
errors.is_valid_dimension_name, relation.src.keys()
)
) + tuple(
itertools.filterfalse(
errors.is_valid_dimension_name, relation.dst.keys()
)
)
if invalid:
raise self.def_err(
f"relation refers to {', '.join(invalid)} that "
+ errors.MSG_INVALID_DIMENSION_NAME
)
for definition in self.redefinitions:
if definition.symbol != definition.name or definition.aliases:
raise self.def_err(
"can't change a unit's symbol or aliases within a context"
)
if definition.is_base:
raise self.def_err("can't define plain units within a context")
missing_pars = set(self.defaults.keys()) - self.variables
if missing_pars:
raise self.def_err(
f"Context parameters {missing_pars} not found in any equation"
)