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 -%}
+{{tag_name}}>
+{%- 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=[],