Source code for pint.facets.context.definitions

"""
    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" )