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( "
Protocol information of the somersault ECUs & cetera
"), - 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) -%}