From 0b70e02cc8e5a0a2eff731485472b10da3d33abd Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 2 Feb 2023 12:04:04 +0100 Subject: [PATCH 01/15] feat: Add `force` mode to CLI With force mode enabled errors during the changeset calculation won't cause a module skip. It will skip the erroneous model object and all related objects depending on it. --- capella_rm_bridge/__main__.py | 10 +++++++ capella_rm_bridge/changeset/__init__.py | 26 ++++++++++++++--- capella_rm_bridge/changeset/change.py | 37 +++++++++++++++++------- tests/test_changeset.py | 38 +++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 15 deletions(-) diff --git a/capella_rm_bridge/__main__.py b/capella_rm_bridge/__main__.py index 5194d80..8ea768e 100644 --- a/capella_rm_bridge/__main__.py +++ b/capella_rm_bridge/__main__.py @@ -67,6 +67,14 @@ def create_errors_statement(errors: cabc.Iterable[str]) -> str: default=True, help="Pull the latest changes from remote.", ) +@click.option( + "--force", + is_flag=True, + default=False, + help="If a non RequirementModule-error was encountered only the object " + "and all related objects will be skipped. Intact objects will still be " + "synchronized", +) @click.option( "--no-safe-mode", is_flag=True, @@ -103,6 +111,7 @@ def main( dry_run: bool, push: bool, pull: bool, + force: bool, no_safe_mode: bool, gather_logs: bool, save_change_history: bool, @@ -143,6 +152,7 @@ def main( model, tconfig, module, + force=force, safe_mode=not no_safe_mode, gather_logs=gather_logs, ) diff --git a/capella_rm_bridge/changeset/__init__.py b/capella_rm_bridge/changeset/__init__.py index d74aded..15128ea 100644 --- a/capella_rm_bridge/changeset/__init__.py +++ b/capella_rm_bridge/changeset/__init__.py @@ -24,8 +24,16 @@ ERROR_MESSAGE_PREFIX = "Skipping module: {module_id}" -def _wrap_errors(module_id: int | str, errors: cabc.Sequence[str]) -> str: - start = ERROR_MESSAGE_PREFIX.format(module_id=module_id) +def _wrap_errors( + module_id: int | str, + errors: cabc.Sequence[str], + include_start: bool = True, +) -> str: + if include_start: + start = ERROR_MESSAGE_PREFIX.format(module_id=module_id) + else: + start = f"Encountered error(s) in {module_id!r}" + first_sep = len(start) * "=" last_sep = len(errors[-1]) * "=" return "\n".join((start, first_sep, *errors, last_sep)) @@ -35,6 +43,7 @@ def calculate_change_set( model: capellambse.MelodyModel, config: actiontypes.TrackerConfig, snapshot: actiontypes.TrackerSnapshot, + force: bool = False, safe_mode: bool = True, gather_logs: bool = True, ) -> tuple[list[dict[str, t.Any]], list[str]]: @@ -54,6 +63,10 @@ def calculate_change_set( calculated ``ChangeSet``. snapshot A snapshot of a tracker or live-document from the external tool. + force + If ``True`` a ``ChangeSet`` will be rendered even if an error + occurred during the change-calculation loop. All related objects + will be skipped. safe_mode If ``True`` no ``ChangeSet`` will be rendered iff atleast one error occurred during the change-calculation loop. If ``False`` @@ -83,9 +96,12 @@ def calculate_change_set( snapshot, model, config, gather_logs=gather_logs ) if tchange.errors: - message = _wrap_errors(module_id, tchange.errors) + message = _wrap_errors( + module_id, tchange.errors, include_start=not force + ) errors.append(message) - else: + + if force: actions.extend(tchange.actions) except ( actiontypes.InvalidTrackerConfig, @@ -99,6 +115,8 @@ def calculate_change_set( prefix = ERROR_MESSAGE_PREFIX.format(module_id=module_id) LOGGER.error("%s. %s", prefix, error.args[0]) + if force: + safe_mode = False if safe_mode and any(errors): actions = [] return actions, errors diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index 35bfefb..be41f2d 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -55,6 +55,7 @@ class TrackerChange: _location_changed: set[RMIdentifier] _req_deletions: dict[helpers.UUIDString, dict[str, t.Any]] _reqtype_ids: set[RMIdentifier] + _faulty_attribute_definitions: set[str] errors: list[str] tracker: cabc.Mapping[str, t.Any] @@ -133,6 +134,7 @@ def __init__( self._location_changed = set[RMIdentifier]() self._req_deletions = {} + self._faulty_attribute_definitions = set[str]() self.errors = [] self.__reqtype_action: dict[str, t.Any] | None = None @@ -209,13 +211,13 @@ def check_requirements_module(self) -> dict[str, t.Any]: self.req_module = self.reqfinder.reqmodule(module_uuid) except KeyError as error: raise act.InvalidTrackerConfig( - "The given module configuration is missing 'UUID' of the " + "The given module configuration is missing UUID of the " "target RequirementsModule" ) from error if self.req_module is None: raise MissingRequirementsModule( - f"No RequirementsModule with UUID '{module_uuid}' found in " + f"No RequirementsModule with UUID {module_uuid!r} found in " + repr(self.model.info) ) @@ -223,7 +225,7 @@ def check_requirements_module(self) -> dict[str, t.Any]: identifier = self.tracker["id"] except KeyError as error: raise act.InvalidSnapshotModule( - "In the snapshot the module is missing an 'id' key" + "In the snapshot the module is missing an id key" ) from error base = {"parent": decl.UUIDReference(self.req_module.uuid)} @@ -407,15 +409,15 @@ def attribute_definition_create_action( name, below=self.reqt_folder ) if etdef is None: + promise_id = f"EnumerationDataTypeDefinition {name}" if name not in self.data_type_definitions: + self._faulty_attribute_definitions.add(promise_id) raise act.InvalidAttributeDefinition( - f"Invalid {cls.__name__} found: '{name}'. Missing its " + f"Invalid {cls.__name__} found: {name!r}. Missing its " "datatype definition in `data_types`." ) - data_type_ref = decl.Promise( - f"EnumerationDataTypeDefinition {name}" - ) + data_type_ref = decl.Promise(promise_id) else: data_type_ref = decl.UUIDReference(etdef.uuid) @@ -591,7 +593,14 @@ def attribute_value_create_action( deftype, attr_def_id, below=self.reqt_folder ) if definition is None: - definition_ref = decl.Promise(f"{deftype} {attr_def_id}") + promise_id = f"{deftype} {attr_def_id}" + if promise_id in self._faulty_attribute_definitions: + raise act.InvalidFieldValue( + f"Invalid field found: No AttributeDefinition {name!r} " + "promised." + ) + else: + definition_ref = decl.Promise(promise_id) else: definition_ref = decl.UUIDReference(definition.uuid) @@ -615,7 +624,13 @@ def check_attribute_value_is_valid( "Unknown field type '%s' for %s: %r", deftype, name, value ) if deftype == "Enum": - options = self.data_type_definitions[name] + options = self.data_type_definitions.get(name) + if options is None: + raise act.InvalidFieldValue( + f"Invalid field found: {name!r}. Missing its " + "datatype definition in `data_types`." + ) + is_faulty = not matches_type or not set(value) & set(options) key = "values" else: @@ -624,7 +639,7 @@ def check_attribute_value_is_valid( if is_faulty: raise act.InvalidFieldValue( - f"Invalid field found: {key} '{value}' for '{name}'" + f"Invalid field found: {key} {value!r} for {name!r}" ) return _AttributeValueBuilder(deftype, key, value) @@ -813,7 +828,7 @@ def yield_requirements_mod_actions( if req_type_id and req_type_id not in self.requirement_types: raise act.InvalidWorkItemType( "Faulty workitem in snapshot: " - f"Unknown workitem-type '{req_type_id}'" + f"Unknown workitem-type {req_type_id!r}" ) reqtype = self.reqfinder.reqtype_by_identifier( diff --git a/tests/test_changeset.py b/tests/test_changeset.py index b8f4862..4c86f5c 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -548,6 +548,44 @@ def test_init_errors_are_gathered( assert errors[0].startswith(self.SKIP_MESSAGE) assert message in errors[0] + def test_forced_calculation_produces_change_set_on_AttributeDefinition_error( + self, clean_model: capellambse.MelodyModel + ) -> None: + """Test that an invalid AttributeDefinition will not prohibit.""" + snapshot = copy.deepcopy(TEST_SNAPSHOT[0]) + missing_enumdt = "Release" + del snapshot["data_types"][missing_enumdt] # type: ignore[attr-defined] + tconfig = TEST_CONFIG["modules"][0] + message = ( + "In RequirementType 'System Requirement': Invalid " + "AttributeDefinitionEnumeration found: 'Release'. Missing its " + "datatype definition in `data_types`.\n" + "Invalid workitem 'REQ-002'. Invalid field found: Release. " + "Missing its datatype definition in `data_types`." + ) + + change_sets, errors = calculate_change_set( + clean_model, tconfig, snapshot, gather_logs=True, force=True + ) + + assert (change_set := change_sets[0]) + for rtfolder in change_set["extend"]["requirement_types_folders"]: + for req_type in rtfolder["requirement_types"]: + for attr_def in req_type["attribute_definitions"]: + assert attr_def["long_name"] != missing_enumdt + + folder = change_set["extend"]["folders"][0] + for req in folder["requirements"]: + for attr_value in req["attributes"]: + assert ( + missing_enumdt not in attr_value["definition"].identifier + ) + + assert errors[0].startswith( + "Encountered error(s) in 'project/space/example title'" + ) + assert message in errors[0] + def test_snapshot_errors_from_ChangeSet_calculation_are_gathered( self, clean_model: capellambse.MelodyModel ) -> None: From a59a7c6053ec0bea43550971d94465d0e9c19153 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 2 Feb 2023 15:03:19 +0100 Subject: [PATCH 02/15] fix: Change order of error log and commit message --- capella_rm_bridge/__main__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/capella_rm_bridge/__main__.py b/capella_rm_bridge/__main__.py index 8ea768e..205bae6 100644 --- a/capella_rm_bridge/__main__.py +++ b/capella_rm_bridge/__main__.py @@ -167,6 +167,12 @@ def main( changed_objs, module["id"], module["category"] ) + if force: + commit_message = reporter.create_commit_message(snapshot["metadata"]) + print(commit_message) + if reporter.store and not dry_run: + model.save(push=push, commit_msg=commit_message) + if errors: error_statement = create_errors_statement(errors) print(error_statement) From ce5870063674afcba0e52c0433f5bb414547e732 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 2 Feb 2023 15:39:28 +0100 Subject: [PATCH 03/15] revert: Remove `--no_safe_mode` As an inbetween mode of `safe_mode` and `force`. --- capella_rm_bridge/__main__.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/capella_rm_bridge/__main__.py b/capella_rm_bridge/__main__.py index 205bae6..23823f9 100644 --- a/capella_rm_bridge/__main__.py +++ b/capella_rm_bridge/__main__.py @@ -75,13 +75,6 @@ def create_errors_statement(errors: cabc.Iterable[str]) -> str: "and all related objects will be skipped. Intact objects will still be " "synchronized", ) -@click.option( - "--no-safe-mode", - is_flag=True, - default=False, - help="Modifications are still done to a RequirementModule if an error in " - "another, independent module in the snapshot was identified.", -) @click.option( "--gather-logs/--no-gather-logs", is_flag=True, @@ -112,7 +105,6 @@ def main( push: bool, pull: bool, force: bool, - no_safe_mode: bool, gather_logs: bool, save_change_history: bool, save_error_log: bool, @@ -153,7 +145,6 @@ def main( tconfig, module, force=force, - safe_mode=not no_safe_mode, gather_logs=gather_logs, ) From a4d9703bb7912b94cb690aeb1c0793808afd43ae Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 2 Feb 2023 16:08:04 +0100 Subject: [PATCH 04/15] fix: Fix empty change-set bug and tests --- capella_rm_bridge/changeset/__init__.py | 4 +++- tests/test_changeset.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/capella_rm_bridge/changeset/__init__.py b/capella_rm_bridge/changeset/__init__.py index 15128ea..713412c 100644 --- a/capella_rm_bridge/changeset/__init__.py +++ b/capella_rm_bridge/changeset/__init__.py @@ -100,8 +100,10 @@ def calculate_change_set( module_id, tchange.errors, include_start=not force ) errors.append(message) + else: + actions.extend(tchange.actions) - if force: + if force and tchange.actions not in actions: actions.extend(tchange.actions) except ( actiontypes.InvalidTrackerConfig, diff --git a/tests/test_changeset.py b/tests/test_changeset.py index 4c86f5c..699de9e 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -205,7 +205,7 @@ def test_faulty_attribute_values_log_InvalidFieldValue_as_error( first_child = titem["children"][0] first_child["attributes"][attr] = faulty_value # type:ignore[index] message_end = ( - f"Invalid field found: {key} '{faulty_value}' for '{attr}'" + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" ) with caplog.at_level(logging.ERROR): @@ -227,7 +227,7 @@ def test_InvalidFieldValue_errors_are_gathered( ] = faulty_value # type:ignore[index] messages.append( "Invalid workitem 'REQ-002'. " - f"Invalid field found: {key} '{faulty_value}' for '{attr}'" + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" ) change = self.tracker_change(clean_model, tracker, gather_logs=True) @@ -338,7 +338,7 @@ def test_faulty_attribute_values_log_InvalidFieldValue_as_error( first_child = titem["children"][0] first_child["attributes"][attr] = faulty_value message_end = ( - f"Invalid field found: {key} '{faulty_value}' for '{attr}'" + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" ) with caplog.at_level(logging.ERROR): @@ -479,7 +479,7 @@ def test_missing_module_UUID_logs_InvalidTrackerConfig_error( config = copy.deepcopy(TEST_CONFIG["modules"][0]) del config["capella-uuid"] # type:ignore[misc] message = ( - "The given module configuration is missing 'UUID' of the target " + "The given module configuration is missing UUID of the target " "RequirementsModule" ) @@ -520,7 +520,7 @@ def test_missing_module_id_logs_InvalidSnapshotModule_error( tconfig = TEST_CONFIG["modules"][0] message = ( "Skipping module: MISSING ID. " - "In the snapshot the module is missing an 'id' key" + "In the snapshot the module is missing an id key" ) with caplog.at_level(logging.ERROR): @@ -537,7 +537,7 @@ def test_init_errors_are_gathered( config = copy.deepcopy(TEST_CONFIG["modules"][0]) del config["capella-uuid"] # type:ignore[misc] message = ( - "The given module configuration is missing 'UUID' of the target " + "The given module configuration is missing UUID of the target " "RequirementsModule" ) @@ -560,7 +560,7 @@ def test_forced_calculation_produces_change_set_on_AttributeDefinition_error( "In RequirementType 'System Requirement': Invalid " "AttributeDefinitionEnumeration found: 'Release'. Missing its " "datatype definition in `data_types`.\n" - "Invalid workitem 'REQ-002'. Invalid field found: Release. " + "Invalid workitem 'REQ-002'. Invalid field found: 'Release'. " "Missing its datatype definition in `data_types`." ) @@ -604,7 +604,7 @@ def test_snapshot_errors_from_ChangeSet_calculation_are_gathered( ] = faulty_value # type:ignore[index] messages.append( "Invalid workitem 'REQ-002'. " - + f"Invalid field found: {key} '{faulty_value}' for '{attr}'" + + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" ) del titem["children"][1]["type"] tconfig = TEST_CONFIG["modules"][0] From b668306551dbf2b93f0db57a5344a39e56528664 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 3 Feb 2023 08:59:09 +0100 Subject: [PATCH 05/15] feat: Add error handling of faulty data-types in modifications --- capella_rm_bridge/changeset/change.py | 13 +++++++--- tests/test_changeset.py | 34 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index be41f2d..fe5a513 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -1087,9 +1087,16 @@ def attribute_definition_mod_action( return None return {"parent": decl.UUIDReference(attrdef.uuid), "modify": mods} except KeyError: - return self.attribute_definition_create_action( - name, data, reqtype.identifier - ) + try: + return self.attribute_definition_create_action( + name, data, reqtype.identifier + ) + except act.InvalidAttributeDefinition as error: + self._handle_user_error( + f"In RequirementType {reqtype.long_name!r}: " + + error.args[0] + ) + return None def make_requirement_delete_actions( diff --git a/tests/test_changeset.py b/tests/test_changeset.py index 699de9e..2105913 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -346,6 +346,40 @@ def test_faulty_attribute_values_log_InvalidFieldValue_as_error( assert caplog.messages[0].endswith(message_end) + def test_faulty_data_types_log_InvalidAttributeDefinition_as_error( + self, + migration_model: capellambse.MelodyModel, + caplog: pytest.LogCaptureFixture, + ) -> None: + tracker = copy.deepcopy(self.tracker) + reqtype = tracker["requirement_types"]["system_requirement"] + reqtype["attributes"]["Not-Defined"] = { # type: ignore[call-overload] + "type": "Enum", + "data_type": "Not-Defined", + } + + with caplog.at_level(logging.ERROR): + self.tracker_change(migration_model, tracker, gather_logs=False) + + assert caplog.messages[0].endswith(INVALID_ATTR_DEF_ERROR_MSG) + + def test_InvalidAttributeDefinition_errors_are_gathered( + self, migration_model: capellambse.MelodyModel + ) -> None: + """Test faulty field data are gathered in errors.""" + tracker = copy.deepcopy(self.tracker) + reqtype = tracker["requirement_types"]["system_requirement"] + reqtype["attributes"]["Not-Defined"] = { # type: ignore[call-overload] + "type": "Enum", + "data_type": "Not-Defined", + } + + change = self.tracker_change( + migration_model, tracker, gather_logs=True + ) + + assert change.errors == [INVALID_ATTR_DEF_ERROR_MSG] + @pytest.mark.integtest def test_calculate_change_sets( self, migration_model: capellambse.MelodyModel From fff347cbc06d5ef062266725afd58b3e9d73001a Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 3 Feb 2023 09:10:18 +0100 Subject: [PATCH 06/15] refactor: Get rid of many Deprecation warnings from capellambse --- capella_rm_bridge/auditing.py | 6 ++-- capella_rm_bridge/changeset/__init__.py | 2 +- capella_rm_bridge/changeset/change.py | 12 ++++---- capella_rm_bridge/changeset/find.py | 41 ++++++++++++++++--------- tests/test_changeset.py | 2 +- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/capella_rm_bridge/auditing.py b/capella_rm_bridge/auditing.py index 25c6c16..6991aad 100644 --- a/capella_rm_bridge/auditing.py +++ b/capella_rm_bridge/auditing.py @@ -362,7 +362,7 @@ def store_change( def _assign_module(self, change: Change) -> LiveDocID | TrackerID | None: try: obj = self.model.by_uuid(change.parent) - while not isinstance(obj, reqif.RequirementsModule): + while not isinstance(obj, reqif.CapellaModule): obj = obj.parent return obj.identifier except (KeyError, AttributeError): @@ -447,11 +447,11 @@ def _is_reqtype_change(self, change: Change) -> bool: reqif.AttributeDefinition, reqif.AttributeDefinitionEnumeration, reqif.DataTypeDefinition, - reqif.EnumDataTypeDefinition, + reqif.EnumerationDataTypeDefinition, reqif.EnumValue, reqif.ModuleType, reqif.RelationType, - reqif.RequirementsTypesFolder, + reqif.CapellaTypesFolder, reqif.RequirementType, } diff --git a/capella_rm_bridge/changeset/__init__.py b/capella_rm_bridge/changeset/__init__.py index 713412c..2e5e6ee 100644 --- a/capella_rm_bridge/changeset/__init__.py +++ b/capella_rm_bridge/changeset/__init__.py @@ -108,7 +108,7 @@ def calculate_change_set( except ( actiontypes.InvalidTrackerConfig, actiontypes.InvalidSnapshotModule, - change.MissingRequirementsModule, + change.MissingCapellaModule, ) as error: if gather_logs: message = _wrap_errors(module_id, [error.args[0]]) diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index fe5a513..86d3894 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -35,7 +35,7 @@ } -WorkItem = t.Union[reqif.Requirement, reqif.RequirementsFolder] +WorkItem = t.Union[reqif.Requirement, reqif.Folder] RMIdentifier = t.NewType("RMIdentifier", str) @@ -45,8 +45,8 @@ class _AttributeValueBuilder(t.NamedTuple): value: act.Primitive | None -class MissingRequirementsModule(Exception): - """A ``RequirementsModule`` with matching UUID could not be found.""" +class MissingCapellaModule(Exception): + """A ``CapellaModule`` with matching UUID could not be found.""" class TrackerChange: @@ -216,7 +216,7 @@ def check_requirements_module(self) -> dict[str, t.Any]: ) from error if self.req_module is None: - raise MissingRequirementsModule( + raise MissingCapellaModule( f"No RequirementsModule with UUID {module_uuid!r} found in " + repr(self.model.info) ) @@ -252,7 +252,7 @@ def invalidate_deletion(self, requirement: WorkItem) -> None: delete action was removed. """ key = "requirements" - if isinstance(requirement, reqif.RequirementsFolder): + if isinstance(requirement, reqif.Folder): key = "folders" try: @@ -903,7 +903,7 @@ def yield_requirements_mod_actions( cf_creations: list[dict[str, t.Any] | decl.UUIDReference] = [] containers = [cr_creations, cf_creations] child_mods: list[dict[str, t.Any]] = [] - if isinstance(req, reqif.RequirementsFolder): + if isinstance(req, reqif.Folder): child_req_ids = set[RMIdentifier]() child_folder_ids = set[RMIdentifier]() for child in children: diff --git a/capella_rm_bridge/changeset/find.py b/capella_rm_bridge/changeset/find.py index 60a21ac..e88ef1b 100644 --- a/capella_rm_bridge/changeset/find.py +++ b/capella_rm_bridge/changeset/find.py @@ -35,25 +35,29 @@ def _get( LOGGER.info("No %s found with %s: %r", types, attr, value) return None - def reqmodule(self, uuid: str) -> reqif.RequirementsModule | None: - """Try to return the ``RequirementsModule``.""" - return self._get(uuid, reqif.XT_MODULE, attr="uuid") + def reqmodule(self, uuid: str) -> reqif.CapellaModule | None: + """Try to return the ``CapellaModule``.""" + return self._get(uuid, reqif.CapellaModule.__name__, attr="uuid") def reqtypesfolder_by_identifier( self, identifier: int | str, below: crosslayer.BaseArchitectureLayer - | reqif.RequirementsModule + | reqif.CapellaModule | None = None, - ) -> reqif.RequirementsTypesFolder | None: + ) -> reqif.CapellaTypesFolder | None: """Try to return the ``RequirementTypesFolder``.""" - return self._get(str(identifier), reqif.XT_REQ_TYPES_F, below=below) + return self._get( + str(identifier), reqif.CapellaTypesFolder.__name__, below=below + ) def reqtype_by_identifier( self, identifier: int | str, below: reqif.ReqIFElement | None = None - ) -> reqif.RequirementsType | None: + ) -> reqif.RequirementType | None: """Try to return a ``RequirementType``.""" - return self._get(str(identifier), reqif.XT_REQ_TYPE, below=below) + return self._get( + str(identifier), reqif.RequirementType.__name__, below=below + ) def attribute_definition_by_identifier( self, xtype: str, identifier: str, below: reqif.ReqIFElement | None @@ -65,21 +69,27 @@ def work_item_by_identifier( self, identifier: int | str, below: reqif.ReqIFElement | None = None, - ) -> reqif.Requirement | reqif.RequirementsFolder | None: + ) -> reqif.Requirement | reqif.Folder | None: """Try to return a ``Requirement``/``RequirementsFolder``.""" return self._get( - str(identifier), reqif.XT_REQUIREMENT, reqif.XT_FOLDER, below=below + str(identifier), + reqif.Requirement.__name__, + reqif.Folder.__name__, + below=below, ) def enum_data_type_definition_by_long_name( self, long_name: str, below: reqif.ReqIFElement | None - ) -> reqif.EnumDataTypeDefinition | None: - """Try to return an ``EnumDataTypeDefinition``. + ) -> reqif.EnumerationDataTypeDefinition | None: + """Try to return an ``EnumerationDataTypeDefinition``. The object is matched with given ``long_name``. """ return self._get( - long_name, reqif.XT_REQ_TYPE_ENUM, attr="long_name", below=below + long_name, + reqif.EnumerationDataTypeDefinition.__name__, + attr="long_name", + below=below, ) def enum_value_by_long_name( @@ -89,5 +99,6 @@ def enum_value_by_long_name( The object is matched with given ``long_name``. """ - xt = reqif.XT_REQ_TYPE_ATTR_ENUM - return self._get(long_name, xt, attr="long_name", below=below) + return self._get( + long_name, reqif.EnumValue.__name__, attr="long_name", below=below + ) diff --git a/tests/test_changeset.py b/tests/test_changeset.py index 2105913..d342f73 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -113,7 +113,7 @@ def test_init_on_missing_module_raises_MissingRequirementsModule( """ del clean_model.la.requirement_modules[0] - with pytest.raises(change.MissingRequirementsModule): + with pytest.raises(change.MissingCapellaModule): self.tracker_change(clean_model, TEST_SNAPSHOT[0]) def test_init_on_missing_module_id_raises_InvalidSnapshotModule( From b054207ddd25b522abb2133ce8200da8e09f2448 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 7 Feb 2023 13:53:14 +0100 Subject: [PATCH 07/15] fix: Create `ChangeSet`s in change-sets folder Previously the created `change-set.yaml` would've been overwritten on synchronizing multiple modules/live-docs. --- capella_rm_bridge/__main__.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/capella_rm_bridge/__main__.py b/capella_rm_bridge/__main__.py index 23823f9..9db37c3 100644 --- a/capella_rm_bridge/__main__.py +++ b/capella_rm_bridge/__main__.py @@ -19,7 +19,8 @@ from . import auditing -CHANGE_PATH = pathlib.Path("change-set.yaml") +CHANGE_FOLDER_PATH = pathlib.Path("change-sets") +CHANGE_FILENAME = "change-set.yaml" CHANGE_HISTORY_PATH = pathlib.Path("change.history") ERROR_PATH = pathlib.Path("change-errors.txt") LOGGER = logging.getLogger(__name__) @@ -30,6 +31,16 @@ def create_errors_statement(errors: cabc.Iterable[str]) -> str: return "\n".join(errors) +def write_change_set(change: str, module: dict[str, t.Any]) -> pathlib.Path: + """Create a change-set.yaml underneath the change-sets folder.""" + CHANGE_FOLDER_PATH.mkdir(parents=True, exist_ok=True) + mid = module["id"].replace("/", "~") + path = CHANGE_FOLDER_PATH / f"{mid}-{CHANGE_FILENAME}" + path.write_text(change, encoding="utf8") + LOGGER.info("Change-set file %s written.", str(path)) + return path + + @click.command() @click.option( "-c", @@ -150,15 +161,15 @@ def main( if change_set: change = decl.dump(change_set) - CHANGE_PATH.write_text(change, encoding="utf8") + change_path = write_change_set(change, module) with auditing.ChangeAuditor(model) as changed_objs: - decl.apply(model, CHANGE_PATH) + decl.apply(model, change_path) reporter.store_change( changed_objs, module["id"], module["category"] ) - if force: + if force or not errors: commit_message = reporter.create_commit_message(snapshot["metadata"]) print(commit_message) if reporter.store and not dry_run: @@ -170,13 +181,7 @@ def main( if save_error_log: ERROR_PATH.write_text(error_statement, encoding="utf8") LOGGER.info("Change-errors file %s written.", ERROR_PATH) - sys.exit(1) - else: - commit_message = reporter.create_commit_message(snapshot["metadata"]) - print(commit_message) - if reporter.store and not dry_run: - model.save(push=push, commit_msg=commit_message) report = reporter.get_change_report() if report and save_change_history: From 11ad70de9a09c31014a8edb770d121cb974d4f8d Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 7 Feb 2023 13:54:53 +0100 Subject: [PATCH 08/15] fix: Fix duplication of change actions in `force` mode --- capella_rm_bridge/changeset/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/capella_rm_bridge/changeset/__init__.py b/capella_rm_bridge/changeset/__init__.py index 2e5e6ee..93b0d9e 100644 --- a/capella_rm_bridge/changeset/__init__.py +++ b/capella_rm_bridge/changeset/__init__.py @@ -103,8 +103,10 @@ def calculate_change_set( else: actions.extend(tchange.actions) - if force and tchange.actions not in actions: - actions.extend(tchange.actions) + if force and tchange.actions: + for action in tchange.actions: + if action not in actions: + actions.append(action) except ( actiontypes.InvalidTrackerConfig, actiontypes.InvalidSnapshotModule, From dfe6796dd7128a650b5e8fca6febf5e063c52cd2 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 7 Feb 2023 13:57:14 +0100 Subject: [PATCH 09/15] feat: Anticipate ambiguous simple attributes The snapshot follows a schema which gives a hint on which attributes should be there. If any unknown/new attribute is in the snapshot, an `AttributeError` will be logged during the `ChangeSet` calculation. --- capella_rm_bridge/changeset/change.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index 86d3894..7274b9c 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -758,9 +758,14 @@ def requirement_type_mod_action( ) assert isinstance(reqtype, reqif.RequirementType) - mods = _compare_simple_attributes( - reqtype, item, filter=("attributes",) - ) + try: + mods = _compare_simple_attributes( + reqtype, item, filter=("attributes",) + ) + except AttributeError as error: + self._handle_user_error( + f"Invalid workitem '{identifier}'. {error.args[0]}" + ) attr_defs_deletions: list[decl.UUIDReference] = [ decl.UUIDReference(adef.uuid) @@ -819,9 +824,16 @@ def yield_requirements_mod_actions( from it. """ base = {"parent": decl.UUIDReference(req.uuid)} - mods = _compare_simple_attributes( - req, item, filter=("id", "type", "attributes", "children") - ) + try: + mods = _compare_simple_attributes( + req, item, filter=("id", "type", "attributes", "children") + ) + except AttributeError as error: + self._handle_user_error( + f"Invalid workitem '{item['id']}'. {error.args[0]}" + ) + return + req_type_id = RMIdentifier(item.get("type", "")) attributes_deletions = list[dict[str, t.Any]]() if req_type_id != req.type.identifier: @@ -933,6 +945,7 @@ def yield_requirements_mod_actions( f"Invalid workitem '{child['id']}'. " + error.args[0] ) + continue if creq.parent != req: container.append(decl.UUIDReference(creq.uuid)) @@ -1159,7 +1172,7 @@ def _compare_simple_attributes( converter = type_conversion.get(name, lambda i: i) converted_value = converter(value) - if getattr(req, name, None) != converted_value: + if getattr(req, name) != converted_value: mods[name] = value return mods From 6c1b473144ca3a950b9943061709997568f5aa9c Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 7 Feb 2023 14:01:40 +0100 Subject: [PATCH 10/15] test: Add test for simple attribute error handling --- tests/test_changeset.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_changeset.py b/tests/test_changeset.py index d342f73..370f2ed 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -363,6 +363,25 @@ def test_faulty_data_types_log_InvalidAttributeDefinition_as_error( assert caplog.messages[0].endswith(INVALID_ATTR_DEF_ERROR_MSG) + def test_faulty_simple_attributes_log_AttributeError( + self, + migration_model: capellambse.MelodyModel, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Test logging ``AttributeError`` on faulty simple attributes.""" + tracker = copy.deepcopy(self.tracker) + titem = tracker["items"][0] + titem["imagination"] = 1 + message_end = ( + "Invalid module 'project/space/example title'. Invalid " + "workitem 'REQ-001'. imagination isn't defined on Folder" + ) + + with caplog.at_level(logging.ERROR): + self.tracker_change(migration_model, tracker, gather_logs=False) + + assert caplog.messages[0].endswith(message_end) + def test_InvalidAttributeDefinition_errors_are_gathered( self, migration_model: capellambse.MelodyModel ) -> None: From 7f463de45ab1da18cedbaec3b7e86bd1a1775876 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 7 Feb 2023 14:41:16 +0100 Subject: [PATCH 11/15] fix: Nasty bug in `create_commit_message` Thanks to jamilraichouni for finding a dumb `KeyError` bug on `Deletion` events. --- capella_rm_bridge/auditing.py | 4 ++-- tests/test_auditing.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/capella_rm_bridge/auditing.py b/capella_rm_bridge/auditing.py index 6991aad..a5874fd 100644 --- a/capella_rm_bridge/auditing.py +++ b/capella_rm_bridge/auditing.py @@ -438,9 +438,9 @@ def _count_changes( return ext_count, mod_count, del_count, type_count def _is_reqtype_change(self, change: Change) -> bool: - if isinstance(change, Modification): + if isinstance(change, (Modification, Deletion)): obj = self.model.by_uuid(change.parent) - else: + elif isinstance(change, Extension): obj = self.model.by_uuid(change.uuid) return type(obj) in { diff --git a/tests/test_auditing.py b/tests/test_auditing.py index 6d775c9..732495a 100644 --- a/tests/test_auditing.py +++ b/tests/test_auditing.py @@ -336,6 +336,8 @@ def test_create_commit_message(migration_model: capellambse.MelodyModel): req_uuid = "163394f5-c1ba-4712-a238-b0b143c66aed" reqtypesfolder_uuid = "a15e8b60-bf39-47ba-b7c7-74ceecb25c9c" dtdef_uuid = "686e198b-8baf-49f9-9d85-24571bd05d93" + req = migration_model.by_uuid(req_uuid) + req.parent.requirements.remove(req) changes: list[auditing.Change] = [ auditing.Deletion( parent="9a9b5a8f-a6ad-4610-9e88-3b5e9c943c19", From 66ee70f9b522ff6305429597f1a9ce264df36a45 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 9 Feb 2023 11:01:28 +0100 Subject: [PATCH 12/15] fix: Remove `ci.skip` from commit summary Added `ci.skip` to `push_options` of `MelodyModel.save`. --- capella_rm_bridge/__main__.py | 4 +++- capella_rm_bridge/auditing.py | 2 +- tests/test_auditing.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/capella_rm_bridge/__main__.py b/capella_rm_bridge/__main__.py index 9db37c3..7a719da 100644 --- a/capella_rm_bridge/__main__.py +++ b/capella_rm_bridge/__main__.py @@ -173,7 +173,9 @@ def main( commit_message = reporter.create_commit_message(snapshot["metadata"]) print(commit_message) if reporter.store and not dry_run: - model.save(push=push, commit_msg=commit_message) + model.save( + push=push, commit_msg=commit_message, push_options=["skip.ci"] + ) if errors: error_statement = create_errors_statement(errors) diff --git a/capella_rm_bridge/auditing.py b/capella_rm_bridge/auditing.py index a5874fd..135b8d5 100644 --- a/capella_rm_bridge/auditing.py +++ b/capella_rm_bridge/auditing.py @@ -407,7 +407,7 @@ def create_commit_message(self, tool_metadata: dict[str, str]) -> str: main_message = generate_main_message(self.categories.items()) main = "\n".join((main_message, "\n".join(list_lines) + "\n")) - summary = f"{summary} from rev.{tool_metadata['revision']} [skip ci]\n" + summary = f"{summary} from rev.{tool_metadata['revision']}\n" rm_bridge_dependencies = get_dependencies() dependencies = "\n".join( ( diff --git a/tests/test_auditing.py b/tests/test_auditing.py index 732495a..e10d92f 100644 --- a/tests/test_auditing.py +++ b/tests/test_auditing.py @@ -374,7 +374,7 @@ def test_create_commit_message(migration_model: capellambse.MelodyModel): commit_message = reporter.create_commit_message(tool_metadata) assert commit_message.startswith( - "Updated model with RM content from rev.123 [skip ci]\n" + "Updated model with RM content from rev.123\n" "\n" "Synchronized 1 category1 and 1 category2:\n" "- 1: created: 1; updated: 1; deleted: 1; type-changes: 0\n" From 1a2607d2518b60e1a107f52a90b2695caec85d4d Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 9 Feb 2023 15:48:11 +0100 Subject: [PATCH 13/15] feat: Implement `Folder` identification based on `children` attribute --- capella_rm_bridge/changeset/change.py | 4 +-- tests/data/changesets/create.yaml | 2 -- tests/data/model/RM Bridge.capella | 2 -- tests/data/snapshots/snapshot.yaml | 6 ----- tests/data/snapshots/snapshot1.yaml | 3 --- tests/data/snapshots/snapshot2.yaml | 1 - tests/test_changeset.py | 36 ++++++++++++++++++++++++--- 7 files changed, 35 insertions(+), 19 deletions(-) diff --git a/capella_rm_bridge/changeset/change.py b/capella_rm_bridge/changeset/change.py index 7274b9c..fc3f22e 100644 --- a/capella_rm_bridge/changeset/change.py +++ b/capella_rm_bridge/changeset/change.py @@ -483,7 +483,7 @@ def yield_requirements_create_actions( base["folders"] = [] child: act.WorkItem for child in item["children"]: - key = "folders" if child.get("children") else "requirements" + key = "folders" if "children" in child else "requirements" creq = self.reqfinder.work_item_by_identifier(child["id"]) if creq is None: child_actions = self.yield_requirements_create_actions( @@ -920,7 +920,7 @@ def yield_requirements_mod_actions( child_folder_ids = set[RMIdentifier]() for child in children: cid = RMIdentifier(str(child["id"])) - if child.get("children", []): + if "children" in child: key = "folders" child_folder_ids.add(cid) else: diff --git a/tests/data/changesets/create.yaml b/tests/data/changesets/create.yaml index df2a203..c7b49da 100644 --- a/tests/data/changesets/create.yaml +++ b/tests/data/changesets/create.yaml @@ -14,8 +14,6 @@ values: - long_name: Unset promise_id: EnumValue Type Unset - - long_name: Folder - promise_id: EnumValue Type Folder - long_name: Functional promise_id: EnumValue Type Functional promise_id: EnumerationDataTypeDefinition Type diff --git a/tests/data/model/RM Bridge.capella b/tests/data/model/RM Bridge.capella index 1b7fd01..7de9981 100644 --- a/tests/data/model/RM Bridge.capella +++ b/tests/data/model/RM Bridge.capella @@ -1839,8 +1839,6 @@ The predator is far away id="686e198b-8baf-49f9-9d85-24571bd05d93" ReqIFLongName="Type"> - diff --git a/tests/data/snapshots/snapshot.yaml b/tests/data/snapshots/snapshot.yaml index 1bf9270..95ab78d 100644 --- a/tests/data/snapshots/snapshot.yaml +++ b/tests/data/snapshots/snapshot.yaml @@ -7,7 +7,6 @@ data_types: # Enumeration Data Type Definitions Type: - Unset - - Folder - Functional Release: - Feature Rel. 1 @@ -47,9 +46,6 @@ text:

Test Description

type: system_requirement # WorkItemType ID NOT name - attributes: - Type: [Folder] # artifact from CODEBEAMER and cleaned in RM Bridge - children: # Folder b/c non-empty children - id: REQ-002 long_name: Function Requirement @@ -65,8 +61,6 @@ - id: REQ-003 long_name: Kinds type: software_requirement - attributes: - Type: [Folder] children: - id: REQ-004 long_name: Kind Requirement diff --git a/tests/data/snapshots/snapshot1.yaml b/tests/data/snapshots/snapshot1.yaml index 680594d..9b3152b 100644 --- a/tests/data/snapshots/snapshot1.yaml +++ b/tests/data/snapshots/snapshot1.yaml @@ -7,7 +7,6 @@ data_types: Type: - Unset - - Folder - Functional - Non-Functional Release: @@ -68,8 +67,6 @@ long_name: Functional Requirements text:

Brand new

type: software_requirement - attributes: - Type: [Folder] children: - id: REQ-004 long_name: Function Requirement diff --git a/tests/data/snapshots/snapshot2.yaml b/tests/data/snapshots/snapshot2.yaml index 4f84b09..b7f5b89 100644 --- a/tests/data/snapshots/snapshot2.yaml +++ b/tests/data/snapshots/snapshot2.yaml @@ -7,7 +7,6 @@ data_types: Type: - Unset - - Folder - Non-Functional requirement_types: system_requirement: diff --git a/tests/test_changeset.py b/tests/test_changeset.py index 370f2ed..03ce91c 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -266,6 +266,22 @@ def test_InvalidAttributeDefinition_errors_are_gathered( assert change.errors == [INVALID_ATTR_DEF_ERROR_MSG] + def test_requirements_with_empty_children_are_rendered_as_folders( + self, clean_model: capellambse.MelodyModel + ) -> None: + tracker = copy.deepcopy(self.tracker) + tracker["items"][0]["children"][1]["children"][0]["children"] = [] + + change_set = self.tracker_change( + clean_model, tracker, gather_logs=True + ) + + assert change_set and (change := change_set.actions[0]["extend"]) + assert ( + change["folders"][0]["folders"][0]["folders"][0]["identifier"] + == "REQ-004" + ) + @pytest.mark.integtest def test_calculate_change_sets( self, clean_model: capellambse.MelodyModel @@ -399,6 +415,19 @@ def test_InvalidAttributeDefinition_errors_are_gathered( assert change.errors == [INVALID_ATTR_DEF_ERROR_MSG] + def test_requirements_with_empty_children_are_rendered_as_folders( + self, clean_model: capellambse.MelodyModel + ) -> None: + tracker = copy.deepcopy(self.tracker) + tracker["items"][1]["children"][0]["children"] = [] + + change_set = self.tracker_change( + clean_model, tracker, gather_logs=True + ) + + assert change_set and (change := change_set.actions[0]["extend"]) + assert change["folders"][1]["folders"][0]["identifier"] == "REQ-004" + @pytest.mark.integtest def test_calculate_change_sets( self, migration_model: capellambse.MelodyModel @@ -628,8 +657,8 @@ def test_forced_calculation_produces_change_set_on_AttributeDefinition_error( assert attr_def["long_name"] != missing_enumdt folder = change_set["extend"]["folders"][0] - for req in folder["requirements"]: - for attr_value in req["attributes"]: + for folder in folder["folders"]: + for attr_value in folder.get("attributes", []): assert ( missing_enumdt not in attr_value["definition"].identifier ) @@ -645,7 +674,7 @@ def test_snapshot_errors_from_ChangeSet_calculation_are_gathered( snapshot = copy.deepcopy(TEST_SNAPSHOT[0]) titem = snapshot["items"][0] first_child = titem["children"][0] - titem["attributes"]["Test"] = 1 # type: ignore[index] + titem["attributes"] = {"Test": 1} # type: ignore[index] messages = [ "Invalid workitem 'REQ-001'. " "Invalid field found: field name 'Test' not defined in " @@ -660,6 +689,7 @@ def test_snapshot_errors_from_ChangeSet_calculation_are_gathered( + f"Invalid field found: {key} {faulty_value!r} for {attr!r}" ) del titem["children"][1]["type"] + titem["children"][1]["attributes"] = {"Test": 1} tconfig = TEST_CONFIG["modules"][0] messages.append( "Invalid workitem 'REQ-003'. Missing type but attributes found" From 244b9b127eee33affdd1d98b262e825510856f9b Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 9 Feb 2023 16:13:16 +0100 Subject: [PATCH 14/15] fix: Remove any previously generated `change-set` --- capella_rm_bridge/__main__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/capella_rm_bridge/__main__.py b/capella_rm_bridge/__main__.py index 7a719da..18f843d 100644 --- a/capella_rm_bridge/__main__.py +++ b/capella_rm_bridge/__main__.py @@ -36,6 +36,9 @@ def write_change_set(change: str, module: dict[str, t.Any]) -> pathlib.Path: CHANGE_FOLDER_PATH.mkdir(parents=True, exist_ok=True) mid = module["id"].replace("/", "~") path = CHANGE_FOLDER_PATH / f"{mid}-{CHANGE_FILENAME}" + if path.is_file(): + path.unlink(missing_ok=True) + path.write_text(change, encoding="utf8") LOGGER.info("Change-set file %s written.", str(path)) return path From aa62394107c45325412fad32cd21b1bf9f930a34 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 9 Feb 2023 17:01:25 +0100 Subject: [PATCH 15/15] fix: Anticipate deletions ef entire `ElementList` --- capella_rm_bridge/auditing.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/capella_rm_bridge/auditing.py b/capella_rm_bridge/auditing.py index 135b8d5..48aee8f 100644 --- a/capella_rm_bridge/auditing.py +++ b/capella_rm_bridge/auditing.py @@ -231,14 +231,14 @@ def __audit(self, event: str, args: tuple[t.Any, ...]) -> None: oval = getattr(obj, attr_name) nrepr = self._get_value_repr(value) orepr = self._get_value_repr(oval) - params = (obj.uuid, attr_name, nrepr, orepr) + events = [EventType(obj.uuid, attr_name, nrepr, orepr)] elif event.endswith("setitem"): assert len(args) == 4 obj, attr_name, index, value = args nrepr = self._get_value_repr(value) oval = getattr(obj, attr_name)[index] orepr = self._get_value_repr(oval) - params = (obj.uuid, attr_name, nrepr, orepr) + events = [EventType(obj.uuid, attr_name, nrepr, orepr)] elif event.endswith("delete"): assert len(args) == 3 obj, attr_name, index = args @@ -247,22 +247,34 @@ def __audit(self, event: str, args: tuple[t.Any, ...]) -> None: if index is not None: oval = oval[index] - assert isinstance(oval, common.GenericElement) - orepr = self._get_value_repr(oval) - params = (obj.uuid, attr_name, orepr, oval.uuid) + if not isinstance(oval, common.GenericElement): + assert isinstance(oval, common.ElementList) + events = [] + assert EventType is Deletion + for elt in oval: + event_type = EventType( + obj.uuid, + attr_name, + self._get_value_repr(elt), + elt.uuid, + ) + events.append(event_type) + else: + orepr = self._get_value_repr(oval) + events = [EventType(obj.uuid, attr_name, orepr, oval.uuid)] elif event.endswith("insert"): assert len(args) == 4 obj, attr_name, _, value = args nrepr = self._get_value_repr(value) assert isinstance(value, common.GenericElement) - params = (obj.uuid, attr_name, nrepr, value.uuid) + events = [EventType(obj.uuid, attr_name, nrepr, value.uuid)] elif event.endswith("create"): assert len(args) == 3 obj, attr_name, value = args repr = self._get_value_repr(value) - params = (obj.uuid, attr_name, repr, value.uuid) + events = [EventType(obj.uuid, attr_name, repr, value.uuid)] - self.context.append(EventType(*params)) + self.context.extend(events) def _get_value_repr(self, value: t.Any) -> str | t.Any: if hasattr(value, "_short_repr_"):