odxtools 5.3.1__py3-none-any.whl → 6.0.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 (88) hide show
  1. odxtools/__init__.py +1 -1
  2. odxtools/basicstructure.py +76 -91
  3. odxtools/cli/_parser_utils.py +12 -9
  4. odxtools/cli/_print_utils.py +7 -7
  5. odxtools/cli/browse.py +94 -73
  6. odxtools/cli/find.py +42 -59
  7. odxtools/cli/list.py +21 -17
  8. odxtools/cli/snoop.py +19 -18
  9. odxtools/communicationparameterref.py +6 -3
  10. odxtools/companydocinfo.py +2 -2
  11. odxtools/companyrevisioninfo.py +1 -1
  12. odxtools/comparamsubset.py +6 -6
  13. odxtools/complexcomparam.py +1 -1
  14. odxtools/compumethods/compumethod.py +6 -9
  15. odxtools/compumethods/createanycompumethod.py +11 -9
  16. odxtools/compumethods/identicalcompumethod.py +5 -4
  17. odxtools/compumethods/limit.py +9 -9
  18. odxtools/compumethods/linearcompumethod.py +25 -17
  19. odxtools/compumethods/scalelinearcompumethod.py +6 -5
  20. odxtools/compumethods/tabintpcompumethod.py +30 -9
  21. odxtools/compumethods/texttablecompumethod.py +22 -24
  22. odxtools/database.py +5 -5
  23. odxtools/dataobjectproperty.py +10 -23
  24. odxtools/decodestate.py +1 -1
  25. odxtools/determinenumberofitems.py +37 -8
  26. odxtools/diagcodedtype.py +14 -9
  27. odxtools/diagdatadictionaryspec.py +60 -37
  28. odxtools/diaglayer.py +30 -21
  29. odxtools/diaglayercontainer.py +40 -40
  30. odxtools/diaglayerraw.py +92 -63
  31. odxtools/diagnostictroublecode.py +2 -2
  32. odxtools/diagservice.py +53 -35
  33. odxtools/docrevision.py +1 -1
  34. odxtools/dopbase.py +14 -3
  35. odxtools/dtcdop.py +15 -9
  36. odxtools/dynamiclengthfield.py +6 -4
  37. odxtools/endofpdufield.py +22 -23
  38. odxtools/environmentdata.py +2 -5
  39. odxtools/environmentdatadescription.py +6 -4
  40. odxtools/field.py +3 -8
  41. odxtools/isotp_state_machine.py +52 -38
  42. odxtools/leadinglengthinfotype.py +9 -7
  43. odxtools/load_file.py +2 -1
  44. odxtools/load_odx_d_file.py +2 -5
  45. odxtools/load_pdx_file.py +2 -6
  46. odxtools/message.py +11 -3
  47. odxtools/minmaxlengthtype.py +107 -78
  48. odxtools/modification.py +2 -2
  49. odxtools/multiplexer.py +23 -21
  50. odxtools/multiplexerswitchkey.py +37 -8
  51. odxtools/nameditemlist.py +59 -58
  52. odxtools/odxlink.py +4 -2
  53. odxtools/odxtypes.py +4 -3
  54. odxtools/parameterinfo.py +6 -6
  55. odxtools/parameters/codedconstparameter.py +15 -25
  56. odxtools/parameters/createanyparameter.py +1 -1
  57. odxtools/parameters/dynamicparameter.py +6 -5
  58. odxtools/parameters/lengthkeyparameter.py +2 -1
  59. odxtools/parameters/matchingrequestparameter.py +8 -11
  60. odxtools/parameters/nrcconstparameter.py +11 -21
  61. odxtools/parameters/parameter.py +4 -18
  62. odxtools/parameters/parameterwithdop.py +14 -29
  63. odxtools/parameters/physicalconstantparameter.py +7 -9
  64. odxtools/parameters/reservedparameter.py +17 -38
  65. odxtools/parameters/systemparameter.py +6 -5
  66. odxtools/parameters/tableentryparameter.py +6 -5
  67. odxtools/parameters/tablekeyparameter.py +8 -15
  68. odxtools/parameters/tablestructparameter.py +11 -12
  69. odxtools/parameters/valueparameter.py +9 -24
  70. odxtools/paramlengthinfotype.py +11 -9
  71. odxtools/physicaldimension.py +1 -1
  72. odxtools/physicaltype.py +2 -2
  73. odxtools/response.py +7 -3
  74. odxtools/singleecujob.py +48 -22
  75. odxtools/standardlengthtype.py +11 -6
  76. odxtools/uds.py +1 -1
  77. odxtools/unit.py +5 -5
  78. odxtools/unitgroup.py +1 -1
  79. odxtools/unitspec.py +2 -2
  80. odxtools/version.py +13 -3
  81. odxtools/write_pdx_file.py +7 -4
  82. {odxtools-5.3.1.dist-info → odxtools-6.0.1.dist-info}/METADATA +7 -5
  83. {odxtools-5.3.1.dist-info → odxtools-6.0.1.dist-info}/RECORD +87 -88
  84. odxtools/positioneddataobjectproperty.py +0 -74
  85. {odxtools-5.3.1.dist-info → odxtools-6.0.1.dist-info}/LICENSE +0 -0
  86. {odxtools-5.3.1.dist-info → odxtools-6.0.1.dist-info}/WHEEL +0 -0
  87. {odxtools-5.3.1.dist-info → odxtools-6.0.1.dist-info}/entry_points.txt +0 -0
  88. {odxtools-5.3.1.dist-info → odxtools-6.0.1.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  from dataclasses import dataclass
3
- from typing import Optional
3
+ from typing import Optional, Tuple
4
4
 
5
5
  from .decodestate import DecodeState
6
6
  from .diagcodedtype import DctType, DiagCodedType
7
7
  from .encodestate import EncodeState
8
- from .exceptions import DecodeError, EncodeError, odxassert
9
- from .odxtypes import DataType
8
+ from .exceptions import DecodeError, EncodeError, odxassert, odxraise
9
+ from .odxtypes import AtomicOdxType, DataType
10
10
 
11
11
 
12
12
  @dataclass
@@ -15,7 +15,7 @@ class MinMaxLengthType(DiagCodedType):
15
15
  max_length: Optional[int]
16
16
  termination: str
17
17
 
18
- def __post_init__(self):
18
+ def __post_init__(self) -> None:
19
19
  odxassert(self.max_length is None or self.min_length <= self.max_length)
20
20
  odxassert(
21
21
  self.base_data_type in [
@@ -34,113 +34,142 @@ class MinMaxLengthType(DiagCodedType):
34
34
  def dct_type(self) -> DctType:
35
35
  return "MIN-MAX-LENGTH-TYPE"
36
36
 
37
- def __termination_character(self):
38
- """Returns the termination character or None if it isn't defined."""
39
- # The termination character is actually not specified by ASAM
37
+ def __termination_sequence(self) -> bytes:
38
+ """Returns the termination byte sequence if it isn't defined."""
39
+ # The termination sequence is actually not specified by ASAM
40
40
  # for A_BYTEFIELD but I assume it is only one byte.
41
- termination_char = None
41
+ termination_sequence = b''
42
42
  if self.termination == "ZERO":
43
43
  if self.base_data_type not in [DataType.A_UNICODE2STRING]:
44
- termination_char = bytes([0x0])
44
+ termination_sequence = bytes([0x0])
45
45
  else:
46
- termination_char = bytes([0x0, 0x0])
46
+ termination_sequence = bytes([0x0, 0x0])
47
47
  elif self.termination == "HEX-FF":
48
48
  if self.base_data_type not in [DataType.A_UNICODE2STRING]:
49
- termination_char = bytes([0xFF])
49
+ termination_sequence = bytes([0xFF])
50
50
  else:
51
- termination_char = bytes([0xFF, 0xFF])
52
- return termination_char
51
+ termination_sequence = bytes([0xFF, 0xFF])
52
+ return termination_sequence
53
53
 
54
- def convert_internal_to_bytes(self, internal_value, encode_state: EncodeState,
54
+ def convert_internal_to_bytes(self, internal_value: AtomicOdxType, encode_state: EncodeState,
55
55
  bit_position: int) -> bytes:
56
- byte_length = self._minimal_byte_length_of(internal_value)
57
-
58
- # The coded value must have at least length min_length
59
- if byte_length < self.min_length:
60
- raise EncodeError(f"The internal value {internal_value} is only {byte_length} bytes"
61
- f" long but the min length is {self.min_length}")
62
- # The coded value must not have a length greater than max_length
63
- if self.max_length and byte_length > self.max_length:
64
- raise EncodeError(f"The internal value {internal_value} requires {byte_length}"
65
- f" bytes, but the max length is {self.max_length}")
66
-
67
- value_byte = self._to_bytes(
68
- internal_value,
69
- bit_position=0,
70
- bit_length=8 * byte_length,
71
- base_data_type=self.base_data_type,
72
- is_highlow_byte_order=self.is_highlow_byte_order,
73
- )
74
-
75
- if encode_state.is_end_of_pdu or byte_length == self.max_length:
76
- # All termination types may be ended by the PDU
77
- return value_byte
56
+ if not isinstance(internal_value, (bytes, str)):
57
+ odxraise("MinMaxLengthType is currently only implemented for strings and byte arrays",
58
+ EncodeError)
59
+
60
+ if self.max_length is not None:
61
+ data_length = min(len(internal_value), self.max_length)
78
62
  else:
79
- termination_char = self.__termination_character()
80
- if self.termination == "END-OF-PDU":
81
- termination_char = bytes()
82
- odxassert(
83
- termination_char is not None,
84
- f"MinMaxLengthType with termination {self.termination}"
85
- f"(min: {self.min_length}, max: {self.max_length}) failed encoding {internal_value}"
86
- )
87
- return value_byte + termination_char
63
+ data_length = len(internal_value)
64
+
65
+ value_bytes = bytearray(
66
+ self._to_bytes(
67
+ internal_value,
68
+ bit_position=0,
69
+ bit_length=8 * data_length,
70
+ base_data_type=self.base_data_type,
71
+ is_highlow_byte_order=self.is_highlow_byte_order,
72
+ ))
73
+
74
+ # TODO: ensure that the termination delimiter is not
75
+ # encountered within the encoded value.
88
76
 
89
- def convert_bytes_to_internal(self, decode_state: DecodeState, bit_position: int = 0):
90
- if decode_state.next_byte_position + self.min_length > len(decode_state.coded_message):
91
- raise DecodeError("The PDU ended before min length was reached.")
77
+ odxassert(self.termination != "END-OF-PDU" or encode_state.is_end_of_pdu)
78
+ if encode_state.is_end_of_pdu or len(value_bytes) == self.max_length:
79
+ # All termination types may be ended by the end of the PDU
80
+ # or once reaching the maximum length. In this case, we
81
+ # must not add the termination sequence
82
+ pass
83
+ else:
84
+ termination_sequence = self.__termination_sequence()
85
+
86
+ # ensure that we don't try to encode an odd-length
87
+ # value when using a two-byte terminator
88
+ odxassert(len(value_bytes) % len(termination_sequence) == 0)
89
+
90
+ value_bytes.extend(termination_sequence)
91
+
92
+ if len(value_bytes) < self.min_length:
93
+ raise EncodeError(f"Encoded value for MinMaxLengthType "
94
+ f"must be at least {self.min_length} bytes long. "
95
+ f"(Is: {len(value_bytes)} bytes.)")
96
+ elif self.max_length is not None and len(value_bytes) > self.max_length:
97
+ raise EncodeError(f"Encoded value for MinMaxLengthType "
98
+ f"must not be longer than {self.max_length} bytes. "
99
+ f"(Is: {len(value_bytes)} bytes.)")
100
+
101
+ return value_bytes
102
+
103
+ def convert_bytes_to_internal(self,
104
+ decode_state: DecodeState,
105
+ bit_position: int = 0) -> Tuple[AtomicOdxType, int]:
106
+ if decode_state.cursor_position + self.min_length > len(decode_state.coded_message):
107
+ raise DecodeError("The PDU ended before minimum length was reached.")
92
108
 
93
109
  coded_message = decode_state.coded_message
94
- byte_position = decode_state.next_byte_position
95
- termination_char = self.__termination_character()
110
+ cursor_pos = decode_state.cursor_position
111
+ termination_seq = self.__termination_sequence()
96
112
 
97
- # If no termination char is found, this is the next byte after the parameter.
98
- max_termination_byte = len(coded_message)
113
+ max_terminator_pos = len(coded_message)
99
114
  if self.max_length is not None:
100
- max_termination_byte = min(max_termination_byte, byte_position + self.max_length)
115
+ max_terminator_pos = min(max_terminator_pos, cursor_pos + self.max_length)
101
116
 
102
117
  if self.termination != "END-OF-PDU":
103
- # The parameter either ends after max length, at the end of the PDU
104
- # or if a termination character is found.
105
- char_length = len(termination_char) # either 1 or 2
106
-
107
- termination_byte = byte_position + self.min_length
108
- found_char = False
109
- # Search the termination character
110
- while termination_byte < max_termination_byte and not found_char:
111
- found_char = (
112
- coded_message[termination_byte:termination_byte +
113
- char_length] == termination_char)
114
- if not found_char:
115
- termination_byte += char_length
116
-
117
- byte_length = termination_byte - byte_position
118
+ # The parameter either ends after the maximum length, at
119
+ # the end of the PDU or if a termination sequence is
120
+ # found.
121
+
122
+ terminator_pos = cursor_pos + self.min_length
123
+ while True:
124
+ # Search the termination sequence
125
+ terminator_pos = coded_message.find(termination_seq, terminator_pos,
126
+ max_terminator_pos)
127
+ if terminator_pos < 0:
128
+ # termination sequence was not found, i.e., we
129
+ # are terminated by either the end of the PDU or
130
+ # our maximum size. (whatever is the smaller
131
+ # value.)
132
+ byte_length = max_terminator_pos - cursor_pos
133
+ break
134
+ elif (terminator_pos - cursor_pos) % len(termination_seq) == 0:
135
+ # we found the termination sequence at a position
136
+ # and it is correctly aligned (two-byte
137
+ # termination sequences must be word aligned
138
+ # relative to the beginning of the parameter)!
139
+ byte_length = terminator_pos - cursor_pos
140
+ break
141
+ else:
142
+ # we found the termination sequence, but its
143
+ # alignment was incorrect. Try again one byte
144
+ # further...
145
+ terminator_pos += 1
118
146
 
119
147
  # Extract the value
120
- value, byte = self._extract_internal(
148
+ value, byte_pos = self._extract_internal(
121
149
  decode_state.coded_message,
122
- byte_position=byte_position,
150
+ byte_position=cursor_pos,
123
151
  bit_position=bit_position,
124
152
  bit_length=8 * byte_length,
125
153
  base_data_type=self.base_data_type,
126
154
  is_highlow_byte_order=self.is_highlow_byte_order,
127
155
  )
128
- odxassert(byte == termination_byte)
129
156
 
130
- # next byte starts after the termination character
131
- next_byte_position = byte + char_length if found_char else byte
132
- return value, next_byte_position
157
+ if byte_pos != len(coded_message) and byte_pos - cursor_pos != self.max_length:
158
+ byte_pos += len(termination_seq)
159
+
160
+ # next byte starts after the actual data and the termination sequence
161
+ return value, byte_pos
133
162
  else:
134
163
  # If termination == "END-OF-PDU", the parameter ends after max_length
135
164
  # or at the end of the PDU.
136
- byte_length = max_termination_byte - byte_position
165
+ byte_length = max_terminator_pos - cursor_pos
137
166
 
138
- value, byte = self._extract_internal(
167
+ value, byte_pos = self._extract_internal(
139
168
  decode_state.coded_message,
140
- byte_position=byte_position,
169
+ byte_position=cursor_pos,
141
170
  bit_position=bit_position,
142
171
  bit_length=8 * byte_length,
143
172
  base_data_type=self.base_data_type,
144
173
  is_highlow_byte_order=self.is_highlow_byte_order,
145
174
  )
146
- return value, byte
175
+ return value, byte_pos
odxtools/modification.py CHANGED
@@ -24,8 +24,8 @@ class Modification:
24
24
  def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
25
25
  return {}
26
26
 
27
- def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase):
27
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
28
28
  pass
29
29
 
30
- def _resolve_snrefs(self, diag_layer: "DiagLayer"):
30
+ def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None:
31
31
  pass
odxtools/multiplexer.py CHANGED
@@ -1,7 +1,6 @@
1
1
  # SPDX-License-Identifier: MIT
2
- from collections import OrderedDict
3
2
  from dataclasses import dataclass
4
- from typing import TYPE_CHECKING, Any, Dict, List, Optional
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
5
4
  from xml.etree import ElementTree
6
5
 
7
6
  from .createsdgs import create_sdgs_from_et
@@ -14,7 +13,7 @@ from .multiplexercase import MultiplexerCase
14
13
  from .multiplexerdefaultcase import MultiplexerDefaultCase
15
14
  from .multiplexerswitchkey import MultiplexerSwitchKey
16
15
  from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId
17
- from .odxtypes import odxstr_to_bool
16
+ from .odxtypes import AtomicOdxType, ParameterValue, odxstr_to_bool
18
17
  from .utils import dataclass_fields_asdict
19
18
 
20
19
  if TYPE_CHECKING:
@@ -58,17 +57,13 @@ class Multiplexer(DopBase):
58
57
  cases=cases,
59
58
  **kwargs)
60
59
 
61
- @property
62
- def bit_length(self):
63
- return None
64
-
65
- def _get_case_limits(self, case: MultiplexerCase):
60
+ def _get_case_limits(self, case: MultiplexerCase) -> Tuple[AtomicOdxType, AtomicOdxType]:
66
61
  key_type = self.switch_key.dop.physical_type.base_data_type
67
62
  lower_limit = key_type.make_from(case.lower_limit)
68
63
  upper_limit = key_type.make_from(case.upper_limit)
69
64
  return lower_limit, upper_limit
70
65
 
71
- def convert_physical_to_bytes(self, physical_value, encode_state: EncodeState,
66
+ def convert_physical_to_bytes(self, physical_value: ParameterValue, encode_state: EncodeState,
72
67
  bit_position: int) -> bytes:
73
68
 
74
69
  if bit_position != 0:
@@ -88,10 +83,11 @@ class Multiplexer(DopBase):
88
83
  case_bytes = case._structure.convert_physical_to_bytes(
89
84
  case_value, encode_state, 0)
90
85
  else:
91
- case_bytes = bytes()
86
+ case_bytes = b''
92
87
 
93
88
  key_value, _ = self._get_case_limits(case)
94
- key_bytes = self.switch_key.convert_physical_to_bytes(key_value, encode_state)
89
+ key_bytes = self.switch_key.dop.convert_physical_to_bytes(
90
+ key_value, encode_state, bit_position=self.switch_key.bit_position or 0)
95
91
 
96
92
  mux_len = max(len(key_bytes), len(case_bytes) + case_pos)
97
93
  mux_bytes = bytearray(mux_len)
@@ -102,18 +98,20 @@ class Multiplexer(DopBase):
102
98
 
103
99
  raise EncodeError(f"The case {case_name} is not found in Multiplexer {self.short_name}")
104
100
 
105
- def convert_bytes_to_physical(self, decode_state: DecodeState, bit_position: int = 0):
101
+ def convert_bytes_to_physical(self,
102
+ decode_state: DecodeState,
103
+ bit_position: int = 0) -> Tuple[ParameterValue, int]:
106
104
 
107
105
  if bit_position != 0:
108
- raise DecodeError("Multiplexer must be aligned, i.e. bit_position=0, but "
106
+ raise DecodeError("Multiplexers must be byte-aligned, i.e. bit_position=0, but "
109
107
  f"{self.short_name} was passed the bit position {bit_position}")
110
- key_value, key_next_byte = self.switch_key.convert_bytes_to_physical(decode_state)
108
+ key_value, key_next_byte = self.switch_key.dop.convert_bytes_to_physical(decode_state)
111
109
 
112
- byte_code = decode_state.coded_message[decode_state.next_byte_position:]
110
+ byte_code = decode_state.coded_message[decode_state.cursor_position:]
113
111
  case_decode_state = DecodeState(
114
112
  coded_message=byte_code[self.byte_position:],
115
- parameter_values=dict(),
116
- next_byte_position=0,
113
+ parameter_values={},
114
+ cursor_position=0,
117
115
  )
118
116
  case_found = False
119
117
  case_next_byte = 0
@@ -135,16 +133,20 @@ class Multiplexer(DopBase):
135
133
 
136
134
  if not case_found:
137
135
  raise DecodeError(
138
- f"Failed to find a matching case in {self.short_name} for value {key_value}")
136
+ f"Failed to find a matching case in {self.short_name} for value {key_value!r}")
139
137
 
140
- mux_value = OrderedDict({case.short_name: case_value})
141
- mux_next_byte = decode_state.next_byte_position + max(
138
+ mux_value = {case.short_name: cast(ParameterValue, case_value)}
139
+ mux_next_byte = decode_state.cursor_position + max(
142
140
  key_next_byte + self.switch_key.byte_position, case_next_byte + self.byte_position)
143
141
  return mux_value, mux_next_byte
144
142
 
145
143
  def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
146
144
  odxlinks = super()._build_odxlinks()
147
145
 
146
+ odxlinks.update(self.switch_key._build_odxlinks())
147
+ if self.default_case is not None:
148
+ odxlinks.update(self.default_case._build_odxlinks())
149
+
148
150
  return odxlinks
149
151
 
150
152
  def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
@@ -157,7 +159,7 @@ class Multiplexer(DopBase):
157
159
  for case in self.cases:
158
160
  case._resolve_odxlinks(odxlinks)
159
161
 
160
- def _resolve_snrefs(self, diag_layer: "DiagLayer"):
162
+ def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None:
161
163
  super()._resolve_snrefs(diag_layer)
162
164
 
163
165
  self.switch_key._resolve_snrefs(diag_layer)
@@ -1,18 +1,47 @@
1
1
  from dataclasses import dataclass
2
- from typing import List
2
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
3
3
  from xml.etree import ElementTree
4
4
 
5
- from .odxlink import OdxDocFragment
6
- from .positioneddataobjectproperty import PositionedDataObjectProperty
7
- from .utils import dataclass_fields_asdict
5
+ from .dataobjectproperty import DataObjectProperty
6
+ from .exceptions import odxrequire
7
+ from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef
8
+
9
+ if TYPE_CHECKING:
10
+ from ..diaglayer import DiagLayer
8
11
 
9
12
 
10
13
  @dataclass
11
- class MultiplexerSwitchKey(PositionedDataObjectProperty):
14
+ class MultiplexerSwitchKey:
15
+ """
16
+ The object that determines the case to be used by a multiplexer
17
+ """
18
+ byte_position: int
19
+ bit_position: Optional[int]
20
+ dop_ref: OdxLinkRef
12
21
 
13
22
  @staticmethod
14
23
  def from_et(et_element: ElementTree.Element,
15
24
  doc_frags: List[OdxDocFragment]) -> "MultiplexerSwitchKey":
16
- kwargs = dataclass_fields_asdict(
17
- PositionedDataObjectProperty.from_et(et_element, doc_frags))
18
- return MultiplexerSwitchKey(**kwargs)
25
+ byte_position = int(odxrequire(et_element.findtext("BYTE-POSITION")))
26
+ bit_position_str = et_element.findtext("BIT-POSITION")
27
+ bit_position = int(bit_position_str) if bit_position_str is not None else None
28
+ dop_ref = odxrequire(OdxLinkRef.from_et(et_element.find("DATA-OBJECT-PROP-REF"), doc_frags))
29
+
30
+ return MultiplexerSwitchKey(
31
+ byte_position=byte_position,
32
+ bit_position=bit_position,
33
+ dop_ref=dop_ref,
34
+ )
35
+
36
+ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
37
+ return {}
38
+
39
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
40
+ self._dop = odxlinks.resolve(self.dop_ref, DataObjectProperty)
41
+
42
+ def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None:
43
+ pass
44
+
45
+ @property
46
+ def dop(self) -> DataObjectProperty:
47
+ return self._dop
odxtools/nameditemlist.py CHANGED
@@ -1,8 +1,9 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  import abc
3
+ from collections import OrderedDict
3
4
  from keyword import iskeyword
4
- from typing import (Callable, Collection, Generic, Iterable, List, Optional, Protocol, Tuple,
5
- TypeVar, Union, cast, overload, runtime_checkable)
5
+ from typing import (Any, Callable, Collection, Dict, Generic, Iterable, Iterator, List, Optional,
6
+ Protocol, Tuple, TypeVar, Union, cast, overload, runtime_checkable)
6
7
 
7
8
  from .exceptions import odxraise
8
9
 
@@ -19,7 +20,7 @@ T = TypeVar("T")
19
20
  TNamed = TypeVar("TNamed", bound=OdxNamed)
20
21
 
21
22
 
22
- class ItemList(Generic[T]):
23
+ class ItemAttributeList(Generic[T]):
23
24
  """A list that provides direct access to its items as named attributes.
24
25
 
25
26
  This is a hybrid between a list and a user-defined object: One can
@@ -34,8 +35,8 @@ class ItemList(Generic[T]):
34
35
  """
35
36
 
36
37
  def __init__(self, input_list: Optional[Iterable[T]] = None) -> None:
37
- self._names: List[str] = []
38
- self._values: List[T] = []
38
+ self._item_dict: OrderedDict[str, T] = OrderedDict()
39
+ self._item_list: List[T] = []
39
40
 
40
41
  if input_list is not None:
41
42
  for item in input_list:
@@ -54,53 +55,57 @@ class ItemList(Generic[T]):
54
55
  """
55
56
  item_name = self._get_item_key(item)
56
57
 
57
- # eliminate conflicts between the item name and existing
58
- # attributes of the NamedItemList object
58
+ # eliminate conflicts between the name of the new item and
59
+ # existing attributes of the ItemAttributeList object
59
60
  i = 1
60
61
  tmp = item_name
61
62
  while True:
62
- # using dir() for checking if there is a name conflict
63
- # might be slow, but NamedItemList is not meant for
64
- # ginormous lists...
65
- if tmp not in dir(self):
63
+ if not hasattr(self, tmp):
66
64
  break
67
65
 
68
66
  i += 1
69
67
  if item_name.endswith("_"):
68
+ # if the item name already ends with an underscore,
69
+ # there's no need to add a second one...
70
70
  tmp = f"{item_name}{i}"
71
71
  else:
72
72
  tmp = f"{item_name}_{i}"
73
+
73
74
  item_name = tmp
74
75
 
75
- self.__dict__[item_name] = item
76
- self._names.append(item_name)
77
- self._values.append(item)
76
+ self._item_dict[item_name] = item
77
+ self._item_list.append(item)
78
78
 
79
79
  def sort(self, key: Optional[Callable[[T], str]] = None, reverse: bool = False) -> None:
80
- tmp = list(zip(self._names, self._values))
81
80
  if key is None:
82
- tmp.sort(reverse=reverse)
81
+ self._item_dict = OrderedDict(
82
+ sorted(self._item_dict.items(), key=lambda x: x[0], reverse=reverse))
83
83
  else:
84
84
  key_fn = cast(Callable[[T], str], key)
85
- tmp.sort(key=lambda x: key_fn(x[1]), reverse=reverse)
85
+ self._item_dict = OrderedDict(
86
+ sorted(self._item_dict.items(), key=lambda x: key_fn(x[1]), reverse=reverse))
86
87
 
87
- self._names = [x[0] for x in tmp]
88
- self._values = [x[1] for x in tmp]
88
+ self._item_list = list(self._item_dict.values())
89
89
 
90
90
  def keys(self) -> Collection[str]:
91
- return self._names
91
+ return self._item_dict.keys()
92
92
 
93
93
  def values(self) -> Collection[T]:
94
- return self._values
94
+ return self._item_list
95
95
 
96
- def items(self) -> List[Tuple[str, T]]:
97
- return list(zip(self._names, self._values))
96
+ def items(self) -> Collection[Tuple[str, T]]:
97
+ return self._item_dict.items()
98
98
 
99
99
  def __contains__(self, x: T) -> bool:
100
- return x in self._values
100
+ return x in self._item_list
101
101
 
102
102
  def __len__(self) -> int:
103
- return len(self._values)
103
+ return len(self._item_list)
104
+
105
+ def __dir__(self) -> Dict[str, Any]:
106
+ result = dict(self.__dict__)
107
+ result.update(self._item_dict)
108
+ return result
104
109
 
105
110
  @overload
106
111
  def __getitem__(self, key: int) -> T:
@@ -116,59 +121,55 @@ class ItemList(Generic[T]):
116
121
 
117
122
  def __getitem__(self, key: Union[int, str, slice]) -> Union[T, List[T]]:
118
123
  if isinstance(key, int):
119
- if abs(key) < -len(self._values) or key >= len(self._values):
120
- # we want to raise a KeyError instead of an IndexError
121
- # if the index is out of range...
122
- raise KeyError(f"Tried to access item {key} of a NamedItemList "
123
- f"of length {len(self)}")
124
-
125
- return self._values[key]
124
+ return self._item_list[key]
126
125
  elif isinstance(key, slice):
127
- # for slices, we unfortunately have to ignore the typing
128
- # because if the key is a slice, we cannot return a single
129
- # item. (alternatively, the return type of this method
130
- # could be defined as Union[T, List[T]], but this leads
131
- # mypy to produce *many* spurious and hard to fix errors.
132
- return self._values[key]
126
+ return self._item_list[key]
133
127
  else:
134
- return self.__dict__[key]
128
+ return self._item_dict[key]
129
+
130
+ def __getattr__(self, key: str) -> T:
131
+ if key not in self._item_dict:
132
+ raise AttributeError(f"ItemAttributeList does not contain an item named '{key}'")
133
+
134
+ return self._item_dict[key]
135
135
 
136
136
  def get(self, key: Union[int, str], default: Optional[T] = None) -> Optional[T]:
137
137
  if isinstance(key, int):
138
- if abs(key) < -len(self._values) or key >= len(self._values):
138
+ if abs(key) < -len(self._item_dict) or key >= len(self._item_dict):
139
139
  return default
140
140
 
141
- return self._values[key]
141
+ return self._item_list[key]
142
142
  else:
143
- return cast(Optional[T], self.__dict__.get(key, default))
143
+ return cast(Optional[T], self._item_dict.get(key, default))
144
144
 
145
145
  def __eq__(self, other: object) -> bool:
146
146
  """
147
- Named item lists are equal if the underlying lists are equal.
148
- Note that this does not consider the map `item_to_name_fn`.
147
+ Item lists are equal if the underlying lists are equal.
149
148
  """
150
- if not isinstance(other, NamedItemList):
149
+ if not isinstance(other, type(self)) or not isinstance(self, type(other)):
151
150
  return False
152
151
  else:
153
- return self._names == other._names and self._values == other._values
152
+ return self._item_dict == other._item_dict
154
153
 
155
- # -> Iterator[T]: # <- this leads to *many* type checking errors
156
- def __iter__(self):
157
- return iter(self._values)
154
+ def __iter__(self) -> Iterator[T]:
155
+ return iter(self._item_list)
158
156
 
159
157
  def __str__(self) -> str:
160
- return f"[{', '.join([x for x in self._names])}]"
158
+ return f"[{', '.join(self._item_dict.keys())}]"
161
159
 
162
160
  def __repr__(self) -> str:
163
161
  return self.__str__()
164
162
 
165
163
 
166
164
  def short_name_as_key(obj: OdxNamed) -> str:
167
- """Retrieve an object's `short_name` attribute into a valid python identifier.
165
+ """Transform an object's `short_name` attribute into a valid
166
+ python identifier
167
+
168
+ Although short names are almost identical to valid python
169
+ identifiers, their first character is allowed to be a number or
170
+ they may be python keywords. This method prepends an underscore to
171
+ such short names.
168
172
 
169
- Although short names are almost identical to python identifiers,
170
- their first character is allowed to be a number. This method
171
- prepends an underscore to such shortnames.
172
173
  """
173
174
  if not isinstance(obj, OdxNamed):
174
175
  odxraise()
@@ -176,16 +177,16 @@ def short_name_as_key(obj: OdxNamed) -> str:
176
177
  if not isinstance(sn, str):
177
178
  odxraise()
178
179
 
179
- # make sure that the name of the item in question:
180
- # * is not a python keyword (this would lead to syntax errors)
181
- # * does not starts with a digit
180
+ # make sure that the name of the item in question is not a python
181
+ # keyword (this would lead to syntax errors) and that does not
182
+ # start with a digit
182
183
  if sn[0].isdigit() or iskeyword(sn):
183
184
  return f"_{sn}"
184
185
 
185
186
  return sn
186
187
 
187
188
 
188
- class NamedItemList(Generic[TNamed], ItemList[TNamed]):
189
+ class NamedItemList(Generic[TNamed], ItemAttributeList[TNamed]):
189
190
 
190
191
  def _get_item_key(self, obj: OdxNamed) -> str:
191
192
  return short_name_as_key(obj)