odxtools 6.6.0__py3-none-any.whl → 6.7.0__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 +27 -35
  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 +14 -8
  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 +30 -10
  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 +27 -3
  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.0.dist-info → odxtools-6.7.0.dist-info}/METADATA +28 -16
  77. {odxtools-6.6.0.dist-info → odxtools-6.7.0.dist-info}/RECORD +81 -79
  78. {odxtools-6.6.0.dist-info → odxtools-6.7.0.dist-info}/WHEEL +1 -1
  79. {odxtools-6.6.0.dist-info → odxtools-6.7.0.dist-info}/LICENSE +0 -0
  80. {odxtools-6.6.0.dist-info → odxtools-6.7.0.dist-info}/entry_points.txt +0 -0
  81. {odxtools-6.6.0.dist-info → odxtools-6.7.0.dist-info}/top_level.txt +0 -0
odxtools/__init__.py CHANGED
@@ -62,11 +62,11 @@ References
62
62
  - _`[ISO22901]` The ISO 22901 Standard: https://www.iso.org/standard/41207.html
63
63
 
64
64
  """
65
- from .load_file import load_file
66
- from .load_odx_d_file import load_odx_d_file
67
- from .load_pdx_file import load_pdx_file
68
- from .version import __version__
69
- from .write_pdx_file import write_pdx_file
65
+ from .load_file import load_file as load_file # noqa: F401
66
+ from .load_odx_d_file import load_odx_d_file as load_odx_d_file # noqa: F401
67
+ from .load_pdx_file import load_pdx_file as load_pdx_file # noqa: F401
68
+ from .version import __version__ as __version__ # noqa: F401
69
+ from .write_pdx_file import write_pdx_file as write_pdx_file # noqa: F401
70
70
 
71
71
  __author__ = "Katrin Bauer"
72
72
 
@@ -68,7 +68,8 @@ class BasicStructure(ComplexDop):
68
68
  cursor += param_bit_length
69
69
  length = max(length, cursor)
70
70
 
71
- # Round up to account for padding bits
71
+ # Round up to account for padding bits (all structures are
72
+ # byte aligned)
72
73
  return ((length + 7) // 8) * 8
73
74
 
74
75
  def coded_const_prefix(self, request_prefix: bytes = b'') -> bytes:
@@ -210,18 +211,18 @@ class BasicStructure(ComplexDop):
210
211
  stacklevel=1)
211
212
 
212
213
  def convert_physical_to_bytes(self,
213
- param_values: ParameterValue,
214
+ physical_value: ParameterValue,
214
215
  encode_state: EncodeState,
215
216
  bit_position: int = 0) -> bytes:
216
- if not isinstance(param_values, dict):
217
+ if not isinstance(physical_value, dict):
217
218
  raise EncodeError(
218
219
  f"Expected a dictionary for the values of structure {self.short_name}, "
219
- f"got {type(param_values)}")
220
+ f"got {type(physical_value)}")
220
221
  if bit_position != 0:
221
222
  raise EncodeError("Structures must be aligned, i.e. bit_position=0, but "
222
223
  f"{self.short_name} was passed the bit position {bit_position}")
223
224
  return self.convert_physical_to_internal(
224
- param_values,
225
+ physical_value,
225
226
  triggering_coded_request=encode_state.triggering_request,
226
227
  is_end_of_pdu=encode_state.is_end_of_pdu,
227
228
  )
@@ -254,9 +255,7 @@ class BasicStructure(ComplexDop):
254
255
  Parameters of the RPC as mapping from SHORT-NAME of the parameter to the value
255
256
  """
256
257
  return self.convert_physical_to_internal(
257
- params, # type: ignore[arg-type]
258
- triggering_coded_request=coded_request,
259
- is_end_of_pdu=True)
258
+ params, triggering_coded_request=coded_request, is_end_of_pdu=True)
260
259
 
261
260
  def decode(self, message: bytes) -> ParameterValueDict:
262
261
  decode_state = DecodeState(coded_message=message)
@@ -2,11 +2,26 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
  import argparse
5
+ from typing import Any, Protocol, runtime_checkable
5
6
 
6
7
  from ..database import Database
7
8
  from ..load_file import load_file as _load_file
8
9
 
9
10
 
