Repository: mercedes-benz/odxtools
Pull request #427: Some fixes
Author: login: andlaus
Created: 2025-05-19T08:36:02Z | Merged: 2025-05-20T10:46:39Z

This PR contains a few smallish fixes: The biggest one is a fix for https://github.com/mercedes-benz/odxtools/issues/424 , while the other two are the inversion of a spurious `if` condition in a relatively exotic part of the code (`SubComponentParamConnector`), and the improvement of the error message one gets if the result of a reference resolution does not exhibit the expected type.

Andreas Lauser &lt;andreas.lauser@mercedes-benz.com&gt;, on behalf of [MBition GmbH](https://mbitionio/).
[Provider Information](https://github.com/mercedes-benz/foss/blob/master/PROVIDER_INFORMATION.md) 



Diff:
diff --git a/odxtools/diaglayers/diaglayer.py b/odxtools/diaglayers/diaglayer.py
index c010ac43..6df31383 100644
--- a/odxtools/diaglayers/diaglayer.py
+++ b/odxtools/diaglayers/diaglayer.py
@@ -7,6 +7,7 @@
 from typing import Any, Union, cast
 from xml.etree import ElementTree
 
+from ..additionalaudience import AdditionalAudience
 from ..admindata import AdminData
 from ..companydata import CompanyData
 from ..description import Description
@@ -14,6 +15,7 @@
 from ..diagdatadictionaryspec import DiagDataDictionarySpec
 from ..diagservice import DiagService
 from ..exceptions import DecodeError, odxassert, odxraise
+from ..functionalclass import FunctionalClass
 from ..library import Library
 from ..message import Message
 from ..nameditemlist import NamedItemList, TNamed
@@ -26,6 +28,7 @@
 from ..singleecujob import SingleEcuJob
 from ..snrefcontext import SnRefContext
 from ..specialdatagroup import SpecialDataGroup
+from ..statechart import StateChart
 from ..subcomponent import SubComponent
 from ..unitgroup import UnitGroup
 from .diaglayerraw import DiagLayerRaw
@@ -223,12 +226,30 @@ def description(self) -> Description | None:
     def admin_data(self) -> AdminData | None:
         return self.diag_layer_raw.admin_data
 
+    @property
+    def company_datas(self) -> NamedItemList[CompanyData]:
+        return self.diag_layer_raw.company_datas
+
+    @property
+    def functional_classes(self) -> NamedItemList[FunctionalClass]:
+        return self.diag_layer_raw.functional_classes
+
+    @property
+    def diag_data_dictionary_spec(self) -> DiagDataDictionarySpec:
+        """The DiagDataDictionarySpec applicable to this DiagLayer"""
+        return self._diag_data_dictionary_spec
+
+    @property
+    def diag_comms_raw(self) -> list[OdxLinkRef | DiagComm]:
+        return self.diag_layer_raw.diag_comms_raw
+
     @property
     def diag_comms(self) -> NamedItemList[DiagComm]:
         return self.diag_layer_raw.diag_comms
 
     @property
     def services(self) -> NamedItemList[DiagService]:
+        """This is an alias for `.diag_services`"""
         return self.diag_layer_raw.services
 
     @property
@@ -239,10 +260,6 @@ def diag_services(self) -> NamedItemList[DiagService]:
     def single_ecu_jobs(self) -> NamedItemList[SingleEcuJob]:
         return self.diag_layer_raw.single_ecu_jobs
 
-    @property
-    def company_datas(self) -> NamedItemList[CompanyData]:
-        return self.diag_layer_raw.company_datas
-
     @property
     def requests(self) -> NamedItemList[Request]:
         return self.diag_layer_raw.requests
@@ -264,21 +281,24 @@ def import_refs(self) -> list[OdxLinkRef]:
         return self.diag_layer_raw.import_refs
 
     @property
-    def libraries(self) -> NamedItemList[Library]:
-        return self.diag_layer_raw.libraries
+    def state_charts(self) -> NamedItemList[StateChart]:
+        return self.diag_layer_raw.state_charts
+
+    @property
+    def additional_audiences(self) -> NamedItemList[AdditionalAudience]:
+        return self.diag_layer_raw.additional_audiences
 
     @property
     def sub_components(self) -> NamedItemList[SubComponent]:
         return self.diag_layer_raw.sub_components
 
     @property
-    def sdgs(self) -> list[SpecialDataGroup]:
-        return self.diag_layer_raw.sdgs
+    def libraries(self) -> NamedItemList[Library]:
+        return self.diag_layer_raw.libraries
 
     @property
-    def diag_data_dictionary_spec(self) -> DiagDataDictionarySpec:
-        """The DiagDataDictionarySpec applicable to this DiagLayer"""
-        return self._diag_data_dictionary_spec
+    def sdgs(self) -> list[SpecialDataGroup]:
+        return self.diag_layer_raw.sdgs
 
     #####
     # </properties forwarded to the "raw" diag layer>
diff --git a/odxtools/diaglayers/ecuvariant.py b/odxtools/diaglayers/ecuvariant.py
index d39e5683..134a58c8 100644
--- a/odxtools/diaglayers/ecuvariant.py
+++ b/odxtools/diaglayers/ecuvariant.py
@@ -33,6 +33,18 @@ def ecu_variant_raw(self) -> EcuVariantRaw:
     def diag_variables_raw(self) -> list[DiagVariable | OdxLinkRef]:
         return self.ecu_variant_raw.diag_variables_raw
 
+    @property
+    def diag_variables(self) -> NamedItemList[DiagVariable]:
+        return self._diag_variables
+
+    @property
+    def variable_groups(self) -> NamedItemList[VariableGroup]:
+        return self._variable_groups
+
+    @property
+    def ecu_variant_patterns(self) -> list[EcuVariantPattern]:
+        return self.ecu_variant_raw.ecu_variant_patterns
+
     @property
     def dyn_defined_spec(self) -> DynDefinedSpec | None:
         return self.ecu_variant_raw.dyn_defined_spec
@@ -56,25 +68,6 @@ def base_variant(self) -> BaseVariant | None:
 
         return None
 
-    @property
-    def ecu_variant_patterns(self) -> list[EcuVariantPattern]:
-        return self.ecu_variant_raw.ecu_variant_patterns
-
-    #######
-    # <properties subject to value inheritance>
-    #######
-    @property
-    def diag_variables(self) -> NamedItemList[DiagVariable]:
-        return self._diag_variables
-
-    @property
-    def variable_groups(self) -> NamedItemList[VariableGroup]:
-        return self._variable_groups
-
-    #######
-    # </properties subject to value inheritance>
-    #######
-
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "EcuVariant":
         ecu_variant_raw = EcuVariantRaw.from_et(et_element, context)
diff --git a/odxtools/diaglayers/hierarchyelement.py b/odxtools/diaglayers/hierarchyelement.py
index c439c464..56a05a9b 100644
--- a/odxtools/diaglayers/hierarchyelement.py
+++ b/odxtools/diaglayers/hierarchyelement.py
@@ -438,6 +438,11 @@ def not_inherited_fn(parent_ref: ParentRef) -> list[str]:
     #######
     # <properties subject to value inheritance>
     #######
+    @property
+    def functional_classes(self) -> NamedItemList[FunctionalClass]:
+        """All functional classes applicable to this DiagLayer"""
+        return self._functional_classes
+
     @property
     def diag_data_dictionary_spec(self) -> DiagDataDictionarySpec:
         return self._diag_data_dictionary_spec
@@ -480,11 +485,6 @@ def global_negative_responses(self) -> NamedItemList[Response]:
         """All global negative responses applicable to this DiagLayer"""
         return self._global_negative_responses
 
-    @property
-    def functional_classes(self) -> NamedItemList[FunctionalClass]:
-        """All functional classes applicable to this DiagLayer"""
-        return self._functional_classes
-
     @property
     def state_charts(self) -> NamedItemList[StateChart]:
         """All state charts applicable to this DiagLayer"""
diff --git a/odxtools/diaglayers/protocol.py b/odxtools/diaglayers/protocol.py
index 9cf03641..fa74c5ce 100644
--- a/odxtools/diaglayers/protocol.py
+++ b/odxtools/diaglayers/protocol.py
@@ -7,6 +7,8 @@
 from ..comparamspec import ComparamSpec
 from ..exceptions import odxassert
 from ..odxdoccontext import OdxDocContext
+from ..odxlink import OdxLinkRef
+from ..parentref import ParentRef
 from ..protstack import ProtStack
 from .hierarchyelement import HierarchyElement
 from .protocolraw import ProtocolRaw
@@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
+
     @property
     def prot_stack(self) -> ProtStack | None:
         return self.protocol_raw.prot_stack
 
+    @property
+    def parent_refs(self) -> list[ParentRef]:
+        return self.protocol_raw.parent_refs
+
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "Protocol":
         protocol_raw = ProtocolRaw.from_et(et_element, context)
diff --git a/odxtools/odxlink.py b/odxtools/odxlink.py
index c9107f5a..6c58b0ed 100644
--- a/odxtools/odxlink.py
+++ b/odxtools/odxlink.py
@@ -204,8 +204,10 @@ def resolve(self, ref: OdxLinkRef, expected_type: Any | None = None) -> Any:
             # locate an object exhibiting with the referenced local ID
             # in the ID database for the document fragment
             if (obj := doc_frag_db.get(ref.ref_id)) is not None:
-                if expected_type is not None:
-                    odxassert(isinstance(obj, expected_type))
+                if expected_type is not None and not isinstance(obj, expected_type):
+                    odxraise(f"Referenced object for link {ref.ref_id} is of type "
+                             f"{type(obj).__name__} which is not a subclass of expected "
+                             f"type {expected_type.__name__}")
 
                 return obj
 
diff --git a/odxtools/subcomponentparamconnector.py b/odxtools/subcomponentparamconnector.py
index 783045ed..f3025d7f 100644
--- a/odxtools/subcomponentparamconnector.py
+++ b/odxtools/subcomponentparamconnector.py
@@ -77,7 +77,7 @@ def _resolve_snrefs(self, context: SnRefContext) -> None:
                                 odxrequire(context.diag_layer).diag_comms, DiagService)
         self._service = service
 
-        if self._service.request is not None:
+        if self._service.request is None:
             odxraise()
             return
         if not self._service.positive_responses:


Reviews:
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T11:31:03Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T11:31:07Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T11:31:36Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T13:13:06Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T13:15:40Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T13:41:41Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T13:42:50Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T21:55:47Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T21:58:28Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T04:18:34Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T09:12:44Z

Review comments:
diff_hunk: @@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
+
     @property
     def prot_stack(self) -> ProtStack | None:
         return self.protocol_raw.prot_stack
 
+    @property
+    def parent_refs(self) -> list[ParentRef]:
path: odxtools/diaglayers/protocol.py
user: login: kayoub5
body: Really needed after the database links has been resolved?
created_at: 2025-05-19T11:31:03Z
author_association: COLLABORATOR
diff_hunk: @@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
path: odxtools/diaglayers/protocol.py
user: login: kayoub5
body: Really needed after the database links has been resolved?
created_at: 2025-05-19T11:31:07Z
author_association: COLLABORATOR
diff_hunk: @@ -204,8 +204,10 @@ def resolve(self, ref: OdxLinkRef, expected_type: Any | None = None) -> Any:
             # locate an object exhibiting with the referenced local ID
             # in the ID database for the document fragment
             if (obj := doc_frag_db.get(ref.ref_id)) is not None:
-                if expected_type is not None:
-                    odxassert(isinstance(obj, expected_type))
+                if expected_type is not None and not isinstance(obj, expected_type):
+                    odxraise(f"Referenced object for link {ref.ref_id} is of type "
+                             f"{type(obj).__name__} which is not a subclass of expected "
+                             f"type {expected_type.__name__}")
path: odxtools/odxlink.py
user: login: kayoub5
body: This happened?
created_at: 2025-05-19T11:31:36Z
author_association: COLLABORATOR
diff_hunk: @@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
+
     @property
     def prot_stack(self) -> ProtStack | None:
         return self.protocol_raw.prot_stack
 
+    @property
+    def parent_refs(self) -> list[ParentRef]:
path: odxtools/diaglayers/protocol.py
user: login: andlaus
body: it is not needed for the value inheritance mechanism, because that queries the [raw diag layer](https://github.com/mercedes-benz/odxtools/blob/main/odxtools/diaglayers/hierarchyelement.py#L263-L267). That said, since the non-raw diagnostic layers are supposed to provide the "logical view" on diag layers, they should IMO provide a superset of all properties which are available in the raw layers...
created_at: 2025-05-19T13:13:06Z
author_association: MEMBER
diff_hunk: @@ -204,8 +204,10 @@ def resolve(self, ref: OdxLinkRef, expected_type: Any | None = None) -> Any:
             # locate an object exhibiting with the referenced local ID
             # in the ID database for the document fragment
             if (obj := doc_frag_db.get(ref.ref_id)) is not None:
-                if expected_type is not None:
-                    odxassert(isinstance(obj, expected_type))
+                if expected_type is not None and not isinstance(obj, expected_type):
+                    odxraise(f"Referenced object for link {ref.ref_id} is of type "
+                             f"{type(obj).__name__} which is not a subclass of expected "
+                             f"type {expected_type.__name__}")
path: odxtools/odxlink.py
user: login: andlaus
body: yes, during my recent enabling of the ECU-CONFIG category (`.odx-e`, PR incomming after this one). I figured it was a good idea to make my own life and the life of anybody who stumbles over this in the future a bit easier...
created_at: 2025-05-19T13:15:40Z
author_association: MEMBER
diff_hunk: @@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
+
     @property
     def prot_stack(self) -> ProtStack | None:
         return self.protocol_raw.prot_stack
 
+    @property
+    def parent_refs(self) -> list[ParentRef]:
path: odxtools/diaglayers/protocol.py
user: login: kayoub5
body: the idea is that the "logical view", should provide the resolved objects, not just their refs 
created_at: 2025-05-19T13:41:40Z
author_association: COLLABORATOR
diff_hunk: @@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
path: odxtools/diaglayers/protocol.py
user: login: kayoub5
body: logic: provide the user with the stack itself, not just its ref
created_at: 2025-05-19T13:42:50Z
author_association: COLLABORATOR
diff_hunk: @@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
+
     @property
     def prot_stack(self) -> ProtStack | None:
         return self.protocol_raw.prot_stack
 
+    @property
+    def parent_refs(self) -> list[ParentRef]:
path: odxtools/diaglayers/protocol.py
user: login: andlaus
body: the "logical view" is supposed to expose *all* properties in their "final" form, i.e., if something needs to be computed -- like value inheritance -- non-raw diaglayers should expose the processed sets of objects, if there's no need for processing -- e.g., when specifying the parent layers (`parent_refs`) -- it should just forward these properties from the respective raw layers. (At least that was my philosophy when writing this code, I probably made a few mistakes besides the ones fixed by this PR...)

> not just their refs

be aware that the parent refs are a bit quirky here: they do reference resolution internally, i.e. they provide access to the respective "full" parent layer object via their `.layer` attribute. This is due to the fact that parent refs define quite a few additional properties (e.g. the NOT-INHERITED-FOO lists) which are required in conjunction with the resolved object.
created_at: 2025-05-19T21:55:47Z
author_association: MEMBER
diff_hunk: @@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
path: odxtools/diaglayers/protocol.py
user: login: andlaus
body: why not have both? That way, raw and "full" diag layers can be used interchangeably (for code which does not use the `.diag_layer_raw` attribute)...
created_at: 2025-05-19T21:58:28Z
author_association: MEMBER
diff_hunk: @@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
path: odxtools/diaglayers/protocol.py
user: login: kayoub5
body: because it's already available as `prot_stack.odx_id`
created_at: 2025-05-20T04:18:34Z
author_association: COLLABORATOR
diff_hunk: @@ -24,14 +26,26 @@ class Protocol(HierarchyElement):
     def protocol_raw(self) -> ProtocolRaw:
         return cast(ProtocolRaw, self.diag_layer_raw)
 
+    @property
+    def comparam_spec_ref(self) -> OdxLinkRef:
+        return self.protocol_raw.comparam_spec_ref
+
     @property
     def comparam_spec(self) -> ComparamSpec:
         return self.protocol_raw.comparam_spec
 
+    @property
+    def prot_stack_snref(self) -> str | None:
+        return self.protocol_raw.prot_stack_snref
path: odxtools/diaglayers/protocol.py
user: login: andlaus
body: the ODX id is not exactly equivalent to a reference, e.g., the reference may feature a different set of document fragments. Also, all other classes which do reference resolution still provide the references in addition to the resolved objects..
created_at: 2025-05-20T09:12:44Z
author_association: MEMBER

PR comments:
user: login: kayoub5
created_at: 2025-05-19T11:33:31Z
body: @andlaus you may want to cherry pick 7bbe195dd550bf4073e28ebe78228926181cdea6 into its own PR for faster fix to users
author_association: COLLABORATOR
user: login: andlaus
created_at: 2025-05-19T13:18:06Z
body: > @andlaus you may want to cherry pick 7bbe195dd550bf4073e28ebe78228926181cdea6 into its own PR for faster fix to users

I don't think it is highly critical (only medium). do you see a longer holdup of the present PR? (I'll move 7bbe195 to a separate PR if the delay is longer than this Friday...
author_association: MEMBER