Repository: mercedes-benz/odxtools
Pull request #410: Convert OdxLinkId.doc_fragments and OdxLinkRef.ref_docs to tuple
Author: login: zariiii9003
Created: 2025-04-04T06:40:33Z | Merged: 2025-04-17T07:57:34Z

As suggested in #405.

I tried converting OdxLinkRef into a frozen dataclass, but subclasses assign attributes after instantiation.


Diff:
diff --git a/examples/somersaultecu.py b/examples/somersaultecu.py
index da68f733..e1550c4d 100755
--- a/examples/somersaultecu.py
+++ b/examples/somersaultecu.py
@@ -102,12 +102,12 @@ class SomersaultSID(IntEnum):
 dlc_short_name = "somersault"
 
 # document fragment for everything except the communication parameters
-doc_frags = [OdxDocFragment(dlc_short_name, DocType.CONTAINER)]
+doc_frags = (OdxDocFragment(dlc_short_name, DocType.CONTAINER),)
 
 # document fragments for communication parameters
-cp_dwcan_doc_frags = [OdxDocFragment("ISO_11898_2_DWCAN", DocType.COMPARAM_SUBSET)]
-cp_iso15765_2_doc_frags = [OdxDocFragment("ISO_15765_2", DocType.COMPARAM_SUBSET)]
-cp_iso15765_3_doc_frags = [OdxDocFragment("ISO_15765_3", DocType.COMPARAM_SUBSET)]
+cp_dwcan_doc_frags = (OdxDocFragment("ISO_11898_2_DWCAN", DocType.COMPARAM_SUBSET),)
+cp_iso15765_2_doc_frags = (OdxDocFragment("ISO_15765_2", DocType.COMPARAM_SUBSET),)
+cp_iso15765_3_doc_frags = (OdxDocFragment("ISO_15765_3", DocType.COMPARAM_SUBSET),)
 
 ##################
 # Base variant of Somersault ECU
@@ -1430,9 +1430,8 @@ class SomersaultSID(IntEnum):
     long_name="Somersault protocol info",
     description=Description.from_string(
         "<p>Protocol information of the somersault ECUs &amp; cetera</p>"),
-    comparam_spec_ref=OdxLinkRef("CPS_ISO_15765_3_on_ISO_15765_2", [
-        OdxDocFragment("ISO_15765_3_on_ISO_15765_2", DocType.COMPARAM_SPEC)
-    ]),
+    comparam_spec_ref=OdxLinkRef("CPS_ISO_15765_3_on_ISO_15765_2", (OdxDocFragment(
+        "ISO_15765_3_on_ISO_15765_2", DocType.COMPARAM_SPEC),)),
     comparam_refs=somersault_comparam_refs,
 )
 somersault_protocol = Protocol(diag_layer_raw=somersault_protocol_raw)
@@ -1764,14 +1763,19 @@ class SomersaultSID(IntEnum):
     odx_cs_root = ElementTree.parse(odx_cs_dir / odx_cs_filename).getroot()
     subset = odx_cs_root.find("COMPARAM-SUBSET")
     if subset is not None:
-        comparam_subsets.append(ComparamSubset.from_et(subset, OdxDocContext(ODX_VERSION, [])))
+        category_sn = odxrequire(subset.findtext("SHORT-NAME"))
+        context = OdxDocContext(ODX_VERSION,
+                                (OdxDocFragment(category_sn, DocType.COMPARAM_SUBSET),))
+        comparam_subsets.append(ComparamSubset.from_et(subset, context))
 
 comparam_specs = []
 for odx_c_filename in ("UDSOnCAN_CPS.odx-c",):
     odx_c_root = ElementTree.parse(odx_cs_dir / odx_c_filename).getroot()
     subset = odx_c_root.find("COMPARAM-SPEC")
     if subset is not None:
-        comparam_specs.append(ComparamSpec.from_et(subset, OdxDocContext(ODX_VERSION, [])))
+        category_sn = odxrequire(subset.findtext("SHORT-NAME"))
+        context = OdxDocContext(ODX_VERSION, (OdxDocFragment(category_sn, DocType.COMPARAM_SPEC),))
+        comparam_specs.append(ComparamSpec.from_et(subset, context))
 
 # create a database object
 database = Database()
diff --git a/odxtools/comparamspec.py b/odxtools/comparamspec.py
index fea8435a..0cf06697 100644
--- a/odxtools/comparamspec.py
+++ b/odxtools/comparamspec.py
@@ -6,7 +6,7 @@
 from .nameditemlist import NamedItemList
 from .odxcategory import OdxCategory
 from .odxdoccontext import OdxDocContext
-from .odxlink import DocType, OdxLinkDatabase, OdxLinkId
+from .odxlink import OdxLinkDatabase, OdxLinkId
 from .protstack import ProtStack
 from .snrefcontext import SnRefContext
 from .utils import dataclass_fields_asdict
@@ -23,7 +23,7 @@ class ComparamSpec(OdxCategory):
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "ComparamSpec":
 