11
+ @runtime_checkable
12
+ class SubparsersList(Protocol):
13
+ """This protocol reproduces the parts of `argparse._SubparsersAction` which are needed by odxtools
14
+
15
+ Unfortunately this is necessary because
16
+ `argparse._SubparsersAction` is a private class of the `argparse`
17
+ module that is used by some of its public API and thus cannot be
18
+ used outside of the `argparse` module.
19
+ """
20
+
21
+ def add_parser(self, name: str, **kwargs: Any) -> "argparse.ArgumentParser":
22
+ ...
23
+
24
+
10
25
  def add_pdx_argument(parser: argparse.ArgumentParser, is_optional: bool = False) -> None:
11
26
  parser.add_argument(
12
27
  "pdx_file",
@@ -36,7 +36,7 @@ def print_diagnostic_service(service: DiagService,
36
36
  print_state_transitions: bool = False,
37
37
  print_audiences: bool = False,
38
38
  allow_unknown_bit_lengths: bool = False,
39
- print_fn: Callable = print) -> None:
39
+ print_fn: Callable[..., Any] = print) -> None:
40
40
 
41
41
  print_fn(f" Service '{service.short_name}':")
42
42
 
@@ -70,7 +70,7 @@ def print_diagnostic_service(service: DiagService,
70
70
 
71
71
  def print_service_parameters(service: DiagService,
72
72
  allow_unknown_bit_lengths: bool = False,
73
- print_fn: Callable = print) -> None:
73
+ print_fn: Callable[..., Any] = print) -> None:
74
74
  # prints parameter details of request, positive response and negative response of diagnostic service
75
75
 
76
76
  # Request
@@ -156,6 +156,7 @@ def extract_parameter_tabulation_data(parameters: List[Parameter]) -> Dict[str,
156
156
  byte.append(param.byte_position)
157
157
  semantic.append(param.semantic)
158
158
  param_type.append(param.parameter_type)
159
+ length = 0
159
160
  if param.get_static_bit_length() is not None:
160
161
  bit_length.append(param.get_static_bit_length())
161
162
  length = (param.get_static_bit_length() or 0) // 4
@@ -227,7 +228,7 @@ def extract_parameter_tabulation_data(parameters: List[Parameter]) -> Dict[str,
227
228
  }
228
229
 
229
230
 
230
- def print_dl_metrics(variants: List[DiagLayer], print_fn: Callable = print) -> None:
231
+ def print_dl_metrics(variants: List[DiagLayer], print_fn: Callable[..., Any] = print) -> None:
231
232
 
232
233
  name = []
233
234
  type = []
odxtools/cli/browse.py CHANGED
@@ -4,7 +4,7 @@ import logging
4
4
  import sys
5
5
  from typing import List, Optional, Union, cast
6
6
 
7
- from InquirerPy import prompt as PI_prompt
7
+ import InquirerPy.prompt as IP_prompt
8
8
  from tabulate import tabulate # TODO: switch to rich tables
9
9
 
10
10
  from ..database import Database
@@ -20,6 +20,7 @@ from ..parameters.valueparameter import ValueParameter
20
20
  from ..request import Request
21
21
  from ..response import Response
22
22
  from . import _parser_utils
23
+ from ._parser_utils import SubparsersList
23
24
  from ._print_utils import extract_parameter_tabulation_data
24
25
 
25
26
  # name of the tool
@@ -88,13 +89,15 @@ def prompt_single_parameter_value(parameter: Parameter) -> Optional[AtomicOdxTyp
88
89
  # else _convert_string_to_odx_type(x, p.physical_type.base_data_type, param=p) # This does not work because the next parameter to be promted is used (for some reason?)
89
90
  }]
90
91
 
91
- if hasattr(parameter, "dop") and hasattr(parameter.dop, "compu_method") \
92
- and hasattr(parameter.dop.compu_method, "get_scales"):
93
- scales = parameter.dop.compu_method.get_scales()
92
+ if (dop := getattr(parameter, "dop", None)) and \
93
+ (compu_method := getattr(dop, "compu_method", None)):
94
+ scales = compu_method.internal_to_phys
94
95
  choices = [scale.compu_const for scale in scales if scale is not None]
96
+ if (cdv := compu_method.compu_default_value) is not None:
97
+ choices.append(cdv.compu_const)
95
98
  param_prompt[0]["choices"] = choices
