odxtools 6.6.1__py3-none-any.whl → 6.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. odxtools/__init__.py +5 -5
  2. odxtools/basicstructure.py +7 -8
  3. odxtools/cli/_parser_utils.py +15 -0
  4. odxtools/cli/_print_utils.py +4 -3
  5. odxtools/cli/browse.py +19 -14
  6. odxtools/cli/compare.py +24 -16
  7. odxtools/cli/decode.py +2 -1
  8. odxtools/cli/dummy_sub_parser.py +3 -1
  9. odxtools/cli/find.py +2 -1
  10. odxtools/cli/list.py +2 -1
  11. odxtools/cli/main.py +1 -0
  12. odxtools/cli/snoop.py +4 -1
  13. odxtools/comparaminstance.py +7 -5
  14. odxtools/compumethods/compumethod.py +2 -4
  15. odxtools/compumethods/compuscale.py +45 -5
  16. odxtools/compumethods/createanycompumethod.py +28 -36
  17. odxtools/compumethods/limit.py +70 -36
  18. odxtools/compumethods/linearcompumethod.py +68 -59
  19. odxtools/compumethods/tabintpcompumethod.py +19 -8
  20. odxtools/compumethods/texttablecompumethod.py +32 -36
  21. odxtools/dataobjectproperty.py +13 -10
  22. odxtools/decodestate.py +6 -3
  23. odxtools/determinenumberofitems.py +1 -1
  24. odxtools/diagcodedtype.py +5 -4
  25. odxtools/diagdatadictionaryspec.py +108 -83
  26. odxtools/diaglayer.py +75 -35
  27. odxtools/diaglayertype.py +17 -5
  28. odxtools/diagservice.py +1 -1
  29. odxtools/dopbase.py +4 -2
  30. odxtools/dtcdop.py +7 -5
  31. odxtools/dynamiclengthfield.py +6 -5
  32. odxtools/endofpdufield.py +4 -4
  33. odxtools/environmentdatadescription.py +4 -2
  34. odxtools/inputparam.py +1 -1
  35. odxtools/internalconstr.py +14 -5
  36. odxtools/isotp_state_machine.py +14 -6
  37. odxtools/message.py +1 -1
  38. odxtools/multiplexer.py +18 -13
  39. odxtools/multiplexercase.py +27 -5
  40. odxtools/multiplexerswitchkey.py +1 -1
  41. odxtools/nameditemlist.py +7 -6
  42. odxtools/odxlink.py +2 -2
  43. odxtools/odxtypes.py +56 -3
  44. odxtools/outputparam.py +2 -2
  45. odxtools/parameterinfo.py +12 -5
  46. odxtools/parameters/codedconstparameter.py +33 -12
  47. odxtools/parameters/createanyparameter.py +19 -193
  48. odxtools/parameters/dynamicparameter.py +21 -1
  49. odxtools/parameters/lengthkeyparameter.py +28 -4
  50. odxtools/parameters/matchingrequestparameter.py +27 -9
  51. odxtools/parameters/nrcconstparameter.py +34 -11
  52. odxtools/parameters/parameter.py +58 -32
  53. odxtools/parameters/parameterwithdop.py +28 -15
  54. odxtools/parameters/physicalconstantparameter.py +28 -4
  55. odxtools/parameters/reservedparameter.py +32 -18
  56. odxtools/parameters/systemparameter.py +25 -2
  57. odxtools/parameters/tableentryparameter.py +45 -6
  58. odxtools/parameters/tablekeyparameter.py +43 -10
  59. odxtools/parameters/tablestructparameter.py +36 -14
  60. odxtools/parameters/valueparameter.py +24 -2
  61. odxtools/paramlengthinfotype.py +4 -1
  62. odxtools/parentref.py +4 -1
  63. odxtools/scaleconstr.py +11 -5
  64. odxtools/statetransition.py +1 -1
  65. odxtools/staticfield.py +101 -0
  66. odxtools/table.py +2 -1
  67. odxtools/tablerow.py +11 -4
  68. odxtools/templates/macros/printDOP.xml.jinja2 +30 -34
  69. odxtools/templates/macros/printMux.xml.jinja2 +3 -2
  70. odxtools/templates/macros/printParam.xml.jinja2 +9 -9
  71. odxtools/templates/macros/printStaticField.xml.jinja2 +15 -0
  72. odxtools/templates/macros/printVariant.xml.jinja2 +8 -0
  73. odxtools/uds.py +2 -2
  74. odxtools/version.py +2 -2
  75. odxtools/write_pdx_file.py +3 -3
  76. {odxtools-6.6.1.dist-info → odxtools-6.7.1.dist-info}/METADATA +28 -16
  77. {odxtools-6.6.1.dist-info → odxtools-6.7.1.dist-info}/RECORD +81 -79
  78. {odxtools-6.6.1.dist-info → odxtools-6.7.1.dist-info}/WHEEL +1 -1
  79. {odxtools-6.6.1.dist-info → odxtools-6.7.1.dist-info}/LICENSE +0 -0
  80. {odxtools-6.6.1.dist-info → odxtools-6.7.1.dist-info}/entry_points.txt +0 -0
  81. {odxtools-6.6.1.dist-info → odxtools-6.7.1.dist-info}/top_level.txt +0 -0
