diff --git a/examples/somersaultecu.py b/examples/somersaultecu.py index 1991b971..9646cc7a 100755 --- a/examples/somersaultecu.py +++ b/examples/somersaultecu.py @@ -424,23 +424,33 @@ class SomersaultSID(IntEnum): upper_limit=None, short_label=None, description=None, + internal_type=DataType.A_UINT32, + physical_type=DataType.A_UNICODE2STRING, compu_inverse_value=None, compu_rational_coeffs=None), internal_to_phys=[ CompuScale( compu_const="false", - lower_limit=Limit(0), - upper_limit=Limit(0), + lower_limit=Limit( + value_raw="0", value_type=DataType.A_UINT32, interval_type=None), + upper_limit=Limit( + value_raw="0", value_type=DataType.A_UINT32, interval_type=None), short_label=None, description=None, + internal_type=DataType.A_UINT32, + physical_type=DataType.A_UNICODE2STRING, compu_inverse_value=None, compu_rational_coeffs=None), CompuScale( compu_const="true", - lower_limit=Limit(1), - upper_limit=Limit(1), + lower_limit=Limit( + value_raw="1", value_type=DataType.A_UINT32, interval_type=None), + upper_limit=Limit( + value_raw="1", value_type=DataType.A_UINT32, interval_type=None), short_label=None, description=None, + internal_type=DataType.A_UINT32, + physical_type=DataType.A_UNICODE2STRING, compu_inverse_value=None, compu_rational_coeffs=None), ], @@ -1252,8 +1262,10 @@ class SomersaultSID(IntEnum): short_name="forward_flip", long_name="Forward Flip", description=None, - lower_limit="1", - upper_limit="3", + lower_limit=Limit( + value_raw="1", value_type=DataType.A_INT32, interval_type=None), + upper_limit=Limit( + value_raw="3", value_type=DataType.A_INT32, interval_type=None), structure_ref=OdxLinkRef.from_id(somersault_dops["num_flips"].odx_id), structure_snref=None, ), @@ -1261,8 +1273,10 @@ class SomersaultSID(IntEnum): short_name="backward_flip", long_name="Backward Flip", description=None, - lower_limit="1", - upper_limit="3", + lower_limit=Limit( + value_raw="1", value_type=DataType.A_INT32, interval_type=None), + upper_limit=Limit( + value_raw="3", value_type=DataType.A_INT32, interval_type=None), structure_ref=OdxLinkRef.from_id(somersault_dops["num_flips"].odx_id), structure_snref=None, ), diff --git a/odxtools/compumethods/compuscale.py b/odxtools/compumethods/compuscale.py index ba3cb41e..e7e6228b 100644 --- a/odxtools/compumethods/compuscale.py +++ b/odxtools/compumethods/compuscale.py @@ -42,13 +42,22 @@ class CompuScale: compu_const: Optional[AtomicOdxType] compu_rational_coeffs: Optional[CompuRationalCoeffs] + # the following two attributes are not specified for COMPU-SCALE + # tags in the XML, but they are required to do anything useful + # with it. + internal_type: DataType + physical_type: DataType + @staticmethod def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *, internal_type: DataType, physical_type: DataType) -> "CompuScale": short_label = et_element.findtext("SHORT-LABEL") description = create_description_from_et(et_element.find("DESC")) - lower_limit = Limit.from_et(et_element.find("LOWER-LIMIT"), internal_type=internal_type) - upper_limit = Limit.from_et(et_element.find("UPPER-LIMIT"), internal_type=internal_type) + + lower_limit = Limit.from_et( + et_element.find("LOWER-LIMIT"), doc_frags, value_type=internal_type) + upper_limit = Limit.from_et( + et_element.find("UPPER-LIMIT"), doc_frags, value_type=internal_type) compu_inverse_value = internal_type.create_from_et(et_element.find("COMPU-INVERSE-VALUE")) compu_const = physical_type.create_from_et(et_element.find("COMPU-CONST")) @@ -64,4 +73,35 @@ def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *, upper_limit=upper_limit, compu_inverse_value=compu_inverse_value, compu_const=compu_const, - compu_rational_coeffs=compu_rational_coeffs) + compu_rational_coeffs=compu_rational_coeffs, + internal_type=internal_type, + physical_type=physical_type) + + def applies(self, internal_value: AtomicOdxType) -> bool: + + if self.lower_limit is None and self.upper_limit is None: + # Everything is allowed: No limits have been specified + return True + elif self.upper_limit is None: + # no upper limit has been specified. the spec says that + # the value specified by the lower limit is the only one + # which is allowed (cf section 7.3.6.6.1) + assert self.lower_limit is not None + + return internal_value == self.lower_limit._value + elif self.lower_limit is None: + # only the upper limit has been specified. the spec is + # ambiguous: it only says that if no upper limit is + # defined, the lower limit shall also be used as the upper + # limit and a closed interval type ought to be assumed, + # but it does not say what happens if the lower limit is + # not defined (which is allowed by the XSD). We thus + # assume that if only the upper limit is defined, is + # treated the same way as if only the lower limit is + # specified. + assert self.upper_limit is not None + + return internal_value == self.upper_limit._value + + return self.lower_limit.complies_to_lower(internal_value) and \ + self.upper_limit.complies_to_upper(internal_value) diff --git a/odxtools/compumethods/createanycompumethod.py b/odxtools/compumethods/createanycompumethod.py index 2f994ee4..c958f8f9 100644 --- a/odxtools/compumethods/createanycompumethod.py +++ b/odxtools/compumethods/createanycompumethod.py @@ -10,7 +10,7 @@ from .compumethod import CompuMethod from .compuscale import CompuScale from .identicalcompumethod import IdenticalCompuMethod -from .limit import IntervalType, Limit +from .limit import Limit from .linearcompumethod import LinearCompuMethod from .scalelinearcompumethod import ScaleLinearCompuMethod from .tabintpcompumethod import TabIntpCompuMethod @@ -18,8 +18,9 @@ def _parse_compu_scale_to_linear_compu_method( + et_element: ElementTree.Element, + doc_frags: List[OdxDocFragment], *, - scale_element: ElementTree.Element, internal_type: DataType, physical_type: DataType, is_scale_linear: bool = False, @@ -47,7 +48,7 @@ def _parse_compu_scale_to_linear_compu_method( kwargs["internal_type"] = internal_type kwargs["physical_type"] = physical_type - coeffs = odxrequire(scale_element.find("COMPU-RATIONAL-COEFFS")) + coeffs = odxrequire(et_element.find("COMPU-RATIONAL-COEFFS")) nums = coeffs.iterfind("COMPU-NUMERATOR/V") offset = computation_python_type(odxrequire(next(nums).text)) @@ -63,26 +64,20 @@ def _parse_compu_scale_to_linear_compu_method( stacklevel=1) # Read lower limit internal_lower_limit = Limit.from_et( - scale_element.find("LOWER-LIMIT"), - internal_type=internal_type, + et_element.find("LOWER-LIMIT"), + doc_frags, + value_type=internal_type, ) - if internal_lower_limit is None: - internal_lower_limit = Limit(0, IntervalType.INFINITE) + kwargs["internal_lower_limit"] = internal_lower_limit # Read upper limit internal_upper_limit = Limit.from_et( - scale_element.find("UPPER-LIMIT"), - internal_type=internal_type, + et_element.find("UPPER-LIMIT"), + doc_frags, + value_type=internal_type, ) - if internal_upper_limit is None: - if not is_scale_linear: - internal_upper_limit = Limit(0, IntervalType.INFINITE) - else: - odxassert(internal_lower_limit is not None and - internal_lower_limit.interval_type == IntervalType.CLOSED) - logger.info("Scale linear without UPPER-LIMIT") - internal_upper_limit = internal_lower_limit + kwargs["internal_upper_limit"] = internal_upper_limit kwargs["denominator"] = denominator kwargs["factor"] = factor @@ -92,7 +87,7 @@ def _parse_compu_scale_to_linear_compu_method( def create_compu_default_value(et_element: Optional[ElementTree.Element], - doc_frags: List[OdxDocFragment], internal_type: DataType, + doc_frags: List[OdxDocFragment], internal_type: DataType, *, physical_type: DataType) -> Optional[CompuScale]: if et_element is None: return None @@ -104,7 +99,7 @@ def create_compu_default_value(et_element: Optional[ElementTree.Element], def create_any_compu_method_from_et(et_element: ElementTree.Element, - doc_frags: List[OdxDocFragment], internal_type: DataType, + doc_frags: List[OdxDocFragment], *, internal_type: DataType, physical_type: DataType) -> CompuMethod: compu_category = et_element.findtext("CATEGORY") odxassert(compu_category in [ @@ -158,13 +153,13 @@ def create_any_compu_method_from_et(et_element: ElementTree.Element, # Compu method can be described by the function f(x) = (offset + factor * x) / denominator scale_elem = odxrequire(et_element.find("COMPU-INTERNAL-TO-PHYS/COMPU-SCALES/COMPU-SCALE")) - return _parse_compu_scale_to_linear_compu_method(scale_element=scale_elem, **kwargs) + return _parse_compu_scale_to_linear_compu_method(scale_elem, doc_frags, **kwargs) elif compu_category == "SCALE-LINEAR": scale_elems = et_element.iterfind("COMPU-INTERNAL-TO-PHYS/COMPU-SCALES/COMPU-SCALE") linear_methods = [ - _parse_compu_scale_to_linear_compu_method(scale_element=scale_elem, **kwargs) + _parse_compu_scale_to_linear_compu_method(scale_elem, doc_frags, **kwargs) for scale_elem in scale_elems ] return ScaleLinearCompuMethod(linear_methods=linear_methods, **kwargs) diff --git a/odxtools/compumethods/limit.py b/odxtools/compumethods/limit.py index 18182942..f6031cd6 100644 --- a/odxtools/compumethods/limit.py +++ b/odxtools/compumethods/limit.py @@ -1,11 +1,12 @@ # SPDX-License-Identifier: MIT from dataclasses import dataclass from enum import Enum -from typing import Optional +from typing import List, Optional, overload from xml.etree import ElementTree -from ..exceptions import odxassert, odxraise, odxrequire -from ..odxtypes import AtomicOdxType, DataType +from ..exceptions import odxraise +from ..odxlink import OdxDocFragment +from ..odxtypes import AtomicOdxType, DataType, compare_odx_values class IntervalType(Enum): @@ -16,42 +17,50 @@ class IntervalType(Enum): @dataclass class Limit: - value: AtomicOdxType - interval_type: IntervalType = IntervalType.CLOSED + value_raw: Optional[str] + value_type: Optional[DataType] + interval_type: Optional[IntervalType] def __post_init__(self) -> None: - if self.interval_type == IntervalType.INFINITE: - self.value = 0 + self._value: Optional[AtomicOdxType] = None + + if self.value_type is not None: + self.set_value_type(self.value_type) + + @staticmethod + @overload + def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], + value_type: Optional[DataType]) -> "Limit": + ... + + @staticmethod + @overload + def from_et(et_element: None, doc_frags: List[OdxDocFragment], + value_type: Optional[DataType]) -> None: + ... @staticmethod - def from_et(et_element: Optional[ElementTree.Element], *, - internal_type: DataType) -> Optional["Limit"]: + def from_et(et_element: Optional[ElementTree.Element], doc_frags: List[OdxDocFragment], + value_type: Optional[DataType]) -> Optional["Limit"]: if et_element is None: return None + interval_type = None if (interval_type_str := et_element.get("INTERVAL-TYPE")) is not None: try: interval_type = IntervalType(interval_type_str) except ValueError: - interval_type = IntervalType.CLOSED odxraise(f"Encountered unknown interval type '{interval_type_str}'") - else: - interval_type = IntervalType.CLOSED - - if interval_type == IntervalType.INFINITE: - if et_element.tag == "LOWER-LIMIT": - return Limit(float("-inf"), interval_type) - else: - odxassert(et_element.tag == "UPPER-LIMIT") - return Limit(float("inf"), interval_type) - elif internal_type == DataType.A_BYTEFIELD: - hex_text = odxrequire(et_element.text) - if len(hex_text) % 2 == 1: - hex_text = "0" + hex_text - return Limit(bytes.fromhex(hex_text), interval_type) - else: - return Limit(internal_type.from_string(odxrequire(et_element.text)), interval_type) + + value_raw = et_element.text + + return Limit(value_raw=value_raw, interval_type=interval_type, value_type=value_type) + + def set_value_type(self, value_type: DataType) -> None: + self.value_type = value_type + if self.value_raw is not None: + self._value = value_type.from_string(self.value_raw) def complies_to_upper(self, value: AtomicOdxType) -> bool: """Checks if the value is in the range w.r.t. the upper limit. @@ -60,13 +69,23 @@ def complies_to_upper(self, value: AtomicOdxType) -> bool: * If the interval type is open, return `value < limit.value`. * If the interval type is infinite, return `True`. """ - if self.interval_type == IntervalType.CLOSED: - return value <= self.value # type: ignore[operator] - elif self.interval_type == IntervalType.OPEN: - return value < self.value # type: ignore[operator] - elif self.interval_type == IntervalType.INFINITE: + if self._value is None: + # if no value is specified, assume interval type INFINITE + # (what are we supposed to compare against?) return True + if self.interval_type is None or self.interval_type == IntervalType.CLOSED: + # assume interval type CLOSED if a value was specified, + # but no interval type + return compare_odx_values(value, self._value) <= 0 + elif self.interval_type == IntervalType.OPEN: + return compare_odx_values(value, self._value) < 0 + + if self.interval_type != IntervalType.INFINITE: + odxraise("Unhandled interval type {self.interval_type}") + + return True + def complies_to_lower(self, value: AtomicOdxType) -> bool: """Checks if the value is in the range w.r.t. the lower limit. @@ -74,9 +93,20 @@ def complies_to_lower(self, value: AtomicOdxType) -> bool: * If the interval type is open, return `limit.value < value`. * If the interval type is infinite, return `True`. """ - if self.interval_type == IntervalType.CLOSED: - return self.value <= value # type: ignore[operator] - elif self.interval_type == IntervalType.OPEN: - return self.value < value # type: ignore[operator] - elif self.interval_type == IntervalType.INFINITE: + + if self._value is None: + # if no value is specified, assume interval type INFINITE + # (what are we supposed to compare against?) return True + + if self.interval_type is None or self.interval_type == IntervalType.CLOSED: + # assume interval type CLOSED if a value was specified, + # but no interval type + return compare_odx_values(value, self._value) >= 0 + elif self.interval_type == IntervalType.OPEN: + return compare_odx_values(value, self._value) > 0 + + if self.interval_type != IntervalType.INFINITE: + odxraise("Unhandled interval type {self.interval_type}") + + return True diff --git a/odxtools/compumethods/linearcompumethod.py b/odxtools/compumethods/linearcompumethod.py index ba90eb9d..129b95d0 100644 --- a/odxtools/compumethods/linearcompumethod.py +++ b/odxtools/compumethods/linearcompumethod.py @@ -1,11 +1,11 @@ # SPDX-License-Identifier: MIT from dataclasses import dataclass -from typing import cast +from typing import Optional from ..exceptions import DecodeError, EncodeError, odxassert, odxraise from ..odxtypes import AtomicOdxType, DataType from .compumethod import CompuMethod, CompuMethodCategory -from .limit import IntervalType, Limit +from .limit import Limit @dataclass @@ -60,8 +60,8 @@ class LinearCompuMethod(CompuMethod): offset: float factor: float denominator: float - internal_lower_limit: Limit - internal_upper_limit: Limit + internal_lower_limit: Optional[Limit] + internal_upper_limit: Optional[Limit] def __post_init__(self) -> None: odxassert(self.denominator > 0) @@ -73,11 +73,11 @@ def category(self) -> CompuMethodCategory: return "LINEAR" @property - def physical_lower_limit(self) -> Limit: + def physical_lower_limit(self) -> Optional[Limit]: return self._physical_lower_limit @property - def physical_upper_limit(self) -> Limit: + def physical_upper_limit(self) -> Optional[Limit]: return self._physical_upper_limit def __compute_physical_limits(self) -> None: @@ -86,67 +86,61 @@ def __compute_physical_limits(self) -> None: This method is only called during the initialization of a LinearCompuMethod. """ - def convert_to_limit_to_physical(limit: Limit, is_upper_limit: bool) -> Limit: + def convert_internal_to_physical_limit(internal_limit: Optional[Limit], + is_upper_limit: bool) -> Optional[Limit]: """Helper method Parameters: - limit + internal_limit the internal limit to be converted to a physical limit is_upper_limit True iff limit is the internal upper limit """ - odxassert(isinstance(limit.value, (int, float))) - if limit.interval_type == IntervalType.INFINITE: - return limit - elif (limit.interval_type.value == limit.interval_type.OPEN and - isinstance(self.internal_type.python_type, int)): - if not isinstance(limit.value, int): - odxraise() - closed_limit = limit.value - 1 if is_upper_limit else limit.value + 1 - return Limit( - value=self._convert_internal_to_physical(closed_limit), - interval_type=IntervalType.CLOSED, - ) - else: - return Limit( - value=self._convert_internal_to_physical(limit.value), - interval_type=limit.interval_type, - ) + if internal_limit is None or internal_limit.value_raw is None: + return None + + internal_value = self.internal_type.from_string(internal_limit.value_raw) + physical_value = self._convert_internal_to_physical(internal_value) + + result = Limit( + value_raw=str(physical_value), + value_type=self.physical_type, + interval_type=internal_limit.interval_type) + + return result + + self._physical_lower_limit = None + self._physical_upper_limit = None if self.factor >= 0: - self._physical_lower_limit = convert_to_limit_to_physical(self.internal_lower_limit, - False) - self._physical_upper_limit = convert_to_limit_to_physical(self.internal_upper_limit, - True) + self._physical_lower_limit = convert_internal_to_physical_limit( + self.internal_lower_limit, False) + self._physical_upper_limit = convert_internal_to_physical_limit( + self.internal_upper_limit, True) else: # If the factor is negative, the lower and upper limit are swapped - self._physical_lower_limit = convert_to_limit_to_physical(self.internal_upper_limit, - True) - self._physical_upper_limit = convert_to_limit_to_physical(self.internal_lower_limit, - False) - - if self.physical_type == DataType.A_UINT32: - # If the data type is unsigned, the physical lower limit should be at least 0. - if (self._physical_lower_limit.interval_type == IntervalType.INFINITE or - cast(float, self._physical_lower_limit.value) < 0): - self._physical_lower_limit = Limit(value=0, interval_type=IntervalType.CLOSED) + self._physical_lower_limit = convert_internal_to_physical_limit( + self.internal_upper_limit, True) + self._physical_upper_limit = convert_internal_to_physical_limit( + self.internal_lower_limit, False) def _convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType: if not isinstance(internal_value, (int, float)): - raise DecodeError("The type of internal values of linear compumethods must " - "either int or float") + raise DecodeError(f"The type of internal values of linear compumethods must " + f"either int or float (is: {type(internal_value).__name__})") if self.denominator is None: result = self.offset + self.factor * internal_value else: result = (self.offset + self.factor * internal_value) / self.denominator - if self.internal_type == DataType.A_FLOAT64 and self.physical_type in [ + if self.physical_type in [ DataType.A_INT32, DataType.A_UINT32, ]: result = round(result) + return self.physical_type.make_from(result) def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType: @@ -155,8 +149,10 @@ def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicO def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType: if not isinstance(physical_value, (int, float)): - raise EncodeError("The type of physical values of linear compumethods must " - "either int or float") + odxraise( + "The type of physical values of linear compumethods must " + "either int or float", EncodeError) + return 0 odxassert( self.is_valid_physical_value(physical_value), @@ -168,7 +164,7 @@ def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicO else: result = ((physical_value * self.denominator) - self.offset) / self.factor - if self.physical_type == DataType.A_FLOAT64 and self.internal_type in [ + if self.internal_type in [ DataType.A_INT32, DataType.A_UINT32, ]: @@ -178,28 +174,39 @@ def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicO def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool: # Do type checks expected_type = self.physical_type.python_type - if expected_type == float and not isinstance(physical_value, (int, float)): - return False - elif expected_type != float and not isinstance(physical_value, expected_type): - return False + if issubclass(expected_type, float): + if not isinstance(physical_value, (int, float)): + return False + else: + if not isinstance(physical_value, expected_type): + return False - # Compare to the limits - if not self.physical_lower_limit.complies_to_lower(physical_value): + # Check the limits + if self.physical_lower_limit is not None and not self.physical_lower_limit.complies_to_lower( + physical_value): return False - if not self.physical_upper_limit.complies_to_upper(physical_value): + if self.physical_upper_limit is not None and not self.physical_upper_limit.complies_to_upper( + physical_value): return False + return True def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool: + # Do type checks expected_type = self.internal_type.python_type - if expected_type == float and not isinstance(internal_value, (int, float)): - return False - elif expected_type != float and not isinstance(internal_value, expected_type): - return False + if issubclass(expected_type, float): + if not isinstance(internal_value, (int, float)): + return False + else: + if not isinstance(internal_value, expected_type): + return False - if not self.internal_lower_limit.complies_to_lower(internal_value): + # Check the limits + if self.internal_lower_limit is not None and not self.internal_lower_limit.complies_to_lower( + internal_value): return False - if not self.internal_upper_limit.complies_to_upper(internal_value): + if self.internal_upper_limit is not None and not self.internal_upper_limit.complies_to_upper( + internal_value): return False return True diff --git a/odxtools/compumethods/tabintpcompumethod.py b/odxtools/compumethods/tabintpcompumethod.py index 4ac33def..50dfe969 100644 --- a/odxtools/compumethods/tabintpcompumethod.py +++ b/odxtools/compumethods/tabintpcompumethod.py @@ -69,8 +69,23 @@ class TabIntpCompuMethod(CompuMethod): physical_points: List[Union[float, int]] def __post_init__(self) -> None: - self._physical_lower_limit = Limit(min(self.physical_points), IntervalType.CLOSED) - self._physical_upper_limit = Limit(max(self.physical_points), IntervalType.CLOSED) + self._physical_lower_limit = Limit( + value_raw=str(min(self.physical_points)), + value_type=self.physical_type, + interval_type=IntervalType.CLOSED) + self._physical_upper_limit = Limit( + value_raw=str(max(self.physical_points)), + value_type=self.physical_type, + interval_type=IntervalType.CLOSED) + + self._internal_lower_limit = Limit( + value_raw=str(min(self.internal_points)), + value_type=self.internal_type, + interval_type=IntervalType.CLOSED) + self._internal_upper_limit = Limit( + value_raw=str(max(self.internal_points)), + value_type=self.internal_type, + interval_type=IntervalType.CLOSED) self._assert_validity() diff --git a/odxtools/compumethods/texttablecompumethod.py b/odxtools/compumethods/texttablecompumethod.py index 0c49de2b..781b8298 100644 --- a/odxtools/compumethods/texttablecompumethod.py +++ b/odxtools/compumethods/texttablecompumethod.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: MIT from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, cast -from ..exceptions import DecodeError, EncodeError, odxassert +from ..exceptions import DecodeError, EncodeError, odxassert, odxraise from ..odxtypes import AtomicOdxType, DataType from .compumethod import CompuMethod, CompuMethodCategory from .compuscale import CompuScale @@ -28,53 +28,49 @@ def __post_init__(self) -> None: def category(self) -> CompuMethodCategory: return "TEXTTABLE" - def get_scales(self) -> List[CompuScale]: - scales = list(self.internal_to_phys) - if self.compu_default_value: - # Default is last, since it's a fallback - scales.append(self.compu_default_value) - return scales - def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType: - matching_scales = [x for x in self.get_scales() if x.compu_const == physical_value] + matching_scales = [x for x in self.internal_to_phys if x.compu_const == physical_value] for scale in matching_scales: if scale.compu_inverse_value is not None: return scale.compu_inverse_value - elif scale.lower_limit is not None: - return scale.lower_limit.value - elif scale.upper_limit is not None: - return scale.upper_limit.value + elif scale.lower_limit is not None and scale.lower_limit._value is not None: + return scale.lower_limit._value + elif scale.upper_limit is not None and scale.upper_limit._value is not None: + return scale.upper_limit._value + + if self.compu_default_value is not None and self.compu_default_value.compu_inverse_value is not None: + return self.compu_default_value.compu_inverse_value raise EncodeError(f"Texttable compu method could not encode '{physical_value!r}'.") def __is_internal_in_scale(self, internal_value: AtomicOdxType, scale: CompuScale) -> bool: if scale == self.compu_default_value: return True - if scale.lower_limit is not None and not scale.lower_limit.complies_to_lower( - internal_value): - return False - # If no UPPER-LIMIT is defined - # the COMPU-SCALE will be applied only for the value defined in LOWER-LIMIT - upper_limit = scale.upper_limit or scale.lower_limit - if upper_limit is not None and not upper_limit.complies_to_upper(internal_value): - return False - # value complies to the defined limits - return True + + return scale.applies(internal_value) def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType: - scale = next( - filter( - lambda scale: self.__is_internal_in_scale(internal_value, scale), - self.get_scales(), - ), None) - if scale is None or scale.compu_const is None: - raise DecodeError( - f"Texttable compu method could not decode {internal_value!r} to string.") - return scale.compu_const + matching_scales = [x for x in self.internal_to_phys if x.applies(internal_value)] + if len(matching_scales) == 0: + if self.compu_default_value is None or self.compu_default_value.compu_const is None: + odxraise(f"Texttable could not decode {internal_value!r}.", DecodeError) + return cast(None, AtomicOdxType) + + return self.compu_default_value.compu_const + + if len(matching_scales) != 1 or matching_scales[0].compu_const is None: + odxraise(f"Texttable could not decode {internal_value!r}.", DecodeError) + + return matching_scales[0].compu_const def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool: - return any(x.compu_const == physical_value for x in self.get_scales()) + if self.compu_default_value is not None: + return True + + return any(x.compu_const == physical_value for x in self.internal_to_phys) def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool: - return any( - self.__is_internal_in_scale(internal_value, scale) for scale in self.get_scales()) + if self.compu_default_value is not None: + return True + + return any(scale.applies(internal_value) for scale in self.internal_to_phys) diff --git a/odxtools/dataobjectproperty.py b/odxtools/dataobjectproperty.py index 118a9355..a8dd3cc4 100644 --- a/odxtools/dataobjectproperty.py +++ b/odxtools/dataobjectproperty.py @@ -59,20 +59,20 @@ def from_et(et_element: ElementTree.Element, compu_method = create_any_compu_method_from_et( odxrequire(et_element.find("COMPU-METHOD")), doc_frags, - diag_coded_type.base_data_type, - physical_type.base_data_type, + internal_type=diag_coded_type.base_data_type, + physical_type=physical_type.base_data_type, ) unit_ref = OdxLinkRef.from_et(et_element.find("UNIT-REF"), doc_frags) internal_constr = None if (internal_constr_elem := et_element.find("INTERNAL-CONSTR")) is not None: internal_constr = InternalConstr.from_et( - internal_constr_elem, internal_type=diag_coded_type.base_data_type) + internal_constr_elem, doc_frags, value_type=diag_coded_type.base_data_type) physical_constr = None if (physical_constr_elem := et_element.find("PHYS-CONSTR")) is not None: physical_constr = InternalConstr.from_et( - physical_constr_elem, internal_type=physical_type.base_data_type) + physical_constr_elem, doc_frags, value_type=physical_type.base_data_type) return DataObjectProperty( diag_coded_type=diag_coded_type, @@ -126,8 +126,9 @@ def convert_physical_to_bytes(self, physical_value: Any, encode_state: EncodeSta Convert a physical representation of a parameter to a string bytes that can be send over the wire """ if not self.is_valid_physical_value(physical_value): - raise EncodeError(f"The value {repr(physical_value)} of type {type(physical_value)}" - f" is not a valid.") + raise EncodeError( + f"The value {repr(physical_value)} of type {type(physical_value).__name__}" + f" is not a valid.") internal_val = self.convert_physical_to_internal(physical_value) return self.diag_coded_type.convert_internal_to_bytes( diff --git a/odxtools/dtcdop.py b/odxtools/dtcdop.py index bbb5dae3..afc01384 100644 --- a/odxtools/dtcdop.py +++ b/odxtools/dtcdop.py @@ -46,8 +46,8 @@ def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> compu_method = create_any_compu_method_from_et( odxrequire(et_element.find("COMPU-METHOD")), doc_frags, - diag_coded_type.base_data_type, - physical_type.base_data_type, + internal_type=diag_coded_type.base_data_type, + physical_type=physical_type.base_data_type, ) dtcs_raw: List[Union[DiagnosticTroubleCode, OdxLinkRef]] = [] if (dtcs_elem := et_element.find("DTCS")) is not None: diff --git a/odxtools/internalconstr.py b/odxtools/internalconstr.py index 1b1e73cf..5f2ed187 100644 --- a/odxtools/internalconstr.py +++ b/odxtools/internalconstr.py @@ -4,6 +4,7 @@ from xml.etree import ElementTree from .compumethods.limit import Limit +from .odxlink import OdxDocFragment from .odxtypes import DataType from .scaleconstr import ScaleConstr @@ -19,16 +20,24 @@ class InternalConstr: upper_limit: Optional[Limit] scale_constrs: List[ScaleConstr] + value_type: DataType + @staticmethod - def from_et(et_element: ElementTree.Element, internal_type: DataType) -> "InternalConstr": + def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *, + value_type: DataType) -> "InternalConstr": - lower_limit = Limit.from_et(et_element.find("LOWER-LIMIT"), internal_type=internal_type) - upper_limit = Limit.from_et(et_element.find("UPPER-LIMIT"), internal_type=internal_type) + lower_limit = Limit.from_et( + et_element.find("LOWER-LIMIT"), doc_frags, value_type=value_type) + upper_limit = Limit.from_et( + et_element.find("UPPER-LIMIT"), doc_frags, value_type=value_type) scale_constrs = [ - ScaleConstr.from_et(sc_el, internal_type) + ScaleConstr.from_et(sc_el, doc_frags, value_type=value_type) for sc_el in et_element.iterfind("SCALE-CONSTRS/SCALE-CONSTR") ] return InternalConstr( - lower_limit=lower_limit, upper_limit=upper_limit, scale_constrs=scale_constrs) + lower_limit=lower_limit, + upper_limit=upper_limit, + scale_constrs=scale_constrs, + value_type=value_type) diff --git a/odxtools/multiplexer.py b/odxtools/multiplexer.py index 5829a5ef..8e1970c0 100644 --- a/odxtools/multiplexer.py +++ b/odxtools/multiplexer.py @@ -36,7 +36,8 @@ class Multiplexer(ComplexDop): @staticmethod def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> "Multiplexer": """Reads a Multiplexer from Diag Layer.""" - kwargs = dataclass_fields_asdict(ComplexDop.from_et(et_element, doc_frags)) + base_obj = ComplexDop.from_et(et_element, doc_frags) + kwargs = dataclass_fields_asdict(base_obj) byte_position = int(et_element.findtext("BYTE-POSITION", "0")) switch_key = MultiplexerSwitchKey.from_et( @@ -164,8 +165,9 @@ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: if self.default_case is not None: self.default_case._resolve_odxlinks(odxlinks) - for case in self.cases: - case._resolve_odxlinks(odxlinks) + for mux_case in self.cases: + mux_case._mux_case_resolve_odxlinks( + odxlinks, key_physical_type=self.switch_key.dop.physical_type.base_data_type) def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None: super()._resolve_snrefs(diag_layer) @@ -174,5 +176,5 @@ def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None: if self.default_case is not None: self.default_case._resolve_snrefs(diag_layer) - for case in self.cases: - case._resolve_snrefs(diag_layer) + for mux_case in self.cases: + mux_case._resolve_snrefs(diag_layer) diff --git a/odxtools/multiplexercase.py b/odxtools/multiplexercase.py index 073e9e76..093a9fdf 100644 --- a/odxtools/multiplexercase.py +++ b/odxtools/multiplexercase.py @@ -4,9 +4,11 @@ from xml.etree import ElementTree from .basicstructure import BasicStructure +from .compumethods.limit import Limit from .element import NamedElement from .exceptions import odxrequire from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef +from .odxtypes import AtomicOdxType, DataType from .utils import dataclass_fields_asdict if TYPE_CHECKING: @@ -19,8 +21,8 @@ class MultiplexerCase(NamedElement): structure_ref: Optional[OdxLinkRef] structure_snref: Optional[str] - lower_limit: str - upper_limit: str + lower_limit: Limit + upper_limit: Limit def __post_init__(self) -> None: self._structure: BasicStructure @@ -28,15 +30,23 @@ def __post_init__(self) -> None: @staticmethod def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> "MultiplexerCase": - """Reads a Case for a Multiplexer.""" + """Reads a case for a Multiplexer.""" kwargs = dataclass_fields_asdict(NamedElement.from_et(et_element, doc_frags)) structure_ref = OdxLinkRef.from_et(et_element.find("STRUCTURE-REF"), doc_frags) structure_snref = None if (structure_snref_elem := et_element.find("STRUCTURE-SNREF")) is not None: structure_snref = odxrequire(structure_snref_elem.get("SHORT-NAME")) - lower_limit = odxrequire(et_element.findtext("LOWER-LIMIT")) - upper_limit = odxrequire(et_element.findtext("UPPER-LIMIT")) + lower_limit = Limit.from_et( + odxrequire(et_element.find("LOWER-LIMIT")), + doc_frags, + value_type=None, + ) + upper_limit = Limit.from_et( + odxrequire(et_element.find("UPPER-LIMIT")), + doc_frags, + value_type=None, + ) return MultiplexerCase( structure_ref=structure_ref, @@ -49,14 +59,26 @@ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: return {} def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None: + raise RuntimeError("Calling MultiplexerCase._resolve_odxlinks() is not allowed. " + "Use ._mux_case_resolve_odxlinks()().") + + def _mux_case_resolve_odxlinks(self, odxlinks: OdxLinkDatabase, *, + key_physical_type: DataType) -> None: if self.structure_ref: self._structure = odxlinks.resolve(self.structure_ref) + self.lower_limit.set_value_type(key_physical_type) + self.upper_limit.set_value_type(key_physical_type) + def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None: if self.structure_snref: ddds = diag_layer.diag_data_dictionary_spec self._structure = odxrequire(ddds.structures.get(self.structure_snref)) + def applies(self, value: AtomicOdxType) -> bool: + return self.lower_limit.complies_to_lower(value) \ + and self.upper_limit.complies_to_upper(value) + @property def structure(self) -> BasicStructure: return self._structure diff --git a/odxtools/odxtypes.py b/odxtools/odxtypes.py index 3c7bdd84..a79dd360 100644 --- a/odxtools/odxtypes.py +++ b/odxtools/odxtypes.py @@ -102,6 +102,58 @@ def parse_int(value: str) -> int: } +def compare_odx_values(a: AtomicOdxType, b: AtomicOdxType) -> int: + # this function implements the comparison according to the ODX + # specification. (cf section 7.3.6.5) + + # numeric values are compared numerically (duh!) + if isinstance(a, (int, float)): + if not isinstance(b, (int, float)): + odxraise() + + tmp = a - b + if tmp < 0: + return -1 + elif tmp > 0: + return 1 + return 0 + + # strings are compared lexicographically. (the spec only allows + # equals, but this cannot easily implemented using a single + # comparison function. + if isinstance(a, str): + if not isinstance(b, str): + odxraise() + + if a < b: + return -1 + elif b < a: + return 1 + else: + return 0 + + # bytefields are treated like long integers: to pad the shorter + # object with zeros and treat the results like strings. + if isinstance(a, (bytes, bytearray)): + if not isinstance(b, (bytes, bytearray)): + odxraise() + + obj_len = max(len(a), len(b)) + + tmp_a = a.ljust(obj_len, b'\x00') + tmp_b = b.ljust(obj_len, b'\x00') + + if tmp_a > tmp_b: + return 1 + elif tmp_a < tmp_b: + return -1 + else: + return 0 + + odxraise(f"Unhandled comparsion between objects of type {type(a).__name__} " + f"and {type(b).__name__}") + + class DataType(Enum): """Types for the physical and internal value. diff --git a/odxtools/parameterinfo.py b/odxtools/parameterinfo.py index 5ac197dd..9301b4f1 100644 --- a/odxtools/parameterinfo.py +++ b/odxtools/parameterinfo.py @@ -84,11 +84,18 @@ def parameter_info(param_list: Iterable[Union[Parameter, EndOfPduField]]) -> str result += f": float\n" ll = cm.physical_lower_limit ul = cm.physical_upper_limit - result += (f" range: " - f"{'[' if ll.interval_type == IntervalType.CLOSED else '('}" - f"{ll.value!r}, " - f"{ul.value!r}" - f"{']' if ul.interval_type == IntervalType.CLOSED else ')'}\n") + if ll is None: + ll_str = "(inf" + else: + ll_delim = '[' if ll.interval_type == IntervalType.CLOSED else '(' + ll_str = f"{ll_delim}{ll._value!r}" + + if ul is None: + ul_str = "inf)" + else: + ul_delim = ']' if ul.interval_type == IntervalType.CLOSED else ')' + ul_str = f"{ul._value!r}{ul_delim}" + result += f" range: {ll_str}, {ul_str}\n" unit = dop.unit unit_str = unit.display_name if unit is not None else None diff --git a/odxtools/scaleconstr.py b/odxtools/scaleconstr.py index be5a7d31..af113258 100644 --- a/odxtools/scaleconstr.py +++ b/odxtools/scaleconstr.py @@ -1,11 +1,12 @@ # SPDX-License-Identifier: MIT from dataclasses import dataclass from enum import Enum -from typing import Optional +from typing import List, Optional from xml.etree import ElementTree from .compumethods.limit import Limit from .exceptions import odxraise, odxrequire +from .odxlink import OdxDocFragment from .odxtypes import DataType from .utils import create_description_from_et @@ -27,14 +28,18 @@ class ScaleConstr: lower_limit: Optional[Limit] upper_limit: Optional[Limit] validity: ValidType + value_type: DataType @staticmethod - def from_et(et_element: ElementTree.Element, internal_type: DataType) -> "ScaleConstr": + def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *, + value_type: DataType) -> "ScaleConstr": short_label = et_element.findtext("SHORT-LABEL") description = create_description_from_et(et_element.find("DESC")) - lower_limit = Limit.from_et(et_element.find("LOWER-LIMIT"), internal_type=internal_type) - upper_limit = Limit.from_et(et_element.find("UPPER-LIMIT"), internal_type=internal_type) + lower_limit = Limit.from_et( + et_element.find("LOWER-LIMIT"), doc_frags, value_type=value_type) + upper_limit = Limit.from_et( + et_element.find("UPPER-LIMIT"), doc_frags, value_type=value_type) validity_str = odxrequire(et_element.get("VALIDITY")) try: @@ -47,4 +52,5 @@ def from_et(et_element: ElementTree.Element, internal_type: DataType) -> "ScaleC description=description, lower_limit=lower_limit, upper_limit=upper_limit, - validity=validity) + validity=validity, + value_type=value_type) diff --git a/odxtools/templates/macros/printDOP.xml.jinja2 b/odxtools/templates/macros/printDOP.xml.jinja2 index eced5568..a550f4a7 100644 --- a/odxtools/templates/macros/printDOP.xml.jinja2 +++ b/odxtools/templates/macros/printDOP.xml.jinja2 @@ -43,12 +43,24 @@ {%- endif %} {%- endmacro -%} -{%- macro printLimitValue(lv) -%} -{%- if hasattr(lv, 'hex') -%} -{#- bytes or bytarray limit #} -{{lv.hex()}} -{%- else -%} -{{lv}} +{%- macro printLimit(tag_name, limit_obj) -%} +{%- if limit_obj is not none %} +<{{tag_name}} +{%- if limit_obj.interval_type is not none %} + {{- make_xml_attrib("INTERVAL-TYPE", limit_obj.interval_type.value) }} + {%- endif %} +{%- if limit_obj.value_raw is none %} + {#- #}/> +{%- else %} + {#- #}> + {%- if hasattr(limit_obj._value, 'hex') -%} + {#- bytes or bytarray limit #} + {{- limit_obj._value.hex().upper() }} + {%- else -%} + {{- limit_obj._value }} + {%- endif -%} + +{%- endif -%} {%- endif -%} {%- endmacro -%} @@ -62,8 +74,8 @@ {{sc.description}} {%- endif %} - {{printLimitValue(sc.lower_limit.value)}} - {{printLimitValue(sc.upper_limit.value)}} + {{printLimit("LOWER-LIMIT", sc.lower_limit) }} + {{printLimit("UPPER-LIMIT", sc.upper_limit) }} {%- endmacro -%} @@ -73,12 +85,8 @@ {%- else %} {%- endif %} - {%- if ic.lower_limit %} - {{printLimitValue(ic.lower_limit.value)}} - {%- endif %} - {%- if ic.upper_limit %} - {{printLimitValue(ic.upper_limit.value)}} - {%- endif %} + {{printLimit("LOWER-LIMIT", ic.lower_limit) }} + {{printLimit("UPPER-LIMIT", ic.upper_limit) }} {%- if ic.scale_constrs %} {%- for sc in ic.scale_constrs %} @@ -109,12 +117,8 @@ {{cs.description}} {%- endif %} - {%- if cs.lower_limit is not none %} - {{printLimitValue(cs.lower_limit.value)}} - {%- endif %} - {%- if cs.upper_limit is not none %} - {{printLimitValue(cs.upper_limit.value)}} - {%- endif %} + {{printLimit("LOWER-LIMIT", cs.lower_limit) }} + {{printLimit("UPPER-LIMIT", cs.upper_limit) }} {%- if cs.compu_inverse_value is not none %} {{cs.compu_inverse_value}} @@ -141,12 +145,8 @@ - {%- if cm.internal_lower_limit is not none and cm.internal_lower_limit.interval_type.value != "INFINITE" %} - {{printLimitValue(cm.internal_lower_limit.value)}} - {%- endif %} - {%- if cm.internal_upper_limit is not none and cm.internal_upper_limit.interval_type.value != "INFINITE" %} - {{printLimitValue(cm.internal_upper_limit.value)}} - {%- endif %} + {{printLimit("LOWER-LIMIT", cm.internal_lower_limit) }} + {{printLimit("UPPER-LIMIT", cm.internal_upper_limit) }} {{cm.offset}} @@ -166,12 +166,8 @@ {%- for lm in cm.linear_methods %} - {%- if lm.internal_lower_limit is not none and lm.internal_lower_limit.interval_type.value != "INFINITE" %} - {{printLimitValue(lm.internal_lower_limit.value)}} - {%- endif %} - {%- if lm.internal_upper_limit is not none and lm.internal_upper_limit.interval_type.value != "INFINITE" %} - {{printLimitValue(lm.internal_upper_limit.value)}} - {%- endif %} + {{printLimit("LOWER-LIMIT", lm.internal_lower_limit) }} + {{printLimit("UPPER-LIMIT", lm.internal_upper_limit) }} {{lm.offset}} @@ -191,8 +187,8 @@ {%- for idx in range( cm.internal_points | length ) %} - - {{ printLimitValue(cm.internal_points[idx]) }} + + {{ cm.internal_points[idx] }} {{ cm.physical_points[idx] }} diff --git a/odxtools/templates/macros/printMux.xml.jinja2 b/odxtools/templates/macros/printMux.xml.jinja2 index 1816ebc2..b0dec7f8 100644 --- a/odxtools/templates/macros/printMux.xml.jinja2 +++ b/odxtools/templates/macros/printMux.xml.jinja2 @@ -4,6 +4,7 @@ -#} {%- import('macros/printElementId.xml.jinja2') as peid %} +{%- import('macros/printDOP.xml.jinja2') as pdop %} {%- macro printMux(mux) %} {%- endif %} - {{case.lower_limit}} - {{case.upper_limit}} + {{ pdop.printLimit("LOWER-LIMIT", case.lower_limit) }} + {{ pdop.printLimit("UPPER-LIMIT", case.upper_limit) }} {%- endfor %} diff --git a/pyproject.toml b/pyproject.toml index c8165025..b2a7e1c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ ignore_missing_imports = true strict = true [tool.ruff] -select = [ +lint.select = [ "E", # pycodestyle Error "W", # pycodestyle Warning "F", # pyflakes @@ -100,7 +100,7 @@ select = [ "UP", # pyupgrade "C4", # flake8-comprehensions ] -ignore = [ +lint.ignore = [ "E501", # line too long "F541", # f-string-missing-placeholders ] @@ -109,13 +109,10 @@ exclude = [ "doc", ] -# Assume Python 3.8. -target-version = "py38" - -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["odxtools"] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] [tool.yapf] diff --git a/tests/test_compu_methods.py b/tests/test_compu_methods.py index 87c00693..e7632f0c 100644 --- a/tests/test_compu_methods.py +++ b/tests/test_compu_methods.py @@ -11,9 +11,11 @@ from odxtools.compumethods.limit import IntervalType, Limit from odxtools.compumethods.linearcompumethod import LinearCompuMethod from odxtools.compumethods.tabintpcompumethod import TabIntpCompuMethod -from odxtools.exceptions import DecodeError, EncodeError +from odxtools.exceptions import DecodeError, EncodeError, OdxError from odxtools.odxlink import OdxDocFragment from odxtools.odxtypes import DataType +from odxtools.write_pdx_file import (get_parent_container_name, jinja2_odxraise_helper, + make_bool_xml_attrib, make_xml_attrib) doc_frags = [OdxDocFragment("UnitTest", "WinneThePoh")] @@ -30,12 +32,12 @@ def _get_jinja_environment() -> jinja2.environment.Environment: jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_dir)) - # allows to put XML attributes on a separate line while it is - # collapsed with the previous line in the rendering - jinja_env.filters["odxtools_collapse_xml_attribute"] = (lambda x: " " + x.strip() - if x.strip() else "") - jinja_env.globals["hasattr"] = hasattr + jinja_env.globals["odxraise"] = jinja2_odxraise_helper + jinja_env.globals["make_xml_attrib"] = make_xml_attrib + jinja_env.globals["make_bool_xml_attrib"] = make_bool_xml_attrib + jinja_env.globals["get_parent_container_name"] = get_parent_container_name + return jinja_env self.jinja_env = _get_jinja_environment() @@ -46,8 +48,8 @@ def _get_jinja_environment() -> jinja2.environment.Environment: denominator=3600, internal_type=DataType.A_INT32, physical_type=DataType.A_INT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=None, + internal_upper_limit=None, ) self.linear_compumethod_odx = f""" @@ -76,8 +78,11 @@ def test_read_odx(self) -> None: expected = self.linear_compumethod et_element = ElementTree.fromstring(self.linear_compumethod_odx) - actual = create_any_compu_method_from_et(et_element, doc_frags, expected.internal_type, - expected.physical_type) + actual = create_any_compu_method_from_et( + et_element, + doc_frags, + internal_type=expected.internal_type, + physical_type=expected.physical_type) self.assertIsInstance(actual, LinearCompuMethod) assert isinstance(actual, LinearCompuMethod) self.assertEqual(expected.physical_type, actual.physical_type) @@ -108,8 +113,10 @@ def test_linear_compu_method_type_denom_not_one(self) -> None: denominator=3600, internal_type=DataType.A_INT32, physical_type=DataType.A_INT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw="0", value_type=DataType.A_INT32, interval_type=IntervalType.INFINITE), + internal_upper_limit=Limit( + value_raw="0", value_type=DataType.A_INT32, interval_type=IntervalType.INFINITE), ) self.assertEqual(compu_method.convert_physical_to_internal(2), 7200) @@ -122,8 +129,10 @@ def test_linear_compu_method_type_int_int(self) -> None: denominator=1, internal_type=DataType.A_INT32, physical_type=DataType.A_INT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw="0", value_type=DataType.A_INT32, interval_type=IntervalType.INFINITE), + internal_upper_limit=Limit( + value_raw="0", value_type=DataType.A_INT32, interval_type=IntervalType.INFINITE), ) self.assertEqual(compu_method.convert_internal_to_physical(4), 13) @@ -141,8 +150,10 @@ def test_linear_compu_method_type_int_float(self) -> None: denominator=1, internal_type=DataType.A_INT32, physical_type=DataType.A_FLOAT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw="0", value_type=DataType.A_INT32, interval_type=IntervalType.INFINITE), + internal_upper_limit=Limit( + value_raw="0", value_type=DataType.A_INT32, interval_type=IntervalType.INFINITE), ) self.assertTrue(compu_method.is_valid_internal_value(123)) self.assertFalse(compu_method.is_valid_internal_value("123")) @@ -159,8 +170,10 @@ def test_linear_compu_method_type_float_int(self) -> None: denominator=1, internal_type=DataType.A_FLOAT32, physical_type=DataType.A_INT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw=None, value_type=DataType.A_FLOAT32, interval_type=IntervalType.INFINITE), + internal_upper_limit=Limit( + value_raw=None, value_type=DataType.A_FLOAT32, interval_type=IntervalType.INFINITE), ) self.assertTrue(compu_method.is_valid_internal_value(1.2345)) self.assertTrue(compu_method.is_valid_internal_value(123)) @@ -171,18 +184,23 @@ def test_linear_compu_method_type_float_int(self) -> None: self.assertFalse(compu_method.is_valid_physical_value(1.2345)) def test_linear_compu_method_type_string(self) -> None: - compu_method = LinearCompuMethod( + self.assertRaises( + OdxError, + LinearCompuMethod, offset=1, factor=3, denominator=1, internal_type=DataType.A_ASCIISTRING, physical_type=DataType.A_UNICODE2STRING, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw="0", + value_type=DataType.A_ASCIISTRING, + interval_type=IntervalType.INFINITE), + internal_upper_limit=Limit( + value_raw="0", + value_type=DataType.A_ASCIISTRING, + interval_type=IntervalType.INFINITE), ) - self.assertTrue(compu_method.is_valid_internal_value("123")) - self.assertFalse(compu_method.is_valid_internal_value(123)) - self.assertFalse(compu_method.is_valid_internal_value(1.2345)) def test_linear_compu_method_limits(self) -> None: compu_method = LinearCompuMethod( @@ -191,8 +209,10 @@ def test_linear_compu_method_limits(self) -> None: denominator=1, internal_type=DataType.A_INT32, physical_type=DataType.A_INT32, - internal_lower_limit=Limit(2), - internal_upper_limit=Limit(15), + internal_lower_limit=Limit( + value_raw="2", value_type=DataType.A_INT32, interval_type=None), + internal_upper_limit=Limit( + value_raw="15", value_type=DataType.A_INT32, interval_type=None), ) self.assertFalse(compu_method.is_valid_internal_value(-3)) self.assertFalse(compu_method.is_valid_internal_value(1)) @@ -220,19 +240,29 @@ def test_linear_compu_method_physical_limits(self) -> None: denominator=1, internal_type=DataType.A_INT32, physical_type=DataType.A_INT32, - internal_lower_limit=Limit(2, interval_type=IntervalType.OPEN), - internal_upper_limit=Limit(15), + internal_lower_limit=Limit( + value_raw="2", value_type=DataType.A_INT32, interval_type=IntervalType.OPEN), + internal_upper_limit=Limit( + value_raw="15", value_type=DataType.A_INT32, interval_type=None), ) - self.assertEqual(compu_method.internal_lower_limit, - Limit(2, interval_type=IntervalType.OPEN)) + assert compu_method.internal_lower_limit is not None + assert compu_method.internal_upper_limit is not None + assert compu_method.physical_lower_limit is not None + assert compu_method.physical_upper_limit is not None + + self.assertEqual( + compu_method.internal_lower_limit, + Limit(value_raw="2", value_type=DataType.A_INT32, interval_type=IntervalType.OPEN)) self.assertEqual(compu_method.internal_upper_limit, - Limit(15, interval_type=IntervalType.CLOSED)) + Limit(value_raw="15", value_type=DataType.A_INT32, interval_type=None)) + self.assertEqual(compu_method.internal_upper_limit.interval_type, None) self.assertEqual(compu_method.physical_lower_limit, - Limit(-74, interval_type=IntervalType.CLOSED)) - self.assertEqual(compu_method.physical_upper_limit, - Limit(-9, interval_type=IntervalType.OPEN)) + Limit(value_raw="-74", value_type=DataType.A_INT32, interval_type=None)) + self.assertEqual( + compu_method.physical_upper_limit, + Limit(value_raw="-9", value_type=DataType.A_INT32, interval_type=IntervalType.OPEN)) self.assertFalse(compu_method.internal_lower_limit.complies_to_lower(2)) self.assertTrue(compu_method.internal_lower_limit.complies_to_lower(3)) @@ -291,19 +321,19 @@ def _get_jinja_environment() -> jinja2.environment.Environment: - {self.compumethod.internal_points[0]} + {self.compumethod.internal_points[0]} {self.compumethod.physical_points[0]} - {self.compumethod.internal_points[1]} + {self.compumethod.internal_points[1]} {self.compumethod.physical_points[1]} - {self.compumethod.internal_points[2]} + {self.compumethod.internal_points[2]} {self.compumethod.physical_points[2]} @@ -340,8 +370,11 @@ def test_read_odx(self) -> None: expected = self.compumethod et_element = ElementTree.fromstring(self.compumethod_odx) - actual = create_any_compu_method_from_et(et_element, doc_frags, expected.internal_type, - expected.physical_type) + actual = create_any_compu_method_from_et( + et_element, + doc_frags, + internal_type=expected.internal_type, + physical_type=expected.physical_type) self.assertIsInstance(actual, TabIntpCompuMethod) assert isinstance(expected, TabIntpCompuMethod) assert isinstance(actual, TabIntpCompuMethod) diff --git a/tests/test_decoding.py b/tests/test_decoding.py index bd9df8be..34385c4d 100644 --- a/tests/test_decoding.py +++ b/tests/test_decoding.py @@ -1369,8 +1369,10 @@ def test_decode_request_linear_compu_method(self) -> None: denominator=1, internal_type=DataType.A_INT32, physical_type=DataType.A_INT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw=None, value_type=DataType.A_INT32, interval_type=IntervalType.INFINITE), + internal_upper_limit=Limit( + value_raw=None, value_type=DataType.A_INT32, interval_type=IntervalType.INFINITE), ) diag_coded_type = StandardLengthType( base_data_type=DataType.A_UINT32, @@ -1978,8 +1980,14 @@ def test_physical_constant_parameter(self) -> None: denominator=1, internal_type=DataType.A_UINT32, physical_type=DataType.A_INT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw=None, + value_type=DataType.A_UINT32, + interval_type=IntervalType.INFINITE), + internal_upper_limit=Limit( + value_raw=None, + value_type=DataType.A_UINT32, + interval_type=IntervalType.INFINITE), ), unit_ref=None, sdgs=[], diff --git a/tests/test_diag_coded_types.py b/tests/test_diag_coded_types.py index 061f72da..4da88605 100644 --- a/tests/test_diag_coded_types.py +++ b/tests/test_diag_coded_types.py @@ -444,8 +444,12 @@ def test_end_to_end(self) -> None: denominator=1, internal_type=DataType.A_UINT32, physical_type=DataType.A_UINT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw="0", value_type=DataType.A_UINT32, interval_type=None), + internal_upper_limit=Limit( + value_raw=None, + value_type=DataType.A_UINT32, + interval_type=IntervalType.INFINITE), ), } diff --git a/tests/test_diag_data_dictionary_spec.py b/tests/test_diag_data_dictionary_spec.py index 6181cc4a..b1f0f932 100644 --- a/tests/test_diag_data_dictionary_spec.py +++ b/tests/test_diag_data_dictionary_spec.py @@ -3,6 +3,7 @@ from examples import somersaultecu from odxtools.compumethods.identicalcompumethod import IdenticalCompuMethod +from odxtools.compumethods.limit import Limit from odxtools.dataobjectproperty import DataObjectProperty from odxtools.diagdatadictionaryspec import DiagDataDictionarySpec from odxtools.diagnostictroublecode import DiagnosticTroubleCode @@ -249,8 +250,10 @@ def test_initialization(self) -> None: short_name="forward_flip", long_name="Forward Flip", description=None, - lower_limit="1", - upper_limit="3", + lower_limit=Limit( + value_raw="1", value_type=DataType.A_INT32, interval_type=None), + upper_limit=Limit( + value_raw="3", value_type=DataType.A_INT32, interval_type=None), structure_ref=OdxLinkRef("structure_ref", doc_frags), structure_snref=None, ), @@ -258,8 +261,10 @@ def test_initialization(self) -> None: short_name="backward_flip", long_name="Backward Flip", description=None, - lower_limit="1", - upper_limit="3", + lower_limit=Limit( + value_raw="1", value_type=DataType.A_INT32, interval_type=None), + upper_limit=Limit( + value_raw="3", value_type=DataType.A_INT32, interval_type=None), structure_ref=OdxLinkRef("structure_ref", doc_frags), structure_snref=None, ), diff --git a/tests/test_encoding.py b/tests/test_encoding.py index 1b190ab3..dd694113 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List, cast from odxtools.compumethods.identicalcompumethod import IdenticalCompuMethod -from odxtools.compumethods.limit import IntervalType, Limit +from odxtools.compumethods.limit import Limit from odxtools.compumethods.linearcompumethod import LinearCompuMethod from odxtools.dataobjectproperty import DataObjectProperty from odxtools.diaglayer import DiagLayer @@ -128,9 +128,9 @@ def test_encode_linear(self) -> None: denominator=1, internal_type=DataType.A_UINT32, physical_type=DataType.A_UINT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), - ) + internal_lower_limit=Limit( + value_raw="0", value_type=DataType.A_UINT32, interval_type=None), + internal_upper_limit=None) dop = DataObjectProperty( odx_id=OdxLinkId("dop.id", doc_frags), short_name="dop_sn", diff --git a/tests/test_singleecujob.py b/tests/test_singleecujob.py index 9fb6ea97..3f19cf57 100644 --- a/tests/test_singleecujob.py +++ b/tests/test_singleecujob.py @@ -90,20 +90,27 @@ class Context(NamedTuple): internal_to_phys=[ CompuScale( "yes", - lower_limit=Limit(0), + lower_limit=Limit( + value_raw="0", value_type=DataType.A_INT32, interval_type=None), compu_const="Yes!", description=None, compu_inverse_value=None, upper_limit=None, - compu_rational_coeffs=None), + compu_rational_coeffs=None, + internal_type=DataType.A_INT32, + physical_type=DataType.A_UNICODE2STRING, + ), CompuScale( "no", - lower_limit=Limit(1), + lower_limit=Limit( + value_raw="1", value_type=DataType.A_INT32, interval_type=None), compu_const="No!", description=None, compu_inverse_value=None, upper_limit=None, - compu_rational_coeffs=None), + compu_rational_coeffs=None, + internal_type=DataType.A_INT32, + physical_type=DataType.A_UNICODE2STRING), ], internal_type=DataType.A_UINT32, ), @@ -134,8 +141,14 @@ class Context(NamedTuple): denominator=1, internal_type=DataType.A_UINT32, physical_type=DataType.A_UINT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw="0", + value_type=DataType.A_INT32, + interval_type=IntervalType.INFINITE), + internal_upper_limit=Limit( + value_raw="0", + value_type=DataType.A_INT32, + interval_type=IntervalType.INFINITE), ), unit_ref=None, sdgs=[], @@ -164,8 +177,14 @@ class Context(NamedTuple): denominator=1, internal_type=DataType.A_UINT32, physical_type=DataType.A_UINT32, - internal_lower_limit=Limit(0, IntervalType.INFINITE), - internal_upper_limit=Limit(0, IntervalType.INFINITE), + internal_lower_limit=Limit( + value_raw="0", + value_type=DataType.A_INT32, + interval_type=IntervalType.INFINITE), + internal_upper_limit=Limit( + value_raw="0", + value_type=DataType.A_INT32, + interval_type=IntervalType.INFINITE), ), unit_ref=None, sdgs=[],