96
99
 
97
- answer = PI_prompt(param_prompt)
100
+ answer = IP_prompt(param_prompt)
98
101
  if answer.get(parameter.short_name) == "" and not parameter.is_required:
99
102
  return None
100
103
  elif parameter.physical_type.base_data_type is not None:
@@ -104,7 +107,7 @@ def prompt_single_parameter_value(parameter: Parameter) -> Optional[AtomicOdxTyp
104
107
  logging.warning(
105
108
  f"Parameter {parameter.short_name} does not have a physical data type. Param details: {parameter}"
106
109
  )
107
- return answer.get(parameter.short_name)
110
+ return cast(str, answer.get(parameter.short_name))
108
111
 
109
112
 
110
113
  def encode_message_interactively(sub_service: Union[Request, Response],
@@ -117,7 +120,7 @@ def encode_message_interactively(sub_service: Union[Request, Response],
117
120
  for param_or_dict in param_dict.values():
118
121
  if isinstance(param_or_dict, dict):
119
122
  for param in param_or_dict.values():
120
- if not isinstance(param_or_dict, dict) and param.is_settable:
123
+ if isinstance(param, Parameter) and param.is_settable:
121
124
  exists_definable_param = True
122
125
  elif param_or_dict.is_settable:
123
126
  exists_definable_param = True
@@ -132,10 +135,11 @@ def encode_message_interactively(sub_service: Union[Request, Response],
132
135
  "message": f"Do you want to encode a message? [y/n]",
133
136
  "choices": ["yes", "no"],
134
137
  }]
135
- answer = PI_prompt(encode_message_prompt)
138
+ answer = IP_prompt(encode_message_prompt)
136
139
  if answer.get("yes_no_prompt") == "no":
137
140
  return
138
141
 
142
+ answered_request = b''
139
143
  if isinstance(sub_service, Response):
140
144
  answered_request_prompt = [{
141
145
  "type":
@@ -147,7 +151,7 @@ def encode_message_interactively(sub_service: Union[Request, Response],
147
151
  "filter":
148
152
  lambda input: _convert_string_to_bytes(input),
149
153
  }]
150
- answer = PI_prompt(answered_request_prompt)
154
+ answer = IP_prompt(answered_request_prompt)
151
155
  answered_request = cast(bytes, answer.get("request"))
152
156
  print(f"Input interpretation as list: {list(answered_request)}")
153
157
 
@@ -250,7 +254,8 @@ def encode_message_from_string_values(
250
254
  parameter_values[parameter_sn] = _convert_string_to_odx_type(
251
255
  parameter_value, DataType.A_BYTEFIELD)
252
256
 
253
- payload = sub_service.encode(**parameter_values) # type: ignore
257
+ payload = sub_service.encode(coded_request=b'\xff' * 100, **parameter_values)
258
+
254
259
  print(f"Message payload: 0x{bytes(payload).hex()}")
255
260
 
256
261
 
@@ -266,7 +271,7 @@ def browse(odxdb: Database) -> None:
266
271
  "message": "Select a Variant.",
267
272
  "choices": list(dl_names) + ["[exit]"],
268
273
  }]
269
- answer = PI_prompt(selection)
274
+ answer = IP_prompt(selection)
270
275
  if answer.get("variant") == "[exit]":
271
276
  return
272
277
 
@@ -304,7 +309,7 @@ def browse(odxdb: Database) -> None:
304
309
  f"The variant {variant.short_name} offers the following services. Select one!",
305
310
  "choices": [s.short_name for s in services] + ["[back]"],
306
311
  }]
307
- answer = PI_prompt(selection)
312
+ answer = IP_prompt(selection)
308
313
  if answer.get("service") == "[back]":
309
314
  break
310
315
 
@@ -339,7 +344,7 @@ def browse(odxdb: Database) -> None:
339
344
  "short": f"Negative response: {nr.short_name}",
340
345
  } for nr in service.negative_responses] + ["[back]"], # type: ignore
341
346
  }]
342
- answer = PI_prompt(selection)
347
+ answer = IP_prompt(selection)
343
348
  if answer.get("message_type") == "[back]":
344
349
  continue
345
350
 
@@ -353,7 +358,7 @@ def browse(odxdb: Database) -> None:
353
358
  encode_message_interactively(codec, ask_user_confirmation=True)