odxtools/diaglayertype.py CHANGED
@@ -20,11 +20,23 @@ class DiagLayerType(Enum):
20
20
  """
21
21
 
22
22
  PRIORITY_OF_DIAG_LAYER_TYPE: Dict[DiagLayerType, int] = {
23
- DiagLayerType.ECU_SHARED_DATA: 0,
24
- DiagLayerType.PROTOCOL: 1,
25
- DiagLayerType.FUNCTIONAL_GROUP: 2,
26
- DiagLayerType.BASE_VARIANT: 3,
27
- DiagLayerType.ECU_VARIANT: 4,
23
+ DiagLayerType.PROTOCOL:
24
+ 1,
25
+ DiagLayerType.FUNCTIONAL_GROUP:
26
+ 2,
27
+ DiagLayerType.BASE_VARIANT:
28
+ 3,
29
+ DiagLayerType.ECU_VARIANT:
30
+ 4,
31
+
32
+ # ECU shared data layers are a bit weird (see section
33
+ # 7.3.2.4.4 of the ASAM specification): they can be
34
+ # inherited from by any other layer but they will
35
+ # override any objects which are also provided by any of
36
+ # the other parent layers. tl;dr: When it comes to
37
+ # inheritance, they have the highest priority.
38
+ DiagLayerType.ECU_SHARED_DATA:
39
+ 100,
28
40
  }
29
41
 
30
42
  return PRIORITY_OF_DIAG_LAYER_TYPE[self]
odxtools/diagservice.py CHANGED
@@ -167,7 +167,7 @@ class DiagService(DiagComm):
167
167
  for cpr in self.comparam_refs:
168
168
  cpr._resolve_odxlinks(odxlinks)
169
169
 
170
- self._request = odxlinks.resolve(self.request_ref)
170
+ self._request = odxlinks.resolve(self.request_ref, Request)
171
171
 
172
172
  self._positive_responses = NamedItemList[Response](
173
173
  [odxlinks.resolve(x, Response) for x in self.pos_response_refs])
odxtools/dopbase.py CHANGED
@@ -64,8 +64,10 @@ class DopBase(IdentifiableElement):
64
64
  """
65
65
  raise NotImplementedError
66
66
 
67
- def convert_physical_to_bytes(self, physical_value: ParameterValue, encode_state: EncodeState,
68
- bit_position: int) -> bytes:
67
+ def convert_physical_to_bytes(self,
68
+ physical_value: ParameterValue,
69
+ encode_state: EncodeState,
70
+ bit_position: int = 0) -> bytes:
69
71
  """Convert the physical value into bytes."""
70
72
  raise NotImplementedError
71
73
 
odxtools/dtcdop.py CHANGED
@@ -46,8 +46,8 @@ class DtcDop(DopBase):
46
46
  compu_method = create_any_compu_method_from_et(
47
47
  odxrequire(et_element.find("COMPU-METHOD")),
48
48
  doc_frags,
49
- diag_coded_type.base_data_type,
50
- physical_type.base_data_type,
49
+ internal_type=diag_coded_type.base_data_type,
50
+ physical_type=physical_type.base_data_type,
51
51
  )
52
52
  dtcs_raw: List[Union[DiagnosticTroubleCode, OdxLinkRef]] = []
53
53
  if (dtcs_elem := et_element.find("DTCS")) is not None:
@@ -59,7 +59,7 @@ class DtcDop(DopBase):
59
59
 
60
60
  # TODO: NOT-INHERITED-DTC-SNREFS
