""" pint.facets.systems.objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: 2022 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ from __future__ import annotations import numbers from typing import Any, Optional from collections.abc import Iterable from typing import Callable, Generic from numbers import Number from ...babel_names import _babel_systems from ...compat import babel_parse from ...util import ( SharedRegistryObject, getattr_maybe_raise, logger, to_units_container, ) from .definitions import SystemDefinition from .. import group from ..plain import MagnitudeT from ..._typing import UnitLike GetRootUnits = Callable[[UnitLike, bool], tuple[Number, UnitLike]] class SystemQuantity(Generic[MagnitudeT], group.GroupQuantity[MagnitudeT]): pass class SystemUnit(group.GroupUnit): pass class System(SharedRegistryObject): """A system is a Group plus a set of plain units. Members are computed dynamically, that is if a unit is added to a group X all groups that include X are affected. The System belongs to one Registry. See SystemDefinition for the definition file syntax. Parameters ---------- name Name of the group. """ def __init__(self, name: str): #: Name of the system #: :type: str self.name = name #: Maps root unit names to a dict indicating the new unit and its exponent. self.base_units: dict[str, dict[str, numbers.Number]] = {} #: Derived unit names. self.derived_units: set[str] = set() #: Names of the _used_groups in used by this system. self._used_groups: set[str] = set() self._computed_members: Optional[frozenset[str]] = None # Add this system to the system dictionary self._REGISTRY._systems[self.name] = self def __dir__(self): return list(self.members) def __getattr__(self, item: str) -> Any: getattr_maybe_raise(self, item) u = getattr(self._REGISTRY, self.name + "_" + item, None) if u is not None: return u return getattr(self._REGISTRY, item) @property def members(self): d = self._REGISTRY._groups if self._computed_members is None: tmp: set[str] = set() for group_name in self._used_groups: try: tmp |= d[group_name].members except KeyError: logger.warning( "Could not resolve {} in System {}".format( group_name, self.name ) ) self._computed_members = frozenset(tmp) return self._computed_members def invalidate_members(self): """Invalidate computed members in this Group and all parent nodes.""" self._computed_members = None def add_groups(self, *group_names: str) -> None: """Add groups to group.""" self._used_groups |= set(group_names) self.invalidate_members() def remove_groups(self, *group_names: str) -> None: """Remove groups from group.""" self._used_groups -= set(group_names) self.invalidate_members() def format_babel(self, locale: str) -> str: """translate the name of the system.""" if locale and self.name in _babel_systems: name = _babel_systems[self.name] locale = babel_parse(locale) return locale.measurement_systems[name] return self.name # TODO: When 3.11 is minimal version, use Self @classmethod def from_lines( cls: type[System], lines: Iterable[str], get_root_func: GetRootUnits, non_int_type: type = float, ) -> System: # TODO: we changed something here it used to be # system_definition = SystemDefinition.from_lines(lines, get_root_func) system_definition = SystemDefinition.from_lines(lines, non_int_type) if system_definition is None: raise ValueError(f"Could not define System from from {lines}") return cls.from_definition(system_definition, get_root_func) @classmethod def from_definition( cls: type[System], system_definition: SystemDefinition, get_root_func: Optional[GetRootUnits] = None, ) -> System: 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 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 unit must be a root dimension if not discarded unit is specified." ) old_unit, value = dict(old_unit_dict).popitem() base_unit_names[old_unit] = {new_unit: 1 / value} else: # 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( 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 new_unit_expanded = to_units_container( get_root_func(new_unit)[1], cls._REGISTRY ) # We require that the old unit is present in the new_unit expanded if old_unit not in new_unit_expanded: raise ValueError("Old unit must be a component of new unit") # Here we invert the equation, in other words # we write old units in terms new unit and expansion new_unit_dict = { new_unit: -1 / value for new_unit, value in new_unit_expanded.items() if new_unit != old_unit } new_unit_dict[new_unit] = 1 / new_unit_expanded[old_unit] base_unit_names[old_unit] = new_unit_dict system = cls(system_definition.name) system.add_groups(*system_definition.using_group_names) system.base_units.update(**base_unit_names) system.derived_units |= set(derived_unit_names) return system class Lister: def __init__(self, d: dict[str, Any]): self.d = d def __dir__(self) -> list[str]: return list(self.d.keys()) def __getattr__(self, item: str) -> Any: getattr_maybe_raise(self, item) return self.d[item]