Source code for 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, Any, Callable, Dict, Set, Tuple

from ... import errors
from ..plain import UnitDefinition

    from pint import Quantity, UnitsContainer

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.

    def variables(self) -> Set[str, ...]:
        """Find all variables names in the equation."""
        return set(self._varname_re.findall(self.equation))

    def transformation(self) -> Callable[..., Quantity[Any]]:
        """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

    def bidirectional(self):
        raise NotImplementedError

class ForwardRelation(Relation):
    """A relation connecting a dimension to another via a transformation function.

    <source dimension> -> <target dimension>: <transformation function>

    def bidirectional(self):
        return False

class BidirectionalRelation(Relation):
    """A bidirectional relation connecting a dimension to another
    via a simple transformation function.

        <source dimension> <-> <target dimension>: <transformation function>


    def bidirectional(self):
        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, non_int_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( 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 != 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" )