summaryrefslogtreecommitdiff
path: root/pint/definitions.py
blob: 2459b4cd180c58dbfaf3e39d19584cabbcadc49b (plain)
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
"""
    pint.definitions
    ~~~~~~~~~~~~~~~~

    Functions and classes related to unit definitions.

    :copyright: 2016 by Pint Authors, see AUTHORS for more details.
    :license: BSD, see LICENSE for more details.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Optional, Tuple, Union

from .converters import Converter


@dataclass(frozen=True)
class PreprocessedDefinition:
    """Splits a definition into the constitutive parts.

    A definition is given as a string with equalities in a single line::

        ---------------> rhs
        a = b = c = d = e
        |   |   |   -------> aliases (optional)
        |   |   |
        |   |   -----------> symbol (use "_" for no symbol)
        |   |
        |   ---------------> value
        |
        -------------------> name
    """

    name: str
    symbol: Optional[str]
    aliases: Tuple[str, ...]
    value: str
    rhs_parts: Tuple[str, ...]

    @classmethod
    def from_string(cls, definition: str) -> PreprocessedDefinition:
        name, definition = definition.split("=", 1)
        name = name.strip()

        rhs_parts = tuple(res.strip() for res in definition.split("="))

        value, aliases = rhs_parts[0], tuple([x for x in rhs_parts[1:] if x != ""])
        symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases)
        if symbol == "_":
            symbol = None
        aliases = tuple([x for x in aliases if x != "_"])

        return cls(name, symbol, aliases, value, rhs_parts)


@dataclass(frozen=True)
class Definition:
    """Base class for definitions.

    Parameters
    ----------
    name : str
        Canonical name of the unit/prefix/etc.
    defined_symbol : str or None
        A short name or symbol for the definition.
    aliases : iterable of str
        Other names for the unit/prefix/etc.
    converter : callable or Converter or None
    """

    name: str
    defined_symbol: Optional[str]
    aliases: Tuple[str, ...]
    converter: Optional[Union[Callable, Converter]]

    _subclasses = []
    _default_subclass = None

    def __init_subclass__(cls, **kwargs):
        if kwargs.pop("default", False):
            if cls._default_subclass is not None:
                raise ValueError("There is already a registered default definition.")
            Definition._default_subclass = cls
        super().__init_subclass__(**kwargs)
        cls._subclasses.append(cls)

    def __post_init__(self):
        if isinstance(self.converter, str):
            raise TypeError(
                "The converter parameter cannot be an instance of `str`. Use `from_string` method"
            )

    @property
    def is_multiplicative(self) -> bool:
        return self.converter.is_multiplicative

    @property
    def is_logarithmic(self) -> bool:
        return self.converter.is_logarithmic

    @classmethod
    def accept_to_parse(cls, preprocessed: PreprocessedDefinition):
        return False

    @classmethod
    def from_string(
        cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
    ) -> Definition:
        """Parse a definition.

        Parameters
        ----------
        definition : str or PreprocessedDefinition
        non_int_type : type

        Returns
        -------
        Definition or subclass of Definition
        """

        if isinstance(definition, str):
            definition = PreprocessedDefinition.from_string(definition)

        for subclass in cls._subclasses:
            if subclass.accept_to_parse(definition):
                return subclass.from_string(definition, non_int_type)

        if cls._default_subclass is None:
            raise ValueError("No matching definition (and no default parser).")

        return cls._default_subclass.from_string(definition, non_int_type)

    @property
    def symbol(self) -> str:
        return self.defined_symbol or self.name

    @property
    def has_symbol(self) -> bool:
        return bool(self.defined_symbol)

    def add_aliases(self, *alias: str) -> None:
        raise Exception("Cannot add aliases, definitions are inmutable.")

    def __str__(self) -> str:
        return self.name