From 4791c2d812c9029443da4a74ab7ba8a42caf3390 Mon Sep 17 00:00:00 2001 From: fnbu Date: Tue, 5 May 2026 16:31:45 -0700 Subject: [PATCH] fix: handle SPEC-ATTRIBUTES on RELATION-GROUP-TYPE and DEFAULT-VALUE on SPECIFICATION-TYPE attributes Bug A: ReqIFRelationGroupType had no attribute_definitions field, so any SPEC-ATTRIBUTES children of RELATION-GROUP-TYPE were silently dropped on parse and never emitted on unparse. Add the field and wire AttributeDefinitionParser into both directions, matching the existing pattern in SpecRelationTypeParser. Bug B: SpecificationTypeParser.unparse had its own attribute-serialization loop that omitted DEFAULT-VALUE. Delegate to AttributeDefinitionParser.unparse_xhtml_attribute_definition (which already handles DEFAULT-VALUE correctly), as SpecObjectTypeParser and SpecRelationTypeParser already do. Add unit tests and integration fixtures covering both cases. --- reqif/models/reqif_relation_group_type.py | 8 +++- .../spec_types/relation_group_type_parser.py | 21 ++++++++-- .../spec_types/specification_type_parser.py | 29 ++------------ .../02_spec_attributes/sample.reqif | 21 ++++++++++ .../02_spec_attributes/test.itest | 3 ++ .../sample.reqif | 24 ++++++++++++ .../test.itest | 3 ++ .../test_relation_group_type_parser.py | 39 +++++++++++++++++++ .../parsers/test_specification_type_parser.py | 24 ++++++++++++ 9 files changed, 142 insertions(+), 30 deletions(-) create mode 100644 tests/integration/reqif/RELATION-GROUP-TYPE/02_spec_attributes/sample.reqif create mode 100644 tests/integration/reqif/RELATION-GROUP-TYPE/02_spec_attributes/test.itest create mode 100644 tests/integration/reqif/SPECIFICATION-TYPE/03_spec_attribute_default_value/sample.reqif create mode 100644 tests/integration/reqif/SPECIFICATION-TYPE/03_spec_attribute_default_value/test.itest create mode 100644 tests/unit/reqif/parsers/test_relation_group_type_parser.py diff --git a/reqif/models/reqif_relation_group_type.py b/reqif/models/reqif_relation_group_type.py index 263cdf6..2e236e5 100644 --- a/reqif/models/reqif_relation_group_type.py +++ b/reqif/models/reqif_relation_group_type.py @@ -1,4 +1,6 @@ -from typing import Optional +from typing import List, Optional + +from reqif.models.reqif_spec_object_type import SpecAttributeDefinition class ReqIFRelationGroupType: @@ -9,9 +11,13 @@ def __init__( # pylint: disable=too-many-arguments last_change: Optional[str] = None, long_name: Optional[str] = None, is_self_closed: bool = True, + attribute_definitions: Optional[List[SpecAttributeDefinition]] = None, ): self.identifier: str = identifier self.description: Optional[str] = description self.last_change: Optional[str] = last_change self.long_name: Optional[str] = long_name self.is_self_closed: bool = is_self_closed + self.attribute_definitions: Optional[List[SpecAttributeDefinition]] = ( + attribute_definitions + ) diff --git a/reqif/parsers/spec_types/relation_group_type_parser.py b/reqif/parsers/spec_types/relation_group_type_parser.py index a8347eb..3b6b165 100644 --- a/reqif/parsers/spec_types/relation_group_type_parser.py +++ b/reqif/parsers/spec_types/relation_group_type_parser.py @@ -3,6 +3,7 @@ from reqif.helpers.lxml import lxml_is_self_closed_tag from reqif.models.reqif_relation_group_type import ReqIFRelationGroupType +from reqif.parsers.attribute_definition_parser import AttributeDefinitionParser class RelationGroupTypeParser: @@ -31,12 +32,17 @@ def parse(xml_spec_relation_type_xml) -> ReqIFRelationGroupType: xml_attributes["LAST-CHANGE"] if "LAST-CHANGE" in xml_attributes else None ) + attribute_definitions = AttributeDefinitionParser.parse_attribute_definitions( + xml_spec_relation_type_xml + ) + return ReqIFRelationGroupType( is_self_closed=is_self_closed, description=description, identifier=identifier, last_change=last_change, long_name=long_name, + attribute_definitions=attribute_definitions, ) @staticmethod @@ -51,7 +57,16 @@ def unparse(spec_relation_type: ReqIFRelationGroupType): output += f' LONG-NAME="{spec_relation_type.long_name}"' if spec_relation_type.is_self_closed: output += "/>\n" - else: - output += ">\n" - output += " \n" + return output + + output += ">\n" + + if spec_relation_type.attribute_definitions is not None: + output += " \n" + output += AttributeDefinitionParser.unparse_xhtml_attribute_definition( + attribute_definitions=spec_relation_type.attribute_definitions + ) + output += " \n" + + output += " \n" return output diff --git a/reqif/parsers/spec_types/specification_type_parser.py b/reqif/parsers/spec_types/specification_type_parser.py index 00eb601..de1684c 100644 --- a/reqif/parsers/spec_types/specification_type_parser.py +++ b/reqif/parsers/spec_types/specification_type_parser.py @@ -75,32 +75,9 @@ def unparse(spec_type: ReqIFSpecificationType) -> str: if spec_type.spec_attributes is not None: output += " \n" - - for attribute in spec_type.spec_attributes: - output += f" <{attribute.attribute_type.get_spec_type_tag()}" - if attribute.description is not None: - output += f' DESC="{attribute.description}"' - output += f' IDENTIFIER="{attribute.identifier}"' - if attribute.editable is not None: - editable_value = "true" if attribute.editable else "false" - output += f' IS-EDITABLE="{editable_value}"' - if attribute.last_change: - output += f' LAST-CHANGE="{attribute.last_change}"' - output += f' LONG-NAME="{attribute.long_name}"' - output += ">\n" - output += " \n" - output += ( - " " - f"<{attribute.attribute_type.get_definition_tag()}>" - f"{attribute.datatype_definition}" - f"" - "\n" - ) - output += " \n" - output += " \n" - + output += AttributeDefinitionParser.unparse_xhtml_attribute_definition( + attribute_definitions=spec_type.spec_attributes + ) output += " \n" output += " \n" diff --git a/tests/integration/reqif/RELATION-GROUP-TYPE/02_spec_attributes/sample.reqif b/tests/integration/reqif/RELATION-GROUP-TYPE/02_spec_attributes/sample.reqif new file mode 100644 index 0000000..b9ea84b --- /dev/null +++ b/tests/integration/reqif/RELATION-GROUP-TYPE/02_spec_attributes/sample.reqif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + _dtype_String + + + + + + + + diff --git a/tests/integration/reqif/RELATION-GROUP-TYPE/02_spec_attributes/test.itest b/tests/integration/reqif/RELATION-GROUP-TYPE/02_spec_attributes/test.itest new file mode 100644 index 0000000..aa3061a --- /dev/null +++ b/tests/integration/reqif/RELATION-GROUP-TYPE/02_spec_attributes/test.itest @@ -0,0 +1,3 @@ +RUN: mkdir -p %S/output +RUN: %reqif passthrough %S/sample.reqif %S/output/sample.reqif +RUN: %diff %S/sample.reqif %S/output/sample.reqif diff --git a/tests/integration/reqif/SPECIFICATION-TYPE/03_spec_attribute_default_value/sample.reqif b/tests/integration/reqif/SPECIFICATION-TYPE/03_spec_attribute_default_value/sample.reqif new file mode 100644 index 0000000..0db9cb2 --- /dev/null +++ b/tests/integration/reqif/SPECIFICATION-TYPE/03_spec_attribute_default_value/sample.reqif @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + _dtype_String + + + + + + + + diff --git a/tests/integration/reqif/SPECIFICATION-TYPE/03_spec_attribute_default_value/test.itest b/tests/integration/reqif/SPECIFICATION-TYPE/03_spec_attribute_default_value/test.itest new file mode 100644 index 0000000..aa3061a --- /dev/null +++ b/tests/integration/reqif/SPECIFICATION-TYPE/03_spec_attribute_default_value/test.itest @@ -0,0 +1,3 @@ +RUN: mkdir -p %S/output +RUN: %reqif passthrough %S/sample.reqif %S/output/sample.reqif +RUN: %diff %S/sample.reqif %S/output/sample.reqif diff --git a/tests/unit/reqif/parsers/test_relation_group_type_parser.py b/tests/unit/reqif/parsers/test_relation_group_type_parser.py new file mode 100644 index 0000000..2379210 --- /dev/null +++ b/tests/unit/reqif/parsers/test_relation_group_type_parser.py @@ -0,0 +1,39 @@ +from lxml import etree + +from reqif.models.reqif_relation_group_type import ReqIFRelationGroupType +from reqif.parsers.spec_types.relation_group_type_parser import RelationGroupTypeParser + + +def test_01_no_spec_attributes() -> None: + xml = etree.fromstring( + '' + ) + result = RelationGroupTypeParser.parse(xml) + assert isinstance(result, ReqIFRelationGroupType) + assert result.identifier == "RGT_ID" + assert result.attribute_definitions is None + + +def test_02_spec_attributes_round_trip() -> None: + xml_string = """\ + + + + + _dtype_String + + + +""" + xml = etree.fromstring(xml_string) + result = RelationGroupTypeParser.parse(xml) + assert isinstance(result, ReqIFRelationGroupType) + assert result.attribute_definitions is not None + assert len(result.attribute_definitions) == 1 + assert result.attribute_definitions[0].identifier == "_attr_comment" + assert result.attribute_definitions[0].long_name == "Comment" + + unparsed = RelationGroupTypeParser.unparse(result) + assert "SPEC-ATTRIBUTES" in unparsed + assert "_attr_comment" in unparsed + assert "Comment" in unparsed diff --git a/tests/unit/reqif/parsers/test_specification_type_parser.py b/tests/unit/reqif/parsers/test_specification_type_parser.py index e30176f..c70e9c6 100644 --- a/tests/unit/reqif/parsers/test_specification_type_parser.py +++ b/tests/unit/reqif/parsers/test_specification_type_parser.py @@ -6,6 +6,30 @@ ) +def test_02_default_value_survives_unparse() -> None: + spec_type_string = """\ + + + + + + + + _dtype_String + + + + """ + xml = etree.fromstring(spec_type_string) + result = SpecificationTypeParser.parse(xml) + assert result.spec_attributes is not None + assert result.spec_attributes[0].default_value == "Untitled" + + unparsed = SpecificationTypeParser.unparse(result) + assert "DEFAULT-VALUE" in unparsed + assert "Untitled" in unparsed + + def test_01_nominal_case() -> None: spec_type_string = """