61
61
  linked_dtc_dop_refs = [
62
- cast(OdxLinkRef, OdxLinkRef.from_et(dtc_ref_elem, doc_frags))
62
+ OdxLinkRef.from_et(dtc_ref_elem, doc_frags)
63
63
  for dtc_ref_elem in et_element.iterfind("LINKED-DTC-DOPS/"
64
64
  "LINKED-DTC-DOP/"
65
65
  "DTC-DOP-REF")
@@ -128,8 +128,10 @@ class DtcDop(DopBase):
128
128
 
129
129
  return dtc
130
130
 
131
- def convert_physical_to_bytes(self, physical_value: ParameterValue, encode_state: EncodeState,
132
- bit_position: int) -> bytes:
131
+ def convert_physical_to_bytes(self,
132
+ physical_value: ParameterValue,
133
+ encode_state: EncodeState,
134
+ bit_position: int = 0) -> bytes:
133
135
  if isinstance(physical_value, DiagnosticTroubleCode):
134
136
  trouble_code = physical_value.trouble_code
135
137
  elif isinstance(physical_value, int):
@@ -99,6 +99,7 @@ class DynamicLengthField(Field):
99
99
  decode_state.cursor_bit_position = det_num_items.bit_position or 0
100
100
 
101
101
  n = det_num_items.dop.decode_from_pdu(decode_state)
102
+ result: List[ParameterValue] = []
102
103
 
103
104
  if not isinstance(n, int):
104
105
  odxraise(f"Number of items specified by a dynamic length field {self.short_name} "
@@ -107,11 +108,11 @@ class DynamicLengthField(Field):
107
108
  odxraise(
108
109
  f"Number of items specified by a dynamic length field {self.short_name} "
109
110
  f"must be positive (is: {n})", DecodeError)
110
- else:
111
- decode_state.cursor_byte_position = decode_state.origin_byte_position + self.offset
112
- result: List[ParameterValue] = []
113
- for _ in range(n):
114
- result.append(self.structure.decode_from_pdu(decode_state))
111
+ n = 0
112
+
113
+ decode_state.cursor_byte_position = decode_state.origin_byte_position + self.offset
114
+ for _ in range(n):
115
+ result.append(self.structure.decode_from_pdu(decode_state))
115
116
 
116
117
  decode_state.origin_byte_position = orig_origin
117
118
  decode_state.cursor_byte_position = max(orig_cursor, decode_state.cursor_byte_position)
odxtools/endofpdufield.py CHANGED
@@ -41,7 +41,7 @@ class EndOfPduField(Field):
41
41
 
42
42
  def convert_physical_to_bytes(
43
43
  self,
44
- physical_values: ParameterValue,
44
+ physical_value: ParameterValue,
45
45
  encode_state: EncodeState,
46
46
  bit_position: int = 0,
47
47
  ) -> bytes:
@@ -49,13 +49,13 @@ class EndOfPduField(Field):
49
49
  odxassert(
50
50
  bit_position == 0, "End of PDU field must be byte aligned. "
51
51
  "Is there an error in reading the .odx?", EncodeError)
52
- if not isinstance(physical_values, list):
52
+ if not isinstance(physical_value, list):
53
53
  odxraise(
54
54
  f"Expected a list of values for end-of-pdu field {self.short_name}, "
55
- f"got {type(physical_values)}", EncodeError)
55
+ f"got {type(physical_value)}", EncodeError)
56
56
 
57
57
  coded_message = b''
58
- for value in physical_values:
58
+ for value in physical_value:
59
59
  coded_message += self.structure.convert_physical_to_bytes(value, encode_state)
60
60
  return coded_message
61
61
 
@@ -86,8 +86,10 @@ class EnvironmentDataDescription(ComplexDop):
86
86
  for ed in self.env_datas:
87
87
  ed._resolve_snrefs(diag_layer)
88
88
 
89
- def convert_physical_to_bytes(self, physical_value: ParameterValue, encode_state: EncodeState,
90
- bit_position: int) -> bytes:
89
+ def convert_physical_to_bytes(self,
90
+ physical_value: ParameterValue,
91
+ encode_state: EncodeState,
92
+ bit_position: int = 0) -> bytes:
91
93
  """Convert the physical value into bytes.
92
94
 
93
95
  Since environmental data is supposed to never appear on the
odxtools/inputparam.py CHANGED
@@ -53,6 +53,6 @@ class InputParam(NamedElement):
53
53
  return self._dop_base
54
54
 
55
55
  @property
56
- @deprecated(details="use .dop_base")
56
+ @deprecated(details="use .dop_base") # type: ignore[misc]
57
57
  def dop(self) -> DopBase:
58
58
  return self._dop_base
@@ -4,6 +4,7 @@ from typing import List, Optional
4
4
  from xml.etree import ElementTree
5
5
 
6
6
  from .compumethods.limit import Limit
7
+ from .odxlink import OdxDocFragment
7
8
  from .odxtypes import DataType
8
9
  from .scaleconstr import ScaleConstr
9
10
 
@@ -19,16 +20,24 @@ class InternalConstr:
19
20
  upper_limit: Optional[Limit]
20
21
  scale_constrs: List[ScaleConstr]
21
22
 
23
+ value_type: DataType
24
+
22
25
  @staticmethod
23
- def from_et(et_element: ElementTree.Element, internal_type: DataType) -> "InternalConstr":
26
+ def constr_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *,
27
+ value_type: DataType) -> "InternalConstr":
24
28
 
25
- lower_limit = Limit.from_et(et_element.find("LOWER-LIMIT"), internal_type=internal_type)
26
- upper_limit = Limit.from_et(et_element.find("UPPER-LIMIT"), internal_type=internal_type)
29
+ lower_limit = Limit.limit_from_et(
30
+ et_element.find("LOWER-LIMIT"), doc_frags, value_type=value_type)
31
+ upper_limit = Limit.limit_from_et(
32
+ et_element.find("UPPER-LIMIT"), doc_frags, value_type=value_type)
27
33
 
28
34
  scale_constrs = [
29
- ScaleConstr.from_et(sc_el, internal_type)
35
+ ScaleConstr.scale_constr_from_et(sc_el, doc_frags, value_type=value_type)
30
36
  for sc_el in et_element.iterfind("SCALE-CONSTRS/SCALE-CONSTR")
31
37
  ]
32
38
 
33
39
  return InternalConstr(
34
- lower_limit=lower_limit, upper_limit=upper_limit, scale_constrs=scale_constrs)
40
+ lower_limit=lower_limit,
41
+ upper_limit=upper_limit,
42
+ scale_constrs=scale_constrs,
43
+ value_type=value_type)
@@ -55,10 +55,13 @@ class IsoTpStateMachine:
55
55
  return # unknown CAN ID
56
56
 
57
57
  # decode the isotp segment
58
- (frame_type,) = bitstruct.unpack("u4", data)
58
+ frame_type, _ = bitstruct.unpack("u4u4", data)
59
+ assert isinstance(frame_type, int)
60
+
59
61
  telegram_len = None
60
62
  if frame_type == IsoTp.FRAME_TYPE_SINGLE:
61
63
  frame_type, telegram_len = bitstruct.unpack("u4u4", data)
64
+ assert isinstance(telegram_len, int)
62
65
 
63
66
  self.on_single_frame(telegram_idx, data[1:1 + telegram_len])
64
67
  self.on_telegram_complete(telegram_idx, data[1:1 + telegram_len])
@@ -67,6 +70,7 @@ class IsoTpStateMachine:
67
70
 
68
71
  elif frame_type == IsoTp.FRAME_TYPE_FIRST:
69
72
  frame_type, telegram_len = bitstruct.unpack("u4u12", data)
73
+ assert isinstance(telegram_len, int)
70
74
 
71
75
  self._telegram_specified_len[telegram_idx] = telegram_len
72
76
  self._telegram_data[telegram_idx] = bytearray(data[2:])
@@ -76,11 +80,13 @@ class IsoTpStateMachine:
76
80
 
77
81
  elif frame_type == IsoTp.FRAME_TYPE_CONSECUTIVE:
78
82
  frame_type, rx_segment_idx = bitstruct.unpack("u4u4", data)
83
+ assert isinstance(rx_segment_idx, int)
79
84
 
80
85
  expected_segment_idx = (self._telegram_last_rx_fragment_idx[telegram_idx] + 1) % 16
81
86
  telegram_data = self._telegram_data[telegram_idx]
82
87
  assert isinstance(telegram_data, bytearray)
83
88
 
89
+ n = -1
84
90
  if expected_segment_idx == rx_segment_idx:
85
91
  self._telegram_last_rx_fragment_idx[telegram_idx] = rx_segment_idx
86
92
  telegram_data += data[1:]
@@ -102,6 +108,8 @@ class IsoTpStateMachine:
102
108
 
103
109
  elif frame_type == IsoTp.FRAME_TYPE_FLOW_CONTROL:
104
110
  frame_type, flow_control_flag = bitstruct.unpack("u4u4", data)
111
+ assert isinstance(flow_control_flag, int)
112
+
105
113
  self.on_flow_control_frame(telegram_idx, flow_control_flag)
106
114
  else:
107
115
  self.on_frame_type_error(telegram_idx, frame_type)
@@ -139,7 +147,7 @@ class IsoTpStateMachine:
139
147
  return
140
148
 
141
149
  if m := self.can_normal_frame_re.match(cur_line.strip()):
142
- #frame_interface = m.group(1)
150
+ # frame_interface = m.group(1)
143
151
  frame_id = int(m.group(2), 16)
144
152
 
145
153
  frame_data_formatted = m.group(3).strip()
@@ -151,7 +159,7 @@ class IsoTpStateMachine:
151
159
  elif (m := self.can_log_frame_re.match(
152
160
  cur_line.strip())) or (m := self.can_fd_log_frame_re.match(
153
161
  cur_line.strip())):
154
- #frame_interface = m.group(2)
162
+ # frame_interface = m.group(2)
155
163
  frame_id = int(m.group(2), 16)
156
164
 
157
165
  frame_data_formatted = m.group(3).strip()
@@ -257,7 +265,7 @@ class IsoTpActiveDecoder(IsoTpStateMachine):
257
265
 
258
266
  def on_single_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
259
267
  # send ACK
260
- #rx_id = self.can_rx_id(telegram_idx)
268
+ # rx_id = self.can_rx_id(telegram_idx)
261
269
  tx_id = self.can_tx_id(telegram_idx)
262
270
  block_size = 0xFF
263
271
  min_separation_time = 0 # ms
@@ -276,7 +284,7 @@ class IsoTpActiveDecoder(IsoTpStateMachine):
276
284
 
277
285
  def on_first_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
278
286
  # send ACK
279
- #rx_id = self.can_rx_id(telegram_idx)
287
+ # rx_id = self.can_rx_id(telegram_idx)
280
288
  tx_id = self.can_tx_id(telegram_idx)
281
289
  block_size = 0xFF # default value, can be overwritten later
282
290
  min_separation_time = 0 # ms
@@ -308,7 +316,7 @@ class IsoTpActiveDecoder(IsoTpStateMachine):
308
316
  # send new ACK if necessary
309
317
  block_size = self._block_size[telegram_idx]
310
318
  if block_size is not None and num_received >= block_size:
311
- #rx_id = self.can_rx_id(telegram_idx)
319
+ # rx_id = self.can_rx_id(telegram_idx)
312
320
  tx_id = self.can_tx_id(telegram_idx)
313
321
  min_separation_time = 0 # ms
314
322
  fc_payload = bitstruct.pack(
odxtools/message.py CHANGED
@@ -31,6 +31,6 @@ class Message:
31
31
  return self.param_dict[key]
32
32
 
33
33
  @property
34
- @deprecated("use .coding_object")
34
+ @deprecated("use .coding_object") # type: ignore[misc]
35
35
  def structure(self) -> Union["Request", "Response"]:
36
36
  return self.coding_object
odxtools/multiplexer.py CHANGED
@@ -36,7 +36,8 @@ class Multiplexer(ComplexDop):
36
36
  @staticmethod
37
37
  def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> "Multiplexer":
38
38
  """Reads a Multiplexer from Diag Layer."""
39
- kwargs = dataclass_fields_asdict(ComplexDop.from_et(et_element, doc_frags))
39
+ base_obj = ComplexDop.from_et(et_element, doc_frags)
40
+ kwargs = dataclass_fields_asdict(base_obj)
40
41
 
41
42
  byte_position = int(et_element.findtext("BYTE-POSITION", "0"))
42
43
  switch_key = MultiplexerSwitchKey.from_et(
@@ -73,8 +74,10 @@ class Multiplexer(ComplexDop):
73
74
  odxraise("Upper and lower bounds of limits must compareable")
74
75
  return lower_limit, upper_limit
75
76
 
76
- def convert_physical_to_bytes(self, physical_value: ParameterValue, encode_state: EncodeState,
77
- bit_position: int) -> bytes:
77
+ def convert_physical_to_bytes(self,
78
+ physical_value: ParameterValue,
79
+ encode_state: EncodeState,
80
+ bit_position: int = 0) -> bytes:
78
81
 
79
82
  if bit_position != 0:
80
83
  raise EncodeError("Multiplexer must be aligned, i.e. bit_position=0, but "
@@ -125,22 +128,23 @@ class Multiplexer(ComplexDop):
125
128
  f" for multiplexer '{self.short_name}')")
126
129
 
127
130
  case_value: Optional[ParameterValue] = None
128
- for case in self.cases or []:
129
- lower, upper = self._get_case_limits(case)
131
+ mux_case = None
132
+ for mux_case in self.cases or []:
133
+ lower, upper = self._get_case_limits(mux_case)
130
134
  if lower <= key_value and key_value <= upper: # type: ignore[operator]
131
- if case._structure:
132
- case_value = case._structure.decode_from_pdu(decode_state)
135
+ if mux_case._structure:
136
+ case_value = mux_case._structure.decode_from_pdu(decode_state)
133
137
  break
134
138
 
135
139
  if case_value is None and self.default_case is not None:
136
140
  if self.default_case._structure:
137
141
  case_value = self.default_case._structure.decode_from_pdu(decode_state)
138
142
 
139
- if case_value is None:
143
+ if mux_case is None or case_value is None:
140
144
  odxraise(f"Failed to find a matching case in {self.short_name} for value {key_value!r}",
141
145
  DecodeError)
142
146
 
143
- mux_value = (case.short_name, case_value)
147
+ mux_value = (mux_case.short_name, case_value)
144
148
 
145
149
  # go back to the original origin
146
150
  decode_state.origin_byte_position = orig_origin
@@ -164,8 +168,9 @@ class Multiplexer(ComplexDop):
164
168
  if self.default_case is not None:
165
169
  self.default_case._resolve_odxlinks(odxlinks)
166
170
 
167
- for case in self.cases:
168
- case._resolve_odxlinks(odxlinks)
171
+ for mux_case in self.cases:
172
+ mux_case._mux_case_resolve_odxlinks(
173
+ odxlinks, key_physical_type=self.switch_key.dop.physical_type.base_data_type)
169
174
 
170
175
  def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None:
171
176
  super()._resolve_snrefs(diag_layer)
@@ -174,5 +179,5 @@ class Multiplexer(ComplexDop):
174
179
  if self.default_case is not None:
175
180
  self.default_case._resolve_snrefs(diag_layer)
176
181
 
177
- for case in self.cases:
178
- case._resolve_snrefs(diag_layer)
182
+ for mux_case in self.cases:
183
+ mux_case._resolve_snrefs(diag_layer)
@@ -4,9 +4,11 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
4
4
  from xml.etree import ElementTree
5
5
 
6
6
  from .basicstructure import BasicStructure
7
+ from .compumethods.limit import Limit
7
8
  from .element import NamedElement
8
9
  from .exceptions import odxrequire
9
10
  from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef
11
+ from .odxtypes import AtomicOdxType, DataType
10
12
  from .utils import dataclass_fields_asdict
11
13
 
12
14
  if TYPE_CHECKING:
@@ -19,8 +21,8 @@ class MultiplexerCase(NamedElement):
19
21
 
20
22
  structure_ref: Optional[OdxLinkRef]
21
23
  structure_snref: Optional[str]
22
- lower_limit: str
23
- upper_limit: str
24
+ lower_limit: Limit
25
+ upper_limit: Limit
24
26
 
25
27
  def __post_init__(self) -> None:
26
28
  self._structure: BasicStructure
@@ -28,15 +30,23 @@ class MultiplexerCase(NamedElement):
28
30
  @staticmethod
29
31
  def from_et(et_element: ElementTree.Element,
30
32
  doc_frags: List[OdxDocFragment]) -> "MultiplexerCase":
31
- """Reads a Case for a Multiplexer."""
33
+ """Reads a case for a Multiplexer."""
32
34
  kwargs = dataclass_fields_asdict(NamedElement.from_et(et_element, doc_frags))
33
35
  structure_ref = OdxLinkRef.from_et(et_element.find("STRUCTURE-REF"), doc_frags)
34
36
  structure_snref = None
35
37
  if (structure_snref_elem := et_element.find("STRUCTURE-SNREF")) is not None:
36
38
  structure_snref = odxrequire(structure_snref_elem.get("SHORT-NAME"))
37
39
 
38
- lower_limit = odxrequire(et_element.findtext("LOWER-LIMIT"))
39
- upper_limit = odxrequire(et_element.findtext("UPPER-LIMIT"))
40
+ lower_limit = Limit.limit_from_et(
41
+ odxrequire(et_element.find("LOWER-LIMIT")),
42
+ doc_frags,
43
+ value_type=None,
44
+ )
45
+ upper_limit = Limit.limit_from_et(
46
+ odxrequire(et_element.find("UPPER-LIMIT")),
47
+ doc_frags,
48
+ value_type=None,
49
+ )
40
50
 
41
51
  return MultiplexerCase(
42
52
  structure_ref=structure_ref,
@@ -49,14 +59,26 @@ class MultiplexerCase(NamedElement):
49
59
  return {}
50
60
 
51
61
  def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
62
+ raise RuntimeError("Calling MultiplexerCase._resolve_odxlinks() is not allowed. "
63
+ "Use ._mux_case_resolve_odxlinks()().")
64
+
65
+ def _mux_case_resolve_odxlinks(self, odxlinks: OdxLinkDatabase, *,
66
+ key_physical_type: DataType) -> None:
52
67
  if self.structure_ref:
53
68
  self._structure = odxlinks.resolve(self.structure_ref)
54
69
 
70
+ self.lower_limit.set_value_type(key_physical_type)
71
+ self.upper_limit.set_value_type(key_physical_type)
72
+
55
73
  def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None:
56
74
  if self.structure_snref:
57
75
  ddds = diag_layer.diag_data_dictionary_spec
58
76
  self._structure = odxrequire(ddds.structures.get(self.structure_snref))
59
77
 
78
+ def applies(self, value: AtomicOdxType) -> bool:
79
+ return self.lower_limit.complies_to_lower(value) \
80
+ and self.upper_limit.complies_to_upper(value)
81
+
60
82
  @property
61
83
  def structure(self) -> BasicStructure:
62
84
  return self._structure
@@ -7,7 +7,7 @@ from .exceptions import odxrequire
7
7
  from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef
8
8
 
9
9
  if TYPE_CHECKING:
10
- from ..diaglayer import DiagLayer
10
+ from .diaglayer import DiagLayer
11
11
 
12
12
 
13
13
  @dataclass
odxtools/nameditemlist.py CHANGED
@@ -12,7 +12,7 @@ class OdxNamed(Protocol):
12
12
 
13
13
  @property
14
14
  def short_name(self) -> str:
15
- pass
15
+ ...
16
16
 
17
17
 
18
18
  T = TypeVar("T")
@@ -127,7 +127,7 @@ class ItemAttributeList(List[T]):
127
127
  result.update(self._item_dict)
128
128
  return result
129
129
 
130
- @overload # type: ignore[override]
130
+ @overload
131
131
  def __getitem__(self, key: SupportsIndex) -> T:
132
132
  ...
133
133
 
@@ -139,7 +139,8 @@ class ItemAttributeList(List[T]):
139
139
  def __getitem__(self, key: slice) -> List[T]:
140
140
  ...
141
141
 
142
- def __getitem__(self, key: Union[SupportsIndex, str, slice]) -> Union[T, List[T]]:
142
+ def __getitem__( # type: ignore
143
+ self, key: Union[SupportsIndex, str, slice]) -> Union[T, List[T]]:
143
144
  if isinstance(key, (SupportsIndex, slice)):
144
145
  return super().__getitem__(key)
145
146
  else:
@@ -177,7 +178,7 @@ class ItemAttributeList(List[T]):
177
178
 
178
179
  class NamedItemList(ItemAttributeList[T]):
179
180
 
180
- def _get_item_key(self, obj: T) -> str:
181
+ def _get_item_key(self, item: T) -> str:
181
182
  """Transform an object's `short_name` attribute into a valid
182
183
  python identifier
183
184
 
@@ -187,9 +188,9 @@ class NamedItemList(ItemAttributeList[T]):
187
188
  such short names.
188
189
 
189
190
  """
190
- if not isinstance(obj, OdxNamed):
191
+ if not isinstance(item, OdxNamed):
191
192
  odxraise()
192
- sn = obj.short_name
193
+ sn = item.short_name
193
194
  if not isinstance(sn, str):
194
195
  odxraise()
195
196
 
odxtools/odxlink.py CHANGED
@@ -181,7 +181,7 @@ class OdxLinkDatabase:
181
181
  def resolve(self, ref: OdxLinkRef, expected_type: Type[T]) -> T:
182
182
  ...
183
183
 
184
- def resolve(self, ref: OdxLinkRef, expected_type: Optional[Type[T]] = None) -> Any:
184
+ def resolve(self, ref: OdxLinkRef, expected_type: Optional[Any] = None) -> Any:
185
185
  """
186
186
  Resolve a reference to an object
187
187
 
@@ -223,7 +223,7 @@ class OdxLinkDatabase:
223
223
 
224
224
  def resolve_lenient(self,
225
225
  ref: OdxLinkRef,
226
- expected_type: Optional[Type[T]] = None) -> Optional[Any]:
226
+ expected_type: Optional[Any] = None) -> Optional[Any]:
227
227
  """
228
228
  Resolve a reference to an object
229
229
 
odxtools/odxtypes.py CHANGED
@@ -102,6 +102,58 @@ _ODX_TYPE_TO_PYTHON_TYPE: Dict[str, Type[AtomicOdxType]] = {
102
102
  }
103
103
 
104
104
 
105
+ def compare_odx_values(a: AtomicOdxType, b: AtomicOdxType) -> int:
106
+ # this function implements the comparison according to the ODX
107
+ # specification. (cf section 7.3.6.5)
108
+
109
+ # numeric values are compared numerically (duh!)
110
+ if isinstance(a, (int, float)):
111
+ if not isinstance(b, (int, float)):
112
+ odxraise()
113
+
114
+ tmp = a - b
115
+ if tmp < 0:
116
+ return -1
117
+ elif tmp > 0:
118
+ return 1
119
+ return 0
120
+
121
+ # strings are compared lexicographically. (the spec only allows
122
+ # equals, but this cannot easily implemented using a single
123
+ # comparison function.
124
+ if isinstance(a, str):
125
+ if not isinstance(b, str):
126
+ odxraise()
127
+
128
+ if a < b:
129
+ return -1
130
+ elif b < a:
131
+ return 1
132
+ else:
133
+ return 0
134
+
135
+ # bytefields are treated like long integers: to pad the shorter
136
+ # object with zeros and treat the results like strings.
137
+ if isinstance(a, (bytes, bytearray)):
138
+ if not isinstance(b, (bytes, bytearray)):
139
+ odxraise()
140
+
141
+ obj_len = max(len(a), len(b))
142
+
143
+ tmp_a = a.ljust(obj_len, b'\x00')
144
+ tmp_b = b.ljust(obj_len, b'\x00')
145
+
146
+ if tmp_a > tmp_b:
147
+ return 1
148
+ elif tmp_a < tmp_b:
149
+ return -1
150
+ else:
151
+ return 0
152
+
153
+ odxraise(f"Unhandled comparsion between objects of type {type(a).__name__} "
154
+ f"and {type(b).__name__}")
155
+
156
+
105
157
  class DataType(Enum):
106
158
  """Types for the physical and internal value.