-        base_obj = OdxCategory.category_from_et(et_element, context, doc_type=DocType.COMPARAM_SPEC)
+        base_obj = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(base_obj)
 
         prot_stacks = NamedItemList([
diff --git a/odxtools/comparamsubset.py b/odxtools/comparamsubset.py
index 82581452..25acfb79 100644
--- a/odxtools/comparamsubset.py
+++ b/odxtools/comparamsubset.py
@@ -9,7 +9,7 @@
 from .nameditemlist import NamedItemList
 from .odxcategory import OdxCategory
 from .odxdoccontext import OdxDocContext
-from .odxlink import DocType, OdxLinkDatabase, OdxLinkId
+from .odxlink import OdxLinkDatabase, OdxLinkId
 from .snrefcontext import SnRefContext
 from .unitspec import UnitSpec
 from .utils import dataclass_fields_asdict
@@ -31,14 +31,7 @@ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "Compara
 
         category_attrib = et_element.attrib.get("CATEGORY")
 
-        # In ODX 2.0, COMPARAM-SPEC is used, whereas in ODX 2.2, it
-        # refers to something else and has been replaced by
-        # COMPARAM-SUBSET.
-        # - If the 'CATEGORY' attribute is missing (ODX 2.0), use
-        #   COMPARAM_SPEC,
-        # - else (ODX 2.2), use COMPARAM_SUBSET.
-        doc_type = DocType.COMPARAM_SUBSET if category_attrib is not None else DocType.COMPARAM_SPEC
-        base_obj = OdxCategory.category_from_et(et_element, context, doc_type=doc_type)
+        base_obj = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(base_obj)
 
         comparams = NamedItemList(
diff --git a/odxtools/database.py b/odxtools/database.py
index 7e2407af..6c578a0e 100644
--- a/odxtools/database.py
+++ b/odxtools/database.py
@@ -21,7 +21,7 @@
 from .exceptions import odxraise, odxrequire
 from .nameditemlist import NamedItemList
 from .odxdoccontext import OdxDocContext
-from .odxlink import OdxLinkDatabase, OdxLinkId
+from .odxlink import DocType, OdxDocFragment, OdxLinkDatabase, OdxLinkId
 from .snrefcontext import SnRefContext
 
 
@@ -77,10 +77,6 @@ def add_auxiliary_file(self,
         self.auxiliary_files[str(aux_file_name)] = aux_file_obj
 
     def _process_xml_tree(self, root: ElementTree.Element) -> None:
-        dlcs: list[DiagLayerContainer] = []
-        comparam_subsets: list[ComparamSubset] = []
-        comparam_specs: list[ComparamSpec] = []
-
         # ODX spec version
         model_version = Version(root.attrib.get("MODEL-VERSION", "2.0"))
         if self.model_version is not None and self.model_version != model_version:
@@ -89,31 +85,33 @@ def _process_xml_tree(self, root: ElementTree.Element) -> None:
 
         self.model_version = model_version
 
-        dlc = root.find("DIAG-LAYER-CONTAINER")
-        if dlc is not None:
-            dlcs.append(DiagLayerContainer.from_et(dlc, OdxDocContext(model_version, [])))
-
-        # In ODX 2.0 there was only COMPARAM-SPEC. In ODX 2.2 the
-        # content of COMPARAM-SPEC was moved to COMPARAM-SUBSET
-        # and COMPARAM-SPEC became a container for PROT-STACKS and
-        # a PROT-STACK references a list of COMPARAM-SUBSET
-        cp_subset = root.find("COMPARAM-SUBSET")
-        if cp_subset is not None:
-            comparam_subsets.append(
-                ComparamSubset.from_et(cp_subset, OdxDocContext(model_version, [])))
-
-        cp_spec = root.find("COMPARAM-SPEC")
-        if cp_spec is not None:
+        child_elements = list(root)
+        if len(child_elements) != 1:
+            odxraise("Each ODX document must contain exactly one category.")
+
+        category_et = child_elements[0]
+        category_sn = odxrequire(category_et.findtext("SHORT-NAME"))
+        category_tag = category_et.tag
+
+        if category_tag == "DIAG-LAYER-CONTAINER":
+            context = OdxDocContext(model_version,
+                                    (OdxDocFragment(category_sn, DocType.CONTAINER),))
+            self._diag_layer_containers.append(DiagLayerContainer.from_et(category_et, context))
+        elif category_tag == "COMPARAM-SUBSET":
+            context = OdxDocContext(model_version,
+                                    (OdxDocFragment(category_sn, DocType.COMPARAM_SUBSET),))
+            self._comparam_subsets.append(ComparamSubset.from_et(category_et, context))
+        elif category_tag == "COMPARAM-SPEC":
+            # In ODX 2.0 there was only COMPARAM-SPEC. In ODX 2.2 the
+            # content of COMPARAM-SPEC was moved to COMPARAM-SUBSET
+            # and COMPARAM-SPEC became a container for PROT-STACKS and
+            # a PROT-STACK references a list of COMPARAM-SUBSET
+            context = OdxDocContext(model_version,
+                                    (OdxDocFragment(category_sn, DocType.COMPARAM_SPEC),))
             if model_version < Version("2.2"):
-                comparam_subsets.append(
-                    ComparamSubset.from_et(cp_spec, OdxDocContext(model_version, [])))
-            else:  # odx >= 2.2
-                comparam_specs.append(
-                    ComparamSpec.from_et(cp_spec, OdxDocContext(model_version, [])))
-
-        self._diag_layer_containers.extend(dlcs)
-        self._comparam_subsets.extend(comparam_subsets)
-        self._comparam_specs.extend(comparam_specs)
+                self._comparam_subsets.append(ComparamSubset.from_et(category_et, context))
+            else:
+                self._comparam_specs.append(ComparamSpec.from_et(category_et, context))
 
     def refresh(self) -> None:
         # Create wrapper objects
diff --git a/odxtools/diaglayercontainer.py b/odxtools/diaglayercontainer.py
index 259fb860..32bf6927 100644
--- a/odxtools/diaglayercontainer.py
+++ b/odxtools/diaglayercontainer.py
@@ -10,10 +10,11 @@
 from .diaglayers.ecuvariant import EcuVariant
 from .diaglayers.functionalgroup import FunctionalGroup
 from .diaglayers.protocol import Protocol
+from .exceptions import odxrequire
 from .nameditemlist import NamedItemList
 from .odxcategory import OdxCategory
 from .odxdoccontext import OdxDocContext
-from .odxlink import DocType, OdxLinkDatabase, OdxLinkId
+from .odxlink import DocType, OdxDocFragment, OdxLinkDatabase, OdxLinkId
 from .snrefcontext import SnRefContext
 from .utils import dataclass_fields_asdict
 
@@ -43,29 +44,30 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
-        ecu_shared_datas = NamedItemList([
-            EcuSharedData.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA")
-        ])
-        base_variants = NamedItemList([
-            BaseVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("BASE-VARIANTS/BASE-VARIANT")
-        ])
-        ecu_variants = NamedItemList([
-            EcuVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT")
-        ])
+        def get_layer_context(diag_layer_et: ElementTree.Element) -> OdxDocContext:
+            layer_sn = odxrequire(diag_layer_et.findtext("SHORT-NAME"))
+            layer_docfrag = OdxDocFragment(layer_sn, DocType.LAYER)
+            # add layer doc fragment to container doc fragment
+            return OdxDocContext(context.version, (context.doc_fragments[0], layer_docfrag))
+
+        protocols = NamedItemList(
+            Protocol.from_et(layer_et, get_layer_context(layer_et))
+            for layer_et in et_element.iterfind("PROTOCOLS/PROTOCOL"))
+        functional_groups = NamedItemList(
+            FunctionalGroup.from_et(layer_et, get_layer_context(layer_et))
+            for layer_et in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP"))
+        ecu_shared_datas = NamedItemList(
+            EcuSharedData.from_et(layer_et, get_layer_context(layer_et))
+            for layer_et in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA"))
+        base_variants = NamedItemList(
+            BaseVariant.from_et(layer_et, get_layer_context(layer_et))
+            for layer_et in et_element.iterfind("BASE-VARIANTS/BASE-VARIANT"))
+        ecu_variants = NamedItemList(
+            EcuVariant.from_et(layer_et, get_layer_context(layer_et))
+            for layer_et in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT"))
 
         return DiagLayerContainer(
             protocols=protocols,
diff --git a/odxtools/diaglayers/diaglayerraw.py b/odxtools/diaglayers/diaglayerraw.py
index 99318173..f9a41739 100644
--- a/odxtools/diaglayers/diaglayerraw.py
+++ b/odxtools/diaglayers/diaglayerraw.py
@@ -10,12 +10,12 @@
 from ..diagdatadictionaryspec import DiagDataDictionarySpec
 from ..diagservice import DiagService
 from ..element import IdentifiableElement
-from ..exceptions import odxassert, odxraise, odxrequire
+from ..exceptions import odxassert, odxraise
 from ..functionalclass import FunctionalClass
 from ..library import Library
 from ..nameditemlist import NamedItemList
 from ..odxdoccontext import OdxDocContext
-from ..odxlink import DocType, OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef
+from ..odxlink import OdxLinkDatabase, OdxLinkId, OdxLinkRef
 from ..request import Request
 from ..response import Response
 from ..singleecujob import SingleEcuJob
@@ -77,10 +77,6 @@ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLay
             variant_type = cast(DiagLayerType, None)
             odxraise(f"Encountered unknown diagnostic layer type '{et_element.tag}'")
 
-        short_name = odxrequire(et_element.findtext("SHORT-NAME"))
-
-        # extend the applicable ODX "document fragments" for the diag layer objects
-        context.doc_fragments.append(OdxDocFragment(short_name, DocType.LAYER))
         kwargs = dataclass_fields_asdict(IdentifiableElement.from_et(et_element, context))
 
         admin_data = None
diff --git a/odxtools/diaglayers/hierarchyelement.py b/odxtools/diaglayers/hierarchyelement.py
index b18ddd92..c439c464 100644
--- a/odxtools/diaglayers/hierarchyelement.py
+++ b/odxtools/diaglayers/hierarchyelement.py
@@ -31,7 +31,7 @@
 from .hierarchyelementraw import HierarchyElementRaw
 
 if TYPE_CHECKING:
-    from .database import Database
+    from ..database import Database
     from .protocol import Protocol
 
 TNamed = TypeVar("TNamed", bound=OdxNamed)
diff --git a/odxtools/odxcategory.py b/odxtools/odxcategory.py
index c99eff49..d4d296da 100644
--- a/odxtools/odxcategory.py
+++ b/odxtools/odxcategory.py
@@ -6,10 +6,9 @@
 from .admindata import AdminData
 from .companydata import CompanyData
 from .element import IdentifiableElement
-from .exceptions import odxrequire
 from .nameditemlist import NamedItemList
 from .odxdoccontext import OdxDocContext
-from .odxlink import DocType, OdxDocFragment, OdxLinkDatabase, OdxLinkId
+from .odxlink import OdxLinkDatabase, OdxLinkId
 from .snrefcontext import SnRefContext
 from .specialdatagroup import SpecialDataGroup
 from .utils import dataclass_fields_asdict
@@ -28,17 +27,7 @@ class OdxCategory(IdentifiableElement):
 
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "OdxCategory":
-        raise Exception("Calling `._from_et()` is not allowed for OdxCategory. "
-                        "Use `OdxCategory.category_from_et()`!")
 
-    @staticmethod
-    def category_from_et(et_element: ElementTree.Element, context: OdxDocContext, *,
-                         doc_type: DocType) -> "OdxCategory":
-
-        short_name = odxrequire(et_element.findtext("SHORT-NAME"))
-        # create the current ODX "document fragment" (description of the
-        # current document for references and IDs)
-        context.doc_fragments.append(OdxDocFragment(short_name, doc_type))
         kwargs = dataclass_fields_asdict(IdentifiableElement.from_et(et_element, context))
 
         admin_data = AdminData.from_et(et_element.find("ADMIN-DATA"), context)
diff --git a/odxtools/odxdoccontext.py b/odxtools/odxdoccontext.py
index dd622d4a..42523788 100644
--- a/odxtools/odxdoccontext.py
+++ b/odxtools/odxdoccontext.py
@@ -1,4 +1,5 @@
-from typing import TYPE_CHECKING, NamedTuple
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
 
 from packaging.version import Version
 
@@ -6,6 +7,10 @@
     from odxtools.odxlink import OdxDocFragment
 
 
-class OdxDocContext(NamedTuple):
+@dataclass(slots=True, frozen=True)
+class OdxDocContext:
     version: Version
-    doc_fragments: list["OdxDocFragment"]
+
+    # the doc_fragments are either tuple(doc_frag(category),)
+    # or tuple(doc_frag(category), doc_frag(diag_layer))
+    doc_fragments: tuple["OdxDocFragment"] | tuple["OdxDocFragment", "OdxDocFragment"]
diff --git a/odxtools/odxlink.py b/odxtools/odxlink.py
index bfa32922..c9107f5a 100644
--- a/odxtools/odxlink.py
+++ b/odxtools/odxlink.py
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: MIT
 import warnings
 from collections.abc import Iterable
-from dataclasses import dataclass, field
+from dataclasses import dataclass
 from enum import Enum
 from typing import Any, Optional, TypeVar, overload
 from xml.etree import ElementTree
@@ -46,7 +46,7 @@ class OdxLinkId:
 
     #: The name and type of the document fragment to which the
     #: `local_id` is relative to
-    doc_fragments: list[OdxDocFragment] = field(default_factory=list)
+    doc_fragments: tuple[OdxDocFragment] | tuple[OdxDocFragment, OdxDocFragment]
 
     def __hash__(self) -> int:
         # we do not hash about the document fragment here, because
@@ -95,7 +95,7 @@ class OdxLinkRef:
     ref_id: str
 
     #: The document fragments to which the `ref_id` refers to (in reverse order)
-    ref_docs: list[OdxDocFragment] = field(default_factory=list)
+    ref_docs: tuple[OdxDocFragment, ...]
 
     # TODO: this is difficult because OdxLinkRef is derived from and
     # we do not want having to specify it mandatorily
@@ -143,10 +143,10 @@ def from_et(et: ElementTree.Element | None, context: OdxDocContext) -> Optional[
         # if the target document fragment is specified by the
         # reference, use it, else use the document fragment containing
         # the reference.
-        if docref is not None:
-            doc_frags = [OdxDocFragment(docref, odxrequire(doctype))]
-        else:
+        if docref is None:
             doc_frags = context.doc_fragments
+        else:
+            doc_frags = (OdxDocFragment(docref, odxrequire(doctype)),)
 
         return OdxLinkRef(id_ref, doc_frags)
 
diff --git a/odxtools/templates/macros/printParentRef.xml.jinja2 b/odxtools/templates/macros/printParentRef.xml.jinja2
index d565cef4..fa27daa3 100644
--- a/odxtools/templates/macros/printParentRef.xml.jinja2
+++ b/odxtools/templates/macros/printParentRef.xml.jinja2
@@ -5,8 +5,10 @@
 
 {%- macro printParentRef(par) -%}
 <PARENT-REF ID-REF="{{par.layer.odx_id.local_id}}"
-            DOCREF="{{get_parent_container_name(par.layer.short_name)}}"
+{%- if par.layer_ref.ref_docs|length == 1 %}
+            DOCREF="{{par.layer_ref.ref_docs[0].doc_name}}"
             DOCTYPE="CONTAINER"
+{%- endif %}
             xsi:type="{{par.layer.variant_type.value}}-REF">
 {%- if par.not_inherited_diag_comms %}
  <NOT-INHERITED-DIAG-COMMS>
diff --git a/odxtools/writepdxfile.py b/odxtools/writepdxfile.py
index 82676f3e..7ca9b0d3 100644
--- a/odxtools/writepdxfile.py
+++ b/odxtools/writepdxfile.py
@@ -21,24 +21,6 @@ def jinja2_odxraise_helper(msg: str) -> None:
     raise Exception(msg)
 
 
-def get_parent_container_name(dl_short_name: str) -> str:
-    """
-    Given the short name of a diagnostic layer, return the name of a container
-    which the layer is part of.
-
-    If no such container exists, a `RuntimeException` is thrown.
-    """
-
-    assert odxdatabase is not None
-
-    for dlc in odxdatabase.diag_layer_containers:
-        if dl_short_name in [dl.short_name for dl in dlc.diag_layers]:
-            return dlc.short_name
-
-    raise RuntimeError(f"get_parent_container_name() could not determine a "
-                       f"container for diagnostic layer '{dl_short_name}'.")
-
-
 def make_xml_attrib(attrib_name: str, attrib_val: Any | None) -> str:
     if attrib_val is None:
         return ""
@@ -145,7 +127,6 @@ def write_pdx_file(
         jinja_env.globals["odxraise"] = jinja2_odxraise_helper
         jinja_env.globals["make_xml_attrib"] = make_xml_attrib
         jinja_env.globals["make_bool_xml_attrib"] = make_bool_xml_attrib
-        jinja_env.globals["get_parent_container_name"] = get_parent_container_name
 
         vars: dict[str, Any] = {}
         vars["odxtools_version"] = odxtools.__version__
diff --git a/tests/test_compu_methods.py b/tests/test_compu_methods.py
index 2921d868..57b2bb08 100644
--- a/tests/test_compu_methods.py
+++ b/tests/test_compu_methods.py
@@ -27,10 +27,9 @@
 from odxtools.odxlink import DocType, OdxDocFragment
 from odxtools.odxtypes import DataType
 from odxtools.progcode import ProgCode
-from odxtools.writepdxfile import (get_parent_container_name, jinja2_odxraise_helper,
-                                   make_bool_xml_attrib, make_xml_attrib)
+from odxtools.writepdxfile import jinja2_odxraise_helper, make_bool_xml_attrib, make_xml_attrib
 
-doc_frags = [OdxDocFragment("UnitTest", DocType.CONTAINER)]
+doc_frags = (OdxDocFragment("UnitTest", DocType.CONTAINER),)
 
 
 class TestLinearCompuMethod(unittest.TestCase):
@@ -50,7 +49,6 @@ def _get_jinja_environment() -> jinja2.environment.Environment:
             jinja_env.globals["odxraise"] = jinja2_odxraise_helper
             jinja_env.globals["make_xml_attrib"] = make_xml_attrib
             jinja_env.globals["make_bool_xml_attrib"] = make_bool_xml_attrib
-            jinja_env.globals["get_parent_container_name"] = get_parent_container_name
 
             return jinja_env
 
diff --git a/tests/test_decoding.py b/tests/test_decoding.py
index 12542e26..78892921 100644
--- a/tests/test_decoding.py
+++ b/tests/test_decoding.py
@@ -46,7 +46,7 @@
 from odxtools.termination import Termination
 from odxtools.text import Text
 
-doc_frags = [OdxDocFragment("UnitTest", DocType.CONTAINER)]
+doc_frags = (OdxDocFragment("UnitTest", DocType.CONTAINER),)
 
 
 class TestIdentifyingService(unittest.TestCase):
diff --git a/tests/test_diag_coded_types.py b/tests/test_diag_coded_types.py
index 9dcc75d0..dece1a42 100644
--- a/tests/test_diag_coded_types.py
+++ b/tests/test_diag_coded_types.py
@@ -38,7 +38,7 @@
 from odxtools.standardlengthtype import StandardLengthType
 from odxtools.termination import Termination
 
-doc_frags = [OdxDocFragment("UnitTest", DocType.CONTAINER)]
+doc_frags = (OdxDocFragment("UnitTest", DocType.CONTAINER),)
 
 
 class TestLeadingLengthInfoType(unittest.TestCase):
diff --git a/tests/test_diag_data_dictionary_spec.py b/tests/test_diag_data_dictionary_spec.py
index 51f64982..720021f3 100644
--- a/tests/test_diag_data_dictionary_spec.py
+++ b/tests/test_diag_data_dictionary_spec.py
@@ -31,7 +31,7 @@
 from odxtools.text import Text
 
 # the document fragment which is used throughout the test
-doc_frags = [OdxDocFragment("DiagDataDictionarySpecTest", DocType.CONTAINER)]
+doc_frags = (OdxDocFragment("DiagDataDictionarySpecTest", DocType.CONTAINER),)
 
 
 class TestDiagDataDictionarySpec(unittest.TestCase):
diff --git a/tests/test_encoding.py b/tests/test_encoding.py
index 8a1b5b57..50ee303a 100644
--- a/tests/test_encoding.py
+++ b/tests/test_encoding.py
@@ -41,7 +41,7 @@
 from odxtools.standardlengthtype import StandardLengthType
 from odxtools.text import Text
 
-doc_frags = [OdxDocFragment("UnitTest", DocType.CONTAINER)]
+doc_frags = (OdxDocFragment("UnitTest", DocType.CONTAINER),)
 
 
 class TestEncodeRequest(unittest.TestCase):
diff --git a/tests/test_readparameters.py b/tests/test_readparameters.py
index 27417732..776c2593 100644
--- a/tests/test_readparameters.py
+++ b/tests/test_readparameters.py
@@ -10,7 +10,7 @@
 from odxtools.parameters.createanyparameter import create_any_parameter_from_et
 from odxtools.parameters.nrcconstparameter import NrcConstParameter
 
-doc_frags = [OdxDocFragment("UnitTest", DocType.CONTAINER)]
+doc_frags = (OdxDocFragment("UnitTest", DocType.CONTAINER),)
 
 
 class TestReadNrcParam(unittest.TestCase):
diff --git a/tests/test_singleecujob.py b/tests/test_singleecujob.py
index 29d83552..f5e71b33 100644
--- a/tests/test_singleecujob.py
+++ b/tests/test_singleecujob.py
@@ -42,7 +42,7 @@
 from odxtools.standardlengthtype import StandardLengthType
 from odxtools.writepdxfile import jinja2_odxraise_helper, make_bool_xml_attrib, make_xml_attrib
 
-doc_frags = [OdxDocFragment("UnitTest", DocType.CONTAINER)]
+doc_frags = (OdxDocFragment("UnitTest", DocType.CONTAINER),)
 
 
 class TestSingleEcuJob(unittest.TestCase):
diff --git a/tests/test_unit_spec.py b/tests/test_unit_spec.py
index 599e76d8..eb7b7e39 100644
--- a/tests/test_unit_spec.py
+++ b/tests/test_unit_spec.py
@@ -26,7 +26,7 @@
 from odxtools.unit import Unit
 from odxtools.unitspec import UnitSpec
 
-doc_frags = [OdxDocFragment("UnitTest", DocType.CONTAINER)]
+doc_frags = (OdxDocFragment("UnitTest", DocType.CONTAINER),)
 
 
 class TestUnitSpec(unittest.TestCase):
diff --git a/tests/test_variant_matching.py b/tests/test_variant_matching.py
index 5dc05aff..463dd4be 100644
--- a/tests/test_variant_matching.py
+++ b/tests/test_variant_matching.py
@@ -21,7 +21,7 @@
 from odxtools.response import Response, ResponseType
 from odxtools.variantmatcher import VariantMatcher
 
-doc_frags = [OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER)]
+doc_frags = (OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER),)
 
 odxlinks = OdxLinkDatabase()
 
diff --git a/tests/test_variant_patterns.py b/tests/test_variant_patterns.py
index d69ed9ac..4dbcdd32 100644
--- a/tests/test_variant_patterns.py
+++ b/tests/test_variant_patterns.py
@@ -10,7 +10,7 @@
 from odxtools.odxdoccontext import OdxDocContext
 from odxtools.odxlink import DocType, OdxDocFragment
 
-doc_frags = [OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER)]
+doc_frags = (OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER),)
 
 
 @pytest.fixture()


Reviews:
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T18:20:40Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T18:59:38Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T19:00:29Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T19:01:58Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T19:03:29Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T19:04:02Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T19:05:38Z
user: login: andlaus
body: looks pretty good. when the `match` statement is replaced and it is verified that ODX 2.0 does not regress (you might need to help out on that front @kayoub5) I think this should be merged... 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T19:14:40Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T19:17:23Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T19:23:47Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T19:33:48Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T19:37:50Z
user: login: zariiii9003
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T21:20:55Z
user: login: zariiii9003
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T21:22:59Z
user: login: zariiii9003
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T21:25:55Z
user: login: zariiii9003
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T21:29:03Z
user: login: zariiii9003
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T21:30:52Z
user: login: zariiii9003
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T21:38:56Z
user: login: zariiii9003
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T21:40:40Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-05T12:23:27Z
user: login: kayoub5
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-05T16:17:46Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T10:40:32Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T10:42:58Z
user: login: zariiii9003
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T11:15:45Z
user: login: andlaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T12:18:18Z

Review comments:
diff_hunk: @@ -31,14 +31,7 @@ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "Compara
 
         category_attrib = et_element.attrib.get("CATEGORY")
 
-        # In ODX 2.0, COMPARAM-SPEC is used, whereas in ODX 2.2, it
-        # refers to something else and has been replaced by
-        # COMPARAM-SUBSET.
-        # - If the 'CATEGORY' attribute is missing (ODX 2.0), use
-        #   COMPARAM_SPEC,
-        # - else (ODX 2.2), use COMPARAM_SUBSET.
-        doc_type = DocType.COMPARAM_SUBSET if category_attrib is not None else DocType.COMPARAM_SPEC
-        base_obj = OdxCategory.category_from_et(et_element, context, doc_type=doc_type)
+        base_obj = OdxCategory.from_et(et_element, context)
path: odxtools/comparamsubset.py
user: login: kayoub5
body: Did you test if this breaks ODX 2.0 compatibility ?
created_at: 2025-04-04T18:20:40Z
author_association: COLLABORATOR
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
-        ecu_shared_datas = NamedItemList([
-            EcuSharedData.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA")
-        ])
-        base_variants = NamedItemList([
-            BaseVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("BASE-VARIANTS/BASE-VARIANT")
-        ])
-        ecu_variants = NamedItemList([
-            EcuVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT")
-        ])
+        def _get_layer_context(_layer_et: ElementTree.Element) -> OdxDocContext:
path: odxtools/diaglayercontainer.py
user: login: kayoub5
body: ```suggestion
        def _get_layer_context(layer_et: ElementTree.Element) -> OdxDocContext:
```
created_at: 2025-04-04T18:59:38Z
author_association: COLLABORATOR
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
-        ecu_shared_datas = NamedItemList([
-            EcuSharedData.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA")
-        ])
-        base_variants = NamedItemList([
-            BaseVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("BASE-VARIANTS/BASE-VARIANT")
-        ])
-        ecu_variants = NamedItemList([
-            EcuVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT")
-        ])
+        def _get_layer_context(_layer_et: ElementTree.Element) -> OdxDocContext:
+            layer_sn = odxrequire(_layer_et.findtext("SHORT-NAME"))
+            layer_docfrag = OdxDocFragment(layer_sn, DocType.LAYER)
+            # add layer doc fragment to container doc fragment
+            return OdxDocContext(context.version, (context.doc_fragments[0], layer_docfrag))
path: odxtools/diaglayercontainer.py
user: login: kayoub5
body: why 0?
created_at: 2025-04-04T19:00:29Z
author_association: COLLABORATOR
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
-        ecu_shared_datas = NamedItemList([
-            EcuSharedData.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA")
-        ])
-        base_variants = NamedItemList([
-            BaseVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("BASE-VARIANTS/BASE-VARIANT")
-        ])
-        ecu_variants = NamedItemList([
-            EcuVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT")
-        ])
+        def _get_layer_context(_layer_et: ElementTree.Element) -> OdxDocContext:
+            layer_sn = odxrequire(_layer_et.findtext("SHORT-NAME"))
+            layer_docfrag = OdxDocFragment(layer_sn, DocType.LAYER)
+            # add layer doc fragment to container doc fragment
+            return OdxDocContext(context.version, (context.doc_fragments[0], layer_docfrag))
+
+        protocols = NamedItemList[Protocol]()
+        for layer_et in et_element.iterfind("PROTOCOLS/PROTOCOL"):
+            protocols.append(Protocol.from_et(layer_et, _get_layer_context(layer_et)))
path: odxtools/diaglayercontainer.py
user: login: kayoub5
body: @andlaus does NamedItemList handle appending and internal name lookup update correctly ?
created_at: 2025-04-04T19:01:58Z
author_association: COLLABORATOR
diff_hunk: @@ -89,31 +85,35 @@ def _process_xml_tree(self, root: ElementTree.Element) -> None:
 
         self.model_version = model_version
 
-        dlc = root.find("DIAG-LAYER-CONTAINER")
-        if dlc is not None:
-            dlcs.append(DiagLayerContainer.from_et(dlc, OdxDocContext(model_version, [])))
-
-        # In ODX 2.0 there was only COMPARAM-SPEC. In ODX 2.2 the
-        # content of COMPARAM-SPEC was moved to COMPARAM-SUBSET
-        # and COMPARAM-SPEC became a container for PROT-STACKS and
-        # a PROT-STACK references a list of COMPARAM-SUBSET
-        cp_subset = root.find("COMPARAM-SUBSET")
-        if cp_subset is not None:
-            comparam_subsets.append(
-                ComparamSubset.from_et(cp_subset, OdxDocContext(model_version, [])))
-
-        cp_spec = root.find("COMPARAM-SPEC")
-        if cp_spec is not None:
-            if model_version < Version("2.2"):
-                comparam_subsets.append(
-                    ComparamSubset.from_et(cp_spec, OdxDocContext(model_version, [])))
-            else:  # odx >= 2.2
-                comparam_specs.append(
-                    ComparamSpec.from_et(cp_spec, OdxDocContext(model_version, [])))
-
-        self._diag_layer_containers.extend(dlcs)
-        self._comparam_subsets.extend(comparam_subsets)
-        self._comparam_specs.extend(comparam_specs)
+        child_elements = list(root)
+        if len(child_elements) != 1:
+            odxraise("Each ODX document must contain exactly one category.")
+
+        category_et = child_elements[0]
+        category_sn = odxrequire(category_et.findtext("SHORT-NAME"))
+
+        match category_et.tag:
path: odxtools/database.py
user: login: andlaus
body: the `match` statement is for "structural pattern matching" ([pep](https://peps.python.org/pep-0622/), I guess I'm too stupid to understand what that is), and AFAIK it is not more performant than a simple `if ... elif` chain. Please replace...
created_at: 2025-04-04T19:03:03Z
author_association: MEMBER
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
path: odxtools/diaglayercontainer.py
user: login: kayoub5
body: this change have nothing to do with PR topic, please revert or move it to its own PR
created_at: 2025-04-04T19:03:28Z
author_association: COLLABORATOR
diff_hunk: @@ -77,10 +77,6 @@ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLay
             variant_type = cast(DiagLayerType, None)
             odxraise(f"Encountered unknown diagnostic layer type '{et_element.tag}'")
 
-        short_name = odxrequire(et_element.findtext("SHORT-NAME"))
-
-        # extend the applicable ODX "document fragments" for the diag layer objects
-        context.doc_fragments.append(OdxDocFragment(short_name, DocType.LAYER))
path: odxtools/diaglayers/diaglayerraw.py
user: login: kayoub5
body: what happened to this logic ?
created_at: 2025-04-04T19:04:02Z
author_association: COLLABORATOR
diff_hunk: @@ -8,4 +8,6 @@
 
 class OdxDocContext(NamedTuple):
     version: Version
-    doc_fragments: list["OdxDocFragment"]
+
+    # the doc_fragments are either tuple(category,) or tuple(category, diag_layer)
+    doc_fragments: tuple["OdxDocFragment"] | tuple["OdxDocFragment", "OdxDocFragment"]
path: odxtools/odxdoccontext.py
user: login: kayoub5
body: is it just me or comment and type hint don't match ?
created_at: 2025-04-04T19:05:37Z
author_association: COLLABORATOR
diff_hunk: @@ -10,7 +10,7 @@
 from odxtools.odxdoccontext import OdxDocContext
 from odxtools.odxlink import DocType, OdxDocFragment
 
-doc_frags = [OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER)]
+doc_frags = (OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER),)
path: tests/test_variant_patterns.py
user: login: andlaus
body: this is not an issue introduced by this PR, but I think that in practice all IDs that are inside a container also contain a document fragment for the layer which they are contained in? (this does not really matter here, though...)
created_at: 2025-04-04T19:10:13Z
author_association: MEMBER
diff_hunk: @@ -8,4 +8,6 @@
 
 class OdxDocContext(NamedTuple):
     version: Version
-    doc_fragments: list["OdxDocFragment"]
+
+    # the doc_fragments are either tuple(category,) or tuple(category, diag_layer)
+    doc_fragments: tuple["OdxDocFragment"] | tuple["OdxDocFragment", "OdxDocFragment"]
path: odxtools/odxdoccontext.py
user: login: andlaus
body: As I see it, the comment is a bit sloppy. it should probably be something like
```
    # the doc_fragments are either tuple(doc_frag(category),) or tuple(doc_frag(category), doc_frag(diag_layer))
```
created_at: 2025-04-04T19:17:23Z
author_association: MEMBER
diff_hunk: @@ -89,31 +85,35 @@ def _process_xml_tree(self, root: ElementTree.Element) -> None:
 
         self.model_version = model_version
 
-        dlc = root.find("DIAG-LAYER-CONTAINER")
-        if dlc is not None:
-            dlcs.append(DiagLayerContainer.from_et(dlc, OdxDocContext(model_version, [])))
-
-        # In ODX 2.0 there was only COMPARAM-SPEC. In ODX 2.2 the
-        # content of COMPARAM-SPEC was moved to COMPARAM-SUBSET
-        # and COMPARAM-SPEC became a container for PROT-STACKS and
-        # a PROT-STACK references a list of COMPARAM-SUBSET
-        cp_subset = root.find("COMPARAM-SUBSET")
-        if cp_subset is not None:
-            comparam_subsets.append(
-                ComparamSubset.from_et(cp_subset, OdxDocContext(model_version, [])))
-
-        cp_spec = root.find("COMPARAM-SPEC")
-        if cp_spec is not None:
-            if model_version < Version("2.2"):
-                comparam_subsets.append(
-                    ComparamSubset.from_et(cp_spec, OdxDocContext(model_version, [])))
-            else:  # odx >= 2.2
-                comparam_specs.append(
-                    ComparamSpec.from_et(cp_spec, OdxDocContext(model_version, [])))
-
-        self._diag_layer_containers.extend(dlcs)
-        self._comparam_subsets.extend(comparam_subsets)
-        self._comparam_specs.extend(comparam_specs)
+        child_elements = list(root)
+        if len(child_elements) != 1:
+            odxraise("Each ODX document must contain exactly one category.")
+
+        category_et = child_elements[0]
+        category_sn = odxrequire(category_et.findtext("SHORT-NAME"))
+
+        match category_et.tag:
path: odxtools/database.py
user: login: kayoub5
body: it's a C++ equivalent of switch/case
created_at: 2025-04-04T19:23:47Z
author_association: COLLABORATOR
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
-        ecu_shared_datas = NamedItemList([
-            EcuSharedData.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA")
-        ])
-        base_variants = NamedItemList([
-            BaseVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("BASE-VARIANTS/BASE-VARIANT")
-        ])
-        ecu_variants = NamedItemList([
-            EcuVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT")
-        ])
+        def _get_layer_context(_layer_et: ElementTree.Element) -> OdxDocContext:
+            layer_sn = odxrequire(_layer_et.findtext("SHORT-NAME"))
+            layer_docfrag = OdxDocFragment(layer_sn, DocType.LAYER)
+            # add layer doc fragment to container doc fragment
+            return OdxDocContext(context.version, (context.doc_fragments[0], layer_docfrag))
+
+        protocols = NamedItemList[Protocol]()
+        for layer_et in et_element.iterfind("PROTOCOLS/PROTOCOL"):
+            protocols.append(Protocol.from_et(layer_et, _get_layer_context(layer_et)))
path: odxtools/diaglayercontainer.py
user: login: andlaus
body: this code seems fine to me: the restriction is that after an item has been added to a `NamedItemList`, its `.short_name` attribute must not change anymore or else accessing the item by its name (a la `foo_list.bar`) will not be consistent with its `.short_name` anymore...
created_at: 2025-04-04T19:33:47Z
author_association: MEMBER
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
path: odxtools/diaglayercontainer.py
user: login: andlaus
body: to me it is still fine; the title of the PR became a bit outdated and should now be something like "refactor document fragment handling" ;)
created_at: 2025-04-04T19:37:49Z
author_association: MEMBER
diff_hunk: @@ -31,14 +31,7 @@ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "Compara
 
         category_attrib = et_element.attrib.get("CATEGORY")
 
-        # In ODX 2.0, COMPARAM-SPEC is used, whereas in ODX 2.2, it
-        # refers to something else and has been replaced by
-        # COMPARAM-SUBSET.
-        # - If the 'CATEGORY' attribute is missing (ODX 2.0), use
-        #   COMPARAM_SPEC,
-        # - else (ODX 2.2), use COMPARAM_SUBSET.
-        doc_type = DocType.COMPARAM_SUBSET if category_attrib is not None else DocType.COMPARAM_SPEC
-        base_obj = OdxCategory.category_from_et(et_element, context, doc_type=doc_type)
+        base_obj = OdxCategory.from_et(et_element, context)
path: odxtools/comparamsubset.py
user: login: zariiii9003
body: The behavior should be unchanged. The doctype is now set within `Database._process_xml_tree()`.
created_at: 2025-04-04T21:20:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
-        ecu_shared_datas = NamedItemList([
-            EcuSharedData.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA")
-        ])
-        base_variants = NamedItemList([
-            BaseVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("BASE-VARIANTS/BASE-VARIANT")
-        ])
-        ecu_variants = NamedItemList([
-            EcuVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT")
-        ])
+        def _get_layer_context(_layer_et: ElementTree.Element) -> OdxDocContext:
path: odxtools/diaglayercontainer.py
user: login: zariiii9003
body: I added the underscore to avoid name collision with `layer_et` from outer scope. I could change it to something else though if needed.
created_at: 2025-04-04T21:22:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
-        ecu_shared_datas = NamedItemList([
-            EcuSharedData.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA")
-        ])
-        base_variants = NamedItemList([
-            BaseVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("BASE-VARIANTS/BASE-VARIANT")
-        ])
-        ecu_variants = NamedItemList([
-            EcuVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT")
-        ])
+        def _get_layer_context(_layer_et: ElementTree.Element) -> OdxDocContext:
+            layer_sn = odxrequire(_layer_et.findtext("SHORT-NAME"))
+            layer_docfrag = OdxDocFragment(layer_sn, DocType.LAYER)
+            # add layer doc fragment to container doc fragment
+            return OdxDocContext(context.version, (context.doc_fragments[0], layer_docfrag))
path: odxtools/diaglayercontainer.py
user: login: zariiii9003
body: The `doc_fragments` tuple only contains one entry at this point (the diag layer container). So i take this first element and create a new tuple with the layer doc fragment.
created_at: 2025-04-04T21:25:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
path: odxtools/diaglayercontainer.py
user: login: zariiii9003
body: It's necessary, because a new context is created for each diag layer instantiation. The new context contains both DLC and LAYER  fragments.
created_at: 2025-04-04T21:29:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,10 +77,6 @@ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLay
             variant_type = cast(DiagLayerType, None)
             odxraise(f"Encountered unknown diagnostic layer type '{et_element.tag}'")
 
-        short_name = odxrequire(et_element.findtext("SHORT-NAME"))
-
-        # extend the applicable ODX "document fragments" for the diag layer objects
-        context.doc_fragments.append(OdxDocFragment(short_name, DocType.LAYER))
path: odxtools/diaglayers/diaglayerraw.py
user: login: zariiii9003
body: The LAYER doc fragment is now created in DiagLayerContainer.
created_at: 2025-04-04T21:30:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,7 +10,7 @@
 from odxtools.odxdoccontext import OdxDocContext
 from odxtools.odxlink import DocType, OdxDocFragment
 
-doc_frags = [OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER)]
+doc_frags = (OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER),)
path: tests/test_variant_patterns.py
user: login: zariiii9003
body: Attributes of the ODX category like company data could have just the container though, right?
created_at: 2025-04-04T21:38:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,4 +8,6 @@
 
 class OdxDocContext(NamedTuple):
     version: Version
-    doc_fragments: list["OdxDocFragment"]
+
+    # the doc_fragments are either tuple(category,) or tuple(category, diag_layer)
+    doc_fragments: tuple["OdxDocFragment"] | tuple["OdxDocFragment", "OdxDocFragment"]
path: odxtools/odxdoccontext.py
user: login: zariiii9003
body: I pushed your comment 
created_at: 2025-04-04T21:40:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,29 +44,35 @@ def ecus(self) -> NamedItemList[EcuVariant]:
     @staticmethod
     def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagLayerContainer":
 
-        cat = OdxCategory.category_from_et(et_element, context, doc_type=DocType.CONTAINER)
+        cat = OdxCategory.from_et(et_element, context)
         kwargs = dataclass_fields_asdict(cat)
 
-        protocols = NamedItemList([
-            Protocol.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("PROTOCOLS/PROTOCOL")
-        ])
-        functional_groups = NamedItemList([
-            FunctionalGroup.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("FUNCTIONAL-GROUPS/FUNCTIONAL-GROUP")
-        ])
-        ecu_shared_datas = NamedItemList([
-            EcuSharedData.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA")
-        ])
-        base_variants = NamedItemList([
-            BaseVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("BASE-VARIANTS/BASE-VARIANT")
-        ])
-        ecu_variants = NamedItemList([
-            EcuVariant.from_et(dl_element, context)
-            for dl_element in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT")
-        ])
+        def _get_layer_context(_layer_et: ElementTree.Element) -> OdxDocContext:
path: odxtools/diaglayercontainer.py
user: login: kayoub5
body: underscore is usually used to mark class members as private

variable names should not defer by just underscore between scoops 
created_at: 2025-04-05T12:23:27Z
author_association: COLLABORATOR
diff_hunk: @@ -31,7 +31,7 @@
 from .hierarchyelementraw import HierarchyElementRaw
 
 if TYPE_CHECKING:
-    from .database import Database
+    from ..database import Database
path: odxtools/diaglayers/hierarchyelement.py
user: login: kayoub5
body: mypy didn't catch this ?
created_at: 2025-04-05T16:17:46Z
author_association: COLLABORATOR
diff_hunk: @@ -10,7 +10,7 @@
 from odxtools.odxdoccontext import OdxDocContext
 from odxtools.odxlink import DocType, OdxDocFragment
 
-doc_frags = [OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER)]
+doc_frags = (OdxDocFragment(doc_name="pytest", doc_type=DocType.CONTAINER),)
path: tests/test_variant_patterns.py
user: login: andlaus
body: right.
created_at: 2025-04-07T10:40:32Z
author_association: MEMBER
diff_hunk: @@ -31,7 +31,7 @@
 from .hierarchyelementraw import HierarchyElementRaw
 
 if TYPE_CHECKING:
-    from .database import Database
+    from ..database import Database
path: odxtools/diaglayers/hierarchyelement.py
user: login: andlaus
body: @zariiii9003: how did you find this? if using tools, can/should we change the CI system to do the same?
created_at: 2025-04-07T10:42:58Z
author_association: MEMBER
diff_hunk: @@ -31,7 +31,7 @@
 from .hierarchyelementraw import HierarchyElementRaw
 
 if TYPE_CHECKING:
-    from .database import Database
+    from ..database import Database
path: odxtools/diaglayers/hierarchyelement.py
user: login: zariiii9003
body: I noticed red squiggly lines in my IDE. I also wonder why both ruff and mypy seem to ignore it
created_at: 2025-04-07T11:15:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,7 +31,7 @@
 from .hierarchyelementraw import HierarchyElementRaw
 
 if TYPE_CHECKING:
-    from .database import Database
+    from ..database import Database
path: odxtools/diaglayers/hierarchyelement.py
user: login: andlaus
body: ok. Seems like you should continue to let your IDE look at odxtools. mine (emacs) is not sophisticated enough ;)
created_at: 2025-04-07T12:18:17Z
author_association: MEMBER

PR comments:
user: login: zariiii9003
created_at: 2025-04-04T06:54:32Z
body: @andlaus Unrelated: i think there are a lot of mypy issues hidden by the `Noreturn` in the `odxraise` method. It should be `NoReturn | None` and the `TYPE_CHECKING` should be removed.
author_association: CONTRIBUTOR
user: login: andlaus
created_at: 2025-04-04T09:23:43Z
body: The `Noreturn` is on purpose: Semantically `odxraise()` should be considered identical to `raise`. The difference is that in non-strict mode it becomes a no-op, i.e. the reason why there is often code after the `odxraise()` call is to provide a sensible fallback for non-strict mode (which does not make any guarantees about undefined behavior or working in general, just that it "does its best"). So `odxraise()` should stay `Noreturn` in the main branch, but for development purposes, it I agree that could be temporarily changed to `None` to uncover the issues which you mentioned (I suppose that you will get a lot of mypy errors then...)
author_association: MEMBER
user: login: andlaus
created_at: 2025-04-04T09:28:30Z
body: > I tried converting OdxLinkRef into a frozen dataclass, but subclasses assign attributes after instantiation.

IIRC, that was also the issue I stumbled over, `ParentRef` was particularly bad because it does quite a bit of reference resolution...
author_association: MEMBER
user: login: zariiii9003
created_at: 2025-04-04T17:48:57Z
body: A few more changes after a bit of research. To my understanding there are two options: the doc fragments are either just the category, or the category+diag_layer.

i changed the type annotation of OdxDocContext.doc_fragments to reflect that. The category context is now completely defined in `Database._process_xml_tree()`. The DiagLayerContainer then creates a new context for its layers.
author_association: CONTRIBUTOR
user: login: kayoub5
created_at: 2025-04-05T12:26:16Z
body: @nada-ben-ali could you check that odx 2.0 compatibility is not broken with those changes ?
author_association: COLLABORATOR
user: login: andlaus
created_at: 2025-04-07T12:24:40Z
body: sorry for the conflict due to #411. as I see it, the `.doc_frags` attribute should not be defaulted anyway...
author_association: MEMBER
user: login: andlaus
created_at: 2025-04-07T18:11:37Z
body: thanks for rebasing. as soon as @nada-ben-ali reports back, I'll merge this.
author_association: MEMBER
user: login: andlaus
created_at: 2025-04-10T10:24:06Z
body: For some strange reason, I ran into some DOCREF-related issue with this. I don't know why it only crept up with this PR, but I made a "meta pull request" for your PR here: https://github.com/zariiii9003/odxtools/pull/1 . would be great if you merged it...
author_association: MEMBER
user: login: andlaus
created_at: 2025-04-10T13:00:21Z
body: @nada-ben-ali: were you able to test this with ODX 2.0? I think it is relatively unlikely that it regresses something on that front, and since I get a weird performance regression which goes away with this PR, I'd like to get it merged quickly...
author_association: MEMBER
user: login: andlaus
created_at: 2025-04-14T08:55:04Z
body: @nada-ben-ali, @kayoub5: If I do not get any objections to this PR, I'll merge it on Thursday...
author_association: MEMBER
user: login: nada-ben-ali
created_at: 2025-04-16T07:06:12Z
body: @zariiii9003 @kayoub5 @andlaus sorryy I've just seen the email notifications. I'll begin checking now.
author_association: COLLABORATOR
user: login: nada-ben-ali
created_at: 2025-04-16T08:30:18Z
body: I got the following exception while loading the pickle file for some pdx files we have:
![image](https://github.com/user-attachments/assets/e6a04559-bd78-4a2c-a882-8e23a99c7812)

I'm checking this...


author_association: COLLABORATOR
user: login: nada-ben-ali
created_at: 2025-04-16T15:30:17Z
body: > I got the following exception while loading the pickle file for some pdx files we have: ![image](https://private-user-images.githubusercontent.com/64044980/434229162-e6a04559-bd78-4a2c-a882-8e23a99c7812.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ4MDAxODAsIm5iZiI6MTc0NDc5OTg4MCwicGF0aCI6Ii82NDA0NDk4MC80MzQyMjkxNjItZTZhMDQ1NTktYmQ3OC00YTJjLWE4ODItOGUyM2E5OWM3ODEyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA0MTYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNDE2VDEwMzgwMFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTgyMTJiMjYwZWNmZWY1MTI3OGNlYTVjM2QwOTc5MmIwYmI3Mzc5NGM3NWEyNGIzZjI1YWZjMDBjNjI3NTAwYWYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.YCTV2jqWPOOpJFxgVTevui0J6cbC-u4-kFnTUg3OoVQ)
> 
> I'm checking this...

the issue is caused by https://github.com/mercedes-benz/odxtools/pull/407.
@kayoub5 @andlaus after removing the changes introduced in https://github.com/mercedes-benz/odxtools/pull/407 and https://github.com/mercedes-benz/odxtools/pull/411/ (which have not been included in a release yet), no issues are observed with the current merge request changes.

I'm currently checking the impact of the dataclass as `Kw_only` on the unpickling operation and how we can fix it. It only occurs with some files, not all of them. For now, I have only identified the MRs that caused the issue.
author_association: COLLABORATOR
user: login: andlaus
created_at: 2025-04-16T16:36:58Z
body: ok, does this mean that this PR can be merged and I should only prepare the next release once the unpickling issues are fixed? are you on it? (I do not currently use pickling, so I have a hard time reproducing it.)
author_association: MEMBER
user: login: nada-ben-ali
created_at: 2025-04-17T07:30:53Z
body: > ok, does this mean that this PR can be merged and I should only prepare the next release once the unpickling issues are fixed? are you on it? (I do not currently use pickling, so I have a hard time reproducing it.)

@andlaus yes, we can merge this, and I'll work on fixing the unpickling problem.
author_association: COLLABORATOR
user: login: andlaus
created_at: 2025-04-17T07:57:30Z
body: ok, cool. merging; thanks everyone!
author_association: MEMBER