summaryrefslogtreecommitdiff
path: root/pint/facets
diff options
context:
space:
mode:
authorHernan Grecco <hernan.grecco@gmail.com>2022-10-21 22:51:30 -0300
committerGitHub <noreply@github.com>2022-10-21 22:51:30 -0300
commitc07fc9b218ea39965d8d4492e52168fe8f8a644b (patch)
tree51574304897d2566dc98138d878a7e582767ca5d /pint/facets
parent64f19ed6a78b1e6c59e07a953febe212818ec4c1 (diff)
parenta943a4f15daf7d6bdd7948dff08b0802f77d7907 (diff)
downloadpint-c07fc9b218ea39965d8d4492e52168fe8f8a644b.tar.gz
Merge pull request #1595 from hgrecco/using_flexparser
Final step to split the registry from the parser
Diffstat (limited to 'pint/facets')
-rw-r--r--pint/facets/__init__.py7
-rw-r--r--pint/facets/context/definitions.py267
-rw-r--r--pint/facets/context/objects.py37
-rw-r--r--pint/facets/context/registry.py20
-rw-r--r--pint/facets/group/definitions.py104
-rw-r--r--pint/facets/group/objects.py26
-rw-r--r--pint/facets/group/registry.py44
-rw-r--r--pint/facets/nonmultiplicative/registry.py65
-rw-r--r--pint/facets/plain/definitions.py378
-rw-r--r--pint/facets/plain/registry.py318
-rw-r--r--pint/facets/system/definitions.py122
-rw-r--r--pint/facets/system/objects.py13
-rw-r--r--pint/facets/system/registry.py28
13 files changed, 645 insertions, 784 deletions
diff --git a/pint/facets/__init__.py b/pint/facets/__init__.py
index a3f2439..d669b9f 100644
--- a/pint/facets/__init__.py
+++ b/pint/facets/__init__.py
@@ -7,8 +7,8 @@
keeping each part small enough to be hackable.
Each facet contains one or more of the following modules:
- - definitions: classes and functions to parse a string into an specific object.
- These objects must be immutable and pickable. (e.g. ContextDefinition)
+ - definitions: classes describing an specific unit related definiton.
+ These objects must be immutable, pickable and not reference the registry (e.g. ContextDefinition)
- objects: classes and functions that encapsulate behavior (e.g. Context)
- registry: implements a subclass of PlainRegistry or class that can be
mixed with it (e.g. ContextRegistry)
@@ -16,8 +16,7 @@
In certain cases, some of these modules might be collapsed into a single one
as the code is very short (like in dask) or expanded as the code is too long
(like in plain, where quantity and unit object are in their own module).
- Additionally, certain facets might not have one of them (e.g. dask adds no
- feature in relation to parsing).
+ Additionally, certain facets might not have one of them.
An important part of this scheme is that each facet should export only a few
classes in the __init__.py and everything else should not be accessed by any
diff --git a/pint/facets/context/definitions.py b/pint/facets/context/definitions.py
index 96f1e90..a24977b 100644
--- a/pint/facets/context/definitions.py
+++ b/pint/facets/context/definitions.py
@@ -8,182 +8,149 @@
from __future__ import annotations
+import itertools
import numbers
import re
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple
+from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Tuple
-from ...definitions import Definition
-from ...errors import DefinitionSyntaxError
-from ...util import ParserHelper, SourceIterator
+from ... import errors
from ..plain import UnitDefinition
if TYPE_CHECKING:
- from pint import Quantity
+ from pint import Quantity, UnitsContainer
-_header_re = re.compile(
- r"@context\s*(?P<defaults>\(.*\))?\s+(?P<name>\w+)\s*(=(?P<aliases>.*))*"
-)
-_varname_re = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
-# TODO: Put back annotation when possible
-# registry_cache: "UnitRegistry"
+@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[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
+ )
+ @property
+ def bidirectional(self):
+ raise NotImplementedError
+
+
+@dataclass(frozen=True)
+class ForwardRelation(Relation):
+ """A relation connecting a dimension to another via a transformation function.
-class Expression:
- def __init__(self, eq):
- self._eq = eq
+ <source dimension> -> <target dimension>: <transformation function>
+ """
- def __call__(self, ureg, value: Any, **kwargs: Any):
- return ureg.parse_expression(self._eq, value=value, **kwargs)
+ @property
+ def bidirectional(self):
+ return False
@dataclass(frozen=True)
-class Relation:
+class BidirectionalRelation(Relation):
+ """A bidirectional relation connecting a dimension to another
+ via a simple transformation function.
+
+ <source dimension> <-> <target dimension>: <transformation function>
- bidirectional: True
- src: ParserHelper
- dst: ParserHelper
- tranformation: Callable[..., Quantity[Any]]
+ """
+
+ @property
+ def bidirectional(self):
+ return True
@dataclass(frozen=True)
-class ContextDefinition:
- """Definition of a Context
-
- @context[(defaults)] <canonical name> [= <alias>] [= <alias>]
- # units can be redefined within the context
- <redefined unit> = <relation to another unit>
-
- # can establish unidirectional relationships between dimensions
- <dimension 1> -> <dimension 2>: <transformation function>
-
- # can establish bidirectionl relationships between dimensions
- <dimension 3> <-> <dimension 4>: <transformation function>
- @end
-
- Example::
-
- @context(n=1) spectroscopy = sp
- # n index of refraction of the medium.
- [length] <-> [frequency]: speed_of_light / n / value
- [frequency] -> [energy]: planck_constant * value
- [energy] -> [frequency]: value / planck_constant
- # allow wavenumber / kayser
- [wavenumber] <-> [length]: 1 / value
- @end
- """
+class ContextDefinition(errors.WithDefErr):
+ """Definition of a Context"""
+ #: name of the context
name: str
+ #: other na
aliases: Tuple[str, ...]
- variables: Tuple[str, ...]
defaults: Dict[str, numbers.Number]
+ relations: Tuple[Relation, ...]
+ redefinitions: Tuple[UnitDefinition, ...]
- # Each element indicates: line number, is_bidirectional, src, dst, transformation func
- relations: Tuple[Tuple[int, Relation], ...]
- redefinitions: Tuple[Tuple[int, UnitDefinition], ...]
+ @property
+ def variables(self) -> Set[str, ...]:
+ """Return all variable names in all transformations."""
+ return set().union(*(r.variables for r in self.relations))
- @staticmethod
- def parse_definition(line, non_int_type) -> UnitDefinition:
- definition = Definition.from_string(line, non_int_type)
- if not isinstance(definition, UnitDefinition):
- raise DefinitionSyntaxError(
- "Expected <unit> = <converter>; got %s" % line.strip()
- )
- if definition.symbol != definition.name or definition.aliases:
- raise DefinitionSyntaxError(
- "Can't change a unit's symbol or aliases within a context"
+ @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(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 definition.is_base:
- raise DefinitionSyntaxError("Can't define plain units within a context")
- return definition
- @classmethod
- def from_lines(cls, lines, non_int_type=float) -> ContextDefinition:
- lines = SourceIterator(lines)
-
- lineno, header = next(lines)
- try:
- r = _header_re.search(header)
- name = r.groupdict()["name"].strip()
- aliases = r.groupdict()["aliases"]
- if aliases:
- aliases = tuple(a.strip() for a in r.groupdict()["aliases"].split("="))
- else:
- aliases = ()
- defaults = r.groupdict()["defaults"]
- except Exception as exc:
- raise DefinitionSyntaxError(
- "Could not parse the Context header '%s'" % header, lineno=lineno
- ) from exc
-
- if defaults:
-
- def to_num(val):
- val = complex(val)
- if not val.imag:
- return val.real
- return val
-
- txt = defaults
- try:
- defaults = (part.split("=") for part in defaults.strip("()").split(","))
- defaults = {str(k).strip(): to_num(v) for k, v in defaults}
- except (ValueError, TypeError) as exc:
- raise DefinitionSyntaxError(
- f"Could not parse Context definition defaults: '{txt}'",
- lineno=lineno,
- ) from exc
- else:
- defaults = {}
-
- variables = set()
- redefitions = []
- relations = []
- for lineno, line in lines:
- try:
- if "=" in line:
- definition = cls.parse_definition(line, non_int_type)
- redefitions.append((lineno, definition))
- elif ":" in line:
- rel, eq = line.split(":")
- variables.update(_varname_re.findall(eq))
-
- func = Expression(eq)
-
- bidir = True
- parts = rel.split("<->")
- if len(parts) != 2:
- bidir = False
- parts = rel.split("->")
- if len(parts) != 2:
- raise Exception
-
- src, dst = (
- ParserHelper.from_string(s, non_int_type) for s in parts
- )
- relation = Relation(bidir, src, dst, func)
- relations.append((lineno, relation))
- else:
- raise Exception
- except Exception as exc:
- raise DefinitionSyntaxError(
- "Could not parse Context %s relation '%s': %s" % (name, line, exc),
- lineno=lineno,
- ) from exc
-
- if defaults:
- missing_pars = defaults.keys() - set(variables)
- if missing_pars:
- raise DefinitionSyntaxError(
- f"Context parameters {missing_pars} not found in any equation"
+ if invalid:
+ raise self.def_err(
+ f"relation refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
)
- return cls(
- name,
- aliases,
- tuple(variables),
- defaults,
- tuple(relations),
- tuple(redefitions),
- )
+ 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"
+ )
diff --git a/pint/facets/context/objects.py b/pint/facets/context/objects.py
index 0aca430..6f2307a 100644
--- a/pint/facets/context/objects.py
+++ b/pint/facets/context/objects.py
@@ -12,7 +12,6 @@ import weakref
from collections import ChainMap, defaultdict
from typing import Optional, Tuple
-from ...errors import DefinitionSyntaxError, RedefinitionError
from ...facets.plain import UnitDefinition
from ...util import UnitsContainer, to_units_container
from .definitions import ContextDefinition
@@ -133,29 +132,22 @@ class Context:
def from_definition(cls, cd: ContextDefinition, to_base_func=None) -> Context:
ctx = cls(cd.name, cd.aliases, cd.defaults)
- for lineno, definition in cd.redefinitions:
- try:
- ctx._redefine(definition)
- except (RedefinitionError, DefinitionSyntaxError) as ex:
- if ex.lineno is None:
- ex.lineno = lineno
- raise ex
+ for definition in cd.redefinitions:
+ ctx._redefine(definition)
- for lineno, relation in cd.relations:
+ for relation in cd.relations:
try:
if to_base_func:
src = to_base_func(relation.src)
dst = to_base_func(relation.dst)
else:
src, dst = relation.src, relation.dst
- ctx.add_transformation(src, dst, relation.tranformation)
+ ctx.add_transformation(src, dst, relation.transformation)
if relation.bidirectional:
- ctx.add_transformation(dst, src, relation.tranformation)
+ ctx.add_transformation(dst, src, relation.transformation)
except Exception as exc:
- raise DefinitionSyntaxError(
- "Could not add Context %s relation on line '%s'"
- % (cd.name, lineno),
- lineno=lineno,
+ raise ValueError(
+ f"Could not add Context {cd.name} relation {relation}"
) from exc
return ctx
@@ -192,11 +184,16 @@ class Context:
definition : str
<unit> = <new definition>``, e.g. ``pound = 0.5 kg``
"""
-
- for line in definition.splitlines():
- # TODO: What is the right non_int_type value.
- definition = ContextDefinition.parse_definition(line, float)
- self._redefine(definition)
+ from ...delegates import ParserConfig, txt_defparser
+
+ # TODO: kept for backwards compatibility.
+ # this is not a good idea as we have no way of known the correct non_int_type
+ cfg = ParserConfig(float)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string(definition)
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, UnitDefinition):
+ self._redefine(definition)
def _redefine(self, definition: UnitDefinition):
self.redefinitions.append(definition)
diff --git a/pint/facets/context/registry.py b/pint/facets/context/registry.py
index 4290e3e..2b56299 100644
--- a/pint/facets/context/registry.py
+++ b/pint/facets/context/registry.py
@@ -14,7 +14,7 @@ from contextlib import contextmanager
from typing import Any, Callable, ContextManager, Dict, Union
from ..._typing import F
-from ...errors import DefinitionSyntaxError, UndefinedUnitError
+from ...errors import UndefinedUnitError
from ...util import find_connected_nodes, find_shortest_path, logger
from ..plain import PlainRegistry, UnitDefinition
from .definitions import ContextDefinition
@@ -67,17 +67,11 @@ class ContextRegistry(PlainRegistry):
# Allow contexts to add override layers to the units
self._units = ChainMap(self._units)
- def _register_directives(self) -> None:
- super()._register_directives()
- self._register_directive("@context", self._load_context, ContextDefinition)
+ def _register_definition_adders(self) -> None:
+ super()._register_definition_adders()
+ self._register_adder(ContextDefinition, self.add_context)
- def _load_context(self, cd: ContextDefinition) -> None:
- try:
- self.add_context(Context.from_definition(cd, self.get_dimensionality))
- except KeyError as e:
- raise DefinitionSyntaxError(f"unknown dimension {e} in context")
-
- def add_context(self, context: Context) -> None:
+ def add_context(self, context: Union[Context, ContextDefinition]) -> None:
"""Add a context object to the registry.
The context will be accessible by its name and aliases.
@@ -85,6 +79,9 @@ class ContextRegistry(PlainRegistry):
Notice that this method will NOT enable the context;
see :meth:`enable_contexts`.
"""
+ if isinstance(context, ContextDefinition):
+ context = Context.from_definition(context, self.get_dimensionality)
+
if not context.name:
raise ValueError("Can't add unnamed context to registry")
if context.name in self._contexts:
@@ -189,7 +186,6 @@ class ContextRegistry(PlainRegistry):
name=basedef.name,
defined_symbol=basedef.symbol,
aliases=basedef.aliases,
- is_base=False,
reference=definition.reference,
converter=definition.converter,
)
diff --git a/pint/facets/group/definitions.py b/pint/facets/group/definitions.py
index d2fa3f2..c0abced 100644
--- a/pint/facets/group/definitions.py
+++ b/pint/facets/group/definitions.py
@@ -1,6 +1,6 @@
"""
- pint.facets.group.defintions
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ pint.facets.group.definitions
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
@@ -8,88 +8,46 @@
from __future__ import annotations
-import re
+import typing as ty
from dataclasses import dataclass
-from typing import Tuple
-from ...definitions import Definition
-from ...errors import DefinitionSyntaxError
-from ...util import SourceIterator
-from ..plain import UnitDefinition
+from ... import errors
+from .. import plain
@dataclass(frozen=True)
-class GroupDefinition:
- """Definition of a group.
-
- @group <name> [using <group 1>, ..., <group N>]
- <definition 1>
- ...
- <definition N>
- @end
-
- Example::
-
- @group AvoirdupoisUS using Avoirdupois
- US_hundredweight = hundredweight = US_cwt
- US_ton = ton
- US_force_ton = force_ton = _ = US_ton_force
- @end
-
- """
-
- #: Regex to match the header parts of a definition.
- _header_re = re.compile(r"@group\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+class GroupDefinition(errors.WithDefErr):
+ """Definition of a group."""
+ #: name of the group
name: str
- units: Tuple[Tuple[int, UnitDefinition], ...]
- using_group_names: Tuple[str, ...]
-
- @property
- def unit_names(self) -> Tuple[str, ...]:
- return tuple(u.name for lineno, u in self.units)
+ #: unit groups that will be included within the group
+ using_group_names: ty.Tuple[str, ...]
+ #: definitions for the units existing within the group
+ definitions: ty.Tuple[plain.UnitDefinition, ...]
@classmethod
- def from_lines(cls, lines, non_int_type=float):
- """Return a Group object parsing an iterable of lines.
+ def from_lines(cls, lines, non_int_type):
+ # TODO: this is to keep it backwards compatible
+ from ...delegates import ParserConfig, txt_defparser
- Parameters
- ----------
- lines : list[str]
- iterable
- define_func : callable
- Function to define a unit in the registry; it must accept a single string as
- a parameter.
+ 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
- Returns
- -------
-
- """
-
- lines = SourceIterator(lines)
- lineno, header = next(lines)
-
- r = cls._header_re.search(header)
-
- if r is None:
- raise ValueError("Invalid Group header syntax: '%s'" % header)
+ @property
+ def unit_names(self) -> ty.Tuple[str, ...]:
+ return tuple(el.name for el in self.definitions)
- name = r.groupdict()["name"].strip()
- groups = r.groupdict()["used_groups"]
- if groups:
- parent_group_names = tuple(a.strip() for a in groups.split(","))
- else:
- parent_group_names = ()
+ def __post_init__(self):
+ if not errors.is_valid_group_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_GROUP_NAME)
- units = []
- for lineno, line in lines:
- definition = Definition.from_string(line, non_int_type=non_int_type)
- if not isinstance(definition, UnitDefinition):
- raise DefinitionSyntaxError(
- "Only UnitDefinition are valid inside _used_groups, not "
- + str(definition),
- lineno=lineno,
+ for k in self.using_group_names:
+ if not errors.is_valid_group_name(k):
+ raise self.def_err(
+ f"refers to '{k}' that " + errors.MSG_INVALID_GROUP_NAME
)
- units.append((lineno, definition))
-
- return cls(name, tuple(units), parent_group_names)
diff --git a/pint/facets/group/objects.py b/pint/facets/group/objects.py
index 4ff775c..67fa136 100644
--- a/pint/facets/group/objects.py
+++ b/pint/facets/group/objects.py
@@ -8,7 +8,6 @@
from __future__ import annotations
-from ...errors import DefinitionSyntaxError, RedefinitionError
from ...util import SharedRegistryObject, getattr_maybe_raise
from .definitions import GroupDefinition
@@ -169,19 +168,24 @@ class Group(SharedRegistryObject):
return cls.from_definition(group_definition, define_func)
@classmethod
- def from_definition(cls, group_definition: GroupDefinition, define_func) -> Group:
- for lineno, definition in group_definition.units:
- try:
- define_func(definition)
- except (RedefinitionError, DefinitionSyntaxError) as ex:
- if ex.lineno is None:
- ex.lineno = lineno
- raise ex
-
+ def from_definition(
+ cls, group_definition: GroupDefinition, add_unit_func=None
+ ) -> Group:
grp = cls(group_definition.name)
- grp.add_units(*(unit.name for lineno, unit in group_definition.units))
+ add_unit_func = add_unit_func or grp._REGISTRY._add_unit
+
+ # We first add all units defined within the group
+ # to the registry.
+ for definition in group_definition.definitions:
+ add_unit_func(definition)
+
+ # Then we add all units defined within the group
+ # to this group (by name)
+ grp.add_units(*group_definition.unit_names)
+ # Finally, we add all grou0ps used by this group
+ # tho this group (by name)
if group_definition.using_group_names:
grp.add_groups(*group_definition.using_group_names)
diff --git a/pint/facets/group/registry.py b/pint/facets/group/registry.py
index f8da191..c4ed0be 100644
--- a/pint/facets/group/registry.py
+++ b/pint/facets/group/registry.py
@@ -10,6 +10,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Dict, FrozenSet
+from ... import errors
+
if TYPE_CHECKING:
from pint import Unit
@@ -29,6 +31,9 @@ class GroupRegistry(PlainRegistry):
- Parse @group directive.
"""
+ # TODO: Change this to Group: Group to specify class
+ # and use introspection to get system class as a way
+ # to enjoy typing goodies
_group_class = Group
def __init__(self, **kwargs):
@@ -69,13 +74,25 @@ class GroupRegistry(PlainRegistry):
all_units = self.get_group("root", False).members
grp.add_units(*(all_units - group_units))
- def _register_directives(self) -> None:
- super()._register_directives()
- self._register_directive(
- "@group",
- lambda gd: self.Group.from_definition(gd, self.define),
- GroupDefinition,
- )
+ def _register_definition_adders(self) -> None:
+ super()._register_definition_adders()
+ self._register_adder(GroupDefinition, self._add_group)
+
+ def _add_unit(self, definition: UnitDefinition):
+ super()._add_unit(definition)
+ # TODO: delta units are missing
+ self.get_group("root").add_units(definition.name)
+
+ def _add_group(self, gd: GroupDefinition):
+
+ if gd.name in self._groups:
+ raise ValueError(f"Group {gd.name} already present in registry")
+ try:
+ # As a Group is a SharedRegistryObject
+ # it adds itself to the registry.
+ self.Group.from_definition(gd)
+ except KeyError as e:
+ raise errors.DefinitionSyntaxError(f"unknown dimension {e} in context")
def get_group(self, name: str, create_if_needed: bool = True) -> Group:
"""Return a Group.
@@ -101,19 +118,6 @@ class GroupRegistry(PlainRegistry):
return self.Group(name)
- def _define(self, definition):
-
- # In addition to the what is done by the PlainRegistry,
- # this adds all units to the `root` group.
-
- definition, d, di = super()._define(definition)
-
- if isinstance(definition, UnitDefinition):
- # We add all units to the root group
- self.get_group("root").add_units(definition.name)
-
- return definition, d, di
-
def _get_compatible_units(self, input_units, group) -> FrozenSet["Unit"]:
ret = super()._get_compatible_units(input_units, group)
diff --git a/pint/facets/nonmultiplicative/registry.py b/pint/facets/nonmultiplicative/registry.py
index ae6b1b0..fc71bc5 100644
--- a/pint/facets/nonmultiplicative/registry.py
+++ b/pint/facets/nonmultiplicative/registry.py
@@ -8,12 +8,12 @@
from __future__ import annotations
-from typing import Any, Optional, Union
+from typing import Any, Optional
-from ...definitions import Definition
from ...errors import DimensionalityError, UndefinedUnitError
-from ...util import UnitsContainer
-from ..plain import PlainRegistry
+from ...util import UnitsContainer, logger
+from ..plain import PlainRegistry, UnitDefinition
+from .definitions import OffsetConverter, ScaleConverter
from .objects import NonMultiplicativeQuantity
@@ -65,31 +65,44 @@ class NonMultiplicativeRegistry(PlainRegistry):
return super()._parse_units(input_string, as_delta, case_sensitive)
- def _define(self, definition: Union[str, Definition]):
- """Add unit to the registry.
+ def _add_unit(self, definition: UnitDefinition):
+ super()._add_unit(definition)
- In addition to what is done by the PlainRegistry,
- registers also non-multiplicative units.
-
- Parameters
- ----------
- definition : str or Definition
- A dimension, unit or prefix definition.
+ if definition.is_multiplicative:
+ return
- Returns
- -------
- Definition, dict, dict
- Definition instance, case sensitive unit dict, case insensitive unit dict.
+ if definition.is_logarithmic:
+ return
- """
-
- definition, d, di = super()._define(definition)
-
- # define additional units for units with an offset
- if getattr(definition.converter, "offset", 0) != 0:
- self._define_adder(definition, d, di)
-
- return definition, d, di
+ if not isinstance(definition.converter, OffsetConverter):
+ logger.debug(
+ "Cannot autogenerate delta version for a unit in "
+ "which the converter is not an OffsetConverter"
+ )
+ return
+
+ delta_name = "delta_" + definition.name
+ if definition.symbol:
+ delta_symbol = "Δ" + definition.symbol
+ else:
+ delta_symbol = None
+
+ delta_aliases = tuple("Δ" + alias for alias in definition.aliases) + tuple(
+ "delta_" + alias for alias in definition.aliases
+ )
+
+ delta_reference = self.UnitsContainer(
+ {ref: value for ref, value in definition.reference.items()}
+ )
+
+ delta_def = UnitDefinition(
+ delta_name,
+ delta_symbol,
+ delta_aliases,
+ ScaleConverter(definition.converter.scale),
+ delta_reference,
+ )
+ super()._add_unit(delta_def)
def _is_multiplicative(self, u) -> bool:
if u in self._units:
diff --git a/pint/facets/plain/definitions.py b/pint/facets/plain/definitions.py
index c1d1d9a..11a3095 100644
--- a/pint/facets/plain/definitions.py
+++ b/pint/facets/plain/definitions.py
@@ -8,259 +8,267 @@
from __future__ import annotations
+import itertools
+import numbers
+import typing as ty
from dataclasses import dataclass
-from typing import Iterable, Optional, Tuple, Union
+from functools import cached_property
+from typing import Callable, Optional
+from ... import errors
from ...converters import Converter
-from ...definitions import Definition, PreprocessedDefinition
-from ...errors import DefinitionSyntaxError
-from ...util import ParserHelper, SourceIterator, UnitsContainer, _is_dim
+from ...util import UnitsContainer
-class _NotNumeric(Exception):
+class NotNumeric(Exception):
"""Internal exception. Do not expose outside Pint"""
def __init__(self, value):
self.value = value
-def numeric_parse(s: str, non_int_type: type = float):
- """Try parse a string into a number (without using eval).
-
- Parameters
- ----------
- s : str
- non_int_type : type
-
- Returns
- -------
- Number
-
- Raises
- ------
- _NotNumeric
- If the string cannot be parsed as a number.
- """
- ph = ParserHelper.from_string(s, non_int_type)
-
- if len(ph):
- raise _NotNumeric(s)
-
- return ph.scale
+########################
+# Convenience functions
+########################
@dataclass(frozen=True)
-class PrefixDefinition(Definition):
- """Definition of a prefix::
-
- <prefix>- = <amount> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+class Equality:
+ """An equality statement contains a left and right hand separated
+ by and equal (=) sign.
- Example::
+ lhs = rhs
- deca- = 1e+1 = da- = deka-
+ lhs and rhs are space stripped.
"""
- @classmethod
- def accept_to_parse(cls, preprocessed: PreprocessedDefinition):
- return preprocessed.name.endswith("-")
+ lhs: str
+ rhs: str
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> PrefixDefinition:
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
- aliases = tuple(alias.strip("-") for alias in definition.aliases)
- if definition.symbol:
- symbol = definition.symbol.strip("-")
- else:
- symbol = definition.symbol
-
- try:
- converter = ScaleConverter(numeric_parse(definition.value, non_int_type))
- except _NotNumeric as ex:
- raise ValueError(
- f"Prefix definition ('{definition.name}') must contain only numbers, not {ex.value}"
- )
+@dataclass(frozen=True)
+class CommentDefinition:
+ """A comment"""
- return cls(definition.name.rstrip("-"), symbol, aliases, converter)
+ comment: str
@dataclass(frozen=True)
-class UnitDefinition(Definition, default=True):
- """Definition of a unit::
-
- <canonical name> = <relation to another unit or dimension> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+class DefaultsDefinition:
+ """Directive to store default values."""
- Example::
+ group: ty.Optional[str]
+ system: ty.Optional[str]
- millennium = 1e3 * year = _ = millennia
+ def items(self):
+ if self.group is not None:
+ yield "group", self.group
+ if self.system is not None:
+ yield "system", self.system
- Parameters
- ----------
- reference : UnitsContainer
- Reference units.
- is_base : bool
- Indicates if it is a plain unit.
- """
+@dataclass(frozen=True)
+class PrefixDefinition(errors.WithDefErr):
+ """Definition of a prefix."""
+
+ #: name of the prefix
+ name: str
+ #: scaling value for this prefix
+ value: numbers.Number
+ #: canonical symbol
+ defined_symbol: Optional[str] = ""
+ #: additional names for the same prefix
+ aliases: ty.Tuple[str, ...] = ()
+
+ @property
+ def symbol(self) -> str:
+ return self.defined_symbol or self.name
+
+ @property
+ def has_symbol(self) -> bool:
+ return bool(self.defined_symbol)
+
+ @cached_property
+ def converter(self):
+ return Converter.from_arguments(scale=self.value)
+
+ def __post_init__(self):
+ if not errors.is_valid_prefix_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_PREFIX_NAME)
+
+ if self.defined_symbol and not errors.is_valid_prefix_symbol(self.name):
+ raise self.def_err(
+ f"the symbol {self.defined_symbol} " + errors.MSG_INVALID_PREFIX_SYMBOL
+ )
- reference: Optional[UnitsContainer] = None
- is_base: bool = False
+ for alias in self.aliases:
+ if not errors.is_valid_prefix_alias(alias):
+ raise self.def_err(
+ f"the alias {alias} " + errors.MSG_INVALID_PREFIX_ALIAS
+ )
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> "UnitDefinition":
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
- if ";" in definition.value:
- [converter, modifiers] = definition.value.split(";", 1)
+@dataclass(frozen=True)
+class UnitDefinition(errors.WithDefErr):
+ """Definition of a unit."""
+
+ #: canonical name of the unit
+ name: str
+ #: canonical symbol
+ defined_symbol: ty.Optional[str]
+ #: additional names for the same unit
+ aliases: ty.Tuple[str, ...]
+ #: A functiont that converts a value in these units into the reference units
+ converter: ty.Optional[ty.Union[Callable, Converter]]
+ #: Reference units.
+ reference: ty.Optional[UnitsContainer]
+
+ def __post_init__(self):
+ if not errors.is_valid_unit_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_UNIT_NAME)
+
+ if not any(map(errors.is_dim, self.reference.keys())):
+ invalid = tuple(
+ itertools.filterfalse(errors.is_valid_unit_name, self.reference.keys())
+ )
+ if invalid:
+ raise self.def_err(
+ f"refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_UNIT_NAME
+ )
+ is_base = False
- try:
- modifiers = dict(
- (key.strip(), numeric_parse(value, non_int_type))
- for key, value in (part.split(":") for part in modifiers.split(";"))
+ elif all(map(errors.is_dim, self.reference.keys())):
+ invalid = tuple(
+ itertools.filterfalse(
+ errors.is_valid_dimension_name, self.reference.keys()
)
- except _NotNumeric as ex:
- raise ValueError(
- f"Unit definition ('{definition.name}') must contain only numbers in modifier, not {ex.value}"
+ )
+ if invalid:
+ raise self.def_err(
+ f"refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
)
- else:
- converter = definition.value
- modifiers = {}
-
- converter = ParserHelper.from_string(converter, non_int_type)
-
- if not any(_is_dim(key) for key in converter.keys()):
- is_base = False
- elif all(_is_dim(key) for key in converter.keys()):
is_base = True
+ scale = getattr(self.converter, "scale", 1)
+ if scale != 1:
+ return self.def_err(
+ "Base unit definitions cannot have a scale different to 1. "
+ f"(`{scale}` found)"
+ )
else:
- raise DefinitionSyntaxError(
+ raise self.def_err(
"Cannot mix dimensions and units in the same definition. "
"Base units must be referenced only to dimensions. "
"Derived units must be referenced only to units."
)
- reference = UnitsContainer(converter)
-
- try:
- converter = Converter.from_arguments(scale=converter.scale, **modifiers)
- except Exception as ex:
- raise DefinitionSyntaxError(
- "Unable to assign a converter to the unit"
- ) from ex
-
- return cls(
- definition.name,
- definition.symbol,
- definition.aliases,
- converter,
- reference,
- is_base,
- )
+ super.__setattr__(self, "_is_base", is_base)
-@dataclass(frozen=True)
-class DimensionDefinition(Definition):
- """Definition of a dimension::
-
- [dimension name] = <relation to other dimensions>
+ if self.defined_symbol and not errors.is_valid_unit_symbol(self.name):
+ raise self.def_err(
+ f"the symbol {self.defined_symbol} " + errors.MSG_INVALID_UNIT_SYMBOL
+ )
- Example::
+ for alias in self.aliases:
+ if not errors.is_valid_unit_alias(alias):
+ raise self.def_err(
+ f"the alias {alias} " + errors.MSG_INVALID_UNIT_ALIAS
+ )
- [density] = [mass] / [volume]
- """
+ @property
+ def is_base(self) -> bool:
+ """Indicates if it is a base unit."""
+ return self._is_base
- reference: Optional[UnitsContainer] = None
- is_base: bool = False
+ @property
+ def is_multiplicative(self) -> bool:
+ return self.converter.is_multiplicative
- @classmethod
- def accept_to_parse(cls, preprocessed: PreprocessedDefinition):
- return preprocessed.name.startswith("[")
+ @property
+ def is_logarithmic(self) -> bool:
+ return self.converter.is_logarithmic
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> DimensionDefinition:
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
+ @property
+ def symbol(self) -> str:
+ return self.defined_symbol or self.name
- converter = ParserHelper.from_string(definition.value, non_int_type)
+ @property
+ def has_symbol(self) -> bool:
+ return bool(self.defined_symbol)
- if not converter:
- is_base = True
- elif all(_is_dim(key) for key in converter.keys()):
- is_base = False
- else:
- raise DefinitionSyntaxError(
- "Base dimensions must be referenced to None. "
- "Derived dimensions must only be referenced "
- "to dimensions."
- )
- reference = UnitsContainer(converter, non_int_type=non_int_type)
-
- return cls(
- definition.name,
- definition.symbol,
- definition.aliases,
- converter,
- reference,
- is_base,
- )
+@dataclass(frozen=True)
+class DimensionDefinition(errors.WithDefErr):
+ """Definition of a root dimension"""
-class AliasDefinition(Definition):
- """Additional alias(es) for an already existing unit::
+ #: name of the dimension
+ name: str
- @alias <canonical name or previous alias> = <alias> [ = <alias> ] [...]
+ @property
+ def is_base(self):
+ return True
- Example::
+ def __post_init__(self):
+ if not errors.is_valid_dimension_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_DIMENSION_NAME)
- @alias meter = my_meter
- """
- def __init__(self, name: str, aliases: Iterable[str]) -> None:
- super().__init__(
- name=name, defined_symbol=None, aliases=aliases, converter=None
- )
+@dataclass(frozen=True)
+class DerivedDimensionDefinition(DimensionDefinition):
+ """Definition of a derived dimension."""
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> AliasDefinition:
+ #: reference dimensions.
+ reference: UnitsContainer
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
+ @property
+ def is_base(self):
+ return False
- name = definition.name[len("@alias ") :].lstrip()
- return AliasDefinition(name, tuple(definition.rhs_parts))
+ def __post_init__(self):
+ if not errors.is_valid_dimension_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_DIMENSION_NAME)
+ if not all(map(errors.is_dim, self.reference.keys())):
+ return self.def_err(
+ "derived dimensions must only reference other dimensions"
+ )
-@dataclass(frozen=True)
-class DefaultsDefinition:
- """Definition for the @default directive"""
+ invalid = tuple(
+ itertools.filterfalse(errors.is_valid_dimension_name, self.reference.keys())
+ )
- content: Tuple[Tuple[str, str], ...]
+ if invalid:
+ raise self.def_err(
+ f"refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
+ )
- @classmethod
- def from_lines(cls, lines, non_int_type=float) -> DefaultsDefinition:
- source_iterator = SourceIterator(lines)
- next(source_iterator)
- out = []
- for lineno, part in source_iterator:
- k, v = part.split("=")
- out.append((k.strip(), v.strip()))
- return DefaultsDefinition(tuple(out))
+@dataclass(frozen=True)
+class AliasDefinition(errors.WithDefErr):
+ """Additional alias(es) for an already existing unit."""
+
+ #: name of the already existing unit
+ name: str
+ #: aditional names for the same unit
+ aliases: ty.Tuple[str, ...]
+
+ def __post_init__(self):
+ if not errors.is_valid_unit_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_UNIT_NAME)
+
+ for alias in self.aliases:
+ if not errors.is_valid_unit_alias(alias):
+ raise self.def_err(
+ f"the alias {alias} " + errors.MSG_INVALID_UNIT_ALIAS
+ )
@dataclass(frozen=True)
class ScaleConverter(Converter):
- """A linear transformation."""
+ """A linear transformation without offset."""
scale: float
diff --git a/pint/facets/plain/registry.py b/pint/facets/plain/registry.py
index 4c62ad1..8b98965 100644
--- a/pint/facets/plain/registry.py
+++ b/pint/facets/plain/registry.py
@@ -10,12 +10,12 @@ from __future__ import annotations
import copy
import functools
+import inspect
import itertools
import locale
import pathlib
import re
from collections import defaultdict
-from dataclasses import dataclass
from decimal import Decimal
from fractions import Fraction
from numbers import Number
@@ -41,19 +41,12 @@ if TYPE_CHECKING:
from ..context import Context
from pint import Quantity, Unit
-from ... import parser
from ..._typing import QuantityOrUnitLike, UnitLike
from ..._vendor import appdirs
from ...compat import HAS_BABEL, babel_parse, tokenizer
-from ...definitions import Definition
-from ...errors import (
- DefinitionSyntaxError,
- DimensionalityError,
- RedefinitionError,
- UndefinedUnitError,
-)
+from ...errors import DimensionalityError, RedefinitionError, UndefinedUnitError
from ...pint_eval import build_eval_tree
-from ...util import ParserHelper, SourceIterator
+from ...util import ParserHelper
from ...util import UnitsContainer
from ...util import UnitsContainer as UnitsContainerT
from ...util import (
@@ -68,9 +61,11 @@ from ...util import (
)
from .definitions import (
AliasDefinition,
+ CommentDefinition,
+ DefaultsDefinition,
+ DerivedDimensionDefinition,
DimensionDefinition,
PrefixDefinition,
- ScaleConverter,
UnitDefinition,
)
from .objects import PlainQuantity, PlainUnit
@@ -89,24 +84,6 @@ T = TypeVar("T")
_BLOCK_RE = re.compile(r"[ (]")
-@dataclass(frozen=True)
-class DefaultsDefinition:
- """Definition for the @default directive"""
-
- content: Tuple[Tuple[str, str], ...]
-
- @classmethod
- def from_lines(cls, lines, non_int_type=float) -> DefaultsDefinition:
- source_iterator = SourceIterator(lines)
- next(source_iterator)
- out = []
- for lineno, part in source_iterator:
- k, v = part.split("=")
- out.append((k.strip(), v.strip()))
-
- return DefaultsDefinition(tuple(out))
-
-
@functools.lru_cache()
def pattern_to_regex(pattern):
if hasattr(pattern, "finditer"):
@@ -212,6 +189,8 @@ class PlainRegistry(metaclass=RegistryMeta):
_quantity_class = PlainQuantity
_unit_class = PlainUnit
+ _def_parser = None
+
def __init__(
self,
filename="",
@@ -226,17 +205,25 @@ class PlainRegistry(metaclass=RegistryMeta):
cache_folder: Union[str, pathlib.Path, None] = None,
separate_format_defaults: Optional[bool] = None,
):
- #: Map context prefix to (loader function, parser function, single_line)
- #: type: Dict[str, Tuple[Callable[[Any], None]], Any]
- self._directives = {}
- self._register_directives()
+ #: Map a definition class to a adder methods.
+ self._adders = dict()
+ self._register_definition_adders()
self._init_dynamic_classes()
if cache_folder == ":auto:":
cache_folder = appdirs.user_cache_dir(appname="pint", appauthor=False)
+ cache_folder = pathlib.Path(cache_folder)
+
+ from ... import delegates # TODO: change thiss
if cache_folder is not None:
- self._diskcache = parser.build_disk_cache_class(non_int_type)(cache_folder)
+ self._diskcache = delegates.build_disk_cache_class(non_int_type)(
+ cache_folder
+ )
+
+ self._def_parser = delegates.txt_defparser.DefParser(
+ delegates.ParserConfig(non_int_type), diskcache=self._diskcache
+ )
self._filename = filename
self.force_ndarray = force_ndarray
@@ -256,7 +243,7 @@ class PlainRegistry(metaclass=RegistryMeta):
self.set_fmt_locale(fmt_locale)
#: Numerical type used for non integer values.
- self.non_int_type = non_int_type
+ self._non_int_type = non_int_type
#: Default unit case sensitivity
self.case_sensitive = case_sensitive
@@ -266,7 +253,9 @@ class PlainRegistry(metaclass=RegistryMeta):
self._defaults: Dict[str, str] = {}
#: Map dimension name (string) to its definition (DimensionDefinition).
- self._dimensions: Dict[str, DimensionDefinition] = {}
+ self._dimensions: Dict[
+ str, Union[DimensionDefinition, DerivedDimensionDefinition]
+ ] = {}
#: Map unit name (string) to its definition (UnitDefinition).
#: Might contain prefixed units.
@@ -279,9 +268,7 @@ class PlainRegistry(metaclass=RegistryMeta):
self._units_casei: Dict[str, Set[str]] = defaultdict(set)
#: Map prefix name (string) to its definition (PrefixDefinition).
- self._prefixes: Dict[str, PrefixDefinition] = {
- "": PrefixDefinition("", "", (), 1)
- }
+ self._prefixes: Dict[str, PrefixDefinition] = {"": PrefixDefinition("", 1)}
#: Map suffix name (string) to canonical , and unit alias to canonical unit name
self._suffixes: Dict[str, str] = {"": "", "s": ""}
@@ -306,7 +293,8 @@ class PlainRegistry(metaclass=RegistryMeta):
"""This should be called after all __init__"""
if self._filename == "":
- loaded_files = self.load_definitions("default_en.txt", True)
+ path = pathlib.Path(__file__).parent.parent.parent / "default_en.txt"
+ loaded_files = self.load_definitions(path, True)
elif self._filename is not None:
loaded_files = self.load_definitions(self._filename)
else:
@@ -315,19 +303,18 @@ class PlainRegistry(metaclass=RegistryMeta):
self._build_cache(loaded_files)
self._initialized = True
- def _register_directives(self) -> None:
- self._register_directive("@alias", self._load_alias, AliasDefinition)
- self._register_directive("@defaults", self._load_defaults, DefaultsDefinition)
+ def _register_adder(self, definition_class, adder_func):
+ """Register a block definition."""
+ self._adders[definition_class] = adder_func
- def _load_defaults(self, defaults_definition: DefaultsDefinition) -> None:
- """Loader for a @default section."""
-
- for k, v in defaults_definition.content:
- self._defaults[k] = v
-
- def _load_alias(self, alias_definition: AliasDefinition) -> None:
- """Loader for an @alias directive"""
- self._define_alias(alias_definition)
+ def _register_definition_adders(self) -> None:
+ self._register_adder(AliasDefinition, self._add_alias)
+ self._register_adder(DefaultsDefinition, self._add_defaults)
+ self._register_adder(CommentDefinition, lambda o: o)
+ self._register_adder(PrefixDefinition, self._add_prefix)
+ self._register_adder(UnitDefinition, self._add_unit)
+ self._register_adder(DimensionDefinition, self._add_dimension)
+ self._register_adder(DerivedDimensionDefinition, self._add_derived_dimension)
def __deepcopy__(self, memo) -> "PlainRegistry":
new = object.__new__(type(self))
@@ -405,7 +392,11 @@ class PlainRegistry(metaclass=RegistryMeta):
return self._diskcache.cache_folder
return None
- def define(self, definition: Union[str, Definition]) -> None:
+ @property
+ def non_int_type(self):
+ return self._non_int_type
+
+ def define(self, definition):
"""Add unit to the registry.
Parameters
@@ -415,162 +406,99 @@ class PlainRegistry(metaclass=RegistryMeta):
"""
if isinstance(definition, str):
- for line in definition.split("\n"):
- if line.startswith("@alias"):
- # TODO why alias can be defined like this but not other directives?
- self._define_alias(
- AliasDefinition.from_string(line, self.non_int_type)
- )
- else:
- self._define(Definition.from_string(line, self.non_int_type))
- else:
- self._define(definition)
-
- def _define(self, definition: Definition) -> Tuple[Definition, dict, dict]:
- """Add unit to the registry.
-
- This method defines only multiplicative units, converting any other type
- to `delta_` units.
+ parsed_project = self._def_parser.parse_string(definition)
- Parameters
- ----------
- definition : Definition
- a dimension, unit or prefix definition.
+ for definition in self._def_parser.iter_parsed_project(parsed_project):
+ self._helper_dispatch_adder(definition)
+ else:
+ self._helper_dispatch_adder(definition)
- Returns
- -------
- Definition, dict, dict
- Definition instance, case sensitive unit dict, case insensitive unit dict.
+ ############
+ # Adders
+ # - we first provide some helpers that deal with repetitive task.
+ # - then we define specific adder for each definition class. :-D
+ ############
+ def _helper_dispatch_adder(self, definition):
+ """Helper function to add a single definition,
+ choosing the appropiate method by class.
"""
-
- if isinstance(definition, DimensionDefinition):
- d, di = self._dimensions, None
-
- elif isinstance(definition, UnitDefinition):
- d, di = self._units, self._units_casei
-
- # For a plain units, we need to define the related dimension
- # (making sure there is only one to define)
- if definition.is_base:
- for dimension in definition.reference.keys():
- if dimension in self._dimensions:
- if dimension != "[]":
- raise DefinitionSyntaxError(
- "Only one unit per dimension can be a plain unit"
- )
- continue
-
- self.define(
- DimensionDefinition(dimension, "", (), None, None, True)
- )
-
- elif isinstance(definition, PrefixDefinition):
- d, di = self._prefixes, None
-
+ for cls in inspect.getmro(definition.__class__):
+ if cls in self._adders:
+ adder_func = self._adders[cls]
+ break
else:
- raise TypeError("{} is not a valid definition.".format(definition))
-
- # define "delta_" units for units with an offset
- if getattr(definition.converter, "offset", 0) != 0:
-
- if definition.name.startswith("["):
- d_name = "[delta_" + definition.name[1:]
- else:
- d_name = "delta_" + definition.name
-
- if definition.symbol:
- d_symbol = "Δ" + definition.symbol
- else:
- d_symbol = None
-
- d_aliases = tuple("Δ" + alias for alias in definition.aliases) + tuple(
- "delta_" + alias for alias in definition.aliases
- )
-
- d_reference = self.UnitsContainer(
- {ref: value for ref, value in definition.reference.items()}
- )
-
- d_def = UnitDefinition(
- d_name,
- d_symbol,
- d_aliases,
- ScaleConverter(definition.converter.scale),
- d_reference,
- definition.is_base,
+ raise TypeError(
+ f"No loader function defined " f"for {definition.__class__.__name__}"
)
- else:
- d_def = definition
-
- self._define_adder(d_def, d, di)
- return definition, d, di
+ adder_func(definition)
- def _define_adder(self, definition, unit_dict, casei_unit_dict):
+ def _helper_adder(self, definition, target_dict, casei_target_dict):
"""Helper function to store a definition in the internal dictionaries.
It stores the definition under its name, symbol and aliases.
"""
- self._define_single_adder(
- definition.name, definition, unit_dict, casei_unit_dict
+ self._helper_single_adder(
+ definition.name, definition, target_dict, casei_target_dict
)
- if definition.has_symbol:
- self._define_single_adder(
- definition.symbol, definition, unit_dict, casei_unit_dict
+ if getattr(definition, "has_symbol", ""):
+ self._helper_single_adder(
+ definition.symbol, definition, target_dict, casei_target_dict
)
- for alias in definition.aliases:
+ for alias in getattr(definition, "aliases", ()):
if " " in alias:
logger.warn("Alias cannot contain a space: " + alias)
- self._define_single_adder(alias, definition, unit_dict, casei_unit_dict)
+ self._helper_single_adder(alias, definition, target_dict, casei_target_dict)
- def _define_single_adder(self, key, value, unit_dict, casei_unit_dict):
+ def _helper_single_adder(self, key, value, target_dict, casei_target_dict):
"""Helper function to store a definition in the internal dictionaries.
It warns or raise error on redefinition.
"""
- if key in unit_dict:
+ if key in target_dict:
if self._on_redefinition == "raise":
raise RedefinitionError(key, type(value))
elif self._on_redefinition == "warn":
logger.warning("Redefining '%s' (%s)" % (key, type(value)))
- unit_dict[key] = value
- if casei_unit_dict is not None:
- casei_unit_dict[key.lower()].add(key)
+ target_dict[key] = value
+ if casei_target_dict is not None:
+ casei_target_dict[key.lower()].add(key)
- def _define_alias(self, definition):
- if not isinstance(definition, AliasDefinition):
- raise TypeError(
- "Not a valid input type for _define_alias. "
- f"(expected: AliasDefinition, found: {type(definition)}"
- )
+ def _add_defaults(self, defaults_definition: DefaultsDefinition):
+ for k, v in defaults_definition.items():
+ self._defaults[k] = v
+ def _add_alias(self, definition: AliasDefinition):
unit_dict = self._units
unit = unit_dict[definition.name]
while not isinstance(unit, UnitDefinition):
unit = unit_dict[unit.name]
for alias in definition.aliases:
- self._define_single_adder(alias, unit, self._units, self._units_casei)
+ self._helper_single_adder(alias, unit, self._units, self._units_casei)
- def _register_directive(self, prefix: str, loaderfunc, definition_class):
- """Register a loader for a given @ directive.
+ def _add_dimension(self, definition: DimensionDefinition):
+ self._helper_adder(definition, self._dimensions, None)
- Parameters
- ----------
- prefix
- string identifying the section (e.g. @context).
- loaderfunc
- function to load the definition into the registry.
- definition_class
- a class that represents the directive content.
- """
- if prefix and prefix[0] == "@":
- self._directives[prefix] = (loaderfunc, definition_class)
- else:
- raise ValueError("Prefix directives must start with '@'")
+ def _add_derived_dimension(self, definition: DerivedDimensionDefinition):
+ for dim_name in definition.reference.keys():
+ if dim_name not in self._dimensions:
+ self._add_dimension(DimensionDefinition(dim_name))
+ self._helper_adder(definition, self._dimensions, None)
+
+ def _add_prefix(self, definition: PrefixDefinition):
+ self._helper_adder(definition, self._prefixes, None)
+
+ def _add_unit(self, definition: UnitDefinition):
+ if definition.is_base:
+ for dim_name in definition.reference.keys():
+ if dim_name not in self._dimensions:
+ self._add_dimension(DimensionDefinition(dim_name))
+
+ self._helper_adder(definition, self._units, self._units_casei)
def load_definitions(self, file, is_resource: bool = False):
"""Add units and prefixes defined in a definition text file.
@@ -584,50 +512,26 @@ class PlainRegistry(metaclass=RegistryMeta):
and therefore should be loaded from the package. (Default value = False)
"""
- loaders = {
- AliasDefinition: self._define,
- UnitDefinition: self._define,
- DimensionDefinition: self._define,
- PrefixDefinition: self._define,
- }
-
- p = parser.Parser(self.non_int_type, cache_folder=self._diskcache)
- for prefix, (loaderfunc, definition_class) in self._directives.items():
- loaders[definition_class] = loaderfunc
- p.register_class(prefix, definition_class)
-
- if isinstance(file, (str, pathlib.Path)):
- try:
- parsed_files = p.parse(file, is_resource)
- except Exception as ex:
- # TODO: Change this is in the future
- # this is kept for backwards compatibility
- msg = getattr(ex, "message", "") or str(ex)
- raise ValueError("While opening {}\n{}".format(file, msg))
+ if isinstance(file, (list, tuple)):
+ # TODO: this hack was to keep it backwards compatible.
+ parsed_project = self._def_parser.parse_string("\n".join(file))
else:
- parsed_files = parser.DefinitionFiles([p.parse_lines(file)])
+ parsed_project = self._def_parser.parse_file(file)
- for lineno, definition in parsed_files.iter_definitions():
- if definition.__class__ in p.handled_classes:
- continue
- loaderfunc = loaders.get(definition.__class__, None)
- if not loaderfunc:
- raise ValueError(
- f"No loader function defined "
- f"for {definition.__class__.__name__}"
- )
- loaderfunc(definition)
+ for definition in self._def_parser.iter_parsed_project(parsed_project):
+ self._helper_dispatch_adder(definition)
- return parsed_files
+ return parsed_project
def _build_cache(self, loaded_files=None) -> None:
"""Build a cache of dimensionality and plain units."""
- if loaded_files and self._diskcache and all(loaded_files):
- cache, cache_basename = self._diskcache.load(loaded_files, "build_cache")
+ diskcache = self._diskcache
+ if loaded_files and diskcache:
+ cache, cache_basename = diskcache.load(loaded_files, "build_cache")
if cache is None:
self._build_cache()
- self._diskcache.save(self._cache, loaded_files, "build_cache")
+ diskcache.save(self._cache, loaded_files, "build_cache")
return
self._cache = RegistryCache()
diff --git a/pint/facets/system/definitions.py b/pint/facets/system/definitions.py
index 368dd71..8243324 100644
--- a/pint/facets/system/definitions.py
+++ b/pint/facets/system/definitions.py
@@ -6,82 +6,76 @@
:license: BSD, see LICENSE for more details.
"""
-
from __future__ import annotations
-import re
+import typing as ty
from dataclasses import dataclass
-from typing import Tuple
-from ...util import SourceIterator
+from ... import errors
@dataclass(frozen=True)
-class SystemDefinition:
- """Definition of a System:
-
- @system <name> [using <group 1>, ..., <group N>]
- <rule 1>
- ...
- <rule N>
- @end
+class BaseUnitRule:
+ """A rule to define a base unit within a system."""
- The syntax for the rule is:
+ #: name of the unit to become base unit
+ #: (must exist in the registry)
+ new_unit_name: str
+ #: name of the unit to be kicked out to make room for the new base uni
+ #: If None, the current base unit with the same dimensionality will be used
+ old_unit_name: ty.Optional[str] = None
- new_unit_name : old_unit_name
+ # 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.
- where:
- - old_unit_name: a root unit part which is going to be removed from the system.
- - new_unit_name: a non root unit which is going to replace the old_unit.
- If the new_unit_name and the old_unit_name, the later and the colon can be omitted.
- """
-
- #: Regex to match the header parts of a context.
- _header_re = re.compile(r"@system\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+@dataclass(frozen=True)
+class SystemDefinition(errors.WithDefErr):
+ """Definition of a System."""
+ #: name of the system
name: str
- unit_replacements: Tuple[Tuple[int, str, str], ...]
- using_group_names: Tuple[str, ...]
+ #: unit groups that will be included within the system
+ using_group_names: ty.Tuple[str, ...]
+ #: rules to define new base unit within the system.
+ rules: ty.Tuple[BaseUnitRule, ...]
@classmethod
- def from_lines(cls, lines, non_int_type=float):
- lines = SourceIterator(lines)
-
- lineno, header = next(lines)
-
- r = cls._header_re.search(header)
-
- if r is None:
- raise ValueError("Invalid System header syntax '%s'" % header)
-
- name = r.groupdict()["name"].strip()
- groups = r.groupdict()["used_groups"]
-
- # If the systems has no group, it automatically uses the root group.
- if groups:
- group_names = tuple(a.strip() for a in groups.split(","))
- else:
- group_names = ("root",)
-
- unit_replacements = []
- for lineno, line in lines:
- line = line.strip()
-
- # We would identify a
- # - old_unit: a root unit part which is going to be removed from the system.
- # - new_unit: a non root unit which is going to replace the old_unit.
-
- if ":" in line:
- # The syntax is new_unit:old_unit
-
- new_unit, old_unit = line.split(":")
- new_unit, old_unit = new_unit.strip(), old_unit.strip()
-
- unit_replacements.append((lineno, new_unit, old_unit))
- else:
- # The syntax is new_unit
- # old_unit is inferred as the root unit with the same dimensionality.
- unit_replacements.append((lineno, line, None))
-
- return cls(name, tuple(unit_replacements), group_names)
+ 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
+
+ @property
+ def unit_replacements(self) -> ty.Tuple[ty.Tuple[str, str], ...]:
+ return tuple((el.new_unit_name, el.old_unit_name) for el in self.rules)
+
+ def __post_init__(self):
+ if not errors.is_valid_system_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_SYSTEM_NAME)
+
+ for k in self.using_group_names:
+ if not errors.is_valid_group_name(k):
+ raise self.def_err(
+ f"refers to '{k}' that " + errors.MSG_INVALID_GROUP_NAME
+ )
+
+ for ndx, rule in enumerate(self.rules, 1):
+ if not errors.is_valid_unit_name(rule.new_unit_name):
+ raise self.def_err(
+ f"rule #{ndx} refers to '{rule.new_unit_name}' that "
+ + errors.MSG_INVALID_UNIT_NAME
+ )
+ if rule.old_unit_name and not errors.is_valid_unit_name(rule.old_unit_name):
+ raise self.def_err(
+ f"rule #{ndx} refers to '{rule.old_unit_name}' that "
+ + errors.MSG_INVALID_UNIT_NAME
+ )
diff --git a/pint/facets/system/objects.py b/pint/facets/system/objects.py
index b81dfc5..829fb5c 100644
--- a/pint/facets/system/objects.py
+++ b/pint/facets/system/objects.py
@@ -119,16 +119,19 @@ class System(SharedRegistryObject):
return cls.from_definition(system_definition, get_root_func)
@classmethod
- def from_definition(cls, system_definition: SystemDefinition, get_root_func):
+ def from_definition(cls, system_definition: SystemDefinition, get_root_func=None):
+ if get_root_func is None:
+ # TODO: kept for backwards compatibility
+ get_root_func = cls._REGISTRY.get_root_units
base_unit_names = {}
derived_unit_names = []
- for lineno, new_unit, old_unit in system_definition.unit_replacements:
+ for new_unit, old_unit in system_definition.unit_replacements:
if old_unit is None:
old_unit_dict = to_units_container(get_root_func(new_unit)[1])
if len(old_unit_dict) != 1:
raise ValueError(
- "The new plain must be a root dimension if not discarded unit is specified."
+ "The new unit must be a root dimension if not discarded unit is specified."
)
old_unit, value = dict(old_unit_dict).popitem()
@@ -138,8 +141,8 @@ class System(SharedRegistryObject):
# The old unit MUST be a root unit, if not raise an error.
if old_unit != str(get_root_func(old_unit)[1]):
raise ValueError(
- "In `%s`, the unit at the right of the `:` (%s) must be a root unit."
- % (lineno, old_unit)
+ f"The old unit {old_unit} must be a root unit "
+ f"in order to be replaced by new unit {new_unit}"
)
# Here we find new_unit expanded in terms of root_units
diff --git a/pint/facets/system/registry.py b/pint/facets/system/registry.py
index ca60766..2bab44b 100644
--- a/pint/facets/system/registry.py
+++ b/pint/facets/system/registry.py
@@ -11,6 +11,8 @@ from __future__ import annotations
from numbers import Number
from typing import TYPE_CHECKING, Dict, FrozenSet, Tuple, Union
+from ... import errors
+
if TYPE_CHECKING:
from pint import Quantity, Unit
@@ -41,6 +43,9 @@ class SystemRegistry(GroupRegistry):
- Parse @group directive.
"""
+ # TODO: Change this to System: System to specify class
+ # and use introspection to get system class as a way
+ # to enjoy typing goodies
_system_class = System
def __init__(self, system=None, **kwargs):
@@ -77,13 +82,22 @@ class SystemRegistry(GroupRegistry):
"system", None
)
- def _register_directives(self) -> None:
- super()._register_directives()
- self._register_directive(
- "@system",
- lambda gd: self.System.from_definition(gd, self.get_root_units),
- SystemDefinition,
- )
+ def _register_definition_adders(self) -> None:
+ super()._register_definition_adders()
+ self._register_adder(SystemDefinition, self._add_system)
+
+ def _add_system(self, sd: SystemDefinition):
+
+ if sd.name in self._systems:
+ raise ValueError(f"System {sd.name} already present in registry")
+
+ try:
+ # As a System is a SharedRegistryObject
+ # it adds itself to the registry.
+ self.System.from_definition(sd)
+ except KeyError as e:
+ # TODO: fix this error message
+ raise errors.DefinitionError(f"unknown dimension {e} in context")
@property
def sys(self):