107
159
 
@@ -125,7 +177,8 @@ class DataType(Enum):
125
177
  A_ASCIISTRING = "A_ASCIISTRING"
126
178
  A_UTF8STRING = "A_UTF8STRING"
127
179
 
128
- def as_python_type(self) -> type:
180
+ @property
181
+ def python_type(self) -> Type[AtomicOdxType]:
129
182
  return _ODX_TYPE_TO_PYTHON_TYPE[self.value]
130
183
 
131
184
  def from_string(self, value: str) -> AtomicOdxType:
@@ -158,10 +211,10 @@ class DataType(Enum):
158
211
  return self.from_string(value)
159
212
  else:
160
213
  # regular type cast of python objects
161
- return self.as_python_type()(value)
214
+ return self.python_type(value)
162
215
 
163
216
  def isinstance(self, value: Any) -> bool:
164
- expected_type = self.as_python_type()
217
+ expected_type = self.python_type
165
218
  if isinstance(value, expected_type):
166
219
  return True
167
220
  elif expected_type == float and isinstance(value, (int, float)):
odxtools/outputparam.py CHANGED
@@ -35,7 +35,7 @@ class OutputParam(IdentifiableElement):
35
35
  return {}
36
36
 
37
37
  def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
38
- self._dop_base = odxlinks.resolve(self.dop_base_ref)
38
+ self._dop_base = odxlinks.resolve(self.dop_base_ref, DopBase)
39
39
 
