From 006219c8aeac89f4245a401b6619bc995cae195a Mon Sep 17 00:00:00 2001 From: Eisinger Date: Tue, 20 Jan 2026 22:12:28 +0100 Subject: [PATCH 01/11] Intermediate push to clarify the 'enum problem' --- src/component_model/range.py | 51 +++++++++++++++++++++++++++++---- src/component_model/variable.py | 41 ++++++++++++++------------ tests/test_enums.py | 10 +++++-- tests/test_range.py | 44 +++++++++++++++++++++++++++- tests/test_variable.py | 8 +++--- 5 files changed, 122 insertions(+), 32 deletions(-) diff --git a/src/component_model/range.py b/src/component_model/range.py index c7157a9..99648c4 100644 --- a/src/component_model/range.py +++ b/src/component_model/range.py @@ -1,6 +1,6 @@ import logging from enum import Enum -from typing import Any +from typing import Any, Sequence, Never from component_model.unit import Unit @@ -29,7 +29,8 @@ class Range(object): def __init__( self, val: bool|int|float|str|Enum, - rng: tuple[int|float|Enum|str|None,int|float|Enum|str|None]|None|tuple[()] = tuple(), # type: ignore[assignment] +# rng: tuple[int|float|Enum|str|None,int|float|Enum|str|None]|None|tuple[()] = tuple(), # type: ignore[assignment] + rng: tuple[Any,Any]|None|Sequence[Never] = tuple(), unit: Unit|None = None, ): self.rng : tuple[int|bool|float|str, int|bool|float|str] @@ -44,14 +45,14 @@ def __init__( self.rng = (unit.from_base(val), unit.from_base(val)) elif isinstance(rng, tuple) and not len(rng): # empty tuple => try automatic range self.rng = Range.auto_extreme(val) # fails if val is an int variable - else: # rng should be a 2-tuple for a bool, int, float, Enum variable. One or two elements might be None - assert isinstance(rng, tuple) and len(rng) == 2, f"Expect 2-tuple at this point. Found {rng}." - assert all( x is None or isinstance(x, (str,int,bool,float,Enum)) for x in rng), f"Unexpected type in {rng}" + elif ( isinstance(rng, tuple) and len(rng) == 2 and + all( x is None or isinstance(x, (str,int,bool,float,Enum)) for x in rng)): l_rng = list(rng) # work on a mutable object for i, r in enumerate(rng): if r is None: l_rng[i] = unit.from_base(val) # replace with fixed value 'val' as display value else: + assert isinstance(r, (str,int,bool,float,Enum)), f"Found type {type(r)}" check, q = unit.compatible(r, typ, strict=True) if not check: raise ValueError(f"Provided range {rng} is not conformant with unit {unit}") from None @@ -63,6 +64,8 @@ def __init__( raise TypeError(f"Incompatible types range {rng} - {val}") from err l_rng[i] = q self.rng = tuple(l_rng) # type: ignore ## cannot see how tuple contains str or None here! + else: + raise TypeError(f"Unhandled range specification {rng}) from None") @classmethod @@ -133,3 +136,41 @@ def check( else: logger.error(f"range check(): value={value}, type={typ}, range={self.rng}") return False + + @classmethod + def is_valid_spec(cls, + rng : tuple[Any,...] | tuple[Any,Any] | None | Sequence[Never], + var_len : int, + typ: type, + level: int=0) -> int: + """Check whether the supplied rng is a valid range specification for a variable. + Applies to scalar and compound variable specs. + Return 0 (ok) or error code >0 if not ok. + """ + if rng is None: + ck = 0 # fixed value(s) + elif isinstance(rng, tuple) and not len(rng): # all automatic + ck = int(typ == int) # 1/0 (not possible for int) + elif isinstance(rng, tuple): # need a tuple now + if var_len == 1: + if len(rng) != 2: # scalar specified by a 2-tuple + ck = 2 + else: # final check of scalar spec 2-tuple + ck = 0 + for i,r in enumerate(rng): + if r is not None and not isinstance(r, (int, bool, float, Enum, str)): + ck += 10 + i + if not any(isinstance(rng[i], str) for i in range(2)): + if rng[0] > rng[1]: # wrong order + ck += 10+9 + + elif var_len > 1: + if len(rng) != var_len: # one range for each variable + ck = 3 + else: + ck = 0 + for i,r in enumerate(rng): + ck += Range.is_valid_spec( r, 1, typ, level=(i+1)*100) + else: + ck = 4 # would need a tuple here + return ck if ck==0 else level+ck diff --git a/src/component_model/variable.py b/src/component_model/variable.py index d669dba..4083003 100644 --- a/src/component_model/variable.py +++ b/src/component_model/variable.py @@ -3,7 +3,7 @@ import logging import xml.etree.ElementTree as ET # noqa: N817 from enum import Enum -from typing import Any, Callable, Sequence, TypeAlias +from typing import Any, Callable, Sequence, TypeAlias, Never import numpy as np from pythonfmu.enums import Fmi2Causality as Causality # type: ignore @@ -18,7 +18,8 @@ logger = logging.getLogger(__name__) PyType: TypeAlias = str | int | float | bool | Enum -RngSingle: TypeAlias = tuple[int | float | None, int | float | None] | None | tuple[()] +#RngSingle: TypeAlias = tuple[int | float | None, int | float | None] | None | tuple[()] +RngSingle: TypeAlias = tuple[Any,Any]|None|Sequence[Never] Numeric: TypeAlias = int | float Compound: TypeAlias = tuple[PyType, ...] | list[PyType] | np.ndarray @@ -67,7 +68,7 @@ class Variable(ScalarVariable): typ (type)=None: Optional explicit type of variable to expect as start and value. Since initial values are often set with strings (with units, see below), this is set explicitly. If None, _typ is set to Enum/str if derived from these after disection or float if a number. 'int' is not automatically detected. - start (PyType): The initial value of the variable. + start (PyType): The initial value(s) of the variable. Optionally, the unit can be included, providing the initial value as string, evaluating to quantity of type typ a display unit and base unit. @@ -168,8 +169,6 @@ def __init__( assert isinstance(basevar, Variable), f"The primitive of {self.name} must be a Variable object" assert basevar.typ is float, f"The primitive of {self.name} shall be float. Found {basevar.typ}" self._typ = float - if start is None: - self._start, self._unit = Unit.derivative(basevar.unit) if self.on_step is None: self.on_step = self.der1 else: @@ -177,24 +176,26 @@ def __init__( else: self.local_name = local_name # use explicitly provided local name + if start is None: + assert local_name is None, f"{self.name} Default start value only defined for derivatives" + self._start, self._unit = Unit.derivative(basevar.unit) + elif self._typ is str or not isinstance(start, (tuple, list, np.ndarray)): + self._start, self._unit = Unit.make(start, self._typ) + else: + self._start, self._unit = Unit.make_tuple(start, self._typ) + self._len = 1 if self._typ is str else len(self._start) + if self._typ is None: # try to adapt using start + self._typ = self.auto_type(self._start) + assert isinstance(self._typ, type) + self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct + + ck = Range.is_valid_spec( rng, self._len, self._typ) + assert 0==ck, f"{self.name} invalid range spec {rng}. Error {ck}" if self._typ is str: # explicit free string. String arrays are so far not implemented assert isinstance(start, str) - self._len = 1 - self._start, self._unit = Unit.make(start, typ=str) self._range = (Range(self._start[0], unit=self._unit[0]),) # Strings have fixed range else: - # if type is provided and no (initial) value. We set a default value of the correct type as 'example' value - if not len(self._start): # not yet set - assert start is not None, f"{self.name}: Provide start value, at least for type and unit determination" - if isinstance(start, (tuple, list, np.ndarray)): - self._start, self._unit = Unit.make_tuple(start, self._typ) - else: - self._start, self._unit = Unit.make(start, self._typ) - self._len = len(self._start) - if self._typ is None: # try to adapt using start - self._typ = self.auto_type(self._start) - assert isinstance(self._typ, type) - self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct + self._range : tuple(Range,...) if self._len == 1: self._range = (Range(self._start[0], rng, self._unit[0]),) else: @@ -630,3 +631,5 @@ def primitive(self) -> Variable | None: else: name = parsed.as_string(("parent", "var", "der"), simplified=True, primitive=True) return self.model.variable_by_name(name) + + diff --git a/tests/test_enums.py b/tests/test_enums.py index a0842ff..3586649 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -1,5 +1,5 @@ import logging -from enum import Enum +from enum import Enum, EnumType, EnumCheck, EnumMeta import pytest from pythonfmu.enums import Fmi2Causality as Causality # type: ignore @@ -14,10 +14,14 @@ def test_enum(): + def enum_func( e:Enum): + return e._member_names_ + f = VariableNamingConvention.flat assert isinstance(f, Enum) assert type(f) is VariableNamingConvention assert type(f)["structured"] == VariableNamingConvention.structured + print("Members: ", enum_func( VariableNamingConvention.flat)) def test_combinations(): @@ -64,9 +68,9 @@ def test_check(): if __name__ == "__main__": - retcode = pytest.main(["-rA", "-v", __file__]) + retcode = 0#pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" - # test_enum() + test_enum() # test_combinations() # test_ensure_enum() # test_check() diff --git a/tests/test_range.py b/tests/test_range.py index 2c68909..05bc847 100644 --- a/tests/test_range.py +++ b/tests/test_range.py @@ -1,9 +1,15 @@ +import logging import numpy as np import pytest +from enum import Enum +from typing import Any, Sequence, Never from component_model.enums import Check from component_model.range import Range from component_model.unit import Unit +from component_model.enums import Check + +logger = logging.getLogger(__name__) @pytest.fixture @@ -45,9 +51,45 @@ def test_auto_extreme(): assert str(err.value) == "Auto-extremes for type cannot be determined" +def test_range_spec(): + def do_check(example:Any, + rng : tuple[Any,...] | tuple[Any,Any] | None | Sequence[Never], + var_len : int, + typ: type, + level: int=0, + expect:int=0, + msg:str = "")-> int: + ck = Range.is_valid_spec( rng, var_len, typ, level) + if ck == 0: + if var_len==1: + try: + _rng = Range(example, rng) + except ValueError as err: + logger.error(f"{rng} set to valid, but {err}") + else: + for e,r in zip(example,rng, strict=True): + try: + _rng = Range(e, r) + except ValueError as err: + logger.error(f"{r} set to valid, but {err}") + assert ck == expect, f"{msg}: Range:{rng}: {ck} != {expect}" + + Unit.ensure_unit_registry() + do_check( 9.9, tuple(), 1, float, expect=0, msg="Valid single variable spec with automatic floats.") + do_check( 9, tuple(), 1, int, expect=1, msg="InValid single variable spec with automatic int.") + do_check( 9, None, 1, int, expect=0, msg="Valid fixed single variable spec (any type).") + do_check( Check.all, None, 1, Enum, expect=0, msg="Valid fixed single variable spec (any type).") + do_check( Check.all, tuple(), 1, Enum, expect=0, msg="Valid automatic enum single variable range.") + do_check( 1, (1,2), 1, int, expect=0, msg="Valid range of single int variable.") + do_check( 5.0, (9.9,2.0), 1, float, expect=19, msg="InValid range of single float variable: wrong order.") + do_check( 1.0, ("1m",2.0), 1, float, expect=0, msg="Valid range of single float variable (partial units).") + do_check( (1.0,2.0,3.0), (None,tuple(),(1.0,2.0)), 3, float, expect=0, msg="Valid mixed vector spec.") + + if __name__ == "__main__": - retcode = pytest.main(["-rP -s -v", __file__]) + retcode = 0#pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" # _unt() # initialize UnitRegistry (otherwise Unit cannot be used) # test_init(_unt()) # test_auto_extreme() + test_range_spec() diff --git a/tests/test_variable.py b/tests/test_variable.py index f464c53..8fcbb92 100644 --- a/tests/test_variable.py +++ b/tests/test_variable.py @@ -521,7 +521,7 @@ def test_init(): ) assert err2.value.args[0] == "Variable int1 already used as index 0 in model MyModel" - with pytest.raises(ValueError) as err3: + with pytest.raises(AssertionError) as err3: int1 = Variable( mod, "bool1", @@ -533,7 +533,7 @@ def test_init(): annotations=None, typ=int, ) - assert err3.value.args[0] == "Auto-extremes for type cannot be determined" + assert err3.value.args[0].startswith("bool1 invalid range spec") assert float1.range[0].rng[1] == 99.0 assert enum1.range[0].rng == (0, 4) assert enum1.check_range([Causality.parameter]) @@ -760,9 +760,9 @@ def test_extremum(): if __name__ == "__main__": - retcode = pytest.main(["-rP -s -v", __file__]) + retcode = 0#pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" - # test_init() + test_init() # test_range() # test_var_check() # test_spherical_cartesian() From 39c78fa2a8812fc681e77e0421106959b1911c20 Mon Sep 17 00:00:00 2001 From: Eisinger Date: Wed, 21 Jan 2026 10:59:46 +0100 Subject: [PATCH 02/11] Another intemediate push, where now mypy errors are resolved. pyright still pending --- src/component_model/enums.py | 29 +++++++----- src/component_model/range.py | 64 +++++++++++++------------- src/component_model/unit.py | 9 ++-- src/component_model/variable.py | 30 +++++++----- src/component_model/variable_naming.py | 11 +++-- tests/test_enums.py | 24 ++++++---- tests/test_range.py | 53 +++++++++++---------- tests/test_solver.py | 4 +- tests/test_variable.py | 2 +- 9 files changed, 128 insertions(+), 98 deletions(-) diff --git a/src/component_model/enums.py b/src/component_model/enums.py index 1de0b74..7e316c0 100644 --- a/src/component_model/enums.py +++ b/src/component_model/enums.py @@ -1,7 +1,7 @@ """Additional Enum objects for component-model and enum-related utilities.""" import logging -from enum import Enum, IntFlag, EnumType +from enum import Enum, EnumType, IntFlag # import pythonfmu.enums # type: ignore from pythonfmu.enums import Fmi2Causality as Causality # type: ignore @@ -11,18 +11,25 @@ logger = logging.getLogger(__name__) -def ensure_enum(org: str | EnumType | None, default: EnumType | None) -> Enum | None: +def ensure_enum(org: str | Enum | None, default: Enum | EnumType | None) -> Enum | None: """Ensure that we have an Enum, based on the input as str, Enum or None.""" - if org is None: + if org is None and default is None: + return None + raise ValueError("org and default shall not both be None") from None + elif org is None: + assert isinstance(default, Enum), f"Need an Enum (member) as default if org=None. Found {type(default)}" return default - elif isinstance(org, str): - assert isinstance(default, Enum), "Need a default Enum here" - if org in type(default).__members__: - return type(default)[org] + elif default is None: + assert isinstance(org, Enum), "When no default is provided, org must be an Enum." + return org + elif isinstance(org, str): # both provided and org is a string + _default = default if isinstance(default, EnumType) else type(default) # need the Enum itself + if org in _default.__members__: + return _default[org] else: - raise Exception(f"The value {org} is not compatible with the Enum {type(default)}") from None - else: # expect already an Enum - assert default is None or isinstance(org, type(default)), f"{org} is not member of the Enum {type(default)}" + raise Exception(f"The value {org} is not compatible with the Enum {_default}") from None + else: # expect already an EnumType + assert isinstance(org, type(default)), f"{org} is not member of the Enum {type(default)}" return org @@ -92,7 +99,7 @@ def check_causality_variability_initial( logger.info(f"(causality {_causality}, variability {variability}) is not allowed: {explanations[res]}") return (None, None, None) else: # allowed - _initial = ensure_enum(initial, initial_default[res][0]) # type: ignore + _initial = ensure_enum(initial, initial_default[res][0]) if _initial not in initial_default[res][1]: logger.info(f"(Causality {_causality}, variability {_variability}, Initial {_initial}) is not allowed") return (None, None, None) diff --git a/src/component_model/range.py b/src/component_model/range.py index 99648c4..c005cda 100644 --- a/src/component_model/range.py +++ b/src/component_model/range.py @@ -1,6 +1,6 @@ import logging from enum import Enum -from typing import Any, Sequence, Never +from typing import Any, Never, Sequence from component_model.unit import Unit @@ -28,12 +28,12 @@ class Range(object): def __init__( self, - val: bool|int|float|str|Enum, -# rng: tuple[int|float|Enum|str|None,int|float|Enum|str|None]|None|tuple[()] = tuple(), # type: ignore[assignment] - rng: tuple[Any,Any]|None|Sequence[Never] = tuple(), - unit: Unit|None = None, + val: bool | int | float | str | Enum, + # rng: tuple[int|float|Enum|str|None,int|float|Enum|str|None]|None|tuple[()] = tuple(), # type: ignore[assignment] + rng: tuple[Any, Any] | None | Sequence[Never] = tuple(), + unit: Unit | None = None, ): - self.rng : tuple[int|bool|float|str, int|bool|float|str] + self.rng: tuple[int | bool | float | str, int | bool | float | str] typ = type(val) if unit is None: unit = Unit() @@ -45,29 +45,31 @@ def __init__( self.rng = (unit.from_base(val), unit.from_base(val)) elif isinstance(rng, tuple) and not len(rng): # empty tuple => try automatic range self.rng = Range.auto_extreme(val) # fails if val is an int variable - elif ( isinstance(rng, tuple) and len(rng) == 2 and - all( x is None or isinstance(x, (str,int,bool,float,Enum)) for x in rng)): + elif ( + isinstance(rng, tuple) + and len(rng) == 2 + and all(x is None or isinstance(x, (str, int, bool, float, Enum)) for x in rng) + ): l_rng = list(rng) # work on a mutable object for i, r in enumerate(rng): if r is None: l_rng[i] = unit.from_base(val) # replace with fixed value 'val' as display value else: - assert isinstance(r, (str,int,bool,float,Enum)), f"Found type {type(r)}" + assert isinstance(r, (str, int, bool, float, Enum)), f"Found type {type(r)}" check, q = unit.compatible(r, typ, strict=True) if not check: raise ValueError(f"Provided range {rng} is not conformant with unit {unit}") from None q = unit.from_base(q) # ensure display units - assert isinstance(q, (int,bool,float)), "Unexpected type {type(q)} in {rng}[{i}]" + assert isinstance(q, (int, bool, float)), "Unexpected type {type(q)} in {rng}[{i}]" try: q = type(val)(q) # ensure correct Python type except Exception as err: raise TypeError(f"Incompatible types range {rng} - {val}") from err l_rng[i] = q - self.rng = tuple(l_rng) # type: ignore ## cannot see how tuple contains str or None here! + self.rng = tuple(l_rng) # type: ignore ## cannot see how tuple contains str or None here! else: raise TypeError(f"Unhandled range specification {rng}) from None") - @classmethod def auto_extreme(cls, var: bool | int | float | str | Enum | type) -> tuple[int | float | bool, int | float | bool]: """Return the extreme values of the variable. @@ -132,45 +134,43 @@ def check( assert typ is int or typ is float, f"Inconsistent type {typ}. Expect int or float" if not disp and unit.du is not None: # check an internal unit values value = unit.from_base(value) - return self.rng[0] <= value <= self.rng[1] # type: ignore[operator] ## There is no str involved! + return self.rng[0] <= value <= self.rng[1] # type: ignore[operator] ## There is no str involved! else: logger.error(f"range check(): value={value}, type={typ}, range={self.rng}") return False @classmethod - def is_valid_spec(cls, - rng : tuple[Any,...] | tuple[Any,Any] | None | Sequence[Never], - var_len : int, - typ: type, - level: int=0) -> int: + def is_valid_spec( + cls, rng: tuple[Any, ...] | tuple[Any, Any] | None | Sequence[Never], var_len: int, typ: type, level: int = 0 + ) -> int: """Check whether the supplied rng is a valid range specification for a variable. Applies to scalar and compound variable specs. Return 0 (ok) or error code >0 if not ok. """ if rng is None: - ck = 0 # fixed value(s) - elif isinstance(rng, tuple) and not len(rng): # all automatic - ck = int(typ == int) # 1/0 (not possible for int) - elif isinstance(rng, tuple): # need a tuple now + ck = 0 # fixed value(s) + elif isinstance(rng, tuple) and not len(rng): # all automatic + ck = int(typ is int) # 1/0 (not possible for int) + elif isinstance(rng, tuple): # need a tuple now if var_len == 1: - if len(rng) != 2: # scalar specified by a 2-tuple + if len(rng) != 2: # scalar specified by a 2-tuple ck = 2 - else: # final check of scalar spec 2-tuple + else: # final check of scalar spec 2-tuple ck = 0 - for i,r in enumerate(rng): + for i, r in enumerate(rng): if r is not None and not isinstance(r, (int, bool, float, Enum, str)): ck += 10 + i if not any(isinstance(rng[i], str) for i in range(2)): - if rng[0] > rng[1]: # wrong order - ck += 10+9 + if rng[0] > rng[1]: # wrong order + ck += 10 + 9 elif var_len > 1: - if len(rng) != var_len: # one range for each variable + if len(rng) != var_len: # one range for each variable ck = 3 else: ck = 0 - for i,r in enumerate(rng): - ck += Range.is_valid_spec( r, 1, typ, level=(i+1)*100) + for i, r in enumerate(rng): + ck += Range.is_valid_spec(r, 1, typ, level=(i + 1) * 100) else: - ck = 4 # would need a tuple here - return ck if ck==0 else level+ck + ck = 4 # would need a tuple here + return ck if ck == 0 else level + ck diff --git a/src/component_model/unit.py b/src/component_model/unit.py index 4496333..5914ee8 100644 --- a/src/component_model/unit.py +++ b/src/component_model/unit.py @@ -136,6 +136,7 @@ def val_unit_display(self, q: Quantity[float]) -> float: def make( cls, quantity: bool | int | float | str | Enum, typ: type | None = None ) -> tuple[tuple[bool | int | float | str | Enum], tuple["Unit"]]: + """Parse quantity and return the resulting value and its unit object.""" u = Unit() val = u.parse_quantity(quantity, typ) return ((val,), (u,)) @@ -143,10 +144,10 @@ def make( @classmethod def make_tuple( cls, - quantities: tuple[bool|int|float|str|Enum, ...] |list[bool|int|float|str|Enum] | np.ndarray, - typ: type | None = None - ) -> tuple[tuple[bool|int|float|str|Enum, ...], tuple["Unit", ...]]: - """Make a tuple of Unit objects from the tuple of quantities.""" + quantities: tuple[bool | int | float | str | Enum, ...] | list[bool | int | float | str | Enum] | np.ndarray, + typ: type | None = None, + ) -> tuple[tuple[bool | int | float | str | Enum, ...], tuple["Unit", ...]]: + """Make a tuple of values and Unit objects from the tuple of quantities, using make().""" values: list[bool | int | float | str | Enum] = [] units: list[Unit] = [] for q in quantities: diff --git a/src/component_model/variable.py b/src/component_model/variable.py index 4083003..fd5c56f 100644 --- a/src/component_model/variable.py +++ b/src/component_model/variable.py @@ -3,7 +3,7 @@ import logging import xml.etree.ElementTree as ET # noqa: N817 from enum import Enum -from typing import Any, Callable, Sequence, TypeAlias, Never +from typing import Any, Callable, Never, Sequence, TypeAlias import numpy as np from pythonfmu.enums import Fmi2Causality as Causality # type: ignore @@ -18,8 +18,8 @@ logger = logging.getLogger(__name__) PyType: TypeAlias = str | int | float | bool | Enum -#RngSingle: TypeAlias = tuple[int | float | None, int | float | None] | None | tuple[()] -RngSingle: TypeAlias = tuple[Any,Any]|None|Sequence[Never] +# RngSingle: TypeAlias = tuple[int | float | None, int | float | None] | None | tuple[()] +RngSingle: TypeAlias = tuple[Any, Any] | None | Sequence[Never] Numeric: TypeAlias = int | float Compound: TypeAlias = tuple[PyType, ...] | list[PyType] | np.ndarray @@ -178,9 +178,10 @@ def __init__( if start is None: assert local_name is None, f"{self.name} Default start value only defined for derivatives" + assert basevar is not None, f"{self.name} basevar needed at this point" self._start, self._unit = Unit.derivative(basevar.unit) elif self._typ is str or not isinstance(start, (tuple, list, np.ndarray)): - self._start, self._unit = Unit.make(start, self._typ) + self._start, self._unit = Unit.make(start, self._typ) # type: ignore ## type of start should be ok else: self._start, self._unit = Unit.make_tuple(start, self._typ) self._len = 1 if self._typ is str else len(self._start) @@ -189,17 +190,26 @@ def __init__( assert isinstance(self._typ, type) self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct - ck = Range.is_valid_spec( rng, self._len, self._typ) - assert 0==ck, f"{self.name} invalid range spec {rng}. Error {ck}" + ck = Range.is_valid_spec(rng, self._len, self._typ) + assert 0 == ck, f"{self.name} invalid range spec {rng}. Error {ck}" + self._range: tuple[Range, ...] if self._typ is str: # explicit free string. String arrays are so far not implemented assert isinstance(start, str) self._range = (Range(self._start[0], unit=self._unit[0]),) # Strings have fixed range else: - self._range : tuple(Range,...) if self._len == 1: - self._range = (Range(self._start[0], rng, self._unit[0]),) + self._range = (Range(self._start[0], rng, self._unit[0]),) # type: ignore[arg-type] ## is_valid_spec else: - self._range = tuple([Range(self._start[i], rng[i], self._unit[i]) for i in range(self._len)]) + self._range = tuple( + [ + Range( + self._start[i], + rng[i], # type: ignore[index] ## is_valid_spec + self._unit[i], + ) + for i in range(self._len) + ] + ) if not self.check_range(self._start, disp=False): # range checks of initial value logger.critical(f"The provided value {self._start} is not in the valid range {self._range}") @@ -631,5 +641,3 @@ def primitive(self) -> Variable | None: else: name = parsed.as_string(("parent", "var", "der"), simplified=True, primitive=True) return self.model.variable_by_name(name) - - diff --git a/src/component_model/variable_naming.py b/src/component_model/variable_naming.py index 207c4b8..b53b071 100644 --- a/src/component_model/variable_naming.py +++ b/src/component_model/variable_naming.py @@ -27,19 +27,21 @@ class ParsedVariable: * der: unsigned integer, defining the derivation order. 0 for no derivation """ - def __init__(self, varname: str, convention: VariableNamingConvention = VariableNamingConvention.structured): + def __init__(self, varname: str, convention: Enum | None = VariableNamingConvention.structured): self.parent: str | None # None indicates no parent self.var: str self.indices: list[int] = [] # empty list indicates no indices self.der: int = 0 # 0 indicates 'no derivative' - if convention == VariableNamingConvention.flat: # expect python-conformant name (with indexing) + if ( + convention is None or convention == VariableNamingConvention.flat + ): # expect (indexed) python-conformant names var, indices = ParsedVariable.disect_indices(varname) self.parent = None self.var = var self.indices = indices self.der = 0 - else: # structured variable naming (only these two are defined) + elif convention == VariableNamingConvention.structured: # structured variable naming self.der = 0 # default and count start var = varname while True: @@ -62,7 +64,8 @@ def __init__(self, varname: str, convention: VariableNamingConvention = Variable self.parent = None self.var, self.indices = ParsedVariable.disect_indices(var) - # assert self.var.isidentifier(), f"The variable name {self.var} is not a valid identifier" + else: + raise ValueError("VariableNamingConvention Enum expected. Got {convention}") def as_tuple(self): """Return all fields as tuple.""" diff --git a/tests/test_enums.py b/tests/test_enums.py index 3586649..a8ba70b 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -1,5 +1,5 @@ import logging -from enum import Enum, EnumType, EnumCheck, EnumMeta +from enum import Enum, EnumType import pytest from pythonfmu.enums import Fmi2Causality as Causality # type: ignore @@ -14,14 +14,22 @@ def test_enum(): - def enum_func( e:Enum): - return e._member_names_ - + def enum_func(e: Enum) -> None: + assert isinstance(e, Enum), f"Argument {e} should be an enum member" + logger.info(f"Name:{e.name}, value:{e.value}") + + def enumtype_func(e: EnumType): + assert isinstance(e, EnumType), f"Argument {e} should be an EnumType, i.e. the Enum Class itself" + logger.info(f"Members:{e._member_names_}") + f = VariableNamingConvention.flat assert isinstance(f, Enum) assert type(f) is VariableNamingConvention assert type(f)["structured"] == VariableNamingConvention.structured - print("Members: ", enum_func( VariableNamingConvention.flat)) + logger.info(f"Type of Enum class itself:{type(VariableNamingConvention)}") + logger.info(f"Type of member:{type(VariableNamingConvention.flat)}") + enum_func(VariableNamingConvention.flat) + enumtype_func(VariableNamingConvention) def test_combinations(): @@ -37,7 +45,7 @@ def test_ensure_enum(): assert str(err.value).startswith("The value input is not compatible with ") assert ensure_enum("discrete", Variability.continuous) == Variability.discrete assert ensure_enum("input", Causality.local) == Causality.input - assert ensure_enum(None, Causality.input) == Causality.input + assert ensure_enum(None, Causality.input) == Causality.input, f"Found {ensure_enum(None, Causality.input)}" def test_check(): @@ -68,9 +76,9 @@ def test_check(): if __name__ == "__main__": - retcode = 0#pytest.main(["-rA", "-v", __file__]) + retcode = pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" - test_enum() + # test_enum() # test_combinations() # test_ensure_enum() # test_check() diff --git a/tests/test_range.py b/tests/test_range.py index 05bc847..efc52dc 100644 --- a/tests/test_range.py +++ b/tests/test_range.py @@ -1,13 +1,13 @@ import logging +from enum import Enum +from typing import Any, Never, Sequence + import numpy as np import pytest -from enum import Enum -from typing import Any, Sequence, Never from component_model.enums import Check from component_model.range import Range from component_model.unit import Unit -from component_model.enums import Check logger = logging.getLogger(__name__) @@ -52,42 +52,45 @@ def test_auto_extreme(): def test_range_spec(): - def do_check(example:Any, - rng : tuple[Any,...] | tuple[Any,Any] | None | Sequence[Never], - var_len : int, - typ: type, - level: int=0, - expect:int=0, - msg:str = "")-> int: - ck = Range.is_valid_spec( rng, var_len, typ, level) + def do_check( + example: Any, + rng: tuple[Any, ...] | tuple[Any, Any] | None | Sequence[Never], + var_len: int, + typ: type, + level: int = 0, + expect: int = 0, + msg: str = "", + ): + ck = Range.is_valid_spec(rng, var_len, typ, level) if ck == 0: - if var_len==1: + if var_len == 1: try: _rng = Range(example, rng) except ValueError as err: logger.error(f"{rng} set to valid, but {err}") else: - for e,r in zip(example,rng, strict=True): + assert isinstance(rng, tuple) and len(rng) == len(example) + for e, r in zip(example, rng, strict=True): try: _rng = Range(e, r) except ValueError as err: logger.error(f"{r} set to valid, but {err}") assert ck == expect, f"{msg}: Range:{rng}: {ck} != {expect}" - + Unit.ensure_unit_registry() - do_check( 9.9, tuple(), 1, float, expect=0, msg="Valid single variable spec with automatic floats.") - do_check( 9, tuple(), 1, int, expect=1, msg="InValid single variable spec with automatic int.") - do_check( 9, None, 1, int, expect=0, msg="Valid fixed single variable spec (any type).") - do_check( Check.all, None, 1, Enum, expect=0, msg="Valid fixed single variable spec (any type).") - do_check( Check.all, tuple(), 1, Enum, expect=0, msg="Valid automatic enum single variable range.") - do_check( 1, (1,2), 1, int, expect=0, msg="Valid range of single int variable.") - do_check( 5.0, (9.9,2.0), 1, float, expect=19, msg="InValid range of single float variable: wrong order.") - do_check( 1.0, ("1m",2.0), 1, float, expect=0, msg="Valid range of single float variable (partial units).") - do_check( (1.0,2.0,3.0), (None,tuple(),(1.0,2.0)), 3, float, expect=0, msg="Valid mixed vector spec.") - + do_check(9.9, tuple(), 1, float, expect=0, msg="Valid single variable spec with automatic floats.") + do_check(9, tuple(), 1, int, expect=1, msg="InValid single variable spec with automatic int.") + do_check(9, None, 1, int, expect=0, msg="Valid fixed single variable spec (any type).") + do_check(Check.all, None, 1, Enum, expect=0, msg="Valid fixed single variable spec (any type).") + do_check(Check.all, tuple(), 1, Enum, expect=0, msg="Valid automatic enum single variable range.") + do_check(1, (1, 2), 1, int, expect=0, msg="Valid range of single int variable.") + do_check(5.0, (9.9, 2.0), 1, float, expect=19, msg="InValid range of single float variable: wrong order.") + do_check(1.0, ("1m", 2.0), 1, float, expect=0, msg="Valid range of single float variable (partial units).") + do_check((1.0, 2.0, 3.0), (None, tuple(), (1.0, 2.0)), 3, float, expect=0, msg="Valid mixed vector spec.") + if __name__ == "__main__": - retcode = 0#pytest.main(["-rP -s -v", __file__]) + retcode = 0 # pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" # _unt() # initialize UnitRegistry (otherwise Unit cannot be used) # test_init(_unt()) diff --git a/tests/test_solver.py b/tests/test_solver.py index 2a597ae..3cf2747 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -102,8 +102,8 @@ def hit_ground(t: np.ndarray, y: np.ndarray): (0, 100), np.array((0, 200), float), t_eval=np.array([t for t in range(100)]), - events=hit_ground # type: ignore - ) + events=hit_ground, # type: ignore + ) assert np.allclose(sol.t_events, [2 * 200 / 9.81]), "Time when hitting the ground" # type: ignore ## it works assert np.allclose(sol.y_events, [[0.0, -200.0]]), "Position and speed when hitting the ground" # type: ignore if show: diff --git a/tests/test_variable.py b/tests/test_variable.py index 8fcbb92..8e7a56d 100644 --- a/tests/test_variable.py +++ b/tests/test_variable.py @@ -760,7 +760,7 @@ def test_extremum(): if __name__ == "__main__": - retcode = 0#pytest.main(["-rP -s -v", __file__]) + retcode = 0 # pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" test_init() # test_range() From 51584c24a66974fbce3d563da97bca1167b3487b Mon Sep 17 00:00:00 2001 From: Eisinger Date: Thu, 22 Jan 2026 14:51:32 +0100 Subject: [PATCH 03/11] New version after upgraded template and all ruff, mypy, pyright and pytest issues resolved --- examples/DrivingForce.fmu | Bin 818209 -> 816344 bytes examples/DrivingForce6D.fmu | Bin 822300 -> 820435 bytes examples/HarmonicOscillator.fmu | Bin 817738 -> 815891 bytes examples/HarmonicOscillator6D.fmu | Bin 825401 -> 823554 bytes examples/axle.py | 21 ++- examples/bouncing_ball_3d.py | 22 +-- examples/bouncing_ball_3d_pythonfmu.py | 5 +- examples/driving_force_fmu.py | 6 +- examples/oscillator.py | 4 +- examples/oscillator_6d.py | 4 +- examples/oscillator_xd.py | 4 +- examples/time_table.py | 24 ++-- examples/time_table_fmu.py | 12 +- pyproject.toml | 4 +- src/component_model/enums.py | 15 +- src/component_model/model.py | 43 +++--- src/component_model/range.py | 52 +++++-- src/component_model/unit.py | 27 ++-- src/component_model/utils/transform.py | 4 +- src/component_model/variable.py | 181 ++++++++----------------- tests/test_analysis.py | 1 - tests/test_axle_fmu.py | 6 +- tests/test_basic.py | 6 +- tests/test_bouncing_ball_3d_fmu.py | 18 +-- tests/test_controls.py | 2 +- tests/test_enums.py | 9 +- tests/test_model.py | 6 +- tests/test_oscillator_6dof_fmu.py | 6 +- tests/test_oscillator_fmu.py | 6 +- tests/test_pint.py | 3 +- tests/test_range.py | 6 +- tests/test_time_table.py | 4 +- tests/test_time_table_fmu.py | 14 +- tests/test_unit.py | 18 ++- tests/test_variable.py | 56 ++++---- 35 files changed, 276 insertions(+), 313 deletions(-) diff --git a/examples/DrivingForce.fmu b/examples/DrivingForce.fmu index 7cb36d846bcf215e55e5d57465b463a401e0c4b6..46758ee927e27a420d4c4c3ecc3ac72923f9faea 100644 GIT binary patch delta 16074 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 Qy;}S`hZX-T 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 P_;+7zS=r(J9^CyuHA_75 diff --git a/examples/DrivingForce6D.fmu b/examples/DrivingForce6D.fmu index c35d34c093c845d3474fd92ab7f3d53b4460b722..f45df5d6adadfe590d98ddbe68c8aba7a177055c 100644 GIT binary patch delta 15904 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 Gfc;-M4MIr( diff --git a/examples/HarmonicOscillator.fmu b/examples/HarmonicOscillator.fmu index 90019fe7ea9492e2a34540c9a4011f18cceccbd2..45367722d97f66d6901123a7c053e26eea186ae1 100644 GIT binary patch delta 16190 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 DLE&w? delta 19166 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 e`sfKyyYk)l^8a np.ndarray: return self._pos @pos.setter - def pos(self, newpos: list[float] | np.ndarray): - self.track = newpos + def pos(self, newpos: np.ndarray): + self.track_add(newpos) self._pos = np.array(newpos, float) @property def track(self) -> list[list[float]]: return self._track - @track.setter - def track(self, newpos: list[float] | np.ndarray | None): - """Remember the track. None: start new track.""" - if newpos is None: # reset - self._track = [[], []] - else: - for i in range(2): - self._track[i].append(newpos[i]) + def track_reset(self): + self._track = [[], []] + + def track_add(self, newpos: np.ndarray): + """Remember the track.""" + for i in range(2): + self._track[i].append(newpos[i]) class Motor: diff --git a/examples/bouncing_ball_3d.py b/examples/bouncing_ball_3d.py index 30afef3..04f920a 100644 --- a/examples/bouncing_ball_3d.py +++ b/examples/bouncing_ball_3d.py @@ -1,6 +1,5 @@ -# pyright: reportAttributeAccessIssue=false -# pyright: reportOptionalMemberAccess=false from math import sqrt +from typing import Any import numpy as np @@ -30,26 +29,29 @@ class BouncingBall3D(Model): def __init__( self, name: str = "BouncingBall3D", - description="Another Python-based BouncingBall model, using Model and Variable to construct a FMU", - pos: tuple = ("0 m", "0 m", "10 inch"), - speed: tuple = ("1 m/s", "0 m/s", "0 m/s"), - g="9.81 m/s^2", - e=0.9, + description: str = "Another Python-based BouncingBall model, using Model and Variable to construct a FMU", + pos: tuple[str | float, ...] = ("0 m", "0 m", "10 inch"), + speed: tuple[str | float, ...] = ("1 m/s", "0 m/s", "0 m/s"), + g: str | float = "9.81 m/s^2", + e: float = 0.9, min_speed_z: float = 1e-6, - **kwargs, + **kwargs: Any, ): super().__init__(name, description, author="DNV, SEACo project", **kwargs) + self.pos: np.ndarray self._pos = self._interface("pos", pos) self._speed = self._interface("speed", speed) + self.g: float self._g = self._interface("g", g) self.a = np.array((0, 0, -self.g), float) + self.e: float self._e = self._interface("e", e) self.min_speed_z = min_speed_z self.stopped = False self.time = 0.0 self._p_bounce = self._interface("p_bounce", ("0m", "0m", "0m")) # Note: 3D, but z always 0 # provoke an update at simulation start: - self.t_bounce, self.p_bounce = (-1.0, self.pos) # type: ignore + self.t_bounce, self.p_bounce = (-1.0, self.pos) def do_step(self, current_time: float, step_size: float) -> bool: """Perform a simulation step from `self.time` to `self.time + step_size`. @@ -110,7 +112,7 @@ def exit_initialization_mode(self): super().exit_initialization_mode() self.a = np.array((0, 0, -self.g), float) - def _interface(self, name: str, start: str | float | tuple) -> Variable: + def _interface(self, name: str, start: str | float | tuple[str | float, ...]) -> Variable: """Define a FMU2 interface variable, using the variable interface. Args: diff --git a/examples/bouncing_ball_3d_pythonfmu.py b/examples/bouncing_ball_3d_pythonfmu.py index f7fd144..7e0e6bf 100644 --- a/examples/bouncing_ball_3d_pythonfmu.py +++ b/examples/bouncing_ball_3d_pythonfmu.py @@ -1,4 +1,5 @@ from math import sqrt +from typing import Any import numpy as np from pythonfmu import Fmi2Causality, Fmi2Slave, Real # type: ignore[import-untyped] @@ -16,7 +17,7 @@ class BouncingBall3D(Fmi2Slave): * Internal units are assumed as SI (m,s,rad) """ - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__( name="BouncingBall3D", description="Another Python-based BouncingBall model, using Model and Variable to construct a FMU", @@ -63,7 +64,7 @@ def __init__(self, **kwargs): self.accelerationY = 0.0 self.accelerationZ = -self.g - def do_step(self, current_time, step_size) -> bool: + def do_step(self, current_time: float, step_size: float) -> bool: """Perform a simulation step from `self.time` to `self.time + step_size`. With respect to bouncing (self.t_bounce should be initialized to a negative value) diff --git a/examples/driving_force_fmu.py b/examples/driving_force_fmu.py index d8b4b7f..2d42637 100644 --- a/examples/driving_force_fmu.py +++ b/examples/driving_force_fmu.py @@ -66,7 +66,7 @@ def __init__( self.freq = np.array((1.0,) * self.dim, float) self.d_freq = np.array((0.0,) * self.dim, float) self.function = func - self.func: Callable + self.func: Callable # type: ignore[reportMissingTypeArgument] ## kwargs self.f = np.array((0.0,) * self.dim, float) self.v_osc = (0.0,) * self.dim self._ampl = Variable(self, "ampl", "The amplitude of the force in N", start=_ampl) @@ -102,7 +102,7 @@ def exit_initialization_mode(self): self.func = partial( self.function, ampl=np.array(self.ampl, float), - omega=np.array(2 * np.pi * self.freq, float), # type: ignore # it is an ndarray! - d_omega=np.array(2 * np.pi * self.d_freq, float), # type: ignore # it is an ndarray! + omega=np.array(2 * np.pi * self.freq, float), + d_omega=np.array(2 * np.pi * self.d_freq, float), ) logger.info(f"Initial settings: ampl={self.ampl}, freq={self.freq}, d_freq={self.d_freq}") diff --git a/examples/oscillator.py b/examples/oscillator.py index 6fbf3b1..ddc8ad2 100644 --- a/examples/oscillator.py +++ b/examples/oscillator.py @@ -33,7 +33,7 @@ def __init__( c: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0), m: float | tuple[float, ...] = 1.0, tolerance: float = 1e-5, - f_func: Callable | None = None, + f_func: Callable | None = None, # type: ignore[reportMissingTypeArgument] ## kwargs ): self.dim = len(k) self.k = np.array(k, float) @@ -106,7 +106,7 @@ class Force: func (callable)=lambda t:np.array( (0,0,0), float): A function of t, producing a 3D vector """ - def __init__(self, func: Callable): + def __init__(self, func: Callable): # type: ignore[reportMissingTypeArgument] ## kwargs self.func = func self.out = np.array((0, 0, 0), float) diff --git a/examples/oscillator_6d.py b/examples/oscillator_6d.py index 6fbf3b1..ddc8ad2 100644 --- a/examples/oscillator_6d.py +++ b/examples/oscillator_6d.py @@ -33,7 +33,7 @@ def __init__( c: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0), m: float | tuple[float, ...] = 1.0, tolerance: float = 1e-5, - f_func: Callable | None = None, + f_func: Callable | None = None, # type: ignore[reportMissingTypeArgument] ## kwargs ): self.dim = len(k) self.k = np.array(k, float) @@ -106,7 +106,7 @@ class Force: func (callable)=lambda t:np.array( (0,0,0), float): A function of t, producing a 3D vector """ - def __init__(self, func: Callable): + def __init__(self, func: Callable): # type: ignore[reportMissingTypeArgument] ## kwargs self.func = func self.out = np.array((0, 0, 0), float) diff --git a/examples/oscillator_xd.py b/examples/oscillator_xd.py index d930372..2b82556 100644 --- a/examples/oscillator_xd.py +++ b/examples/oscillator_xd.py @@ -123,14 +123,14 @@ class Force: denoting the force dependencies """ - def __init__(self, dim: int, func: Callable): + def __init__(self, dim: int, func: Callable): # type: ignore[reportMissingTypeArgument] ## kwargs self.dim = dim self.func = func self.current_time = 0.0 self.dt = 0 self.out = np.array((0,) * self.dim) - def __call__(self, **kwargs): + def __call__(self, **kwargs: Any): """Calculate the force in dependence on keyword arguments 't', 'x' or 'v'.""" if "t" in kwargs: t = kwargs["t"] diff --git a/examples/time_table.py b/examples/time_table.py index cc59a28..8c7408e 100644 --- a/examples/time_table.py +++ b/examples/time_table.py @@ -1,4 +1,4 @@ -from typing import Sequence +from typing import Any import numpy as np from scipy.interpolate import make_interp_spline @@ -21,16 +21,18 @@ class TimeTable: def __init__( self, - data: Sequence = ( - (0.0, 1, 0, 0), - (1.0, 1, 1, 1), - (3.0, 1, 3, 9), - (7.0, 1, 7, 49), - ), # default data set useful for testing - header: Sequence[str] | None = None, + data: list[list[int | float]] | None = None, + header: list[str] | None = None, interpolate: int = 1, - **kwargs, + **kwargs: Any, ): + if data is None: + data = [ # default data set useful for testing + [0.0, 1, 0, 0], + [1.0, 1, 1, 1], + [3.0, 1, 3, 9], + [7.0, 1, 7, 49], + ] self._rows = len(data) assert self._rows > 0, "Empty lookup table detected, which does not make sense" self._cols = len(data[0]) - 1 @@ -46,7 +48,7 @@ def __init__( assert len(header) == self._cols, "Number of header elements does not match number of columns in data" self.header = tuple(header) self.outs = self.data[0] # initial values - self.interpolate = self.set_interpolate(interpolate) + self.interpolate = self.set_interpolate(int(interpolate)) def set_interpolate(self, interpolate: int): assert 0 <= interpolate <= 4, f"Erroneous interpolation exponent {self.interpolate}" @@ -54,7 +56,7 @@ def set_interpolate(self, interpolate: int): self.interpolate = interpolate return interpolate - def lookup(self, time): + def lookup(self, time: float): """Do a simulation step of size 'stepSize at time 'time.""" self.outs = self._bspl(time) return self.outs diff --git a/examples/time_table_fmu.py b/examples/time_table_fmu.py index cdae87f..acce822 100644 --- a/examples/time_table_fmu.py +++ b/examples/time_table_fmu.py @@ -1,5 +1,5 @@ import logging -from typing import Sequence +from typing import Any import numpy as np # noqa @@ -29,12 +29,14 @@ class TimeTableFMU(Model, TimeTable): # refer to Model first! def __init__( self, - data: Sequence = ((0.0, 1, 0, 0), (1.0, 1, 1, 1), (3.0, 1, 3, 9), (7.0, 1, 7, 49)), # data useful for testing - header: Sequence[str] | None = None, + data: list[list[int | float]] | None = None, + header: list[str] | None = None, interpolate: int = 1, default_experiment: dict[str, float] | None = None, - **kwargs, + **kwargs: Any, ): + if data is None: + data = [[0.0, 1, 0, 0], [1.0, 1, 1, 1], [3.0, 1, 3, 9], [7.0, 1, 7, 49]] # ex.data TimeTable.__init__(self, data, header, interpolate) if default_experiment is None: default_experiment = {"startTime": 0, "stopTime": 10.0, "stepSize": 0.1, "tolerance": 1e-5} @@ -58,7 +60,7 @@ def __init__( initial="exact", start=interpolate, rng=(0, 4), - on_set=self.set_interpolate, + on_set=self.set_interpolate, # type: ignore ) # the interpolation type can be set as parameter self._outs = Variable( self, diff --git a/pyproject.toml b/pyproject.toml index fb2fe46..567f24f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,7 +149,7 @@ typeCheckingMode = "basic" useLibraryCodeForTypes = true reportMissingParameterType = "error" reportUnknownParameterType = "warning" -reportUnknownMemberType = "warning" # consider to set to `false` if you work a lot with matplotlib and pandas, which are both not properly typed and known to trigger this warning +reportUnknownMemberType = false # consider to set to `false` if you work a lot with matplotlib and pandas, which are both not properly typed and known to trigger this warning reportMissingTypeArgument = "error" reportPropertyTypeMismatch = "error" reportFunctionMemberAccess = "warning" @@ -165,7 +165,7 @@ reportUnnecessaryIsInstance = "information" reportUnnecessaryCast = "warning" reportUnnecessaryComparison = "warning" reportUnnecessaryContains = "warning" -reportUnusedCallResult = "warning" +reportUnusedCallResult = false # was "warning" reportUnusedExpression = "warning" reportMatchNotExhaustive = "warning" reportUntypedFunctionDecorator = "warning" diff --git a/src/component_model/enums.py b/src/component_model/enums.py index 7e316c0..e86cdf1 100644 --- a/src/component_model/enums.py +++ b/src/component_model/enums.py @@ -3,10 +3,9 @@ import logging from enum import Enum, EnumType, IntFlag -# import pythonfmu.enums # type: ignore -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability logger = logging.getLogger(__name__) @@ -25,7 +24,9 @@ def ensure_enum(org: str | Enum | None, default: Enum | EnumType | None) -> Enum elif isinstance(org, str): # both provided and org is a string _default = default if isinstance(default, EnumType) else type(default) # need the Enum itself if org in _default.__members__: - return _default[org] + e: Enum = _default[org] # type: ignore[reportAssignmentType] + assert isinstance(e, Enum) + return e else: raise Exception(f"The value {org} is not compatible with the Enum {_default}") from None else: # expect already an EnumType @@ -92,8 +93,8 @@ def check_causality_variability_initial( variability: str | Enum | None, # EnumType | None, initial: str | Enum | None, ) -> tuple[Causality | None, Variability | None, Initial | None]: - _causality = ensure_enum(causality, Causality.parameter) # type: ignore - _variability = ensure_enum(variability, Variability.constant) # type: ignore + _causality = ensure_enum(causality, Causality.parameter) + _variability = ensure_enum(variability, Variability.constant) res = combination(_variability, _causality) # type: ignore if res in ("a", "b", "c", "d", "e"): # combination is not allowed logger.info(f"(causality {_causality}, variability {variability}) is not allowed: {explanations[res]}") diff --git a/src/component_model/model.py b/src/component_model/model.py index 41467e5..e0d31d8 100644 --- a/src/component_model/model.py +++ b/src/component_model/model.py @@ -9,15 +9,15 @@ from math import log from numbers import Real from pathlib import Path -from typing import Any, Generator, TypeAlias +from typing import Any, Generator, Sequence, TypeAlias -from pythonfmu import Fmi2Slave, FmuBuilder # type: ignore[import-untyped] +from pythonfmu import Fmi2Slave, FmuBuilder from pythonfmu import __version__ as pythonfmu_version -from pythonfmu.default_experiment import DefaultExperiment # type: ignore[import-untyped] -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore[import-untyped] -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore[import-untyped] -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore[import-untyped] -from pythonfmu.fmi2slave import FMI2_MODEL_OPTIONS # type: ignore[import-untyped] +from pythonfmu.default_experiment import DefaultExperiment +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability +from pythonfmu.fmi2slave import FMI2_MODEL_OPTIONS from component_model.enums import ensure_enum from component_model.variable import Unit, Variable @@ -187,7 +187,7 @@ def _unit_ensure_registered(self, candidate: Variable): break self._units.append(cu) - def owner_hierarchy(self, parent: str | None) -> list[Variable]: + def owner_hierarchy(self, parent: str | None) -> Sequence["Model"]: """Analyse the parent of a variable down to the Model and return the owners as list.""" ownernames: list[tuple[str, int | None]] = [] assert isinstance(self.variable_naming, VariableNamingConvention), ( @@ -569,16 +569,9 @@ def xml_unit_definitions(self): du_done: list[str] = [] for _u in self._units: # list also the displays (if defined) if _u.u not in u_done and u.u == _u.u and _u.du is not None and _u.du not in du_done: - unit.append( - ET.Element( - "DisplayUnit", - { - "name": _u.du, - "factor": str(_u.to_base(1.0) - _u.to_base(0.0)), - "offset": str(_u.to_base(0.0)), - }, - ) - ) + fac = 1.0 if _u.to_base is None else _u.to_base(1.0) - _u.to_base(0.0) + offs = 0.0 if _u.to_base is None else _u.to_base(0.0) + unit.append(ET.Element("DisplayUnit", {"name": _u.du, "factor": str(fac), "offset": str(offs)})) if isinstance(_u.du, str): du_done.append(_u.du) u_done.append(u.u) @@ -589,14 +582,10 @@ def xml_unit_definitions(self): def _xml_default_experiment(self): attrib: dict[str, str] = {} if self.default_experiment is not None: - if self.default_experiment.start_time is not None: - attrib["startTime"] = str(self.default_experiment.start_time) - if self.default_experiment.stop_time is not None: - attrib["stopTime"] = str(self.default_experiment.stop_time) - if self.default_experiment.step_size is not None: - attrib["stepSize"] = str(self.default_experiment.step_size) - if self.default_experiment.tolerance is not None: - attrib["tolerance"] = str(self.default_experiment.tolerance) + attrib["startTime"] = str(self.default_experiment.start_time) + attrib["stopTime"] = str(self.default_experiment.stop_time) + attrib["stepSize"] = str(self.default_experiment.step_size) + attrib["tolerance"] = str(self.default_experiment.tolerance) de = ET.Element("DefaultExperiment", attrib) return de @@ -765,7 +754,7 @@ def _vrs_slices(self, vrs: tuple[int, ...] | list[int]) -> Generator[tuple[Varia raise AssertionError(f"valueReference={vr} does not exist in model {self.name}") from err if vr != _vr + 1 or test is not None: # new slice if var is not None: # only if initialized - yield (var, slice(start, start + i - i0), slice(i0, i)) # type: ignore + yield (var, slice(start, start + i - i0), slice(i0, i)) vr0 = vr i0 = i diff --git a/src/component_model/range.py b/src/component_model/range.py index c005cda..36a5623 100644 --- a/src/component_model/range.py +++ b/src/component_model/range.py @@ -11,9 +11,10 @@ class Range(object): """Utility class to store and handle the variable range of a single-valued variable. Args: - val: value for which the range is defined. At least an example value of the same type shall be provided. + val: value for which the range is defined. + At least an example value of the same type in base units shall be provided. rng (tuple) = (): Optional range of the variable in terms of a tuple of the same type as initial value. - Should be specified with units (as string). + Should be specified with units (as string) and is expected in display units. * If an empty tuple is specified, the range is automatically determined. That is only possible for float or enum type variables, where the former evaluates to (-inf, inf). @@ -23,13 +24,15 @@ class Range(object): `None` can be applied to the whole tuple or to single elements of the tuple. E.g. (1,None) sets the range to (1, start) * For some variable types (e.g. str) no range is expected. + * Internally, the range is stored in base units. + For range checking of a new value, the new value must be converted to base units before check. + unit (Unit): expected Unit (should be determined for start value before range is determined) """ def __init__( self, val: bool | int | float | str | Enum, - # rng: tuple[int|float|Enum|str|None,int|float|Enum|str|None]|None|tuple[()] = tuple(), # type: ignore[assignment] rng: tuple[Any, Any] | None | Sequence[Never] = tuple(), unit: Unit | None = None, ): @@ -39,10 +42,10 @@ def __init__( unit = Unit() assert isinstance(val, (bool, int, float, str, Enum)), f"Only primitive types allowed for Range. Found {typ}" if isinstance(val, str): - assert unit.u == "dimensionless", "A free string cannot have units." + assert unit.u == "dimensionless", f"A free string cannot have units. Found {unit.u}" self.rng = (val, val) # no range for free strings - elif rng is None: # fixed value in any case - self.rng = (unit.from_base(val), unit.from_base(val)) + elif rng is None: # fixed value in any case. val provided in base units. No conversion + self.rng = (val, val) # type: ignore[assignment] ## see def above elif isinstance(rng, tuple) and not len(rng): # empty tuple => try automatic range self.rng = Range.auto_extreme(val) # fails if val is an int variable elif ( @@ -52,20 +55,19 @@ def __init__( ): l_rng = list(rng) # work on a mutable object for i, r in enumerate(rng): - if r is None: - l_rng[i] = unit.from_base(val) # replace with fixed value 'val' as display value + if r is None: # fixed value on this side. val provided in base units. + l_rng[i] = val # type: ignore[reportArgumentType] ## l_rng is not empty # fixed display value else: assert isinstance(r, (str, int, bool, float, Enum)), f"Found type {type(r)}" - check, q = unit.compatible(r, typ, strict=True) + check, q = unit.compatible(r, typ, strict=True) # q in base units if not check: raise ValueError(f"Provided range {rng} is not conformant with unit {unit}") from None - q = unit.from_base(q) # ensure display units assert isinstance(q, (int, bool, float)), "Unexpected type {type(q)} in {rng}[{i}]" try: q = type(val)(q) # ensure correct Python type except Exception as err: raise TypeError(f"Incompatible types range {rng} - {val}") from err - l_rng[i] = q + l_rng[i] = q # type: ignore[reportArgumentType] ## l_rng is not empty self.rng = tuple(l_rng) # type: ignore ## cannot see how tuple contains str or None here! else: raise TypeError(f"Unhandled range specification {rng}) from None") @@ -111,7 +113,7 @@ def check( value: the Python value to check with respect to the internally defined Range typ (type): the expected Python type of the value unit (Unit): the Unit object related to the variable - disp (bool): check value as display units (True) or base units (False) + disp (bool): denotes whether 'value' is in display units (True) or base units (False) """ if unit is None: unit = Unit() @@ -132,8 +134,8 @@ def check( elif isinstance(value, (int, float)) and all(isinstance(x, (int, float)) for x in self.rng): assert typ is int or typ is float, f"Inconsistent type {typ}. Expect int or float" - if not disp and unit.du is not None: # check an internal unit values - value = unit.from_base(value) + if disp and unit.to_base is not None: # check a display unit values + value = unit.to_base(value) return self.rng[0] <= value <= self.rng[1] # type: ignore[operator] ## There is no str involved! else: logger.error(f"range check(): value={value}, type={typ}, range={self.rng}") @@ -160,7 +162,7 @@ def is_valid_spec( for i, r in enumerate(rng): if r is not None and not isinstance(r, (int, bool, float, Enum, str)): ck += 10 + i - if not any(isinstance(rng[i], str) for i in range(2)): + if rng[i] is not None and not any(isinstance(rng[i], str) for i in range(2)): if rng[0] > rng[1]: # wrong order ck += 10 + 9 @@ -174,3 +176,23 @@ def is_valid_spec( else: ck = 4 # would need a tuple here return ck if ck == 0 else level + ck + + @classmethod + def err_code_msg(cls, code: int) -> str: + if code == 0: + return "Ok" + elif code == 1: + return "Automatic range for int variables is not defined." + elif code == 2: + return "Full range specification of scalar expects a 2-tuple" + elif code == 3: + return "Range specification of compound variable expects one spec per sub-variable." + elif 10 <= code < 19: + return "Wrong entry in full range specification of scalar." + elif code == 19: + return "Wrong order of entries in full range specification of scalar" + elif code > 100: + sub = Range.err_code_msg(code % 100) + return f"Error in compound variable: {sub}" + else: + return f"Unknown error code {code}" diff --git a/src/component_model/unit.py b/src/component_model/unit.py index 5914ee8..cd66f8e 100644 --- a/src/component_model/unit.py +++ b/src/component_model/unit.py @@ -1,7 +1,7 @@ import logging from enum import Enum from functools import partial -from typing import Callable +from typing import Any, Callable import numpy as np from pint import Quantity, UnitRegistry # management of units @@ -16,15 +16,16 @@ class Unit: One Unit object represents one scalar variable. """ - _ureg: UnitRegistry | None = None + _ureg: UnitRegistry[Any] | None = None def __init__(self, quantity: bool | int | float | str | Enum | None = None, typ: type | None = None): assert Unit._ureg is not None, "Before units can be instantiated, Unit.ensure_unit_registry() must be called." # properties with default values. Initialized through parse_quantity self.u: str = "dimensionless" # default: dimensionless unit (placeholder) self.du: str | None = None # display unit (default: same as u, no transformation) - self.to_base: Callable[float] = partial(Unit.identity) # f(display-value) -> base-value - self.from_base: Callable[float] = partial(Unit.identity) # f(base-value) -> display-value + # Transformations f(display-value) -> base-value and f(base-value) -> display-value + self.to_base: Callable[[int | float], int | float] | None = None + self.from_base: Callable[[int | float], int | float] | None = None if quantity is not None: # if parse-value is called on class it also returns the (parsed,converted) base-value _val = self.parse_quantity(quantity, typ) @@ -35,7 +36,7 @@ def ensure_unit_registry(cls, system: str = "SI", autoconvert: bool = True): def __str__(self): txt = f"Unit {self.u}, display:{self.du}" - if self.du is not None: + if self.du is not None and not (self.to_base is None or self.from_base is None): txt += f". Offset:{self.to_base(0)}, factor:{self.to_base(1.0) - self.to_base(0.0)}" return txt @@ -52,11 +53,12 @@ def parse_quantity( the magnitude in base units, the base unit and the unit as given (display units), together with the conversion functions between the units. """ - if typ is str: + if typ is str or typ is Enum: self.u = "dimensionless" self.du = None val = quantity elif isinstance(quantity, str): # only string variable make sense to disect + assert Unit._ureg is not None, "UnitRegistry not yet instantiated!" try: q = Unit._ureg(quantity) # parse the quantity-unit and return a Pint Quantity object if isinstance(q, (int, float)): @@ -88,14 +90,10 @@ def parse_quantity( return val @classmethod - def linear(cls, x: float, b: float, a: float = 0.0): + def linear(cls, x: int | float, b: int | float, a: int | float = 0) -> int | float: return a + b * x - @classmethod - def identity(cls, x: float): - return x - - def val_unit_display(self, q: Quantity[float]) -> float: + def val_unit_display(self, q: Quantity[int | float]) -> int | float: """Identify base units and calculate the transformations between display and base units. Returns @@ -125,8 +123,9 @@ def val_unit_display(self, q: Quantity[float]) -> float: if abs(a) < 1e-9: # multiplicative conversion if abs(b - 1.0) < 1e-9: # unit and display unit are compatible. No transformation self.du = None - self.to_base = partial(Unit.linear, b=b) - self.from_base = partial(Unit.linear, b=1.0 / b) + else: + self.to_base = partial(Unit.linear, b=b, a=0.0) + self.from_base = partial(Unit.linear, b=1.0 / b, a=0.0) else: # there is a constant (e.g. Celsius to Fahrenheit) self.to_base = partial(Unit.linear, b=b, a=a) self.from_base = partial(Unit.linear, b=1.0 / b, a=-a / b) diff --git a/src/component_model/utils/transform.py b/src/component_model/utils/transform.py index bce336f..f0a99f5 100644 --- a/src/component_model/utils/transform.py +++ b/src/component_model/utils/transform.py @@ -97,7 +97,7 @@ def euler_rot_spherical( tp = [np.radians(x) for x in tp] st = np.sin(tp[0]) # rotate the cartesian vector (r is definitely not a list, even if pyright might think so) - x = r.apply((st * np.cos(tp[1]), st * np.sin(tp[1]), np.cos(tp[0]))) # type: ignore[reportAttributeAccessIssue] + x = r.apply((st * np.cos(tp[1]), st * np.sin(tp[1]), np.cos(tp[0]))) x2 = x[2] if abs(x2) < 1.0: pass @@ -137,7 +137,7 @@ def rot_from_vectors(vec1: np.ndarray, vec2: np.ndarray) -> Rot: if abs(n - 1.0) > 1e-10: vec1 /= n vec2 /= n - _c = vec1.dot(vec2) # type: ignore + _c = vec1.dot(vec2) if abs(_c + 1.0) < 1e-10: # vectors are exactly opposite to each other imax, vmax, _sum = (-1, float("-inf"), 0.0) for k, v in enumerate(vec1): diff --git a/src/component_model/variable.py b/src/component_model/variable.py index fd5c56f..0aff0dc 100644 --- a/src/component_model/variable.py +++ b/src/component_model/variable.py @@ -6,10 +6,10 @@ from typing import Any, Callable, Never, Sequence, TypeAlias import numpy as np -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore -from pythonfmu.variables import ScalarVariable # type: ignore +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability +from pythonfmu.variables import ScalarVariable from component_model.enums import Check, check_causality_variability_initial, use_start from component_model.range import Range @@ -122,8 +122,8 @@ def __init__( rng: RngSingle | tuple[RngSingle, ...] = tuple(), annotations: dict[str, Any] | None = None, value_check: Check = Check.all, - on_step: Callable | None = None, - on_set: Callable | None = None, + on_step: Callable[[float, float], None] | None = None, + on_set: Callable[[int | float | np.ndarray], int | float | np.ndarray] | None = None, owner: Any | None = None, local_name: str | None = None, ): @@ -191,8 +191,10 @@ def __init__( self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct ck = Range.is_valid_spec(rng, self._len, self._typ) - assert 0 == ck, f"{self.name} invalid range spec {rng}. Error {ck}" + if ck != 0: + raise ValueError(f"{self.name} invalid range spec '{rng}'. Error: {Range.err_code_msg(ck)}") from None self._range: tuple[Range, ...] + # Defining the _range. self._start is in base units, while rng is in display units if self._typ is str: # explicit free string. String arrays are so far not implemented assert isinstance(start, str) self._range = (Range(self._start[0], unit=self._unit[0]),) # Strings have fixed range @@ -200,16 +202,13 @@ def __init__( if self._len == 1: self._range = (Range(self._start[0], rng, self._unit[0]),) # type: ignore[arg-type] ## is_valid_spec else: - self._range = tuple( - [ - Range( - self._start[i], - rng[i], # type: ignore[index] ## is_valid_spec - self._unit[i], - ) - for i in range(self._len) - ] - ) + _rng: list[Range] = [] + for i in range(self._len): + if rng is None or not len(rng): + _rng.append(Range(self._start[i], rng, self._unit[i])) # type: ignore[arg-type] + else: + _rng.append(Range(self._start[i], rng[i], self._unit[i])) + self._range = tuple(_rng) if not self.check_range(self._start, disp=False): # range checks of initial value logger.critical(f"The provided value {self._start} is not in the valid range {self._range}") @@ -223,25 +222,12 @@ def __init__( def der1(self, current_time: float, step_size: float): """Ramp the base variable value up or down within step_size.""" - der = getattr(self.owner, self.local_name) # the current slope value - if (isinstance(der, float) and der != 0.0) or ( - isinstance(der, (Sequence, np.ndarray)) and any(x != 0.0 for x in der) - ): # there is a slope - # varname = self.local_name[5:] # local name of the base variable + der = np.array(getattr(self.owner, self.local_name)) # the current slope value + if not np.allclose(der, 0.0): basevar = self.model.derivatives[self.name] # base variable object - val = getattr( - self.owner, basevar.local_name - ) # getattr(self.owner, varname) # previous value of base variable # - if not isinstance(der, (Sequence, np.ndarray)): - der = [der] - assert not isinstance(val, (Sequence, np.ndarray)), "Should be the same as der" - val = [val] - if isinstance(val, np.ndarray): - newval = val + step_size * np.array(der, float) - basevar.setter_internal(newval, -1, True) - else: - newval_list = [val[i] + step_size * der[i] for i in range(len(der))] - basevar.setter_internal(newval_list, -1, False) + val = np.array(getattr(self.owner, basevar.local_name)) # previous value of base variable + newval = val + step_size * der + basevar.setter_internal(newval, -1) # , True) # disable super() functions and properties which are not in use here def to_xml(self) -> ET.Element: @@ -253,16 +239,9 @@ def __len__(self) -> int: return self._len # This works also compound variables, as long as _len is properly set @property - def start(self): + def start(self) -> tuple[PyType, ...]: return self._start - @start.setter - def start(self, val: PyType | Compound): - if isinstance(val, (Sequence, np.ndarray)): - self._start = tuple(val) - else: - self._start = (val,) - @property def unit(self): """Get the unit object.""" @@ -301,7 +280,6 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, """ dvals: list[int | float | bool | str | Enum | None] logger.debug(f"SETTER0 {self.name}, {values}[{idx}] => {getattr(self.owner, self.local_name)}") - is_ndarray = isinstance(values, np.ndarray) assert self._typ is not None, "Need a proper type at this stage" assert isinstance(values, (Sequence, np.ndarray)), "A sequence is expected as values" if idx == -1 and self._len == 0: # the whole scalar @@ -317,11 +295,10 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, if self._check & Check.units: #'values' expected as displayUnit. Convert to unit if idx >= 0: # explicit index of single values - if self._unit[idx].du is None: + if self._unit[idx].to_base is None: dvals = list(values) else: - # assert isinstance(values[0], float) - dvals = [self._unit[idx].to_base(values[0])] # type: ignore ## values[0] is float! + dvals = [self._unit[idx].to_base(values[0])] # type: ignore else: # the whole array dvals = [] for i in range(self._len): @@ -331,7 +308,10 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, dvals.append(values[i]) else: # assert isinstance(values[i], float) or (self._typ is int and isinstance(values[i], int)) - dvals.append(self._unit[i].to_base(values[i])) # type: ignore ## it is a float! + val_i = values[i] + if self._unit[i].to_base is not None: + val_i = self._unit[i].to_base(values[i]) # type: ignore + dvals.append(val_i) else: # no unit issues if self._len == 1: dvals = [values[0] if values[0] is not None else getattr(self.owner, self.local_name)] @@ -340,17 +320,20 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, values[i] if values[i] is not None else getattr(self.owner, self.local_name)[i] for i in range(self._len) ] - self.setter_internal(dvals, idx, is_ndarray) # do the setting, or flag as dirty + self.setter_internal(dvals, idx) # do the setting, or flag as dirty def setter_internal( self, values: Sequence[int | float | bool | str | Enum | None] | np.ndarray, idx: int = -1, - is_ndarray: bool = False, ): """Do internal setting of values (no range checking and units expected internal), including dirty flags.""" if self._len == 1: - setattr(self.owner, self.local_name, values[0] if self.on_set is None else self.on_set(values[0])) # type: ignore + try: + _val = values[0] + except IndexError: # Exception as err: + _val = values + setattr(self.owner, self.local_name, _val if self.on_set is None else self.on_set(_val)) # type: ignore elif idx >= 0: if values[0] is not None: # Note: only the indexed value is provided, as list! val = getattr(self.owner, self.local_name) @@ -359,11 +342,8 @@ def setter_internal( if self.on_set is not None: self.model.dirty_ensure(self) else: # the whole array - if is_ndarray: # Note: on_set might contain array operations - arr: np.ndarray = np.array(values, self._typ) - setattr(self.owner, self.local_name, arr if self.on_set is None else self.on_set(arr)) - else: - setattr(self.owner, self.local_name, values if self.on_set is None else self.on_set(values)) + arr: np.ndarray = np.array(values, self._typ) + setattr(self.owner, self.local_name, arr if self.on_set is None else self.on_set(arr)) if self.on_set is None: logger.debug(f"SETTER {self.name}, {values}[{idx}] => {getattr(self.owner, self.local_name)}") @@ -379,9 +359,9 @@ def getter(self) -> list[PyType]: values = [value.value] else: if not isinstance(value, self._typ): # other type conversion - value = self._typ(value) # type: ignore[call-arg] + value = self._typ(value) # type: ignore[call-arg] ## only mypy if self._check & Check.units: # Convert 'value' base unit -> display.u - if self._unit[0].du is not None: + if self._unit[0].from_base is not None: assert isinstance(value, float) value = self._unit[0].from_base(value) values = [value] @@ -394,74 +374,16 @@ def getter(self) -> list[PyType]: else: for i in range(self._len): # check whether conversion to _typ is necessary if not isinstance(values[i], self._typ): - values[i] = self._typ(values[i]) # type: ignore[call-arg] + values[i] = self._typ(values[i]) # type: ignore[call-arg] ## only mypy if self._check & Check.units: # Convert 'value' base unit -> display.u for i in range(self._len): - if self._unit[i].du is not None: - values[i] = self._unit[i].from_base(values[i]) + if self._unit[i].from_base is not None: + values[i] = self._unit[i].from_base(values[i]) # type: ignore[reportOptionalCall] ##checked! if self._check & Check.ranges and not self.check_range(values, -1): # check the range if so instructed logger.error(f"getter(): Value of {self.name}: {values} outside range {self.range}!") return values - # def _init_range(self, rng: RngSpec) -> tuple[tuple[Any,Any],None]: - # """Initialize the variable range(s) of the variable - # The _start and _unit shall exist when calling this. - # - # Args: - # rng (tuple): The tuple of range tuples. - # Always for the whole variable with scalar variables packed in a singleton - # """ - # - # assert hasattr(self, "_start") and hasattr(self, "_unit"), "Missing self._start / self._unit" - # assert isinstance(self._typ, type), "init_range(): Need a defined _typ at this stage" - # # Configure input. Could be None, () or (min,max) of scalar - # if rng is None or rng == tuple() or (self._len == 1 and len(rng) == 2): - # rng = (rng,) * self._len - # - # _range = [] - # for idx in range(self._len): # go through all elements - # assert rng is not None, "rng None detected" - # _rng = rng[idx] - # if _rng is None: # => no range. Used for compound variables if not all elements have a range - # s0 = self._start[idx] - # assert isinstance(s0, float) - # v = self._unit[idx].from_base(s0) if self._unit[idx].du is not None else s0 - # _range.append((v, v)) - # elif isinstance(_rng, tuple) and not len(_rng): # empty tuple => try automatic range - # _range.append(self._auto_extreme(self._start[idx])) - # elif isinstance(_rng, tuple) and len(_rng) == 2: # normal range as 2-tuple - # i_range: list[float] = [] # collect range as list - # for r in _rng: - # if r is None: # no range => fixed to initial value - # q = self._start[idx] - # else: - # check, q = self._unit[idx].compatible(r, self.model.ureg, self._typ, strict=True) - # if not check: - # check, q = self._unit[idx].compatible(r, self.model.ureg, self._typ, strict=False) - # if check: - # logger.warn(f"{self.name}[{idx}] range {r}: Use display units {self._unit[idx].du}!") - # else: - # msg = f"{self.name}[{idx}]: range {r} not conformant to the unit type {self._unit[idx]}" - # logger.critical(msg) - # raise VariableInitError(msg) - # assert isinstance(q, float) or (self._typ is int and isinstance(q, int)) - # if self._unit[idx].du is not None: - # q = self._unit[idx].from_base(q) - # i_range.append(q) - # - # try: # check variable type - # i_range = [self._typ(x) for x in i_range] - # except Exception as err: - # logger.critical(f"Incompatible types range {rng} - {self._start}") - # raise VariableRangeError(f"Incompatible types range {rng} - {self._start}") from err - # assert all(isinstance(x, self._typ) for x in i_range) - # _range.append(tuple(i_range)) # type: ignore - # else: - # logger.critical(f"init_range(): Unhandled range argument {rng}") - # raise AssertionError(f"init_range(): Unhandled range argument {rng}") - # return tuple(_range) - def check_range(self, values: Sequence[PyType | None] | np.ndarray, idx: int = 0, disp: bool = True) -> bool: """Check the provided 'values' with respect to the range. @@ -608,18 +530,23 @@ def xml_scalarvariables(self): # detailed variable definition info = ET.Element(_type) if do_use_start: # a start value is to be used - info.attrib.update({"start": self.fmi_type_str(self._start[i])}) + _start = self._start[i] + if self._unit[i].from_base is not None: + _start = self._unit[i].from_base(_start) # type: ignore + info.attrib.update({"start": self.fmi_type_str(_start)}) if _type in ("Real", "Integer", "Enumeration"): # range to be specified - xMin = self.range[i].rng[0] - if _type != "Real" or xMin > float("-inf"): - info.attrib.update({"min": str(xMin)}) - else: + xmin = self.range[i].rng[0] + if _type == "Real" and isinstance(xmin, float) and xmin == float("-inf"): info.attrib.update({"unbounded": "true"}) - xMax = self.range[i].rng[1] - if _type != "Real" or xMax < float("inf"): - info.attrib.update({"max": str(xMax)}) else: + xmin = xmin if self._unit[i].from_base is None else self._unit[i].from_base(xmin) # type: ignore + info.attrib.update({"min": str(xmin)}) + xmax = self.range[i].rng[1] + if _type == "Real" and isinstance(xmax, float) and xmax == float("inf"): info.attrib.update({"unbounded": "true"}) + else: + xmax = xmax if self._unit[i].from_base is None else self._unit[i].from_base(xmax) # type: ignore + info.attrib.update({"max": str(xmax)}) if _type == "Real": # other attributes apply only to Real variables info.attrib.update({"unit": self.unit[i].u}) if isinstance(self._unit[i].du, str) and self.unit[i].du != self._unit[i].u: diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 8bd08ea..aba25e2 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -1,4 +1,3 @@ -# pyright: ignore[reportAttributeAccessIssue] # PythonFMU generates variable value objects using setattr() import logging import numpy as np diff --git a/tests/test_axle_fmu.py b/tests/test_axle_fmu.py index 6ac665a..a6d3fae 100644 --- a/tests/test_axle_fmu.py +++ b/tests/test_axle_fmu.py @@ -6,9 +6,9 @@ import numpy as np import pytest from examples.axle import Axle -from fmpy.simulation import simulate_fmu # type: ignore[import-untyped] -from fmpy.util import fmu_info # type: ignore[import-untyped] -from fmpy.validation import validate_fmu # type: ignore[import-untyped] +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info +from fmpy.validation import validate_fmu from component_model.model import Model diff --git a/tests/test_basic.py b/tests/test_basic.py index aed4903..5ce7dc9 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,8 +2,8 @@ from typing import Any import pytest -from fmpy import simulate_fmu # type: ignore -from pythonfmu import ( # type: ignore +from fmpy import simulate_fmu +from pythonfmu import ( Boolean, DefaultExperiment, Fmi2Causality, @@ -131,7 +131,7 @@ def test_make_fmu(build_fmu: Path): def test_use_fmu(build_fmu: Path): - _ = simulate_fmu( # type: ignore #fmpy does not comply to pyright expectations + _ = simulate_fmu( build_fmu, stop_time=1, step_size=0.1, diff --git a/tests/test_bouncing_ball_3d_fmu.py b/tests/test_bouncing_ball_3d_fmu.py index 50ddc5c..092e0b3 100644 --- a/tests/test_bouncing_ball_3d_fmu.py +++ b/tests/test_bouncing_ball_3d_fmu.py @@ -9,10 +9,10 @@ import matplotlib.pyplot as plt import numpy as np import pytest -from fmpy import plot_result, simulate_fmu # type: ignore[import-untyped] -from fmpy.util import fmu_info # type: ignore[import-untyped] -from fmpy.validation import validate_fmu # type: ignore[import-untyped] -from pythonfmu.default_experiment import DefaultExperiment # type: ignore[import-untyped] +from fmpy import plot_result, simulate_fmu +from fmpy.util import fmu_info +from fmpy.validation import validate_fmu +from pythonfmu.default_experiment import DefaultExperiment from component_model.model import Model from component_model.utils.fmu import model_from_fmu @@ -101,9 +101,9 @@ def get_result(): if len(bb._pos) > 1 and bb._pos.unit[2].du is not None: bb._pos.setter((0, 0, 10)) t_b, p_b = bb.next_bounce() - assert t_bounce == t_b + assert t_bounce == t_b, f"Bounce time {t_bounce} != {t_b}" # print("Bounce", t_bounce, x_bounce, p_b) - assert np.allclose((x_bounce, 0, 0), p_b), f"x_bounce:{x_bounce} != {p_b[0]}" # type: ignore ##?? + assert np.allclose((x_bounce, 0, 0), p_b), f"x_bounce:{x_bounce} != {p_b[0]}" get_result() # after one step bb.do_step(time, dt) @@ -224,7 +224,7 @@ def check_result(res: np.ndarray, expected: tuple[float, ...], eps: float = 1e-1 assert bouncing_ball_fmu.exists(), f"File {bouncing_ball_fmu} does not exist" dt = 0.01 - result = simulate_fmu( # type: ignore[reportArgumentType] + result = simulate_fmu( bouncing_ball_fmu, start_time=0.0, stop_time=3.0, @@ -363,12 +363,12 @@ def test_from_fmu(bouncing_ball_fmu: Path): if __name__ == "__main__": - retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) + retcode = 0 # pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" import os os.chdir(Path(__file__).parent / "test_working_directory") - # test_bouncing_ball_class(show=False) + test_bouncing_ball_class(show=False) # test_make_bouncing_ball(_bouncing_ball_fmu()) # test_use_fmu(_bouncing_ball_fmu(), True) # test_from_fmu( _bouncing_ball_fmu()) diff --git a/tests/test_controls.py b/tests/test_controls.py index c972ab8..8ff6341 100644 --- a/tests/test_controls.py +++ b/tests/test_controls.py @@ -36,7 +36,7 @@ def test_limits(): # try to set goal outside limits _b.limit_err = logging.CRITICAL - with pytest.raises(ValueError) as err: # type: ignore[assignment] #it is a 'ValueError' + with pytest.raises(ValueError) as err: # type: ignore[assignment] ## mypy believes that it is an AssertionError?? _b.setgoal(1, 2, 9.9, 0.0) assert err.value.args[0] == "Goal 'polar'@ 9.9 is above the limit 0.0. Stopping execution." _b.limit_err = logging.WARNING diff --git a/tests/test_enums.py b/tests/test_enums.py index a8ba70b..258dddb 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -2,9 +2,9 @@ from enum import Enum, EnumType import pytest -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability from component_model.enums import check_causality_variability_initial, combination, combinations, ensure_enum from component_model.variable_naming import VariableNamingConvention @@ -21,6 +21,9 @@ def enum_func(e: Enum) -> None: def enumtype_func(e: EnumType): assert isinstance(e, EnumType), f"Argument {e} should be an EnumType, i.e. the Enum Class itself" logger.info(f"Members:{e._member_names_}") + if "flat" in e.__members__: + m: Enum = e["flat"] # type: ignore[reportAssignmentType] + logger.info(f"Member flat: {m}") f = VariableNamingConvention.flat assert isinstance(f, Enum) diff --git a/tests/test_model.py b/tests/test_model.py index 1557485..5f6e1f8 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -6,7 +6,7 @@ import pytest -from component_model.model import Model # type: ignore +from component_model.model import Model from component_model.utils.fmu import model_from_fmu from component_model.variable import Check, Variable from component_model.variable_naming import ParsedVariable, VariableNamingConvention @@ -148,9 +148,9 @@ def test_from_fmu(bouncing_ball_fmu: Path): if __name__ == "__main__": - retcode = pytest.main(["-rA", "-v", __file__]) + retcode = 0 # pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" # test_license() - # test_xml() + test_xml() # test_from_fmu(_bouncing_ball_fmu()) # test_variable_naming() diff --git a/tests/test_oscillator_6dof_fmu.py b/tests/test_oscillator_6dof_fmu.py index 073e749..4b501b6 100644 --- a/tests/test_oscillator_6dof_fmu.py +++ b/tests/test_oscillator_6dof_fmu.py @@ -4,9 +4,9 @@ from typing import Any import matplotlib.pyplot as plt import pytest -from fmpy.simulation import simulate_fmu # type: ignore[import-untyped] -from fmpy.util import fmu_info, plot_result # type: ignore[import-untyped] -from fmpy.validation import validate_fmu # type: ignore[import-untyped] +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info, plot_result +from fmpy.validation import validate_fmu from component_model.model import Model diff --git a/tests/test_oscillator_fmu.py b/tests/test_oscillator_fmu.py index ab514cd..cbfd32f 100644 --- a/tests/test_oscillator_fmu.py +++ b/tests/test_oscillator_fmu.py @@ -3,9 +3,9 @@ import matplotlib.pyplot as plt import pytest -from fmpy.simulation import simulate_fmu # type: ignore[import-untyped] -from fmpy.util import fmu_info, plot_result # type: ignore[import-untyped] -from fmpy.validation import validate_fmu # type: ignore[import-untyped] +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info, plot_result +from fmpy.validation import validate_fmu from component_model.model import Model diff --git a/tests/test_pint.py b/tests/test_pint.py index ef20206..a4d6d88 100644 --- a/tests/test_pint.py +++ b/tests/test_pint.py @@ -1,6 +1,7 @@ """Test the pint package and identify the functions we need for this package""" import logging +from typing import Any import pytest from pint import UnitRegistry @@ -9,7 +10,7 @@ def test_needed_functions(): - _reg: UnitRegistry = UnitRegistry( + _reg: UnitRegistry[Any] = UnitRegistry( system="SI", autoconvert_offset_to_baseunit=True ) # , auto_reduce_dimensions=True) print("AVAILABLE UNITS", dir(_reg.sys.SI)) diff --git a/tests/test_range.py b/tests/test_range.py index efc52dc..7619918 100644 --- a/tests/test_range.py +++ b/tests/test_range.py @@ -37,7 +37,7 @@ def test_init(unt: Unit): float3 = Range(1.0, ("0m", "10m"), unit=Unit("1 m")) assert float3.rng == (0.0, 10.0), f"Found {float3.rng}" float4 = Range(0.55, ("0 %", None), unit=Unit("55%")) - assert np.allclose(float4.rng, (0.0, 55.0)), f"Found {float4.rng} != {(0.0, 55.0)}" + assert np.allclose(float4.rng, (0.0, 0.55)), f"Found {float4.rng} != {(0.0, 0.55)} (base units)" def test_auto_extreme(): @@ -90,9 +90,9 @@ def do_check( if __name__ == "__main__": - retcode = 0 # pytest.main(["-rP -s -v", __file__]) + retcode = pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" # _unt() # initialize UnitRegistry (otherwise Unit cannot be used) # test_init(_unt()) # test_auto_extreme() - test_range_spec() + # test_range_spec() diff --git a/tests/test_time_table.py b/tests/test_time_table.py index a12e40a..6fcb3b5 100644 --- a/tests/test_time_table.py +++ b/tests/test_time_table.py @@ -31,8 +31,8 @@ def test_time_table(show: bool = False): from examples.time_table import TimeTable tbl = TimeTable( - data=((0.0, 1, 0, 0), (1.0, 1, 1, 1), (3.0, 1, 3, 9), (7.0, 1, 7, 49)), - header=("x", "y", "z"), + data=[[0.0, 1, 0, 0], [1.0, 1, 1, 1], [3.0, 1, 3, 9], [7.0, 1, 7, 49]], + header=["x", "y", "z"], interpolate=0, ) assert not tbl.interpolate, f"Interpolation=0 expected. Found {tbl.interpolate}" diff --git a/tests/test_time_table_fmu.py b/tests/test_time_table_fmu.py index 5aab099..47f02fa 100644 --- a/tests/test_time_table_fmu.py +++ b/tests/test_time_table_fmu.py @@ -6,9 +6,9 @@ import numpy as np import pytest -from fmpy.simulation import simulate_fmu # type: ignore -from fmpy.util import fmu_info, plot_result # type: ignore -from fmpy.validation import validate_fmu # type: ignore +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info, plot_result +from fmpy.validation import validate_fmu from pythonfmu.enums import Fmi2Causality as Causality from pythonfmu.enums import Fmi2Variability as Variability @@ -112,7 +112,7 @@ def test_use_fmu(time_table_fmu: Path, show: bool = False): print(fmu_info(str(time_table_fmu))) _t = np.linspace(0, 10, 101) for ipol in range(4): - result = simulate_fmu( # type: ignore[reportArgumentType] + result = simulate_fmu( time_table_fmu, stop_time=10.0, step_size=0.1, @@ -158,7 +158,7 @@ def test_make_with_new_data(): # @pytest.mark.skip(reason="Does so far not work within pytest, only stand-alone") def test_use_with_new_data(show: bool): fmu_path = Path(__file__).parent / "test_working_directory" / "TimeTableFMU.fmu" - result = simulate_fmu( # type: ignore[reportArgumentType] + result = simulate_fmu( fmu_path, stop_time=2 * np.pi, step_size=0.1, @@ -180,13 +180,13 @@ def test_use_with_new_data(show: bool): if __name__ == "__main__": - retcode = pytest.main(["-rA", "-v", __file__]) + retcode = 0 # pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" import os os.chdir(Path(__file__).parent.absolute() / "test_working_directory") # test_time_table_fmu() - # test_make_time_table(_time_table_fmu()) + test_make_time_table(_time_table_fmu()) # test_use_fmu(_time_table_fmu(), show=True) # test_make_with_new_data() # test_use_with_new_data(show=True) diff --git a/tests/test_unit.py b/tests/test_unit.py index 826ce40..6dd48d8 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -1,4 +1,5 @@ from math import degrees, radians +from typing import Any import pytest from pint import UnitRegistry @@ -17,7 +18,7 @@ def _ureg(): return _registry -def test_parsing(ureg: UnitRegistry): +def test_parsing(ureg: UnitRegistry[Any]): u1 = Unit() # default values: assert u1.u == "dimensionless" @@ -27,6 +28,7 @@ def test_parsing(ureg: UnitRegistry): assert u1.u == "meter" assert u1.du is None val = u1.parse_quantity("9.9inch") + assert u1.to_base is not None and u1.from_base is not None assert val == u1.to_base(9.9), f"Found val={val}" assert u1.u == "meter" assert u1.du == "inch" @@ -41,10 +43,10 @@ def test_parsing(ureg: UnitRegistry): assert uf.u == "kelvin" assert uf.du == "degree_Fahrenheit" assert uf.parse_quantity("0.0 degF") == 255.37222222222223 - assert uf.to_base(0.0) == 255.37222222222223 + assert uf.to_base is not None and uf.to_base(0.0) == 255.37222222222223 -def test_make(ureg: UnitRegistry): +def test_make(ureg: UnitRegistry[Any]): val, unit = Unit.make("2m") assert val[0] == 2 assert unit[0].u == "meter", f"Found {unit[0].u}" @@ -59,7 +61,7 @@ def test_make(ureg: UnitRegistry): assert unit[0].du == "percent" -def test_make_tuple(ureg: UnitRegistry): +def test_make_tuple(ureg: UnitRegistry[Any]): vals, units = Unit.make_tuple(("2m", "3deg", "0.0 degF")) k2degc = 273.15 assert units[0].u == "meter" @@ -67,18 +69,22 @@ def test_make_tuple(ureg: UnitRegistry): assert vals[0] == 2 assert units[1].u == "radian", f"Found {units[1].u}" assert units[1].du == "degree" + assert units[1].to_base is not None assert units[1].to_base(1.0) == radians(1.0) + assert units[1].from_base is not None assert units[1].from_base(1.0) == degrees(1.0) assert vals[1] == radians(3) assert units[2].u == "kelvin", f"Found {units[2].u}" assert units[2].du == "degree_Fahrenheit", f"Found {units[2].du}" + assert units[2].from_base is not None assert abs(units[2].from_base(k2degc) - (k2degc * 9 / 5 - 459.67)) < 1e-10 + assert units[2].to_base is not None assert abs(units[2].to_base(0.0) - (0.0 + 459.67) * 5 / 9) < 1e-10, ( f"Found {units[2].to_base(0.0)}, {(0.0 + 459.67) * 5 / 9}" ) -def test_derivative(ureg: UnitRegistry): +def test_derivative(ureg: UnitRegistry[Any]): bv, bu = Unit.make_tuple(("2m", "3deg")) vals, units = Unit.derivative(bu) assert vals == (0.0, 0.0) @@ -90,7 +96,7 @@ def test_derivative(ureg: UnitRegistry): assert units[1].from_base == bu[1].from_base -def test_compatible(ureg: UnitRegistry): +def test_compatible(ureg: UnitRegistry[Any]): v, u = Unit.make_tuple(("2m", "3deg")) ck, q = u[0].compatible("4m", strict=True) assert ck diff --git a/tests/test_variable.py b/tests/test_variable.py index 8e7a56d..c80a24f 100644 --- a/tests/test_variable.py +++ b/tests/test_variable.py @@ -1,4 +1,3 @@ -# pyright: ignore[reportAttributeAccessIssue] # PythonFMU generates variable value objects using setattr() import logging import math import xml.etree.ElementTree as ET # noqa: N817 @@ -7,9 +6,9 @@ import numpy as np import pytest -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability from scipy.spatial.transform import Rotation as Rot from component_model.model import Model @@ -94,7 +93,7 @@ def test_range(): mod = DummyModel("MyModel2", instance_name="MyModel2") with pytest.raises(ValueError) as err: _int1 = Variable(mod, "int1", typ=int, start=1) - assert err.value.args[0] == "Auto-extremes for type cannot be determined" + assert err.value.args[0] == "int1 invalid range spec '()'. Error: Automatic range for int variables is not defined." int2 = Variable(mod, "int2", start=1, rng=(0, 5)) assert int2.range[0].rng == (0, 5), "That works" mod.int2 = 6 @@ -385,12 +384,13 @@ def test_init(): assert float1.check == Check.all # internally packed into tuple: assert float1.start == (0.99,) - assert float1.range[0].rng == (0.0, 99.0), f"Range: {float1.range[0].rng} in display units." assert float1.unit[0].u == "dimensionless" assert float1.unit[0].du == "percent", f"Display: {float1.unit[0].du}" - assert float1.unit[0].to_base(99) == 0.99, "Transform to dimensionless" - assert float1.unit[0].from_base(0.99) == 99, "... and back." - assert float1.check_range([0.5]) + assert float1.range[0].rng == (0.0, 0.99), f"Stored range: {float1.range[0].rng}!=(0.0,0.99) in base units." + assert float1.unit[0].to_base is not None and float1.unit[0].from_base is not None, "Transformations needed" + assert float1.unit[0].to_base(99) == 0.99, f"Transform % to dimensionless: {float1.unit[0].to_base(99)}" + assert float1.unit[0].from_base(0.99) == 99.0, f"... and back:: {float1.unit[0].from_base(0.99)}" + assert float1.check_range([50]) assert not float1.check_range([1.0], disp=False), "Check as internal units" assert not float1.check_range([100.0]), "Check as display units" assert mod.float1 == 0.99, "Value directly accessible as model variable" @@ -466,7 +466,7 @@ def test_init(): mod.set_string([mod.variable_by_name("str1").value_reference], ["Hello"]) # simulate setting from outside assert mod.get_string([mod.variable_by_name("str1").value_reference]) == ["Hello"] - assert np1.typ is float + assert np1.typ is float # np1: start=("1.0m","2deg","3rad"), rng=((0,"3m"),("1deg","5deg"),(float("-inf"), "5rad")) assert np1 == mod.variable_by_name("np1") assert np1.description == "A NP variable" assert mod.variable_by_name("np1[1]") == mod.variable_by_name("np1"), "Returns always the parent" @@ -476,7 +476,9 @@ def test_init(): assert np1.check == Check.all # internally packed into tuple: assert np1.start == (1, math.radians(2), 3) - for r, expect in zip(np1.range, ((0.0, 3.0), (1.0, 5.0), (float("-inf"), 5.0)), strict=True): + for r, expect in zip( + np1.range, ((0.0, 3.0), (np.radians(1.0), np.radians(5.0)), (float("-inf"), 5.0)), strict=True + ): assert np.allclose(r.rng, expect), f"{r.rng} != {expect}" assert not np1.check_range([5.1], idx=1), "Checks performed on display units!" assert not np1.check_range([0.9], idx=1), "Checks performed on display units!" @@ -521,7 +523,7 @@ def test_init(): ) assert err2.value.args[0] == "Variable int1 already used as index 0 in model MyModel" - with pytest.raises(AssertionError) as err3: + with pytest.raises(ValueError) as err3: int1 = Variable( mod, "bool1", @@ -533,8 +535,10 @@ def test_init(): annotations=None, typ=int, ) - assert err3.value.args[0].startswith("bool1 invalid range spec") - assert float1.range[0].rng[1] == 99.0 + assert err3.value.args[0].startswith( + "bool1 invalid range spec '()'. Error: Automatic range for int variables is not defined." + ) + assert float1.range[0].rng[1] == 0.99 assert enum1.range[0].rng == (0, 4) assert enum1.check_range([Causality.parameter]) assert str1.range[0].rng == ("Hello World!", "Hello World!"), "Just a placeholder. Range of str is not checked" @@ -670,7 +674,7 @@ def test_set(): assert mod.int1 == 60 assert mod.vars[1].getter() == [61], f"Found {mod.vars[99].getter()}" with pytest.raises(AssertionError) as err: - mod.set_integer([6, 7], [2.0, "30 deg"]) # type: ignore # we want to produce an error! + mod.set_integer([6, 7], [2.0, "30 deg"]) assert str(err.value) == "Invalid type in 'set_'. Found variable np1 with type " mod.set_real([6, 7], [2.0, 3.0]) # "3 deg"]) @@ -691,11 +695,17 @@ def test_xml(): lst = np2.xml_scalarvariables() assert len(lst) == 3 expected = '' - assert ET.tostring(lst[0], encoding="unicode") == expected, ET.tostring(lst[0], encoding="unicode") - expected = '' - assert ET.tostring(lst[1], encoding="unicode") == expected, ET.tostring(lst[1], encoding="unicode") - expected = '' - assert ET.tostring(lst[2], encoding="unicode") == expected, ET.tostring(lst[2], encoding="unicode") + assert ET.tostring(lst[0], encoding="unicode") == expected, ( + f"Found:\n{ET.tostring(lst[0], encoding='unicode')} != \n{expected}" + ) + expected = '' + assert ET.tostring(lst[1], encoding="unicode") == expected, ( + f"Found:\n{ET.tostring(lst[1], encoding='unicode')} != \n{expected}" + ) + expected = '' + assert ET.tostring(lst[2], encoding="unicode") == expected, ( + f"Found:\n{ET.tostring(lst[2], encoding='unicode')} != \n{expected}" + ) int1 = Variable( mod, @@ -709,7 +719,7 @@ def test_xml(): value_check=Check.all, ) lst = int1.xml_scalarvariables() - expected = '' + expected = '' found = ET.tostring(lst[0], encoding="unicode") assert found == expected, f"\nFound :{found}\nExpected:{expected}" @@ -760,9 +770,9 @@ def test_extremum(): if __name__ == "__main__": - retcode = 0 # pytest.main(["-rP -s -v", __file__]) + retcode = pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" - test_init() + # test_init() # test_range() # test_var_check() # test_spherical_cartesian() From ccc164260444d6f8e4e3a031b745884afe162604 Mon Sep 17 00:00:00 2001 From: Claas Date: Thu, 22 Jan 2026 18:14:38 +0100 Subject: [PATCH 04/11] Sphinx documentation: Removed component_model.plotter from the list of modules to create documentation for (in file docs/source/component_model.rst) . --- docs/source/component_model.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/component_model.rst b/docs/source/component_model.rst index 8c8a4df..98d187b 100644 --- a/docs/source/component_model.rst +++ b/docs/source/component_model.rst @@ -25,4 +25,3 @@ Modules component_model.variable_naming component_model.enums component_model.analytic - component_model.plotter From 37600b2e74892ec924cdb789d87b60aeada293ba Mon Sep 17 00:00:00 2001 From: Claas Date: Thu, 22 Jan 2026 18:15:08 +0100 Subject: [PATCH 05/11] updated uv.lock --- uv.lock | 115 +++++++++++++++++++++----------------------------------- 1 file changed, 43 insertions(+), 72 deletions(-) diff --git a/uv.lock b/uv.lock index bc0537d..33a5663 100644 --- a/uv.lock +++ b/uv.lock @@ -2,12 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version >= '3.12' and sys_platform == 'linux'", - "python_full_version < '3.12' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12'", + "python_full_version < '3.12'", ] required-markers = [ "sys_platform == 'win32'", @@ -201,8 +197,7 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, - { name = "sourcery", version = "1.40.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "sourcery", version = "1.41.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "sourcery" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-argparse-cli" }, @@ -1320,11 +1315,11 @@ numpy = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -1505,11 +1500,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.3.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -1676,28 +1671,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/81/5fd87d61352fb0f86b4436f278fe19b3770a8b73d42e8b3405d28df6b759/ruff-0.14.12.tar.gz", hash = "sha256:276b0821947f2afff8ee6da282bade96459d2e29f5a203eef04eb7b7a85b119f", size = 6055422, upload-time = "2026-01-15T16:22:12.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/de/58c5e3b4e6be8d0e007856128be59688cd4025a5345693dd169d61df8eb2/ruff-0.14.12-py3-none-linux_armv6l.whl", hash = "sha256:59434a99f0af57111f62cd77e86b4d4896a2c72bb90cc039d1ac501b151b798b", size = 13046871, upload-time = "2026-01-15T16:21:53.033Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b3/d9710419b6aed406a41c7eb215d604a55d2137f2a60e24d0939dae081b1b/ruff-0.14.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5912e8270c6b9ed28ae50b993032d195046293fefa56127a8dffbaa5e5bfc04", size = 13433477, upload-time = "2026-01-15T16:21:40.401Z" }, - { url = "https://files.pythonhosted.org/packages/da/4f/25cfc2c4b9fa22c90038bdd966e8e3aea92826790e5350f537f7b3f84609/ruff-0.14.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f59c587a413c9bd3259ec2d006853671f823ac4b4f974653bbcc84c180271de", size = 12356766, upload-time = "2026-01-15T16:21:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5d/8da8aaca205ad94c87848e5f67b2cd014c022a76ea485ef9f1004ebc5118/ruff-0.14.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd31832569b06c75fdbf6f5f63a3458b7038d97f6e908b6e08fd02d00c20a3ae", size = 12771650, upload-time = "2026-01-15T16:22:00.387Z" }, - { url = "https://files.pythonhosted.org/packages/78/cc/75bc23144392ed21cd3413baf2476b9cd48eb056d87080530a1fe748108e/ruff-0.14.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc1ba7b0d74d14b75c3a9d7e7ca01040bd126bd6bde2cffe3c31c1defe649ec6", size = 12820601, upload-time = "2026-01-15T16:21:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/66d03158d2c477da458d9ea22feed4d40609d596dd6fa10bf158039ce8f8/ruff-0.14.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1a90feecc2e44c01737d9d879ba6fc84b5626a7bd31e81991443198596ebe22", size = 13671644, upload-time = "2026-01-15T16:22:17.561Z" }, - { url = "https://files.pythonhosted.org/packages/22/df/0fa8920f4f1ca00d48a6f9b0b2310c426f4fcc943967af2d2723f7476d04/ruff-0.14.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b4c3478b459b71940f49d4dac1f91efa3852bb566368ad9f94dae2c257b6e63e", size = 15147221, upload-time = "2026-01-15T16:21:32.998Z" }, - { url = "https://files.pythonhosted.org/packages/54/9a/64365c7be12c51100f4159d15a8b818e4ffdb6aa592080932aa954a59cbd/ruff-0.14.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c14202522c2887e6644308827e72a6a0561fa965854e42ef14b325fa617c2634", size = 14709017, upload-time = "2026-01-15T16:22:02.69Z" }, - { url = "https://files.pythonhosted.org/packages/a7/58/2696c48a5ac88e9716a25048895f450772ce2f3e4cb37d67efc1e11da884/ruff-0.14.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e15471064058c5f22ad17aec4cab3920f71bf6b2dd8e1c8cfb05e8e9ee6d9d6", size = 14133819, upload-time = "2026-01-15T16:21:58.031Z" }, - { url = "https://files.pythonhosted.org/packages/74/81/ce6aea2dcb40cba3d865f4fda5b5058ddc7786002d5f8413e6bc542ed665/ruff-0.14.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:697c2e00f00cb1027b91fc8930e276bed580b2976a3e4aca50eae2b3db291f92", size = 13849027, upload-time = "2026-01-15T16:21:50.801Z" }, - { url = "https://files.pythonhosted.org/packages/db/42/402928ed9a377f15abea9331bc77315f1c83ecffa6477251cb435df674bb/ruff-0.14.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0864023ac5b2c90af354372529926b4fce4980bff828b722bedf115a0677131b", size = 14030353, upload-time = "2026-01-15T16:22:04.835Z" }, - { url = "https://files.pythonhosted.org/packages/02/1a/bc4ee626929034fa1f0d97f03edf334bfdc947cc2609baed7529a4adb352/ruff-0.14.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6372d9e07c0e61342582b29ba825c0712930c08ee72312d1764abfe761de2806", size = 12666546, upload-time = "2026-01-15T16:22:07.392Z" }, - { url = "https://files.pythonhosted.org/packages/f2/3f/e00efbf5ecf9279b06fcc26821178c46d89b402facdf9b7974b2a76f1da0/ruff-0.14.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c59c4941400a86c60219fd1457012e6da23a92590755ab4ed1ae25bbe4aab948", size = 12802512, upload-time = "2026-01-15T16:21:48.13Z" }, - { url = "https://files.pythonhosted.org/packages/6f/05/610b462fc211eb877457ee08062af50c56d924849038ec1e6005d6e50791/ruff-0.14.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:65ce54567c42e3a6d2548b49b8e4e87148eaf455ad76d7a20f83c902bc7cc5e4", size = 13205003, upload-time = "2026-01-15T16:21:37.904Z" }, - { url = "https://files.pythonhosted.org/packages/43/87/68fb2335cf969f3b5a7808265e6998cc735bce9a8efc57f6a3c1c167b3bb/ruff-0.14.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:28255d5a1828bc1da67c8c5a2cea5c83de6f0b6185eef46f4fc12957396fb169", size = 13925981, upload-time = "2026-01-15T16:21:45.453Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a7/2f02a56b457c7cf568778fbbc2302ce6aa00bd20f9dd6149329a376a6964/ruff-0.14.12-py3-none-win32.whl", hash = "sha256:a111f79ba789257177fe5971eea061c8545af3c0cab3529fa00155f6621c68da", size = 12897262, upload-time = "2026-01-15T16:22:15Z" }, - { url = "https://files.pythonhosted.org/packages/8b/fd/86c2309a254b4e0982b3423504614d9f05b077961167a31bdb5b533be8a9/ruff-0.14.12-py3-none-win_amd64.whl", hash = "sha256:8da366e9942b26ac1fb0df43264a3550655160b9536cdb66a2b070a22b6e5d6a", size = 14105034, upload-time = "2026-01-15T16:22:10.139Z" }, - { url = "https://files.pythonhosted.org/packages/5c/0a/8f0d458479113587b1dfd3fabedfd3f1df4653801273993b1f4dc31a935f/ruff-0.14.12-py3-none-win_arm64.whl", hash = "sha256:f0672b72872dc204580d2d3f8bdc4e922c88a0f93e2a3fb799ca085797a5a7c4", size = 13064991, upload-time = "2026-01-15T16:21:35.436Z" }, +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, ] [[package]] @@ -1803,38 +1798,22 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.1" +version = "2.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] name = "sourcery" -version = "1.40.0" +version = "1.43.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'linux'", - "python_full_version < '3.12' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/ef/42fa2226110cba8f0b656a0f30cac9e66fe0285fa249698bc1d9c5d6b8b4/sourcery-1.40.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:19116cc91aed5db2badca22cd2d745b8e2151bf22bac037d4b00aa2edd52217c", size = 124203805, upload-time = "2025-10-28T11:37:28.078Z" }, - { url = "https://files.pythonhosted.org/packages/e3/3d/b5bd0bc987472a7adb11b6c2fa8612ba3e445d41fed8b473b156af09d519/sourcery-1.40.0-py2.py3-none-win_amd64.whl", hash = "sha256:7a45fd88840cd167747519984213091912fe3782bc167af0c2de1c78d3d9672d", size = 101322491, upload-time = "2025-10-28T11:37:33.595Z" }, -] - -[[package]] -name = "sourcery" -version = "1.41.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", -] wheels = [ - { url = "https://files.pythonhosted.org/packages/85/92/8150f339ca39a3bbca83cb70a49b22e7c2234ec8d8129bc6bfbe1a6aaf47/sourcery-1.41.1-py2.py3-none-macosx_10_9_universal2.whl", hash = "sha256:9ecb7636301e9dea8934f897151e504127274ea60c7709a65bed7457850f994c", size = 101735565, upload-time = "2025-10-30T14:04:17.397Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7a/624a3539c5355de0ee9cec4cb9c6d2be6d36ab916c190b365c8462f0f0d5/sourcery-1.43.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6279f505406b9b3455dac2f9646c0e110fdc72857479dc05b788a2cfd28dfa4f", size = 107493269, upload-time = "2026-01-19T15:47:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/78/f9/058e1026479d2af655be608803d75d96297f4d0a2e98bb510488e384f415/sourcery-1.43.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:89034e4a800294093614c8c01317e0b051c35d2e7abf089e78f1830984516e04", size = 93553650, upload-time = "2026-01-19T15:48:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/86/18/4075fe0f6b6a3692d80c9c490792d66f839ed91111afa7aea7869f6f1682/sourcery-1.43.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:a142fb1156dfc205752b546fd24cd6e6bd405ebd7aec1097c339ed0e05724dcd", size = 132906772, upload-time = "2026-01-19T15:49:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/8ac262bd2d1c5527a9d8c437f5bdbabc5dc68690110dbf34b20be090798a/sourcery-1.43.0-py2.py3-none-win_amd64.whl", hash = "sha256:2bd293af561261ffc0bccff82c9f764cd654247aaa29a0b46d4927e4711b0570", size = 91181924, upload-time = "2026-01-19T15:50:24.796Z" }, ] [[package]] @@ -1842,9 +1821,7 @@ name = "sphinx" version = "9.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform == 'linux'", - "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12'", ] dependencies = [ { name = "alabaster", marker = "python_full_version < '3.12'" }, @@ -1875,9 +1852,7 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version >= '3.12' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12'", ] dependencies = [ { name = "alabaster", marker = "python_full_version >= '3.12'" }, @@ -1921,9 +1896,7 @@ name = "sphinx-autodoc-typehints" version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform == 'linux'", - "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12'", ] dependencies = [ { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -1938,9 +1911,7 @@ name = "sphinx-autodoc-typehints" version = "3.6.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version >= '3.12' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12'", ] dependencies = [ { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, From fbe375f013bfc525c77338525508ee09df064cdf Mon Sep 17 00:00:00 2001 From: Claas Date: Thu, 22 Jan 2026 18:29:10 +0100 Subject: [PATCH 06/11] Sphinx documentation: Added component_model.unit and component_model.range to the list of modules to create documentation for (in file docs/source/component_model.rst) . --- docs/source/component_model.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/component_model.rst b/docs/source/component_model.rst index 98d187b..9ee9b7c 100644 --- a/docs/source/component_model.rst +++ b/docs/source/component_model.rst @@ -23,5 +23,7 @@ Modules component_model.model component_model.variable component_model.variable_naming + component_model.unit + component_model.range component_model.enums component_model.analytic From 64d179e0edf8666fb514ac120f894ac1f4d9ac5b Mon Sep 17 00:00:00 2001 From: Claas Date: Thu, 22 Jan 2026 18:30:25 +0100 Subject: [PATCH 07/11] updated CHANGELOG.md --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1026376..30a0635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,18 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/e * -/- -## [0.3.2] - 2026-01-16 +## [0.3.2] - 2026-01-23 ### Added +* Added new module unit.py, containing class `Unit`, a helper class to store and manage units and display units. One `Unit` object represents one scalar variable. +* Added new module range.py, containing class `Range`, a utility class to store and handle the variable range of a single-valued variable. * Sphinx documentation: - * Added docs for modules variable_naming.py, enums.py, analytic.py and plotter.py + * Added docs for modules variable_naming.py, unit.py, range.py, enums.py and analytic.py * Added Visual Studio Code settings +### Removed +* Removed module plotter.py + ### Changed * Updated code base with latest changes in python_project_template v0.2.6 * pyproject.toml: From 7996ac26ebed87366a99dd1b5b3b7475ca83edd1 Mon Sep 17 00:00:00 2001 From: Eisinger Date: Sat, 24 Jan 2026 10:08:13 +0100 Subject: [PATCH 08/11] Updated and simplified Unit management. --- examples/DrivingForce.fmu | Bin 816344 -> 817154 bytes examples/DrivingForce6D.fmu | Bin 820435 -> 821341 bytes examples/HarmonicOscillator.fmu | Bin 815891 -> 816372 bytes examples/HarmonicOscillator6D.fmu | Bin 823554 -> 824921 bytes src/component_model/range.py | 28 ++++--- src/component_model/unit.py | 123 +++++++++++++-------------- src/component_model/variable.py | 133 ++++++++++++++---------------- tests/test_time_table_fmu.py | 4 +- tests/test_unit.py | 13 +-- tests/test_variable.py | 69 ++++++++++------ 10 files changed, 196 insertions(+), 174 deletions(-) diff --git a/examples/DrivingForce.fmu b/examples/DrivingForce.fmu index 46758ee927e27a420d4c4c3ecc3ac72923f9faea..3f54e30e5c579a09384f6f418b23cfddbd16a4d8 100644 GIT binary patch delta 5843 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 diff --git a/examples/DrivingForce6D.fmu b/examples/DrivingForce6D.fmu index f45df5d6adadfe590d98ddbe68c8aba7a177055c..42411c40d66672902ea920fc850a2308b6932fe7 100644 GIT binary patch delta 6343 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 Z?gG5NPsvN4e4!4{->*~`jn64`@PC(0D1QI| delta 5243 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 CJ3%J^ delta 4949 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 diff --git a/src/component_model/range.py b/src/component_model/range.py index 36a5623..f55028d 100644 --- a/src/component_model/range.py +++ b/src/component_model/range.py @@ -42,7 +42,7 @@ def __init__( unit = Unit() assert isinstance(val, (bool, int, float, str, Enum)), f"Only primitive types allowed for Range. Found {typ}" if isinstance(val, str): - assert unit.u == "dimensionless", f"A free string cannot have units. Found {unit.u}" + assert unit.u == "", f"A free string cannot have units. Found {unit.u}" self.rng = (val, val) # no range for free strings elif rng is None: # fixed value in any case. val provided in base units. No conversion self.rng = (val, val) # type: ignore[assignment] ## see def above @@ -59,9 +59,9 @@ def __init__( l_rng[i] = val # type: ignore[reportArgumentType] ## l_rng is not empty # fixed display value else: assert isinstance(r, (str, int, bool, float, Enum)), f"Found type {type(r)}" - check, q = unit.compatible(r, typ, strict=True) # q in base units + check, q = unit.compatible(r, no_unit=False, strict=True) # q in base units if not check: - raise ValueError(f"Provided range {rng} is not conformant with unit {unit}") from None + raise ValueError(f"Provided range {rng}[{i}] is not conformant with unit {unit}") from None assert isinstance(q, (int, bool, float)), "Unexpected type {type(q)} in {rng}[{i}]" try: q = type(val)(q) # ensure correct Python type @@ -120,20 +120,28 @@ def check( if value is None: # denotes unchanged values (of compound variables) return True if not isinstance(value, typ): - try: - value = typ(value) # try to cast the values - except Exception: # give up - return False + if issubclass(typ, Enum): + if isinstance(value, Enum): + value = value.value + assert isinstance(value, int) and isinstance(self.rng[0], int) and isinstance(self.rng[0], int), ( + f"Enum range is managed as int. Found {self.rng}, {value}" + ) + return self.rng[0] <= value <= self.rng[1] # type: ignore[operator] ## all arguments int! + else: + try: + assert typ is not None, "Need a proper typ argument here" + value = typ(value) # type: ignore ## try to cast the values + except Exception: # give up + return False # special types if typ is str: # no range checking on str return True elif typ is bool: return isinstance(value, bool) elif isinstance(value, Enum): - return isinstance(value, typ) - + return self.rng[0] <= value.value <= self.rng[1] # type: ignore[operator] ## There is no str involved! elif isinstance(value, (int, float)) and all(isinstance(x, (int, float)) for x in self.rng): - assert typ is int or typ is float, f"Inconsistent type {typ}. Expect int or float" + # assert typ is int or typ is float, f"Inconsistent type {typ} for value {value}. Expected int or float" if disp and unit.to_base is not None: # check a display unit values value = unit.to_base(value) return self.rng[0] <= value <= self.rng[1] # type: ignore[operator] ## There is no str involved! diff --git a/src/component_model/unit.py b/src/component_model/unit.py index cd66f8e..3b51f11 100644 --- a/src/component_model/unit.py +++ b/src/component_model/unit.py @@ -11,23 +11,33 @@ class Unit: """Helper class to store and manage units and display units, - i.e. base unit of variable and unit differences 'outside' and 'inside' the model. + i.e. base unit of variable and unit differences 'outside'(display units) and 'inside'(base units) the model. - One Unit object represents one scalar variable. + Args: + quantity (bool, int, float, str, Enum, None): The quantity to be disected for unit definition. 3 possibilites: + + * None: no units. Instantiates an 'empty' Unit object + * str: is parsed to disect the unit. + If a unit is identified this is treated as a variable with units (and possibly display units) + If no unit is identified this is treated as a free string variable (no units). Use 'None' to ensure free str. + * bool, int, Enum: variables with no units and no display units + + * one Unit object represents one scalar variable. + * many variables do not have units (i.e. str, Enum, int variables). If These get the unit .u="" + * variables without separate display units get the display unit .du=None and a reduced set of properties + * only float variables may have separate display units and transformations """ _ureg: UnitRegistry[Any] | None = None - def __init__(self, quantity: bool | int | float | str | Enum | None = None, typ: type | None = None): + def __init__(self, quantity: bool | int | float | str | Enum | None = None): assert Unit._ureg is not None, "Before units can be instantiated, Unit.ensure_unit_registry() must be called." - # properties with default values. Initialized through parse_quantity - self.u: str = "dimensionless" # default: dimensionless unit (placeholder) + self.u: str = "" # default: no units self.du: str | None = None # display unit (default: same as u, no transformation) - # Transformations f(display-value) -> base-value and f(base-value) -> display-value - self.to_base: Callable[[int | float], int | float] | None = None - self.from_base: Callable[[int | float], int | float] | None = None + self.to_base: Callable[[Any], Any] = Unit.identity # default transformation is identity + self.from_base: Callable[[Any], Any] = Unit.identity # default transformation is identity if quantity is not None: # if parse-value is called on class it also returns the (parsed,converted) base-value - _val = self.parse_quantity(quantity, typ) + _val = self.parse_quantity(quantity) @classmethod def ensure_unit_registry(cls, system: str = "SI", autoconvert: bool = True): @@ -40,11 +50,9 @@ def __str__(self): txt += f". Offset:{self.to_base(0)}, factor:{self.to_base(1.0) - self.to_base(0.0)}" return txt - def parse_quantity( - self, quantity: bool | int | float | str | Enum, typ: type | None = None - ) -> bool | int | float | str | Enum: + def parse_quantity(self, quantity: bool | int | float | str | Enum) -> bool | int | float | str | Enum: """Parse the provided quantity in terms of magnitude and unit, if provided as string. - If another type is provided, dimensionless units are assumed. + If another type is provided, no units are assumed. Args: quantity: the quantity to disect. Should be provided as string, but also the trivial cases (int,float,Enum) are allowed. @@ -53,45 +61,37 @@ def parse_quantity( the magnitude in base units, the base unit and the unit as given (display units), together with the conversion functions between the units. """ - if typ is str or typ is Enum: - self.u = "dimensionless" - self.du = None - val = quantity - elif isinstance(quantity, str): # only string variable make sense to disect + if isinstance(quantity, str): # only string variable make sense to disect assert Unit._ureg is not None, "UnitRegistry not yet instantiated!" try: q = Unit._ureg(quantity) # parse the quantity-unit and return a Pint Quantity object if isinstance(q, (int, float)): - self.u = "" + self.u = "dimensionless" self.du = None return q # integer or float variable with no units provided elif isinstance(q, Quantity): # pint.Quantity object # transform to base units ('SI' units). All internal calculations will be performed with these val = self.val_unit_display(q) - else: - logger.critical(f"Unknown quantity {quantity} to disect") - raise ValueError(f"Unknown quantity {quantity} to disect") from None - # no recognized units. Assume a free string. ??Maybe we should be more selective about the exact error type: - except Exception as warn: - logger.warning(f"Unhandled quantity {quantity}: {warn}. A str? Set explicit 'typ=str'.") - self.u = "" - self.du = None - val = str(quantity) - else: - self.u = "dimensionless" - self.du = None - val = quantity - if typ is not None and type(val) is not typ: # check variable type - try: # try to convert the magnitude to the correct type. - val = typ(val) + return val + else: # since this is not a recognized quantity, we assume an implicit str + pass except Exception as err: - logger.critical(f"Value {val} is not of the correct type {typ}") - raise TypeError(f"Value {val} is not of the correct type {typ}") from err + logger.warning(f"Quantity {quantity} could not be disected: {err}. Assume free string.") + self.u = "" + self.du = None + return quantity + + @classmethod + def identity(cls, val: Any) -> Any: return val @classmethod - def linear(cls, x: int | float, b: int | float, a: int | float = 0) -> int | float: - return a + b * x + def slope(cls, val: float, slope: float) -> float: + return slope * val + + @classmethod + def linear(cls, val: float, intercept: float, slope: float) -> float: + return intercept + slope * val def val_unit_display(self, q: Quantity[int | float]) -> int | float: """Identify base units and calculate the transformations between display and base units. @@ -120,37 +120,41 @@ def val_unit_display(self, q: Quantity[int | float]) -> int | float: qb2 = q2.to_base_units() a = (qb.magnitude * q2.magnitude - qb2.magnitude * q.magnitude) / (q2.magnitude - q.magnitude) b = (qb2.magnitude - qb.magnitude) / (q2.magnitude - q.magnitude) - if abs(a) < 1e-9: # multiplicative conversion - if abs(b - 1.0) < 1e-9: # unit and display unit are compatible. No transformation - self.du = None - else: - self.to_base = partial(Unit.linear, b=b, a=0.0) - self.from_base = partial(Unit.linear, b=1.0 / b, a=0.0) + if abs(a) < 1e-9 and abs(b) < 1e-9: # identity + self.to_base = self.from_base = Unit.identity + if abs(a) < 1e-9: # multiplicative conversion (only slope) + self.to_base = partial(Unit.slope, slope=b) + self.from_base = partial(Unit.slope, slope=1.0 / b) else: # there is a constant (e.g. Celsius to Fahrenheit) - self.to_base = partial(Unit.linear, b=b, a=a) - self.from_base = partial(Unit.linear, b=1.0 / b, a=-a / b) + self.to_base = partial(Unit.linear, intercept=a, slope=b) + self.from_base = partial(Unit.linear, intercept=-a / b, slope=1.0 / b) return val @classmethod def make( - cls, quantity: bool | int | float | str | Enum, typ: type | None = None + cls, quantity: bool | int | float | str | Enum, no_unit: bool = False ) -> tuple[tuple[bool | int | float | str | Enum], tuple["Unit"]]: - """Parse quantity and return the resulting value and its unit object.""" + """Parse quantity and return the resulting value and its unit object. + If no_unit, only a default object is generated. + """ u = Unit() - val = u.parse_quantity(quantity, typ) - return ((val,), (u,)) + if no_unit: + return ((quantity,), (u,)) + else: + val = u.parse_quantity(quantity) + return ((val,), (u,)) @classmethod def make_tuple( cls, quantities: tuple[bool | int | float | str | Enum, ...] | list[bool | int | float | str | Enum] | np.ndarray, - typ: type | None = None, + no_unit: bool = False, ) -> tuple[tuple[bool | int | float | str | Enum, ...], tuple["Unit", ...]]: """Make a tuple of values and Unit objects from the tuple of quantities, using make().""" values: list[bool | int | float | str | Enum] = [] units: list[Unit] = [] for q in quantities: - val, u = cls.make(q, typ) + val, u = cls.make(q, no_unit=no_unit) values.extend(val) units.extend(u) return (tuple(values), tuple(units)) @@ -162,28 +166,25 @@ def derivative(cls, baseunits: tuple["Unit", ...], tu: str = "s") -> tuple[tuple for bu in baseunits: u = Unit() u.u = f"{bu.u}/{tu}" + u.to_base = bu.to_base # link the functions + u.from_base = bu.from_base # link the functions u.du = None if bu.du is None else f"{bu.du}/{tu}" - if bu.du is not None: - u.to_base = bu.to_base - u.from_base = bu.from_base units.append(u) values = [0.0] * len(baseunits) return (tuple(values), tuple(units)) def compatible( - self, quantity: bool | int | float | str | Enum, typ: type | None = None, strict: bool = True + self, quantity: bool | int | float | str | Enum, no_unit: bool = False, strict: bool = True ) -> tuple[bool, bool | int | float | str | Enum]: """Check whether the supplied quantity 'q' is compatible with this unit. If strict==True, the supplied quantity shall be in display units. """ - _q, _unit = Unit.make(quantity, typ) + _q, _unit = Unit.make(quantity, no_unit=no_unit) q = _q[0] unit = _unit[0] # no explicit unit needed when the quantity is 0 or inf (anything compatible) if ( - ( - (q == 0 or q == float("inf") or q == float("-inf")) and unit.u == "dimensionless" - ) # 0, +/-inf without unit + (q == 0 or q == float("inf") or q == float("-inf")) # 0, +/-inf with any unit or (strict and self.u == unit.u and self.du == unit.du) or (not strict and self.u == unit.u) ): diff --git a/src/component_model/variable.py b/src/component_model/variable.py index 0aff0dc..072b1a3 100644 --- a/src/component_model/variable.py +++ b/src/component_model/variable.py @@ -180,15 +180,19 @@ def __init__( assert local_name is None, f"{self.name} Default start value only defined for derivatives" assert basevar is not None, f"{self.name} basevar needed at this point" self._start, self._unit = Unit.derivative(basevar.unit) - elif self._typ is str or not isinstance(start, (tuple, list, np.ndarray)): - self._start, self._unit = Unit.make(start, self._typ) # type: ignore ## type of start should be ok + elif self._typ is str: + assert isinstance(start, str), f"Scalar str expected. Found {start}" + self._start, self._unit = (start,), (Unit(None),) # explicit free string variable + elif not isinstance(start, (tuple, list, np.ndarray)): + self._start, self._unit = Unit.make(start, no_unit=False) else: - self._start, self._unit = Unit.make_tuple(start, self._typ) + self._start, self._unit = Unit.make_tuple(start, no_unit=False) self._len = 1 if self._typ is str else len(self._start) - if self._typ is None: # try to adapt using start - self._typ = self.auto_type(self._start) + if self._typ is None: # try to adapt using _start and _unit + self._typ = self.auto_type(self._start, self._unit) assert isinstance(self._typ, type) - self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct + if self._typ is not Enum: # Enums are already checked and casting does not work + self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct ck = Range.is_valid_spec(rng, self._len, self._typ) if ck != 0: @@ -229,6 +233,24 @@ def der1(self, current_time: float, step_size: float): newval = val + step_size * der basevar.setter_internal(newval, -1) # , True) + # def _parse_start( start:None|PyType|tuple[PyType])-> tuple[PyType|np.ndarray,): + # """Read start value(s), extract unit(s) and return everything needed for the simulation.""" + # if start is None: + # assert local_name is None, f"{self.name} Default start value only defined for derivatives" + # assert basevar is not None, f"{self.name} basevar needed at this point" + # self._start, self._unit = Unit.derivative(basevar.unit) + # elif self._typ is str or self._typ is Enum: + # + # not isinstance(start, tuple)): + # self._start, self._unit = Unit.make(start, no_unit=True) # type: ignore ## type of start should be ok + # else: + # self._start, self._unit = Unit.make_tuple(start, no_unit=False) + # self._len = 1 if self._typ is str else len(self._start) + # if self._typ is None: # try to adapt using start + # self._typ = self.auto_type(self._start) + # assert isinstance(self._typ, type) + # self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct + # disable super() functions and properties which are not in use here def to_xml(self) -> ET.Element: logger.critical("The function to_xml() shall not be used from component-model") @@ -285,9 +307,10 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, if idx == -1 and self._len == 0: # the whole scalar idx = 0 - if issubclass(self._typ, Enum): # Enum types are supplied as int. Convert + if issubclass(self._typ, Enum): # Enum types may be supplied as int. Convert for i in range(self._len): - values[i] = self._typ(values[i]) # type: ignore + if isinstance(values[i], int): + values[i] = self._typ(values[i]) # type: ignore if self._check & Check.ranges: # do that before unit conversion, since range is stored in display units! if not self.check_range(values, idx): @@ -295,23 +318,14 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, if self._check & Check.units: #'values' expected as displayUnit. Convert to unit if idx >= 0: # explicit index of single values - if self._unit[idx].to_base is None: - dvals = list(values) - else: - dvals = [self._unit[idx].to_base(values[0])] # type: ignore + dvals = [self._unit[idx].to_base(values[0])] # type: ignore else: # the whole array dvals = [] for i in range(self._len): if values[i] is None: # keep the value dvals.append(getattr(self.owner, self.local_name)[i]) - elif self._unit[i].du is None: - dvals.append(values[i]) else: - # assert isinstance(values[i], float) or (self._typ is int and isinstance(values[i], int)) - val_i = values[i] - if self._unit[i].to_base is not None: - val_i = self._unit[i].to_base(values[i]) # type: ignore - dvals.append(val_i) + dvals.append(self._unit[i].to_base(values[i])) else: # no unit issues if self._len == 1: dvals = [values[0] if values[0] is not None else getattr(self.owner, self.local_name)] @@ -361,10 +375,9 @@ def getter(self) -> list[PyType]: if not isinstance(value, self._typ): # other type conversion value = self._typ(value) # type: ignore[call-arg] ## only mypy if self._check & Check.units: # Convert 'value' base unit -> display.u - if self._unit[0].from_base is not None: - assert isinstance(value, float) - value = self._unit[0].from_base(value) - values = [value] + values = [self._unit[0].from_base(value)] + else: + values = [value] else: # compound variable values = list(getattr(self.owner, self.local_name)) # make value available as copy @@ -377,8 +390,7 @@ def getter(self) -> list[PyType]: values[i] = self._typ(values[i]) # type: ignore[call-arg] ## only mypy if self._check & Check.units: # Convert 'value' base unit -> display.u for i in range(self._len): - if self._unit[i].from_base is not None: - values[i] = self._unit[i].from_base(values[i]) # type: ignore[reportOptionalCall] ##checked! + values[i] = self._unit[i].from_base(values[i]) if self._check & Check.ranges and not self.check_range(values, -1): # check the range if so instructed logger.error(f"getter(): Value of {self.name}: {values} outside range {self.range}!") @@ -420,48 +432,33 @@ def fmi_type_str(self, val: PyType) -> str: return str(val) @classmethod - def auto_type(cls, val: PyType | Compound, allow_int: bool = False): - """Determine the Variable type from a provided example value. + def auto_type(cls, vals: tuple[PyType, ...], units: tuple[Unit, ...]) -> type: + """Determine the Variable type from a set of example values and related Unit objects. + + Variable type must be unique for the whole set of vals/units. Since variables can be initialized using strings with units, - the type can only be determined when the value is disected. + the type can only be determined when the value is disected and the units defined. Moreover, the value may indicate an integer, while the variable is designed a float. - Therefore int Variables must be explicitly specified. + int type is therefore only decided if all vals are int and if no unit is disected. """ - assert val is not None, "'val is None'!" - if isinstance(val, (tuple, list, np.ndarray)): - types = [cls.auto_type(x, allow_int) for x in val] - typ = None - for t in types: - if t is not None and typ is None: - typ = t - elif t is not None and typ is not None: - if t == typ: - pass - elif t != typ: # identify the super-type - if issubclass(t, typ): # is a sub-class. Ok - pass - elif issubclass(typ, t): - typ = t - elif typ is float and t is int: # we allow that, even if no subclass - pass - elif typ is int and t is float: # we allow that, even if no subclass - typ = float - else: - logger.critical(f"Incompatible variable types {typ}, {t} in {val}") - raise TypeError(f"Incompatible variable types {typ}, {t} in {val}") from None - else: - logger.critical(f"auto_type(). Unhandled {t}, {typ}") - raise ValueError(f"auto_type(). Unhandled {t}, {typ}") - return typ - else: # single value - if isinstance(val, bool): - return bool - elif allow_int: - return type(val) - elif not allow_int and isinstance(val, (int, float)): + types: list[type] = [] + for v, u in zip(vals, units, strict=True): + types.append(type(v)) + if isinstance(v, (bool, Enum, str)) and u.u != "": + raise ValueError(f"{type(v).__name__} value {v} with unit '{u.u}' is not allowed.") + elif isinstance(v, int): + if u.u != "": # must be a 'hidden float' + types[-1] = float + if len(types) == 1 or all(types[0] is t for t in types[1:]): # all element types equal + if issubclass(types[0], float): # e.g. numpy.float64 is tracked as float return float else: - return type(val) + return types[0] + if any(t is float for t in types) and all(t is float or t is int for t in types): # int&float -> float + return float + else: + _units = tuple([u.u for u in units]) + raise ValueError(f"Auto-type cannot be determined for values {vals} with units {_units}") @classmethod def _auto_extreme(cls, var: PyType) -> tuple[float | bool, ...]: @@ -498,7 +495,6 @@ def xml_scalarvariables(self): ------- List of ScalarVariable xml elements """ - _type = {"int": "Integer", "bool": "Boolean", "float": "Real", "str": "String", "Enum": "Enumeration"}[ self.typ.__qualname__ ] # translation of python to FMI primitives. Same for all components @@ -530,23 +526,18 @@ def xml_scalarvariables(self): # detailed variable definition info = ET.Element(_type) if do_use_start: # a start value is to be used - _start = self._start[i] - if self._unit[i].from_base is not None: - _start = self._unit[i].from_base(_start) # type: ignore - info.attrib.update({"start": self.fmi_type_str(_start)}) + info.attrib.update({"start": self.fmi_type_str(self._unit[i].from_base(self._start[i]))}) if _type in ("Real", "Integer", "Enumeration"): # range to be specified xmin = self.range[i].rng[0] if _type == "Real" and isinstance(xmin, float) and xmin == float("-inf"): info.attrib.update({"unbounded": "true"}) else: - xmin = xmin if self._unit[i].from_base is None else self._unit[i].from_base(xmin) # type: ignore - info.attrib.update({"min": str(xmin)}) + info.attrib.update({"min": str(self._unit[i].from_base(xmin))}) xmax = self.range[i].rng[1] if _type == "Real" and isinstance(xmax, float) and xmax == float("inf"): info.attrib.update({"unbounded": "true"}) else: - xmax = xmax if self._unit[i].from_base is None else self._unit[i].from_base(xmax) # type: ignore - info.attrib.update({"max": str(xmax)}) + info.attrib.update({"max": str(self._unit[i].from_base(xmax))}) if _type == "Real": # other attributes apply only to Real variables info.attrib.update({"unit": self.unit[i].u}) if isinstance(self._unit[i].du, str) and self.unit[i].du != self._unit[i].u: diff --git a/tests/test_time_table_fmu.py b/tests/test_time_table_fmu.py index 47f02fa..81822b9 100644 --- a/tests/test_time_table_fmu.py +++ b/tests/test_time_table_fmu.py @@ -180,13 +180,13 @@ def test_use_with_new_data(show: bool): if __name__ == "__main__": - retcode = 0 # pytest.main(["-rA", "-v", __file__]) + retcode = pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" import os os.chdir(Path(__file__).parent.absolute() / "test_working_directory") # test_time_table_fmu() - test_make_time_table(_time_table_fmu()) + # test_make_time_table(_time_table_fmu()) # test_use_fmu(_time_table_fmu(), show=True) # test_make_with_new_data() # test_use_with_new_data(show=True) diff --git a/tests/test_unit.py b/tests/test_unit.py index 6dd48d8..b6ed2ab 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -1,6 +1,7 @@ from math import degrees, radians from typing import Any +import numpy as np import pytest from pint import UnitRegistry @@ -21,14 +22,13 @@ def _ureg(): def test_parsing(ureg: UnitRegistry[Any]): u1 = Unit() # default values: - assert u1.u == "dimensionless" + assert u1.u == "" assert u1.du is None val = u1.parse_quantity("9.9m") assert val == 9.9 assert u1.u == "meter" assert u1.du is None val = u1.parse_quantity("9.9inch") - assert u1.to_base is not None and u1.from_base is not None assert val == u1.to_base(9.9), f"Found val={val}" assert u1.u == "meter" assert u1.du == "inch" @@ -51,9 +51,9 @@ def test_make(ureg: UnitRegistry[Any]): assert val[0] == 2 assert unit[0].u == "meter", f"Found {unit[0].u}" assert unit[0].du is None - val, unit = Unit.make("Hello World", typ=str) + val, unit = Unit.make("Hello World", no_unit=True) assert val[0] == "Hello World" - assert unit[0].u == "dimensionless" + assert unit[0].u == "" assert unit[0].du is None val, unit = Unit.make("99.0%") assert val[0] == 0.99 @@ -91,9 +91,10 @@ def test_derivative(ureg: UnitRegistry[Any]): assert units[0].u == "meter/s" assert units[0].du is None assert units[1].u == "radian/s", f"Found {units[1].u}" - assert units[1].du == "degree/s" - assert units[1].to_base == bu[1].to_base + assert units[1].du == "degree/s", f"{units[1].du} != 'degree/s'" + assert units[1].to_base == bu[1].to_base, f"{units[1].to_base} != {bu[1].to_base}" assert units[1].from_base == bu[1].from_base + assert units[1].to_base(360) == 2 * np.pi def test_compatible(ureg: UnitRegistry[Any]): diff --git a/tests/test_variable.py b/tests/test_variable.py index c80a24f..7cbdab5 100644 --- a/tests/test_variable.py +++ b/tests/test_variable.py @@ -6,12 +6,14 @@ import numpy as np import pytest +from pint import UnitRegistry from pythonfmu.enums import Fmi2Causality as Causality from pythonfmu.enums import Fmi2Initial as Initial from pythonfmu.enums import Fmi2Variability as Variability from scipy.spatial.transform import Rotation as Rot from component_model.model import Model +from component_model.unit import Unit from component_model.utils.analysis import extremum, extremum_series from component_model.utils.transform import ( cartesian_to_spherical, @@ -28,6 +30,17 @@ logging.basicConfig(level=logging.INFO) +@pytest.fixture +def ureg(scope: str = "module", autouse: bool = True): + return _ureg() + + +def _ureg(): + _registry = Unit.ensure_unit_registry("SI") + assert isinstance(_registry, UnitRegistry) + return _registry + + class DummyModel(Model): def __init__(self, name: str, **kwargs: Any): super().__init__(name=name, description="Just a dummy model to be able to do testing", **kwargs) @@ -110,21 +123,29 @@ def test_range(): assert r.rng == exp, f"{r.rng} != {exp}" -def test_auto_type(): - assert Variable.auto_type(1) is float, "int not allowed (default)" - assert Variable.auto_type(1, allow_int=True) is int, "int allowed" - assert Variable.auto_type(0.99, allow_int=True) is float - assert Variable.auto_type(0.99, allow_int=False) is float - assert Variable.auto_type((1, 2, 0.99), allow_int=False) is float - assert Variable.auto_type((1, 2, 0.99), allow_int=True) is float, "Ok by our rules" - assert Variable.auto_type((1, 2, 3), allow_int=True) is int - assert Variable.auto_type((True, False, 3), allow_int=True) is int - assert Variable.auto_type((True, False), allow_int=True) is bool - assert Variable.auto_type((True, False), allow_int=False) is bool - assert Variable.auto_type((True, 1, 9.9), allow_int=False) is bool - # with pytest.raises(VariableInitError) as err: # that goes too far - # assert Variable.auto_type( (True,1, 9.9), allow_int=False) == float - # assert str(err.value).startswith("Incompatible variable types") +def test_auto_type(ureg: UnitRegistry[Any]): + no_unit = (Unit(),) + no_units = (Unit(), Unit(), Unit()) + lengths = (Unit("1 m"), Unit("1 inch"), Unit("100 m")) + assert Variable.auto_type((1,), no_unit) is int + assert Variable.auto_type((1, 2, 3), lengths) is float, "int with units become float" + assert Variable.auto_type((0.99,), no_unit) is float + assert Variable.auto_type((1, 2, 0.99), no_units) is float, "float overrides int" + assert Variable.auto_type((1, 2, 3), no_units) is int + with pytest.raises(ValueError) as err: + assert Variable.auto_type((True, False, 3), no_units) is int + assert str(err.value).startswith("Auto-type cannot be determined for values") + assert Variable.auto_type((True, False, True), no_units) is bool + with pytest.raises(ValueError) as err: + assert Variable.auto_type((True, False, True), lengths) is bool + assert str(err.value) == "bool value True with unit 'meter' is not allowed." + with pytest.raises(ValueError) as err: + assert Variable.auto_type((True, 1, 9.9), no_units) is bool + assert str(err.value).startswith("Auto-type cannot be determined for values") + assert Variable.auto_type((Causality.input, Causality.output, Causality.parameter), no_units) is Causality + with pytest.raises(ValueError) as err: + assert Variable.auto_type((Causality.input, Variability.continuous, Causality.parameter), no_units) is Enum + assert str(err.value).startswith("Auto-type cannot be determined for values") def test_spherical_cartesian(): @@ -363,7 +384,7 @@ def test_init(): # internally packed into tuple: assert int1.start == (99,) assert int1.range[0].rng == (0, 100), f"Found {int1.range[0].rng}" - assert int1.unit[0].u == "dimensionless" + assert int1.unit[0].u == "" assert int1.unit[0].du is None assert int1.check_range([50]) assert not int1.check_range([110]) @@ -377,7 +398,7 @@ def test_init(): mod.set_integer([mod.variable_by_name("int1").value_reference], [99]) # simulate setting from outside assert mod.get_integer([mod.variable_by_name("int1").value_reference]) == [99] - assert float1.typ is float + assert float1.typ is float, f"Found {float1.typ}" assert float1.causality == Causality.input assert float1.variability == Variability.continuous assert float1.initial is None, f"initial: {float1.initial}" @@ -412,7 +433,7 @@ def test_init(): # internally packed into tuple: assert enum1.start == (Causality.parameter,) assert enum1.range[0].rng == (0, 4), f"Range: {enum1.range[0].rng}" - assert enum1.unit[0].u == "dimensionless" + assert enum1.unit[0].u == "" assert enum1.unit[0].du is None, f"Display: {enum1.unit[0].du}" assert enum1.check_range([1]) assert not enum1.check_range([7]) @@ -423,7 +444,6 @@ def test_init(): assert enum1.getter() == [4], f"Value {enum1.getter()}. Translated to int!" mod.set_integer([mod.variable_by_name("enum1").value_reference], [2]) # simulate setting from outside assert mod.get_integer([mod.variable_by_name("enum1").value_reference]) == [2] - assert bool1.typ is bool assert bool1.causality == Causality.parameter assert bool1.variability == Variability.fixed @@ -432,7 +452,7 @@ def test_init(): # internally packed into tuple: assert bool1.start == (True,) assert bool1.range[0].rng == (False, True) - assert bool1.unit[0].u == "dimensionless" + assert bool1.unit[0].u == "" assert bool1.unit[0].du is None assert bool1.check_range([True]) assert bool1.check_range([100.5]), "Any number will work" @@ -454,7 +474,7 @@ def test_init(): # internally packed into tuple: assert str1.start == ("Hello World!",) assert str1.range[0].rng == ("Hello World!", "Hello World!"), f"Range: {str1.range[0].rng}. Basically irrelevant" - assert str1.unit[0].u == "dimensionless", f"Unit {str1.unit}" + assert str1.unit[0].u == "", f"Unit {str1.unit}" assert str1.unit[0].du is None, f"Display: {str1.unit[0].du}" assert str1.check_range([0.5]), "Everything is ok" assert mod.str1 == "Hello World!", f"Value {mod.str1} directly accessible as model variable" @@ -770,13 +790,14 @@ def test_extremum(): if __name__ == "__main__": - retcode = pytest.main(["-rP -s -v", __file__]) + retcode = 0 # pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" - # test_init() + # ureg = _ureg() + test_init() # test_range() # test_var_check() # test_spherical_cartesian() - # test_auto_type() + # test_auto_type(ureg) # test_dirty() # test_var_ref() # test_vars_iter() From 5d956c7eae6a39cbdec7dd9e16ad8a659bf1def7 Mon Sep 17 00:00:00 2001 From: Eisinger Date: Mon, 2 Feb 2026 09:36:21 +0100 Subject: [PATCH 09/11] Minor change in connection with crane-fmu documentation --- src/component_model/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/component_model/model.py b/src/component_model/model.py index e0d31d8..897e966 100644 --- a/src/component_model/model.py +++ b/src/component_model/model.py @@ -814,7 +814,6 @@ def _set( var.setter((values[_svr],), idx=_sv) else: # simple Variable var.setter(values[svr], idx=0) - # print(f"{self.name}. Set {vrs}:{values}") def set_integer( self, From e4639a39524cac6aa4c131c887ca4eb8f3503f50 Mon Sep 17 00:00:00 2001 From: Claas Date: Mon, 2 Feb 2026 09:45:04 +0100 Subject: [PATCH 10/11] updated uv.lock --- uv.lock | 216 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/uv.lock b/uv.lock index 33a5663..506adb0 100644 --- a/uv.lock +++ b/uv.lock @@ -324,89 +324,89 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, - { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, - { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, - { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, - { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, - { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, - { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, - { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, - { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [package.optional-dependencies] @@ -1671,28 +1671,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, - { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, - { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, - { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] @@ -1768,14 +1768,14 @@ wheels = [ [[package]] name = "scipy-stubs" -version = "1.17.0.1" +version = "1.17.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "optype", extra = ["numpy"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/a2/7f52edf1185ffcbf26cae1adede995f923c60a3a1f366bd1cb4cbae41817/scipy_stubs-1.17.0.1.tar.gz", hash = "sha256:029ef77b3984be53a914ac90af3b78c5543af7275eb126c8cec09e7bc72f623c", size = 372323, upload-time = "2026-01-14T16:34:52.838Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/fe/5fa7da49821ea94d60629ae71277fa8d7e16eb20602f720062b6c30a644c/scipy_stubs-1.17.0.2.tar.gz", hash = "sha256:3981bd7fa4c189a8493307afadaee1a830d9a0de8e3ae2f4603f192b6260ef2a", size = 379897, upload-time = "2026-01-22T19:17:08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/7f/6b99d2f0b75738487e3127dc8fbfff04214cd29900118f5a1f945c34271f/scipy_stubs-1.17.0.1-py3-none-any.whl", hash = "sha256:235bdebce396a9bb48236525aedf04a6efa66dcca8b46105549f35f1f5c4cbb7", size = 577357, upload-time = "2026-01-14T16:34:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/51/e3/20233497e4a27956e7392c3f7879e6ee7f767f268079f24f4b089b70f563/scipy_stubs-1.17.0.2-py3-none-any.whl", hash = "sha256:99d1aa75b7d72a7ee36a68d18bcf1149f62ab577bbd1236c65c471b3b465d824", size = 586137, upload-time = "2026-01-22T19:17:05.802Z" }, ] [[package]] From b97c86f7acb231341e5aa69eb869cd05e7cf4cc4 Mon Sep 17 00:00:00 2001 From: Claas Date: Mon, 2 Feb 2026 09:45:36 +0100 Subject: [PATCH 11/11] updated CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a0635..bda7521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/e * -/- -## [0.3.2] - 2026-01-23 +## [0.3.2] - 2026-02-02 ### Added * Added new module unit.py, containing class `Unit`, a helper class to store and manage units and display units. One `Unit` object represents one scalar variable.