""" 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 collections.abc import Callable, Iterable from dataclasses import dataclass from typing import TYPE_CHECKING 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. -> : """ @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. <-> : """ @property def bidirectional(self) -> bool: return True @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" )