354
359
 
355
360
 
356
- def add_subparser(subparsers: "argparse._SubParsersAction") -> None:
361
+ def add_subparser(subparsers: SubparsersList) -> None:
357
362
  # Browse interactively to avoid spamming the console.
358
363
  parser = subparsers.add_parser(
359
364
  "browse",
odxtools/cli/compare.py CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import argparse
5
5
  import os
6
- from typing import Any, Dict, List, Optional, Set, Union
6
+ from typing import Any, Dict, List, Optional, Set, Union, cast
7
7
 
8
8
  from rich import print
9
9
  from tabulate import tabulate # TODO: switch to rich tables
@@ -19,6 +19,7 @@ from ..parameters.parameter import Parameter
19
19
  from ..parameters.physicalconstantparameter import PhysicalConstantParameter
20
20
  from ..parameters.valueparameter import ValueParameter
21
21
  from . import _parser_utils
22
+ from ._parser_utils import SubparsersList
22
23
  from ._print_utils import (extract_service_tabulation_data, print_dl_metrics,
23
24
  print_service_parameters)
24
25
 
@@ -104,8 +105,10 @@ class Display:
104
105
  f" Detailed changes of diagnostic service [u cyan]{service.short_name}[/u cyan]"
105
106
  )
106
107
  # detailed_info in [infotext1, dict1, infotext2, dict2, ...]
107
- for detailed_info in service_dict["changed_parameters_of_service"][2][
108
- service_idx]: # type: ignore[arg-type, index, union-attr]
108
+ info_list = cast(
109
+ list, # type: ignore[type-arg]
110
+ service_dict["changed_parameters_of_service"])[2][service_idx]
111
+ for detailed_info in info_list:
109
112
  if isinstance(detailed_info, str):
110
113
  print()
111
114
  print(detailed_info)
@@ -256,7 +259,8 @@ class Comparison(Display):
256
259
 
257
260
  return {"Property": property, "Old Value": old, "New Value": new}
258
261
 
259
- def compare_services(self, service1: DiagService, service2: DiagService) -> list:
262
+ def compare_services(self, service1: DiagService,
263
+ service2: DiagService) -> List[SpecsServiceDict]:
260
264
  # compares request, positive response and negative response parameters of two diagnostic services
261
265
 
262
266
  information: List[Union[str, Dict[str, Any]]] = [
@@ -382,9 +386,10 @@ class Comparison(Display):
382
386
  str(service2.negative_responses)]
383
387
  })
384
388
 
385
- return [information, changed_params]
389
+ return [information, changed_params] # type: ignore[list-item]
386
390
 
387
- def compare_diagnostic_layers(self, dl1: DiagLayer, dl2: DiagLayer) -> dict:
391
+ def compare_diagnostic_layers(self, dl1: DiagLayer,
392
+ dl2: DiagLayer) -> dict: # type: ignore[type-arg]
388
393
  # compares diagnostic services of two diagnostic layers with each other
389
394
  # save changes in dictionary (service_dict)
390
395
  # TODO: add comparison of SingleECUJobs
@@ -467,21 +472,23 @@ class Comparison(Display):
467
472
  # add parameters which have been changed (type: String)
468
473
  service_dict["changed_parameters_of_service"][
469
474
  1].append( # type: ignore[union-attr]
470
- detailed_information[1])
475
+ detailed_information[1]) # type: ignore[arg-type]
471
476
  # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...]
472
477
  service_dict["changed_parameters_of_service"][
473
478
  2].append( # type: ignore[union-attr]
474
- detailed_information[0])
479
+ detailed_information[0]) # type: ignore[arg-type]
475
480
 
476
481
  for service2_idx, service2 in enumerate(dl2.services):
477
482
 
478
483
  # check for deleted diagnostic services
479
484
  if service2.short_name not in dl1_service_names and dl2_request_prefixes[
480
- service2_idx] not in dl1_request_prefixes and service2 not in service_dict[
481
- "deleted_services"]:
485
+ service2_idx] not in dl1_request_prefixes:
482
486
 
483
- service_dict["deleted_services"].append( # type: ignore[union-attr]
484
- service2) # type: ignore[arg-type]
487
+ deleted_list = service_dict["deleted_services"]
488
+ assert isinstance(deleted_list, list)
489
+ if service2 not in deleted_list:
490
+ service_dict["deleted_services"].append( # type: ignore[union-attr]
491
+ service2) # type: ignore[arg-type]
485
492
 