40
40
  def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None:
41
41
  pass
@@ -46,6 +46,6 @@ class OutputParam(IdentifiableElement):
46
46
  return self._dop_base
47
47
 
48
48
  @property
49
- @deprecated(details="use .dop_base")
49
+ @deprecated(details="use .dop_base") # type: ignore[misc]
50
50
  def dop(self) -> DopBase:
51
51
  return self._dop_base
odxtools/parameterinfo.py CHANGED
@@ -84,11 +84,18 @@ def parameter_info(param_list: Iterable[Union[Parameter, EndOfPduField]]) -> str
84
84
  result += f": float\n"
85
85
  ll = cm.physical_lower_limit
86
86
  ul = cm.physical_upper_limit
87
- result += (f" range: "
88
- f"{'[' if ll.interval_type == IntervalType.CLOSED else '('}"
89
- f"{ll.value!r}, "
90
- f"{ul.value!r}"
91
- f"{']' if ul.interval_type == IntervalType.CLOSED else ')'}\n")
87
+ if ll is None:
88
+ ll_str = "(inf"
89
+ else:
90
+ ll_delim = '[' if ll.interval_type == IntervalType.CLOSED else '('
91
+ ll_str = f"{ll_delim}{ll._value!r}"
92
+
93
+ if ul is None:
94
+ ul_str = "inf)"
95
+ else:
96
+ ul_delim = ']' if ul.interval_type == IntervalType.CLOSED else ')'
97
+ ul_str = f"{ul._value!r}{ul_delim}"
98
+ result += f" range: {ll_str}, {ul_str}\n"
92
99
 
93
100
  unit = dop.unit
94
101
  unit_str = unit.display_name if unit is not None else None