diff --git a/pint/_typing.py b/pint/_typing.py index 65e355cf1..05110ef7e 100644 --- a/pint/_typing.py +++ b/pint/_typing.py @@ -63,3 +63,7 @@ def __setitem__(self, key: Any, value: Any) -> None: FuncType = Callable[..., Any] F = TypeVar("F", bound=FuncType) + + +# TODO: Improve or delete types +QuantityArgument = Any diff --git a/pint/facets/context/objects.py b/pint/facets/context/objects.py index 38d8805e1..e2fc5eb69 100644 --- a/pint/facets/context/objects.py +++ b/pint/facets/context/objects.py @@ -10,12 +10,20 @@ import weakref from collections import ChainMap, defaultdict -from typing import Any +from typing import Any, Callable from collections.abc import Iterable from ...facets.plain import UnitDefinition from ...util import UnitsContainer, to_units_container from .definitions import ContextDefinition +from ..._typing import Magnitude + +Transformation = Callable[ + [ + Magnitude, + ], + Magnitude, +] class Context: @@ -75,14 +83,14 @@ def __init__( aliases: tuple[str] = tuple(), defaults: dict[str, Any] | None = None, ) -> None: - self.name = name - self.aliases = aliases + self.name: str | None = name + self.aliases: tuple[str] = aliases #: Maps (src, dst) -> transformation function - self.funcs = {} + self.funcs: dict[tuple[UnitsContainer, UnitsContainer], Transformation] = {} #: Maps defaults variable names to values - self.defaults = defaults or {} + self.defaults: dict[str, Any] = defaults or {} # Store Definition objects that are context-specific self.redefinitions = [] @@ -154,7 +162,9 @@ def from_definition(cls, cd: ContextDefinition, to_base_func=None) -> Context: return ctx - def add_transformation(self, src, dst, func) -> None: + def add_transformation( + self, src: UnitsContainer, dst: UnitsContainer, func: Transformation + ) -> None: """Add a transformation function to the context.""" _key = self.__keytransform__(src, dst) @@ -202,7 +212,7 @@ def _redefine(self, definition: UnitDefinition): def hashable( self, - ) -> tuple[str | None, tuple[str, ...], frozenset, frozenset, tuple]: + ) -> tuple[str | None, tuple[str], frozenset, frozenset, tuple]: """Generate a unique hashable and comparable representation of self, which can be used as a key in a dict. This class cannot define ``__hash__`` because it is mutable, and the Python interpreter does cache the output of ``__hash__``. diff --git a/pint/facets/group/objects.py b/pint/facets/group/objects.py index 200a3232e..f8c6ded48 100644 --- a/pint/facets/group/objects.py +++ b/pint/facets/group/objects.py @@ -8,10 +8,28 @@ from __future__ import annotations +from typing import Callable, Any, TYPE_CHECKING + from collections.abc import Generator, Iterable from ...util import SharedRegistryObject, getattr_maybe_raise from .definitions import GroupDefinition +if TYPE_CHECKING: + from ..plain import UnitDefinition + + DefineFunc = Callable[ + [ + Any, + ], + None, + ] + AddUnitFunc = Callable[ + [ + UnitDefinition, + ], + None, + ] + class Group(SharedRegistryObject): """A group is a set of units. @@ -57,7 +75,7 @@ def __init__(self, name: str): self._computed_members: frozenset[str] | None = None @property - def members(self): + def members(self) -> frozenset[str]: """Names of the units that are members of the group. Calculated to include to all units in all included _used_groups. @@ -143,7 +161,7 @@ def remove_groups(self, *group_names: str) -> None: @classmethod def from_lines( - cls, lines: Iterable[str], define_func, non_int_type: type = float + cls, lines: Iterable[str], define_func: DefineFunc, non_int_type: type = float ) -> Group: """Return a Group object parsing an iterable of lines. @@ -160,11 +178,15 @@ def from_lines( """ group_definition = GroupDefinition.from_lines(lines, non_int_type) + + if group_definition is None: + raise ValueError(f"Could not define group from {lines}") + return cls.from_definition(group_definition, define_func) @classmethod def from_definition( - cls, group_definition: GroupDefinition, add_unit_func=None + cls, group_definition: GroupDefinition, add_unit_func: AddUnitFunc | None = None ) -> Group: grp = cls(group_definition.name) diff --git a/pint/facets/plain/registry.py b/pint/facets/plain/registry.py index d3baff423..da0ec6c0c 100644 --- a/pint/facets/plain/registry.py +++ b/pint/facets/plain/registry.py @@ -20,6 +20,8 @@ from fractions import Fraction from numbers import Number from token import NAME, NUMBER +from tokenize import TokenInfo + from typing import ( TYPE_CHECKING, Any, @@ -33,7 +35,7 @@ from ..context import Context from ..._typing import Quantity, Unit -from ..._typing import QuantityOrUnitLike, UnitLike +from ..._typing import QuantityOrUnitLike, UnitLike, QuantityArgument from ..._vendor import appdirs from ...compat import HAS_BABEL, babel_parse, tokenizer from ...errors import DimensionalityError, RedefinitionError, UndefinedUnitError @@ -75,8 +77,10 @@ @functools.lru_cache -def pattern_to_regex(pattern): - if hasattr(pattern, "finditer"): +def pattern_to_regex(pattern: str | re.Pattern[str]) -> re.Pattern[str]: + # TODO: This has been changed during typing improvements. + # if hasattr(pattern, "finditer"): + if not isinstance(pattern, str): pattern = pattern.pattern # Replace "{unit_name}" match string with float regex with unit_name as group @@ -197,7 +201,15 @@ def __init__( mpl_formatter: str = "{:P}", ): #: Map a definition class to a adder methods. - self._adders = {} + self._adders: dict[ + type[T], + Callable[ + [ + T, + ], + None, + ], + ] = {} self._register_definition_adders() self._init_dynamic_classes() @@ -297,7 +309,16 @@ def _after_init(self) -> None: self._build_cache(loaded_files) self._initialized = True - def _register_adder(self, definition_class, adder_func): + def _register_adder( + self, + definition_class: type[T], + adder_func: Callable[ + [ + T, + ], + None, + ], + ) -> None: """Register a block definition.""" self._adders[definition_class] = adder_func @@ -316,18 +337,18 @@ def __deepcopy__(self, memo) -> PlainRegistry: new._init_dynamic_classes() return new - def __getattr__(self, item): + def __getattr__(self, item: str) -> Unit: getattr_maybe_raise(self, item) return self.Unit(item) - def __getitem__(self, item): + def __getitem__(self, item: str): logger.warning( "Calling the getitem method from a UnitRegistry is deprecated. " "use `parse_expression` method or use the registry as a callable." ) return self.parse_expression(item) - def __contains__(self, item) -> bool: + def __contains__(self, item: str) -> bool: """Support checking prefixed units with the `in` operator""" try: self.__getattr__(item) @@ -390,7 +411,7 @@ def cache_folder(self) -> pathlib.Path | None: def non_int_type(self): return self._non_int_type - def define(self, definition): + def define(self, definition: str | type) -> None: """Add unit to the registry. Parameters @@ -413,7 +434,7 @@ def define(self, definition): # - then we define specific adder for each definition class. :-D ############ - def _helper_dispatch_adder(self, definition): + def _helper_dispatch_adder(self, definition: Any) -> None: """Helper function to add a single definition, choosing the appropiate method by class. """ @@ -474,19 +495,19 @@ def _add_alias(self, definition: AliasDefinition): for alias in definition.aliases: self._helper_single_adder(alias, unit, self._units, self._units_casei) - def _add_dimension(self, definition: DimensionDefinition): + def _add_dimension(self, definition: DimensionDefinition) -> None: self._helper_adder(definition, self._dimensions, None) - def _add_derived_dimension(self, definition: DerivedDimensionDefinition): + def _add_derived_dimension(self, definition: DerivedDimensionDefinition) -> None: 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): + def _add_prefix(self, definition: PrefixDefinition) -> None: self._helper_adder(definition, self._prefixes, None) - def _add_unit(self, definition: UnitDefinition): + def _add_unit(self, definition: UnitDefinition) -> None: if definition.is_base: self._base_units.append(definition.name) for dim_name in definition.reference.keys(): @@ -673,7 +694,7 @@ def _get_dimensionality_recurse(self, ref, exp, accumulator): if reg.reference is not None: self._get_dimensionality_recurse(reg.reference, exp2, accumulator) - def _get_dimensionality_ratio(self, unit1, unit2): + def _get_dimensionality_ratio(self, unit1: UnitLike, unit2: UnitLike): """Get the exponential ratio between two units, i.e. solve unit2 = unit1**x for x. Parameters @@ -780,7 +801,9 @@ def _get_root_units(self, input_units, check_nonmult=True): cache[input_units] = factor, units return factor, units - def get_base_units(self, input_units, check_nonmult=True, system=None): + def get_base_units( + self, input_units: UnitsContainer | str, check_nonmult: bool = True, system=None + ): """Convert unit or dict of units to the plain units. If any unit is non multiplicative and check_converter is True, @@ -1104,7 +1127,32 @@ def _parse_units( return ret - def _eval_token(self, token, case_sensitive=None, **values): + def _eval_token( + self, + token: TokenInfo, + case_sensitive: bool | None = None, + **values: QuantityArgument, + ): + """Evaluate a single token using the following rules: + + 1. numerical values as strings are replaced by their numeric counterparts + - integers are parsed as integers + - other numeric values are parses of non_int_type + 2. strings in (inf, infinity, nan, dimensionless) with their numerical value. + 3. strings in values.keys() are replaced by Quantity(values[key]) + 4. in other cases, the values are parsed as units and replaced by their canonical name. + + Parameters + ---------- + token + Token to evaluate. + case_sensitive, optional + If true, a case sensitive matching of the unit name will be done in the registry. + If false, a case INsensitive matching of the unit name will be done in the registry. + (Default value = None, which uses registry setting) + **values + Other string that will be parsed using the Quantity constructor on their corresponding value. + """ token_type = token[0] token_text = token[1] if token_type == NAME: @@ -1139,28 +1187,25 @@ def parse_pattern( Parameters ---------- - input_string : + input_string pattern_string: - The regex parse string - case_sensitive : - (Default value = None, which uses registry setting) - many : + The regex parse string + case_sensitive, optional + If true, a case sensitive matching of the unit name will be done in the registry. + If false, a case INsensitive matching of the unit name will be done in the registry. + (Default value = None, which uses registry setting) + many, optional Match many results (Default value = False) - - - Returns - ------- - """ if not input_string: return [] if many else None # Parse string - pattern = pattern_to_regex(pattern) - matched = re.finditer(pattern, input_string) + regex = pattern_to_regex(pattern) + matched = re.finditer(regex, input_string) # Extract result(s) results = [] @@ -1196,16 +1241,14 @@ def parse_expression( Parameters ---------- - input_string : - - case_sensitive : - (Default value = None, which uses registry setting) - **values : - - - Returns - ------- - + input_string + + case_sensitive, optional + If true, a case sensitive matching of the unit name will be done in the registry. + If false, a case INsensitive matching of the unit name will be done in the registry. + (Default value = None, which uses registry setting) + **values + Other string that will be parsed using the Quantity constructor on their corresponding value. """ if not input_string: return self.Quantity(1) @@ -1215,8 +1258,9 @@ def parse_expression( input_string = string_preprocessor(input_string) gen = tokenizer(input_string) - return build_eval_tree(gen).evaluate( - lambda x: self._eval_token(x, case_sensitive=case_sensitive, **values) - ) + def _define_op(s: str): + return self._eval_token(s, case_sensitive=case_sensitive, **values) + + return build_eval_tree(gen).evaluate(_define_op) __call__ = parse_expression diff --git a/pint/facets/system/objects.py b/pint/facets/system/objects.py index 69b1c84e5..d8af8ea1d 100644 --- a/pint/facets/system/objects.py +++ b/pint/facets/system/objects.py @@ -14,7 +14,9 @@ from typing import Any from collections.abc import Iterable -from ..._typing import Self + +from typing import Callable +from numbers import Number from ...babel_names import _babel_systems from ...compat import babel_parse @@ -26,6 +28,10 @@ ) from .definitions import SystemDefinition +from ..._typing import UnitLike + +GetRootUnits = Callable[[UnitLike, bool], tuple[Number, UnitLike]] + class System(SharedRegistryObject): """A system is a Group plus a set of plain units. @@ -116,17 +122,26 @@ def format_babel(self, locale: str) -> str: return locale.measurement_systems[name] return self.name + # TODO: When 3.11 is minimal version, use Self + @classmethod def from_lines( - cls: type[Self], lines: Iterable[str], get_root_func, non_int_type: type = float - ) -> Self: + 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) return cls.from_definition(system_definition, get_root_func) @classmethod - def from_definition(cls, system_definition: SystemDefinition, get_root_func=None): + def from_definition( + cls: type[System], + system_definition: SystemDefinition, + get_root_func: GetRootUnits | None = None, + ) -> System: if get_root_func is None: # TODO: kept for backwards compatibility get_root_func = cls._REGISTRY.get_root_units diff --git a/pint/testing.py b/pint/testing.py index 8e4f15fea..d99df0be7 100644 --- a/pint/testing.py +++ b/pint/testing.py @@ -34,7 +34,7 @@ def _get_comparable_magnitudes(first, second, msg): return m1, m2 -def assert_equal(first, second, msg=None): +def assert_equal(first, second, msg: str | None = None) -> None: if msg is None: msg = f"Comparing {first!r} and {second!r}. " @@ -57,7 +57,9 @@ def assert_equal(first, second, msg=None): assert m1 == m2, msg -def assert_allclose(first, second, rtol=1e-07, atol=0, msg=None): +def assert_allclose( + first, second, rtol: float = 1e-07, atol: float = 0, msg: str | None = None +) -> None: if msg is None: try: msg = f"Comparing {first!r} and {second!r}. " diff --git a/pint/util.py b/pint/util.py index d75d1b5f4..793de47a4 100644 --- a/pint/util.py +++ b/pint/util.py @@ -47,6 +47,7 @@ T = TypeVar("T") TH = TypeVar("TH", bound=Hashable) +TT = TypeVar("TT", bound=type) # TODO: Change when Python 3.10 becomes minimal version. # ItMatrix: TypeAlias = Iterable[Iterable[PintScalar]] @@ -1124,7 +1125,9 @@ def sized(y: Any) -> bool: return True -def create_class_with_registry(registry: UnitRegistry, base_class: type) -> type: +def create_class_with_registry( + registry: UnitRegistry, base_class: type[TT] +) -> type[TT]: """Create new class inheriting from base_class and filling _REGISTRY class attribute with an actual instanced registry. """