1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
|
"""
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.
<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
@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"
)
|