File: definitions.py

package info (click to toggle)
python-pint 0.24.4-2
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 1,924 kB
  • sloc: python: 19,983; makefile: 149
file content (157 lines) | stat: -rw-r--r-- 4,800 bytes parent folder | download
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"
            )