486
493
  if service1.short_name == service2.short_name:
487
494
  # compare request, pos. response and neg. response parameters of both diagnostic services
@@ -496,13 +503,14 @@ class Comparison(Display):
496
503
  service1)
497
504
  # add parameters which have been changed (type: String)
498
505
  service_dict["changed_parameters_of_service"][ # type: ignore[union-attr]
499
- 1].append(detailed_information[1])
506
+ 1].append(detailed_information[1]) # type: ignore[arg-type]
500
507
  # add detailed information about changed service parameters (type: list) [infotext1, table1, infotext2, table2, ...]
501
508
  service_dict["changed_parameters_of_service"][ # type: ignore[union-attr]
502
- 2].append(detailed_information[0])
509
+ 2].append(detailed_information[0]) # type: ignore[arg-type]
503
510
  return service_dict
504
511
 
505
- def compare_databases(self, database_new: Database, database_old: Database) -> dict:
512
+ def compare_databases(self, database_new: Database,
513
+ database_old: Database) -> dict: # type: ignore[type-arg]
506
514
  # compares two PDX-files with each other
507
515
 
508
516
  new_variants: NewVariants = []
@@ -539,7 +547,7 @@ class Comparison(Display):
539
547
  return changes_variants
540
548
 
541
549
 
542
- def add_subparser(subparsers: "argparse._SubParsersAction") -> None:
550
+ def add_subparser(subparsers: SubparsersList) -> None:
543
551
  parser = subparsers.add_parser(
544
552
  "compare",
545
553
  description="\n".join([
odxtools/cli/decode.py CHANGED
@@ -9,6 +9,7 @@ from ..exceptions import odxraise
9
9
  from ..odxtypes import ParameterValue
10
10
  from ..singleecujob import SingleEcuJob
11
11
  from . import _parser_utils
12
+ from ._parser_utils import SubparsersList
12
13
 
13
14
  # name of the tool
14
15
  _odxtools_tool_name_ = "decode"
@@ -70,7 +71,7 @@ def print_summary(
70
71
  print(f" {param_name}={get_display_value(param_value)}")
71
72
 
72
73
 
73
- def add_subparser(subparsers: "argparse._SubParsersAction") -> None:
74
+ def add_subparser(subparsers: SubparsersList) -> None:
74
75
  parser = subparsers.add_parser(
75
76
  "decode",
76
77
  description="\n".join([
@@ -4,6 +4,8 @@ import sys
4
4
  import traceback
5
5
  from io import StringIO
6
6
 
7
+ from ._parser_utils import SubparsersList
8
+
7
9
 
8
10
  class DummyTool:
9
11
  """A tool which acts as a placeholder for a "real" tool that
@@ -20,7 +22,7 @@ class DummyTool:
20
22
  self._odxtools_tool_name_ = tool_name
21
23
  self._error = error
22
24
 
23
- def add_subparser(self, subparser_list: "argparse._SubParsersAction") -> None:
25
+ def add_subparser(self, subparser_list: SubparsersList) -> None:
24
26
  desc = StringIO()
25
27
 
26
28
  print(f"Tool '{self._odxtools_tool_name_}' is unavailable: {self._error}", file=desc)
odxtools/cli/find.py CHANGED
@@ -7,6 +7,7 @@ from ..diagservice import DiagService
7
7
  from ..odxtypes import ParameterValue
8
8
  from ..singleecujob import SingleEcuJob
9
9
  from . import _parser_utils
10
+ from ._parser_utils import SubparsersList
10
11
  from ._print_utils import print_diagnostic_service
11
12
 
12
13
  # name of the tool
@@ -66,7 +67,7 @@ def print_summary(odxdb: Database,
66
67
  print(f"Unknown service: {service}")
67
68
 
68
69
 
69
- def add_subparser(subparsers: "argparse._SubParsersAction") -> None:
70
+ def add_subparser(subparsers: SubparsersList) -> None:
70
71
  parser = subparsers.add_parser(
71
72
  "find",
72
73
  description="\n".join([
odxtools/cli/list.py CHANGED
@@ -10,6 +10,7 @@ from ..diaglayer import DiagLayer
10
10
  from ..diagservice import DiagService
11
11
  from ..singleecujob import SingleEcuJob
12
12
  from . import _parser_utils
13
+ from ._parser_utils import SubparsersList
13
14
  from ._print_utils import format_desc, print_diagnostic_service, print_dl_metrics
14
15
 
15
16
  # name of the tool
@@ -111,7 +112,7 @@ def print_summary(odxdb: Database,
111
112
  rich.print(f" {com_param.short_name}: {com_param.value}")
112
113
 
113
114
 
114
- def add_subparser(subparsers: "argparse._SubParsersAction") -> None:
115
+ def add_subparser(subparsers: SubparsersList) -> None:
115
116
  parser = subparsers.add_parser(
116
117
  "list",
117
118
  description="\n".join([
odxtools/cli/main.py CHANGED
@@ -4,6 +4,7 @@ import importlib
4
4
  from typing import Any, List
5
5
 
6
6
  import odxtools
7
+ import odxtools.exceptions
7
8
 
8
9
  from ..version import __version__ as odxtools_version
9
10
  from .dummy_sub_parser import DummyTool
odxtools/cli/snoop.py CHANGED
@@ -15,6 +15,7 @@ from odxtools.isotp_state_machine import IsoTpStateMachine
15
15
  from odxtools.response import Response, ResponseType
16
16
 
17
17
  from . import _parser_utils
18
+ from ._parser_utils import SubparsersList
18
19
 
19
20
  # name of the tool
20
21
  _odxtools_tool_name_ = "snoop"
@@ -30,6 +31,8 @@ def handle_telegram(telegram_id: int, payload: bytes) -> None:
30
31
  global odx_diag_layer
31
32
  global last_request
32
33
 
34
+ assert odx_diag_layer is not None
35
+
33
36
  if telegram_id == ecu_tx_id:
34
37
  if uds.is_response_pending(payload):
35
38
  print(f" ... (response pending)")
@@ -209,7 +212,7 @@ def add_cli_arguments(parser: argparse.ArgumentParser) -> None:
209
212
  _parser_utils.add_pdx_argument(parser)
210
213
 
211
214
 
212
- def add_subparser(subparsers: "argparse._SubParsersAction") -> None:
215
+ def add_subparser(subparsers: SubparsersList) -> None:
213
216
  parser = subparsers.add_parser(
214
217
  "snoop",
215
218
  description="Live decoding of a diagnostic session.",
@@ -7,7 +7,7 @@ from xml.etree import ElementTree
7
7
  from .basecomparam import BaseComparam
8
8
  from .comparam import Comparam
9
9
  from .complexcomparam import ComplexComparam, ComplexValue, create_complex_value_from_et
10
- from .exceptions import OdxWarning, odxassert, odxraise, odxrequire
10
+ from .exceptions import OdxWarning, odxraise, odxrequire
11
11
  from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef
12
12
  from .utils import create_description_from_et
13
13
 
@@ -75,7 +75,7 @@ class ComparamInstance:
75
75
  def spec(self) -> BaseComparam:
76
76
  return self._spec
77
77
 
78
- def get_value(self) -> Optional[str]:
78
+ def get_value(self) -> str:
79
79
  """Retrieve the value of a simple communication parameter
80
80
 
81
81
  This takes the default value of the comparam (if any) into
@@ -91,7 +91,8 @@ class ComparamInstance:
91
91
  else:
92
92
  result = self.spec.physical_default_value
93
93
 
94
- odxassert(isinstance(result, str))
94
+ if not isinstance(result, str):
95
+ odxraise()
95
96
 
96
97
  return result
97
98
 
@@ -119,6 +120,7 @@ class ComparamInstance:
119
120
  name_list = [cp.short_name for cp in comparam_spec.subparams]
120
121
  try:
121
122
  idx = name_list.index(subparam_name)
123
+ subparam = comparam_spec.subparams[idx]
122
124
  except ValueError:
123
125
  warnings.warn(
124
126
  f"Communication parameter '{self.short_name}' "
@@ -129,8 +131,8 @@ class ComparamInstance:
129
131
  return None
130
132
 
131
133
  result = value_list[idx]
132
- if result is None:
133
- result = comparam_spec.subparams[idx].physical_default_value
134
+ if result is None and isinstance(subparam, (Comparam, ComplexComparam)):
135
+ result = subparam.physical_default_value
134
136
  if not isinstance(result, str):
135
137
  odxraise()
136
138
 
@@ -1,5 +1,4 @@
1
1
  # SPDX-License-Identifier: MIT
2
- import abc
3
2
  from dataclasses import dataclass
4
3
  from typing import Literal
5
4
 
@@ -15,14 +14,13 @@ CompuMethodCategory = Literal[
15
14
 
16
15
 
17
16
  @dataclass
18
- class CompuMethod(abc.ABC):
17
+ class CompuMethod:
19
18
  internal_type: DataType
20
19
  physical_type: DataType
21
20
 
22
21
  @property
23
- @abc.abstractmethod
24
22
  def category(self) -> CompuMethodCategory:
25
- pass
23
+ raise NotImplementedError()
26
24
 
27
25
  def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType:
28
26
  raise NotImplementedError()
@@ -42,13 +42,22 @@ class CompuScale:
42
42
  compu_const: Optional[AtomicOdxType]
43
43
  compu_rational_coeffs: Optional[CompuRationalCoeffs]
44
44
 
45
+ # the following two attributes are not specified for COMPU-SCALE
46
+ # tags in the XML, but they are required to do anything useful
47
+ # with it.
48
+ internal_type: DataType
49
+ physical_type: DataType
50
+
45
51
  @staticmethod
46
- def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *,
47
- internal_type: DataType, physical_type: DataType) -> "CompuScale":
52
+ def compuscale_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *,
53
+ internal_type: DataType, physical_type: DataType) -> "CompuScale":
48
54
  short_label = et_element.findtext("SHORT-LABEL")
49
55
  description = create_description_from_et(et_element.find("DESC"))
50
- lower_limit = Limit.from_et(et_element.find("LOWER-LIMIT"), internal_type=internal_type)
51
- upper_limit = Limit.from_et(et_element.find("UPPER-LIMIT"), internal_type=internal_type)
56
+
57
+ lower_limit = Limit.limit_from_et(
58
+ et_element.find("LOWER-LIMIT"), doc_frags, value_type=internal_type)
59
+ upper_limit = Limit.limit_from_et(
60
+ et_element.find("UPPER-LIMIT"), doc_frags, value_type=internal_type)
52
61
 
53
62
  compu_inverse_value = internal_type.create_from_et(et_element.find("COMPU-INVERSE-VALUE"))
54
63
  compu_const = physical_type.create_from_et(et_element.find("COMPU-CONST"))
@@ -64,4 +73,35 @@ class CompuScale:
64
73
  upper_limit=upper_limit,
65
74
  compu_inverse_value=compu_inverse_value,
66
75
  compu_const=compu_const,
67
- compu_rational_coeffs=compu_rational_coeffs)
76
+ compu_rational_coeffs=compu_rational_coeffs,
77
+ internal_type=internal_type,
78
+ physical_type=physical_type)
79
+
80
+ def applies(self, internal_value: AtomicOdxType) -> bool:
81
+
82
+ if self.lower_limit is None and self.upper_limit is None:
83
+ # Everything is allowed: No limits have been specified
84
+ return True
85
+ elif self.upper_limit is None:
86
+ # no upper limit has been specified. the spec says that
87
+ # the value specified by the lower limit is the only one
88
+ # which is allowed (cf section 7.3.6.6.1)
89
+ assert self.lower_limit is not None
90
+
91
+ return internal_value == self.lower_limit.value
92
+ elif self.lower_limit is None:
93
+ # only the upper limit has been specified. the spec is
94
+ # ambiguous: it only says that if no upper limit is
95
+ # defined, the lower limit shall also be used as the upper
96
+ # limit and a closed interval type ought to be assumed,
97
+ # but it does not say what happens if the lower limit is
98
+ # not defined (which is allowed by the XSD). We thus
99
+ # assume that if only the upper limit is defined, is
100
+ # treated the same way as if only the lower limit is
101
+ # specified.
102
+ assert self.upper_limit is not None
103
+
104
+ return internal_value == self.upper_limit.value
105
+
106
+ return self.lower_limit.complies_to_lower(internal_value) and \
107
+ self.upper_limit.complies_to_upper(internal_value)