From 64d06926dd09e400dfb3c7e17254d910108ffa82 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 12 Dec 2024 16:41:46 +0100 Subject: [PATCH 01/10] feat: Add `layer` custom field Added `layer` custom field to generic and diagram serializer. --- .../converters/element_converter.py | 19 ++++++ .../converters/model_converter.py | 31 ++++++++- tests/test_workitem_attachments.py | 67 ++++++++----------- 3 files changed, 76 insertions(+), 41 deletions(-) diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 56b9409..476d691 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -1,6 +1,7 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 """Objects for serialization of capella objects to workitems.""" + from __future__ import annotations import collections @@ -33,6 +34,14 @@ logger = logging.getLogger(__name__) C2P_IMAGE_PREFIX = "__C2P__" JINJA_RENDERED_IMG_CLS = "jinja-rendered-image" +ARCHITECTURE_LAYERS: dict[str, str] = { + "common": "Common", + "oa": "Operational Analysis", + "sa": "System Analysis", + "la": "Logical Architecture", + "pa": "Physical Architecture", + "epbs": "EPBS", +} def resolve_element_type(type_: str) -> str: @@ -424,6 +433,10 @@ def __generic_work_item( obj, raw_description or markupsafe.Markup("") ) converter_data.description_references = uuids + layer = polarion_api.TextContent( + type="string", + value=ARCHITECTURE_LAYERS.get(converter_data.layer, "UNKNOWN"), + ) requirement_types = self._get_requirement_types_text(obj) converter_data.work_item = data_model.CapellaWorkItem( @@ -433,6 +446,7 @@ def __generic_work_item( uuid_capella=obj.uuid, description=polarion_api.HtmlContent(value), status="open", + layer=layer, **requirement_types, # type:ignore[arg-type] ) assert converter_data.work_item is not None @@ -451,6 +465,10 @@ def _diagram( assert converter_data.work_item is not None assert isinstance(diagram, m.Diagram) work_item_id = converter_data.work_item.id + layer = polarion_api.TextContent( + type="string", + value=ARCHITECTURE_LAYERS.get(converter_data.layer, "UNKNOWN"), + ) diagram_html, attachment = self._draw_diagram_svg( diagram, @@ -473,6 +491,7 @@ def _diagram( uuid_capella=diagram.uuid, description=polarion_api.HtmlContent(diagram_html), status="open", + layer=layer, ) if attachment: self._add_attachment(converter_data.work_item, attachment) diff --git a/capella2polarion/converters/model_converter.py b/capella2polarion/converters/model_converter.py index 07204c3..eb7d331 100644 --- a/capella2polarion/converters/model_converter.py +++ b/capella2polarion/converters/model_converter.py @@ -9,6 +9,7 @@ import capellambse import polarion_rest_api_client as polarion_api +from capellambse import model as m from capella2polarion import data_model from capella2polarion.connectors import polarion_repo @@ -63,8 +64,9 @@ def read_model( if config.diagram_config: for d in self.model.diagrams: + layer = get_layer_name(d) self.converter_session[d.uuid] = data_session.ConverterData( - "", config.diagram_config, d + layer, config.diagram_config, d ) if missing_types: @@ -176,3 +178,30 @@ def generate_work_item_links( link_serializer.create_grouped_back_link_fields( converter_data.work_item, local_back_links ) + + +def get_layer_name(diagram: m.Diagram) -> str: + """Return the layer name for a diagram.""" + match diagram.type.name: + case ( + "OEBD" + | "OAIB" + | "OAB" + | "OABD" + | "ORB" + | "OES" + | "OAS" + | "OPD" + | "OCB" + ): + return "oa" + case "CM" | "MB" | "CC" | "MCB" | "SFBD" | "SDFB" | "SAB" | "CSA": + return "sa" + case "LCBD" | "LFBD" | "LDFB" | "LAB" | "CRR": + return "la" + case "PFBD" | "PDFB" | "PCBD" | "PAB" | "PPD": + return "pa" + case "EAB" | "CIBD": + return "epbs" + case _: + return "common" diff --git a/tests/test_workitem_attachments.py b/tests/test_workitem_attachments.py index 4c19f99..5642c5c 100644 --- a/tests/test_workitem_attachments.py +++ b/tests/test_workitem_attachments.py @@ -98,34 +98,38 @@ def set_attachment_ids(attachments: list[polarion_api.WorkItemAttachment]): def test_diagram_no_attachments(model: capellambse.MelodyModel): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) + converter.generate_work_items( polarion_repo.PolarionDataRepository(), False, False ) + work_item = converter.converter_session[TEST_DIAG_UUID].work_item assert work_item is not None assert work_item.attachments == [] def test_diagram_has_attachments(model: capellambse.MelodyModel): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) + converter.generate_work_items( polarion_repo.PolarionDataRepository(), False, True ) work_item = converter.converter_session[TEST_DIAG_UUID].work_item assert work_item is not None - assert len(work_item.attachments) == 2 @@ -134,11 +138,11 @@ def test_diagram_attachments_new( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID)] ) - worker.project_client.work_items.get.return_value = ( data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID) ) @@ -146,15 +150,13 @@ def test_diagram_attachments_new( worker.project_client.work_items.attachments.create.side_effect = ( set_attachment_ids ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -162,21 +164,18 @@ def test_diagram_attachments_new( assert worker.project_client.work_items.update.call_count == 1 assert worker.project_client.work_items.attachments.create.call_count == 1 assert worker.project_client.work_items.attachments.get_all.call_count == 0 - created_attachments: list[polarion_api.WorkItemAttachment] = ( worker.project_client.work_items.attachments.create.call_args.args[0] ) work_item: data_model.CapellaWorkItem = ( worker.project_client.work_items.update.call_args.args[0] ) - assert len(created_attachments) == 2 assert created_attachments[0].title == created_attachments[1].title assert ( created_attachments[0].file_name[:3] == created_attachments[0].file_name[:3] ) - assert work_item.description.value == TEST_DIAG_DESCR.format( title="Diagram", attachment_id="1-__C2P__diagram.svg", @@ -191,10 +190,9 @@ def test_new_diagram( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") - checksum = json.dumps({"__C2P__WORK_ITEM": DIAGRAM_WI_CHECKSUM}) - worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ data_model.CapellaWorkItem( @@ -202,7 +200,6 @@ def test_new_diagram( ) ] ) - worker.project_client.work_items.get.return_value = ( data_model.CapellaWorkItem( WORKITEM_ID, uuid_capella=TEST_DIAG_UUID, checksum=checksum @@ -212,15 +209,13 @@ def test_new_diagram( worker.project_client.work_items.attachments.create.side_effect = ( set_attachment_ids ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -241,6 +236,7 @@ def test_diagram_attachments_updated( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID)] @@ -259,7 +255,6 @@ def test_diagram_attachments_updated( file_name="__C2P__diagram.png", ), ] - worker.project_client.work_items.get.return_value = ( data_model.CapellaWorkItem( WORKITEM_ID, @@ -267,20 +262,17 @@ def test_diagram_attachments_updated( attachments=existing_attachments, ) ) - worker.project_client.work_items.attachments.get_all = mock.MagicMock() worker.project_client.work_items.attachments.get_all.return_value = ( existing_attachments ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -289,11 +281,9 @@ def test_diagram_attachments_updated( assert worker.project_client.work_items.attachments.create.call_count == 0 assert worker.project_client.work_items.attachments.update.call_count == 2 assert worker.project_client.work_items.attachments.get_all.call_count == 1 - work_item: data_model.CapellaWorkItem = ( worker.project_client.work_items.update.call_args.args[0] ) - assert work_item.description.value == TEST_DIAG_DESCR.format( title="Diagram", attachment_id="SVG-ATTACHMENT", @@ -306,6 +296,7 @@ def test_diagram_attachments_unchanged_work_item_changed( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") diagram_work_item = data_model.CapellaWorkItem( WORKITEM_ID, @@ -339,15 +330,13 @@ def test_diagram_attachments_unchanged_work_item_changed( worker.project_client.work_items.attachments.get_all.return_value = ( diagram_work_item.attachments ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -357,11 +346,9 @@ def test_diagram_attachments_unchanged_work_item_changed( assert worker.project_client.work_items.attachments.get_all.call_count == 1 assert worker.project_client.work_items.attachments.create.call_count == 0 assert worker.project_client.work_items.attachments.update.call_count == 0 - work_item: data_model.CapellaWorkItem = ( worker.project_client.work_items.update.call_args.args[0] ) - assert work_item.description.value == TEST_DIAG_DESCR.format( title="Diagram", attachment_id="SVG-ATTACHMENT", @@ -374,6 +361,7 @@ def test_diagram_attachments_fully_unchanged( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ @@ -384,15 +372,13 @@ def test_diagram_attachments_fully_unchanged( ) ] ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item( converter.converter_session[TEST_DIAG_UUID] ) @@ -557,6 +543,7 @@ def test_diagram_delete_attachments( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ @@ -596,9 +583,9 @@ def test_diagram_delete_attachments( ] converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - "", + model_converter.get_layer_name(diag), converter_config.CapellaTypeConfig("diagram", "diagram", []), - model.diagrams.by_uuid(TEST_DIAG_UUID), + diag, ) converter.generate_work_items(worker.polarion_data_repo, False, True) From ee26a4f936645a7e072b894f1ed6d8e9cc3e55e5 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 12 Dec 2024 17:16:25 +0100 Subject: [PATCH 02/10] test: Improve tests --- tests/test_elements.py | 16 ++-- tests/test_workitem_attachments.py | 133 +++++++++++------------------ 2 files changed, 59 insertions(+), 90 deletions(-) diff --git a/tests/test_elements.py b/tests/test_elements.py index a0f5424..08672d6 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -50,6 +50,7 @@ TEST_ACTOR_UUID = "08e02248-504d-4ed8-a295-c7682a614f66" TEST_PHYS_COMP = "b9f9a83c-fb02-44f7-9123-9d86326de5f1" TEST_PHYS_NODE = "8a6d68c8-ac3d-4654-a07e-ada7adeed09f" +TEST_PHYS_FNC = "11906f7b-3ae9-4343-b998-95b170be2e2b" TEST_SCENARIO = "afdaa095-e2cd-4230-b5d3-6cb771a90f51" TEST_CAP_REAL = "b80b3141-a7fc-48c7-84b2-1467dcef5fce" TEST_CONSTRAINT = "95cbd4af-7224-43fe-98cb-f13dda540b8e" @@ -1861,7 +1862,6 @@ def test_generic_work_item( @staticmethod def test_add_context_diagram(model: capellambse.MelodyModel): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" type_config = converter_config.CapellaTypeConfig( "test", "add_context_diagram", [] ) @@ -1869,16 +1869,16 @@ def test_add_context_diagram(model: capellambse.MelodyModel): model, polarion_repo.PolarionDataRepository(), { - uuid: data_session.ConverterData( + TEST_PHYS_FNC: data_session.ConverterData( "pa", type_config, - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) }, True, ) - work_item = serializer.serialize(uuid) + work_item = serializer.serialize(TEST_PHYS_FNC) assert work_item is not None assert "context_diagram" in work_item.additional_attributes @@ -2154,7 +2154,6 @@ def test_read_config_tree_view_with_params( with mock.patch.object( context.ContextDiagram, "render" ) as wrapped_render: - wis = serializer.serialize_all() _ = wis[0].attachments[0].content_bytes @@ -2204,7 +2203,6 @@ def test_read_config_links(caplog: pytest.LogCaptureFixture): @staticmethod def test_add_context_diagram_with_caption(model: capellambse.MelodyModel): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" type_config = converter_config.CapellaTypeConfig( "test", "add_context_diagram", [] ) @@ -2212,17 +2210,17 @@ def test_add_context_diagram_with_caption(model: capellambse.MelodyModel): model, polarion_repo.PolarionDataRepository(), { - uuid: data_session.ConverterData( + TEST_PHYS_FNC: data_session.ConverterData( "pa", type_config, - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) }, True, generate_figure_captions=True, ) - work_item = serializer.serialize(uuid) + work_item = serializer.serialize(TEST_PHYS_FNC) assert work_item is not None assert "context_diagram" in work_item.additional_attributes diff --git a/tests/test_workitem_attachments.py b/tests/test_workitem_attachments.py index 5642c5c..d1a82e7 100644 --- a/tests/test_workitem_attachments.py +++ b/tests/test_workitem_attachments.py @@ -21,16 +21,16 @@ # pylint: disable=relative-beyond-top-level, useless-suppression from .conftest import TEST_DIAGRAM_CACHE -from .test_elements import TEST_DIAG_DESCR +from .test_elements import TEST_DIAG_DESCR, TEST_PHYS_FNC DIAGRAM_WI_CHECKSUM = ( - "76fc1f7e4b73891488de7e47de8ef75fc24e85fc3cdde80661503201e70b1733" + "1239ced17306dc92213cd1b729e3652a1cbc9b07997683ed36033a9d05adcb75" ) WI_CONTEXT_DIAGRAM_CHECKSUM = ( - "0ed1417e8e4717524bc91162dcf8633afca686e93f8b036d0bc48d81f0444f56" + "69d8b2ca4e690ccaf70fff16d2e42bbf2ecf434307d49e020d2138160ba35cb2" ) CONTEXT_DIAGRAM_CHECKSUM = ( - "2b86192f2f65353512e1b4af0e652577d0ca3d0cf8595f5dcfba7d52bcb6d702" + "572cb7ba53bcde56638a119fafc1304af294467d8c851f4b2cc35ce2f5d231eb" ) TEST_DIAG_UUID = "_APOQ0QPhEeynfbzU12yy7w" @@ -62,6 +62,21 @@ ) +# pylint: disable=redefined-outer-name +@pytest.fixture +def converter( + model: capellambse.MelodyModel, +) -> model_converter.ModelConverter: + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) + converter = model_converter.ModelConverter(model, "TEST") + converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( + model_converter.get_layer_name(diag), + converter_config.CapellaTypeConfig("diagram", "diagram", []), + diag, + ) + return converter + + @pytest.fixture def worker(monkeypatch: pytest.MonkeyPatch): mock_api_client = mock.MagicMock(spec=polarion_api.PolarionClient) @@ -97,17 +112,11 @@ def set_attachment_ids(attachments: list[polarion_api.WorkItemAttachment]): counter += 1 -def test_diagram_no_attachments(model: capellambse.MelodyModel): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) - +def test_diagram_no_attachments(converter: model_converter.ModelConverter): converter.generate_work_items( - polarion_repo.PolarionDataRepository(), False, False + polarion_repo.PolarionDataRepository(), + generate_links=False, + generate_attachments=False, ) work_item = converter.converter_session[TEST_DIAG_UUID].work_item @@ -115,17 +124,11 @@ def test_diagram_no_attachments(model: capellambse.MelodyModel): assert work_item.attachments == [] -def test_diagram_has_attachments(model: capellambse.MelodyModel): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) - +def test_diagram_has_attachments(converter: model_converter.ModelConverter): converter.generate_work_items( - polarion_repo.PolarionDataRepository(), False, True + polarion_repo.PolarionDataRepository(), + generate_links=False, + generate_attachments=True, ) work_item = converter.converter_session[TEST_DIAG_UUID].work_item @@ -135,11 +138,9 @@ def test_diagram_has_attachments(model: capellambse.MelodyModel): # pylint: disable=redefined-outer-name def test_diagram_attachments_new( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID)] ) @@ -150,11 +151,6 @@ def test_diagram_attachments_new( worker.project_client.work_items.attachments.create.side_effect = ( set_attachment_ids ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -187,11 +183,9 @@ def test_diagram_attachments_new( # pylint: disable=redefined-outer-name def test_new_diagram( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") checksum = json.dumps({"__C2P__WORK_ITEM": DIAGRAM_WI_CHECKSUM}) worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ @@ -209,11 +203,6 @@ def test_new_diagram( worker.project_client.work_items.attachments.create.side_effect = ( set_attachment_ids ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -233,11 +222,9 @@ def test_new_diagram( def test_diagram_attachments_updated( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID)] ) @@ -266,11 +253,6 @@ def test_diagram_attachments_updated( worker.project_client.work_items.attachments.get_all.return_value = ( existing_attachments ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -293,11 +275,9 @@ def test_diagram_attachments_updated( def test_diagram_attachments_unchanged_work_item_changed( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") diagram_work_item = data_model.CapellaWorkItem( WORKITEM_ID, uuid_capella=TEST_DIAG_UUID, @@ -330,11 +310,6 @@ def test_diagram_attachments_unchanged_work_item_changed( worker.project_client.work_items.attachments.get_all.return_value = ( diagram_work_item.attachments ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -358,11 +333,9 @@ def test_diagram_attachments_unchanged_work_item_changed( def test_diagram_attachments_fully_unchanged( - model: capellambse.MelodyModel, + converter: model_converter.ModelConverter, worker: polarion_worker.CapellaPolarionWorker, ): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ data_model.CapellaWorkItem( @@ -372,11 +345,6 @@ def test_diagram_attachments_fully_unchanged( ) ] ) - converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( - model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), - diag, - ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( @@ -393,16 +361,15 @@ def test_add_context_diagram( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( - [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=uuid)] + [data_model.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_PHYS_FNC)] ) - converter.converter_session[uuid] = data_session.ConverterData( - "", + converter.converter_session[TEST_PHYS_FNC] = data_session.ConverterData( + "pa", converter_config.CapellaTypeConfig("test", "add_context_diagram", []), - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) worker.project_client.work_items.attachments.create = mock.MagicMock() @@ -412,7 +379,9 @@ def test_add_context_diagram( converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item(converter.converter_session[uuid]) + worker.compare_and_update_work_item( + converter.converter_session[TEST_PHYS_FNC] + ) assert worker.project_client.work_items.update.call_count == 1 assert worker.project_client.work_items.attachments.create.call_count == 1 @@ -445,13 +414,12 @@ def test_update_context_diagram_no_changes( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ data_model.CapellaWorkItem( WORKITEM_ID, - uuid_capella=uuid, + uuid_capella=TEST_PHYS_FNC, checksum=json.dumps( { "__C2P__WORK_ITEM": WI_CONTEXT_DIAGRAM_CHECKSUM, @@ -462,15 +430,17 @@ def test_update_context_diagram_no_changes( ] ) - converter.converter_session[uuid] = data_session.ConverterData( - "", + converter.converter_session[TEST_PHYS_FNC] = data_session.ConverterData( + "pa", converter_config.CapellaTypeConfig("test", "add_context_diagram", []), - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) with mock.patch.object(context.ContextDiagram, "render") as wrapped_render: converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item(converter.converter_session[uuid]) + worker.compare_and_update_work_item( + converter.converter_session[TEST_PHYS_FNC] + ) assert worker.project_client.work_items.update.call_count == 0 assert worker.project_client.work_items.attachments.update.call_count == 0 @@ -481,13 +451,12 @@ def test_update_context_diagram_with_changes( model: capellambse.MelodyModel, worker: polarion_worker.CapellaPolarionWorker, ): - uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b" converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ data_model.CapellaWorkItem( WORKITEM_ID, - uuid_capella=uuid, + uuid_capella=TEST_PHYS_FNC, checksum=json.dumps( { "__C2P__WORK_ITEM": WI_CONTEXT_DIAGRAM_CHECKSUM, @@ -498,10 +467,10 @@ def test_update_context_diagram_with_changes( ] ) - converter.converter_session[uuid] = data_session.ConverterData( + converter.converter_session[TEST_PHYS_FNC] = data_session.ConverterData( "", converter_config.CapellaTypeConfig("test", "add_context_diagram", []), - model.by_uuid(uuid), + model.by_uuid(TEST_PHYS_FNC), ) worker.project_client.work_items.attachments.get_all.return_value = [ polarion_api.WorkItemAttachment( @@ -532,7 +501,9 @@ def test_update_context_diagram_with_changes( 'width="100" height="100">' ) converter.generate_work_items(worker.polarion_data_repo, False, True) - worker.compare_and_update_work_item(converter.converter_session[uuid]) + worker.compare_and_update_work_item( + converter.converter_session[TEST_PHYS_FNC] + ) assert worker.project_client.work_items.update.call_count == 1 assert worker.project_client.work_items.attachments.update.call_count == 2 From fac39491757d48936a957318626b7f2f0d4d4962 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 12 Dec 2024 19:16:01 +0100 Subject: [PATCH 03/10] feat(serializer): Add `add_attributes` serializer For now this serializer is only supports adding enum attributes as custom fields. --- .../converters/converter_config.py | 89 ++++++++++--------- .../converters/element_converter.py | 37 ++++++++ docs/source/features/sync.rst | 4 + tests/test_elements.py | 29 ++++++ 4 files changed, 115 insertions(+), 44 deletions(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index 5d5aff1..af08fd6 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -52,7 +52,50 @@ class CapellaTypeConfig: def __post_init__(self): """Post processing for the initialization.""" - self.converters = _force_dict(self.converters) + self.converters = self._force_dict() + + def _force_dict(self) -> dict[str, dict[str, t.Any]]: + match self.converters: + case None: + return {} + case str(): + return {self.converters: {}} + case list(): + return {c: {} for c in self.converters} + case dict(): + return self._filter_converter_config() + case _: + raise TypeError("Unsupported Type") + + def _filter_converter_config(self) -> dict[str, dict[str, t.Any]]: + custom_converters = ( + "include_pre_and_post_condition", + "linked_text_as_description", + "add_attributes", + "add_context_diagram", + "add_tree_diagram", + "add_jinja_fields", + "jinja_as_description", + ) + filtered_config = {} + assert isinstance(self.converters, dict) + for name, params in self.converters.items(): + params = params or {} + if name not in custom_converters: + logger.error("Unknown converter in config %r", name) + continue + + if name in ("add_context_diagram", "add_tree_diagram"): + assert isinstance(params, dict) + params = _filter_context_diagram_config(params) + + if name in ("add_attributes"): + assert isinstance(params, list) # type: ignore[unreachable] + params = {"attributes": params} # type: ignore[unreachable] + + filtered_config[name] = params + + return filtered_config def _default_type_conversion(c_type: str) -> str: @@ -283,7 +326,7 @@ def config_matches(config: CapellaTypeConfig | None, **kwargs: t.Any) -> bool: def _read_capella_type_configs( - conf: dict[str, t.Any] | list[dict[str, t.Any]] | None + conf: dict[str, t.Any] | list[dict[str, t.Any]] | None, ) -> list[dict]: if conf is None: return [{}] @@ -299,22 +342,6 @@ def _read_capella_type_configs( ) -def _force_dict( - config: str | list[str] | dict[str, dict[str, t.Any]] | None -) -> dict[str, dict[str, t.Any]]: - match config: - case None: - return {} - case str(): - return {config: {}} - case list(): - return {c: {} for c in config} - case dict(): - return _filter_converter_config(config) - case _: - raise TypeError("Unsupported Type") - - def add_prefix(polarion_type: str, prefix: str) -> str: """Add a prefix to the given ``polarion_type``.""" if prefix: @@ -322,32 +349,6 @@ def add_prefix(polarion_type: str, prefix: str) -> str: return polarion_type -def _filter_converter_config( - config: dict[str, dict[str, t.Any]] -) -> dict[str, dict[str, t.Any]]: - custom_converters = ( - "include_pre_and_post_condition", - "linked_text_as_description", - "add_context_diagram", - "add_tree_diagram", - "add_jinja_fields", - "jinja_as_description", - ) - filtered_config = {} - for name, params in config.items(): - params = params or {} - if name not in custom_converters: - logger.error("Unknown converter in config %r", name) - continue - - if name in ("add_context_diagram", "add_tree_diagram"): - params = _filter_context_diagram_config(params) - - filtered_config[name] = params - - return filtered_config - - def _filter_context_diagram_config( config: dict[str, t.Any] ) -> dict[str, t.Any]: diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 476d691..90eeb92 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -5,6 +5,7 @@ from __future__ import annotations import collections +import enum import hashlib import logging import mimetypes @@ -66,6 +67,16 @@ def _format(texts: list[str]) -> dict[str, str]: return requirement_types +def _resolve_capella_attribute( + element: m.ModelElement | m.Diagram, attribute: str +) -> polarion_api.TextContent: + value = getattr(element, attribute) + if isinstance(value, enum.Enum): + return polarion_api.TextContent(type="string", value=value.name) + + raise ValueError("Unsupported attribute type: %r", value) + + class CapellaWorkItemSerializer(polarion_html_helper.JinjaRendererMixin): """The general serializer class for CapellaWorkItems.""" @@ -455,6 +466,32 @@ def __generic_work_item( return converter_data.work_item + def _add_attributes( + self, + converter_data: data_session.ConverterData, + attributes: list[dict[str, t.Any]], + ): + assert converter_data.work_item is not None + for attribute in attributes: + try: + value = _resolve_capella_attribute( + converter_data.capella_element, attribute["capella_attr"] + ) + setattr( + converter_data.work_item, attribute["polarion_id"], value + ) + except AttributeError: + logger.error( + "Attribute %r not found on %r", + attribute["capella_attr"], + converter_data.type_config.p_type, + ) + continue + except ValueError as error: + logger.error(error.args[0]) + + return converter_data.work_item + def _diagram( self, converter_data: data_session.ConverterData, diff --git a/docs/source/features/sync.rst b/docs/source/features/sync.rst index 1c2c247..24ce3a9 100644 --- a/docs/source/features/sync.rst +++ b/docs/source/features/sync.rst @@ -59,6 +59,10 @@ specific serializer alone: | linked_text_as_description | A serializer resolving ``Constraint`` s and their | | | linked text. | +--------------------------------------+------------------------------------------------------+ +| add_attributes | A serializer adding arbitrary attributes as custom | +| | fields to the work item. For now only supports enum | +| | attributes! | ++--------------------------------------+------------------------------------------------------+ | add_context_diagram | A serializer adding a context diagram to the work | | | item. This requires node.js to be installed. | | | The Capella objects where ``context_diagram`` is | diff --git a/tests/test_elements.py b/tests/test_elements.py index 08672d6..74c5203 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -1860,6 +1860,35 @@ def test_generic_work_item( assert work_item == data_model.CapellaWorkItem(**expected) assert status == "open" + def test_add_attributes(self, model: capellambse.MelodyModel): + converters = { + "add_attributes": [ + {"capella_attr": "nature", "polarion_id": "nature"}, + {"capella_attr": "kind", "polarion_id": "kind"}, + ] + } + type_config = converter_config.CapellaTypeConfig( + "PhysicalComponent", converters, [] + ) + serializer = element_converter.CapellaWorkItemSerializer( + model, + polarion_repo.PolarionDataRepository(), + { + TEST_PHYS_COMP: data_session.ConverterData( + "pa", + type_config, + model.by_uuid(TEST_PHYS_COMP), + ) + }, + True, + ) + + work_item = serializer.serialize(TEST_PHYS_COMP) + + assert work_item is not None + assert work_item.nature == {"type": "string", "value": "UNSET"} + assert work_item.kind == {"type": "string", "value": "UNSET"} + @staticmethod def test_add_context_diagram(model: capellambse.MelodyModel): type_config = converter_config.CapellaTypeConfig( From f9a300bd1d1990cb296b20ce5ef9782c1077c12e Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 13 Dec 2024 12:20:59 +0100 Subject: [PATCH 04/10] fix: Remove obvious error --- capella2polarion/converters/converter_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index af08fd6..f63c9ff 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -89,7 +89,7 @@ def _filter_converter_config(self) -> dict[str, dict[str, t.Any]]: assert isinstance(params, dict) params = _filter_context_diagram_config(params) - if name in ("add_attributes"): + if name in ("add_attributes",): assert isinstance(params, list) # type: ignore[unreachable] params = {"attributes": params} # type: ignore[unreachable] From d0a31553a8ab8039abd5b3759099d88f30f91dfd Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 13 Dec 2024 12:27:26 +0100 Subject: [PATCH 05/10] ci: Please pylint --- capella2polarion/converters/element_converter.py | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 90eeb92..1e09f11 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -74,7 +74,7 @@ def _resolve_capella_attribute( if isinstance(value, enum.Enum): return polarion_api.TextContent(type="string", value=value.name) - raise ValueError("Unsupported attribute type: %r", value) + raise ValueError(f"Unsupported attribute type: {value!r}") class CapellaWorkItemSerializer(polarion_html_helper.JinjaRendererMixin): diff --git a/pyproject.toml b/pyproject.toml index 14f5b64..dfe0262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,7 @@ disable = [ "too-many-public-methods", "too-many-return-statements", "too-many-statements", + "too-many-positional-arguments", # Auto-formatting "bad-indentation", From 20da2f4d7dcb5bcbe22802a0ca65a0f1d7a98f8a Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 13 Dec 2024 16:20:28 +0100 Subject: [PATCH 06/10] test: Revert context diagram checksum --- tests/test_workitem_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_workitem_attachments.py b/tests/test_workitem_attachments.py index d1a82e7..a9837ae 100644 --- a/tests/test_workitem_attachments.py +++ b/tests/test_workitem_attachments.py @@ -30,7 +30,7 @@ "69d8b2ca4e690ccaf70fff16d2e42bbf2ecf434307d49e020d2138160ba35cb2" ) CONTEXT_DIAGRAM_CHECKSUM = ( - "572cb7ba53bcde56638a119fafc1304af294467d8c851f4b2cc35ce2f5d231eb" + "2b86192f2f65353512e1b4af0e652577d0ca3d0cf8595f5dcfba7d52bcb6d702" ) TEST_DIAG_UUID = "_APOQ0QPhEeynfbzU12yy7w" From 71af41208b33ada57d1cadba003ff2f90c9a766c Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 6 Jan 2025 09:14:44 +0100 Subject: [PATCH 07/10] feat: Enable serializers for `*` section Also make `layer` custom field configurable from config instead of built-in. --- .../converters/converter_config.py | 127 +++++++++++------- .../converters/element_converter.py | 50 ++++--- 2 files changed, 107 insertions(+), 70 deletions(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index f63c9ff..61f40b3 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -1,6 +1,7 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 """Module providing capella2polarion config class.""" + from __future__ import annotations import dataclasses @@ -17,6 +18,9 @@ _C2P_DEFAULT = "_C2P_DEFAULT" DESCRIPTION_REFERENCE_SERIALIZER = "description_reference" DIAGRAM_ELEMENTS_SERIALIZER = "diagram_elements" +ConvertersType: t.TypeAlias = dict[ + str, dict[str, t.Any] | list[dict[str, t.Any]] +] @dataclasses.dataclass @@ -39,35 +43,70 @@ class LinkConfig: link_field: str = "" reverse_field: str = "" + @staticmethod + def _force_config( + links: list[str | dict[str, t.Any]], role_prefix: str = "" + ) -> list[LinkConfig]: + result: list[LinkConfig] = [] + for link in links: + if isinstance(link, str): + config = LinkConfig( + capella_attr=link, + polarion_role=add_prefix(link, role_prefix), + link_field=link, + reverse_field=f"{link}_reverse", + ) + elif isinstance(link, dict): + config = LinkConfig( + capella_attr=(lid := link["capella_attr"]), + polarion_role=add_prefix( + (pid := link.get("polarion_role", lid)), + role_prefix, + ), + include=link.get("include", {}), + link_field=(lf := link.get("link_field", pid)), + reverse_field=link.get("reverse_field", f"{lf}_reverse"), + ) + else: + logger.error( + "Link not configured correctly: %r", + link, + ) + continue + result.append(config) + return result + @dataclasses.dataclass class CapellaTypeConfig: """A single Capella Type configuration.""" p_type: str | None = None - converters: str | list[str] | dict[str, dict[str, t.Any]] | None = None + converters: dict[str, dict[str, t.Any]] | None = None links: list[LinkConfig] = dataclasses.field(default_factory=list) is_actor: bool | None = None nature: str | None = None - def __post_init__(self): - """Post processing for the initialization.""" - self.converters = self._force_dict() - - def _force_dict(self) -> dict[str, dict[str, t.Any]]: - match self.converters: + @staticmethod + def _force_dict( + converters: str | list[str] | ConvertersType | None, + ) -> dict[str, dict[str, t.Any]]: + match converters: case None: return {} case str(): - return {self.converters: {}} + return {converters: {}} case list(): - return {c: {} for c in self.converters} + return {c: {} for c in converters} case dict(): - return self._filter_converter_config() + return CapellaTypeConfig._filter_config(converters) case _: raise TypeError("Unsupported Type") - def _filter_converter_config(self) -> dict[str, dict[str, t.Any]]: + @staticmethod + def _filter_config( + converters: dict[str, t.Any], + ) -> dict[str, dict[str, t.Any]]: custom_converters = ( "include_pre_and_post_condition", "linked_text_as_description", @@ -78,8 +117,8 @@ def _filter_converter_config(self) -> dict[str, dict[str, t.Any]]: "jinja_as_description", ) filtered_config = {} - assert isinstance(self.converters, dict) - for name, params in self.converters.items(): + assert isinstance(converters, dict) + for name, params in converters.items(): params = params or {} if name not in custom_converters: logger.error("Unknown converter in config %r", name) @@ -124,9 +163,12 @@ def read_config_file( global_config_dict = config_dict.pop("*", {}) all_type_config = global_config_dict.pop("*", {}) global_links = all_type_config.get("links", []) - self.__global_config.links = self._force_link_config( + self.__global_config.links = LinkConfig._force_config( global_links, role_prefix ) + self.__global_config.converters = CapellaTypeConfig._force_dict( + all_type_config.get("serializer", {}) + ) if "Diagram" in global_config_dict: diagram_config = global_config_dict.pop("Diagram") or {} @@ -186,13 +228,19 @@ def set_layer_config( type_prefix, ) self.polarion_types.add(p_type) - links = self._force_link_config( + links = LinkConfig._force_config( type_config.get("links", []), role_prefix ) + converters = CapellaTypeConfig._force_dict( + type_config.get("serializer") + ) + assert self.__global_config.converters is not None + assert closest_config.converters is not None self._layer_configs[layer][c_type].append( CapellaTypeConfig( p_type, - type_config.get("serializer") or closest_config.converters, + self.__global_config.converters + | (converters or closest_config.converters), _filter_links(c_type, links) + closest_links, type_config.get("is_actor", _C2P_DEFAULT), type_config.get("nature", _C2P_DEFAULT), @@ -213,12 +261,16 @@ def set_global_config( type_prefix, ) self.polarion_types.add(p_type) - link_config = self._force_link_config( + link_config = LinkConfig._force_config( type_config.get("links", []), role_prefix ) + converters = CapellaTypeConfig._force_dict( + type_config.get("serializer") + ) + assert self.__global_config.converters is not None self._global_configs[c_type] = CapellaTypeConfig( p_type, - type_config.get("serializer"), + self.__global_config.converters | converters, _filter_links(c_type, link_config) + self._get_global_links(c_type), type_config.get("is_actor", _C2P_DEFAULT), @@ -237,48 +289,19 @@ def set_diagram_config( diagram_config.get("polarion_type") or "diagram", type_prefix ) self.polarion_types.add(p_type) - link_config = self._force_link_config( + link_config = LinkConfig._force_config( diagram_config.get("links", []), role_prefix ) links = _filter_links(c_type, link_config) + converters = CapellaTypeConfig._force_dict( + diagram_config.get("serializer") or "diagram" + ) self.diagram_config = CapellaTypeConfig( p_type, - diagram_config.get("serializer") or "diagram", + self.__global_config.converters or {} | converters, links + self._get_global_links(c_type), ) - def _force_link_config( - self, links: t.Any, role_prefix: str = "" - ) -> list[LinkConfig]: - result: list[LinkConfig] = [] - for link in links: - if isinstance(link, str): - config = LinkConfig( - capella_attr=link, - polarion_role=add_prefix(link, role_prefix), - link_field=link, - reverse_field=f"{link}_reverse", - ) - elif isinstance(link, dict): - config = LinkConfig( - capella_attr=(lid := link["capella_attr"]), - polarion_role=add_prefix( - (pid := link.get("polarion_role", lid)), - role_prefix, - ), - include=link.get("include", {}), - link_field=(lf := link.get("link_field", pid)), - reverse_field=link.get("reverse_field", f"{lf}_reverse"), - ) - else: - logger.error( - "Link not configured correctly: %r", - link, - ) - continue - result.append(config) - return result - def get_type_config( self, layer: str, c_type: str, **attributes: t.Any ) -> CapellaTypeConfig | None: diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 1e09f11..0530149 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -68,11 +68,37 @@ def _format(texts: list[str]) -> dict[str, str]: def _resolve_capella_attribute( - element: m.ModelElement | m.Diagram, attribute: str -) -> polarion_api.TextContent: - value = getattr(element, attribute) - if isinstance(value, enum.Enum): - return polarion_api.TextContent(type="string", value=value.name) + converter_data: data_session.ConverterData, attribute: dict[str, t.Any] +) -> polarion_api.TextContent | str: + def _handle_value( + value: str | enum.Enum, capella_attr: str, enum_id: str | None + ) -> polarion_api.TextContent | str: + if enum_id is not None: + return polarion_api.TextContent( + type=f"enum:{enum_id}", + value=value if isinstance(value, str) else value.name, + ) + + if isinstance(value, enum.Enum): + logger.warning( + "The attribute %r has no enum configured in Polarion. " + "The attribute name is posted as a string instead.", + capella_attr, + ) + return value.name + + return value + + if attribute["capella_attr"] == "layer": + value = converter_data.layer + else: + value = getattr( + converter_data.capella_element, attribute["capella_attr"] + ) + + enum_id = attribute.get("enum_id") + if isinstance(value, (str, enum.Enum)): + return _handle_value(value, attribute["capella_attr"], enum_id) raise ValueError(f"Unsupported attribute type: {value!r}") @@ -444,10 +470,6 @@ def __generic_work_item( obj, raw_description or markupsafe.Markup("") ) converter_data.description_references = uuids - layer = polarion_api.TextContent( - type="string", - value=ARCHITECTURE_LAYERS.get(converter_data.layer, "UNKNOWN"), - ) requirement_types = self._get_requirement_types_text(obj) converter_data.work_item = data_model.CapellaWorkItem( @@ -457,7 +479,6 @@ def __generic_work_item( uuid_capella=obj.uuid, description=polarion_api.HtmlContent(value), status="open", - layer=layer, **requirement_types, # type:ignore[arg-type] ) assert converter_data.work_item is not None @@ -474,9 +495,7 @@ def _add_attributes( assert converter_data.work_item is not None for attribute in attributes: try: - value = _resolve_capella_attribute( - converter_data.capella_element, attribute["capella_attr"] - ) + value = _resolve_capella_attribute(converter_data, attribute) setattr( converter_data.work_item, attribute["polarion_id"], value ) @@ -502,10 +521,6 @@ def _diagram( assert converter_data.work_item is not None assert isinstance(diagram, m.Diagram) work_item_id = converter_data.work_item.id - layer = polarion_api.TextContent( - type="string", - value=ARCHITECTURE_LAYERS.get(converter_data.layer, "UNKNOWN"), - ) diagram_html, attachment = self._draw_diagram_svg( diagram, @@ -528,7 +543,6 @@ def _diagram( uuid_capella=diagram.uuid, description=polarion_api.HtmlContent(diagram_html), status="open", - layer=layer, ) if attachment: self._add_attachment(converter_data.work_item, attachment) From 956981e6fd9b67cddfef9e98d93c0ad29c0fad5c Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 6 Jan 2025 09:16:09 +0100 Subject: [PATCH 08/10] test: Fix tests --- .../converters/converter_config.py | 2 +- .../converters/element_converter.py | 66 +++++-------------- .../data_model/work_item_attachments.py | 10 ++- tests/data/model_elements/config.yaml | 5 ++ tests/test_elements.py | 35 ++++++---- tests/test_workitem_attachments.py | 31 +++++---- 6 files changed, 74 insertions(+), 75 deletions(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index 61f40b3..22fcd76 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -68,7 +68,7 @@ def _force_config( reverse_field=link.get("reverse_field", f"{lf}_reverse"), ) else: - logger.error( + logger.error( # type: ignore[unreachable] "Link not configured correctly: %r", link, ) diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 0530149..e39808a 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -27,22 +27,12 @@ from capella2polarion.connectors import polarion_repo from capella2polarion.converters import data_session, polarion_html_helper -RE_DESCR_LINK_PATTERN = re.compile( - r"([^<]+)<\/a>" -) +RE_DESCR_LINK_PATTERN = re.compile(r"([^<]+)<\/a>") RE_CAMEL_CASE_2ND_WORD_PATTERN = re.compile(r"([a-z]+)([A-Z][a-z]+)") logger = logging.getLogger(__name__) C2P_IMAGE_PREFIX = "__C2P__" JINJA_RENDERED_IMG_CLS = "jinja-rendered-image" -ARCHITECTURE_LAYERS: dict[str, str] = { - "common": "Common", - "oa": "Operational Analysis", - "sa": "System Analysis", - "la": "Logical Architecture", - "pa": "Physical Architecture", - "epbs": "EPBS", -} def resolve_element_type(type_: str) -> str: @@ -50,9 +40,7 @@ def resolve_element_type(type_: str) -> str: return type_[0].lower() + type_[1:] -def _format_texts( - type_texts: dict[str, list[str]] -) -> dict[str, dict[str, str]]: +def _format_texts(type_texts: dict[str, list[str]]) -> dict[str, dict[str, str]]: def _format(texts: list[str]) -> dict[str, str]: if len(texts) > 1: items = "".join(f"
  • {text}
  • " for text in texts) @@ -92,9 +80,7 @@ def _handle_value( if attribute["capella_attr"] == "layer": value = converter_data.layer else: - value = getattr( - converter_data.capella_element, attribute["capella_attr"] - ) + value = getattr(converter_data.capella_element, attribute["capella_attr"]) enum_id = attribute.get("enum_id") if isinstance(value, (str, enum.Enum)): @@ -133,9 +119,7 @@ def serialize(self, uuid: str) -> data_model.CapellaWorkItem | None: """Return a CapellaWorkItem for the given diagram or element.""" converter_data = self.converter_session[uuid] work_item_id = None - if old := self.capella_polarion_mapping.get_work_item_by_capella_uuid( - uuid - ): + if old := self.capella_polarion_mapping.get_work_item_by_capella_uuid(uuid): work_item_id = old.id self.__generic_work_item(converter_data, work_item_id) @@ -150,9 +134,7 @@ def serialize(self, uuid: str) -> data_model.CapellaWorkItem | None: ] = getattr(self, f"_{converter}") serializer(converter_data, **params) except Exception as error: - converter_data.errors.add( - ", ".join([str(a) for a in error.args]) - ) + converter_data.errors.add(", ".join([str(a) for a in error.args])) converter_data.work_item = None if converter_data.errors: @@ -228,9 +210,7 @@ def _render_jinja_template( model=self.model, work_item=converter_data.work_item, ) - _, text, _ = self._sanitize_text( - converter_data.capella_element, rendered_jinja - ) + _, text, _ = self._sanitize_text(converter_data.capella_element, rendered_jinja) return text def setup_env(self, env: jinja2.Environment): @@ -311,7 +291,9 @@ def _draw_additional_attributes_diagram( "value": diagram_html, } - def _sanitize_linked_text(self, obj: m.ModelElement | m.Diagram) -> tuple[ + def _sanitize_linked_text( + self, obj: m.ModelElement | m.Diagram + ) -> tuple[ list[str], markupsafe.Markup, list[data_model.Capella2PolarionAttachment], @@ -337,9 +319,7 @@ def _sanitize_text( ]: referenced_uuids: list[str] = [] replaced_markup = RE_DESCR_LINK_PATTERN.sub( - lambda match: self._replace_markup( - obj.uuid, match, referenced_uuids, 2 - ), + lambda match: self._replace_markup(obj.uuid, match, referenced_uuids, 2), text, ) @@ -358,9 +338,7 @@ def repair_images(node: etree._Element) -> None: file_path = pathlib.PurePosixPath(*file_url.parts[1:]) mime_type, _ = mimetypes.guess_type(file_url) resources = self.model.resources - filehandler = resources[ - ["\x00", workspace][workspace in resources] - ] + filehandler = resources[["\x00", workspace][workspace in resources]] try: with filehandler.open(file_path, "r") as img: content = img.read() @@ -422,9 +400,7 @@ def _replace_markup( self.converter_session[origin_uuid].errors.add( f"Non-existing model element referenced in description: {uuid}" ) - return polarion_html_helper.strike_through( - match.group(default_group) - ) + return polarion_html_helper.strike_through(match.group(default_group)) if pid := self.capella_polarion_mapping.get_work_item_id(uuid): referenced_uuids.append(uuid) return polarion_html_helper.POLARION_WORK_ITEM_URL.format(pid=pid) @@ -446,9 +422,7 @@ def _get_requirement_types_text( continue if not (req.type and req.text): - identifier = ( - req.long_name or req.name or req.summary or req.uuid - ) + identifier = req.long_name or req.name or req.summary or req.uuid self.converter_session[obj.uuid].errors.add( f"Found Requirement without text or type on {identifier!r}" ) @@ -496,9 +470,7 @@ def _add_attributes( for attribute in attributes: try: value = _resolve_capella_attribute(converter_data, attribute) - setattr( - converter_data.work_item, attribute["polarion_id"], value - ) + setattr(converter_data.work_item, attribute["polarion_id"], value) except AttributeError: logger.error( "Attribute %r not found on %r", @@ -568,9 +540,7 @@ def get_condition(cap: m.ModelElement, name: str) -> str: post_condition = get_condition(obj, "postcondition") assert converter_data.work_item, "No work item set yet" - converter_data.work_item.preCondition = polarion_api.HtmlContent( - pre_condition - ) + converter_data.work_item.preCondition = polarion_api.HtmlContent(pre_condition) converter_data.work_item.postCondition = polarion_api.HtmlContent( post_condition ) @@ -669,9 +639,7 @@ def _jinja_as_description( assert ( converter_data.work_item.description ), "Description should already be defined" - converter_data.work_item.description.value = ( - self._render_jinja_template( - template_folder, template_path, converter_data - ) + converter_data.work_item.description.value = self._render_jinja_template( + template_folder, template_path, converter_data ) return converter_data.work_item diff --git a/capella2polarion/data_model/work_item_attachments.py b/capella2polarion/data_model/work_item_attachments.py index 62f2add..9a83efb 100644 --- a/capella2polarion/data_model/work_item_attachments.py +++ b/capella2polarion/data_model/work_item_attachments.py @@ -1,6 +1,7 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 """Module providing the CapellaWorkItemAttachment classes.""" + from __future__ import annotations import base64 @@ -116,9 +117,14 @@ def content_checksum(self) -> str: try: elk_input = self.diagram.elk_input_data(self.render_params) if isinstance(elk_input, tuple): - input_str = ";".join(eit.json() for eit in elk_input) + input_str = ";".join( + eit.model_dump_json(exclude_defaults=True) + for eit in elk_input + ) else: - input_str = elk_input.json() + input_str = elk_input.model_dump_json( + exclude_defaults=True + ) self._checksum = hashlib.sha256( input_str.encode("utf-8") ).hexdigest() diff --git a/tests/data/model_elements/config.yaml b/tests/data/model_elements/config.yaml index fbae34a..adb84d9 100644 --- a/tests/data/model_elements/config.yaml +++ b/tests/data/model_elements/config.yaml @@ -6,6 +6,11 @@ links: # Specify workitem links - parent - description_reference + serializer: + add_attributes: + - capella_attr: layer + polarion_id: layer + enum_id: layer Class: serializer: add_tree_diagram: diff --git a/tests/test_elements.py b/tests/test_elements.py index 74c5203..523be02 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -159,7 +159,7 @@ "" ) } -DIAGRAM_CONFIG = converter_config.CapellaTypeConfig("diagram", "diagram") +DIAGRAM_CONFIG = converter_config.CapellaTypeConfig("diagram", {"diagram": {}}) class GroupedLinksBaseObject(t.TypedDict): @@ -345,7 +345,7 @@ def test_create_work_items_with_special_polarion_type( uuid: data_session.ConverterData( "oa", converter_config.CapellaTypeConfig( - _type[0].lower() + _type[1:] + _type[0].lower() + _type[1:], {} ), model.by_uuid(uuid), ) @@ -1369,6 +1369,7 @@ def test_grouped_links_attributes_with_includes( ex = model.by_uuid(TEST_SYS_FNC_EX) fnc_config = converter_config.CapellaTypeConfig( "systemFunction", + {}, links=[ converter_config.LinkConfig( capella_attr="inputs.exchanges", @@ -1381,6 +1382,7 @@ def test_grouped_links_attributes_with_includes( ) ex_config = converter_config.CapellaTypeConfig( "systemFunctionalExchange", + {}, links=[ converter_config.LinkConfig( capella_attr="exchange_items", @@ -1390,7 +1392,7 @@ def test_grouped_links_attributes_with_includes( ) ], ) - ex_item_config = converter_config.CapellaTypeConfig("exchangeItem") + ex_item_config = converter_config.CapellaTypeConfig("exchangeItem", {}) base_object.mc.converter_session = { TEST_SYS_FNC: data_session.ConverterData( "sa", fnc_config, fnc, None @@ -1578,9 +1580,7 @@ def test_grouped_linked_work_items_order_consistency( base_object.pw.polarion_params.project_id, base_object.c2pcli.capella_model, ) - config = converter_config.CapellaTypeConfig( - "fakeModelObject", - ) + config = converter_config.CapellaTypeConfig("fakeModelObject", {}) work_item = data_model.CapellaWorkItem("id", title="Dummy") converter_data = data_session.ConverterData( "test", config, FakeModelObject(""), work_item @@ -1863,12 +1863,19 @@ def test_generic_work_item( def test_add_attributes(self, model: capellambse.MelodyModel): converters = { "add_attributes": [ + { + "capella_attr": "layer", + "polarion_id": "layer", + "enum_id": "layer", + }, {"capella_attr": "nature", "polarion_id": "nature"}, {"capella_attr": "kind", "polarion_id": "kind"}, ] } type_config = converter_config.CapellaTypeConfig( - "PhysicalComponent", converters, [] + "PhysicalComponent", + converter_config.CapellaTypeConfig._force_dict(converters), + [], ) serializer = element_converter.CapellaWorkItemSerializer( model, @@ -1886,13 +1893,14 @@ def test_add_attributes(self, model: capellambse.MelodyModel): work_item = serializer.serialize(TEST_PHYS_COMP) assert work_item is not None - assert work_item.nature == {"type": "string", "value": "UNSET"} - assert work_item.kind == {"type": "string", "value": "UNSET"} + assert work_item.layer == {"type": "enum:layer", "value": "pa"} + assert work_item.nature == "UNSET" + assert work_item.kind == "UNSET" @staticmethod def test_add_context_diagram(model: capellambse.MelodyModel): type_config = converter_config.CapellaTypeConfig( - "test", "add_context_diagram", [] + "test", {"add_context_diagram": {}}, [] ) serializer = element_converter.CapellaWorkItemSerializer( model, @@ -1951,6 +1959,9 @@ def test_add_context_diagram_with_params( type_config = converter_config.CapellaTypeConfig( "systemFunction", config, [] ) + type_config.converters = ( + converter_config.CapellaTypeConfig._force_dict(config) + ) expected_filter = getattr(filters, context_diagram_filter) monkeypatch.setattr( element_converter.CapellaWorkItemSerializer, @@ -2034,7 +2045,7 @@ def test_multiple_serializers(model: capellambse.MelodyModel, prefix: str): cap = model.by_uuid(TEST_OCAP_UUID) type_config = converter_config.CapellaTypeConfig( f"{prefix}_test" if prefix else "test", - ["include_pre_and_post_condition", "add_context_diagram"], + {"include_pre_and_post_condition": {}, "add_context_diagram": {}}, [], ) serializer = element_converter.CapellaWorkItemSerializer( @@ -2233,7 +2244,7 @@ def test_read_config_links(caplog: pytest.LogCaptureFixture): @staticmethod def test_add_context_diagram_with_caption(model: capellambse.MelodyModel): type_config = converter_config.CapellaTypeConfig( - "test", "add_context_diagram", [] + "test", {"add_context_diagram": {}}, [] ) serializer = element_converter.CapellaWorkItemSerializer( model, diff --git a/tests/test_workitem_attachments.py b/tests/test_workitem_attachments.py index a9837ae..2b19b26 100644 --- a/tests/test_workitem_attachments.py +++ b/tests/test_workitem_attachments.py @@ -24,13 +24,13 @@ from .test_elements import TEST_DIAG_DESCR, TEST_PHYS_FNC DIAGRAM_WI_CHECKSUM = ( - "1239ced17306dc92213cd1b729e3652a1cbc9b07997683ed36033a9d05adcb75" + "76fc1f7e4b73891488de7e47de8ef75fc24e85fc3cdde80661503201e70b1733" ) WI_CONTEXT_DIAGRAM_CHECKSUM = ( - "69d8b2ca4e690ccaf70fff16d2e42bbf2ecf434307d49e020d2138160ba35cb2" + "0ed1417e8e4717524bc91162dcf8633afca686e93f8b036d0bc48d81f0444f56" ) CONTEXT_DIAGRAM_CHECKSUM = ( - "2b86192f2f65353512e1b4af0e652577d0ca3d0cf8595f5dcfba7d52bcb6d702" + "73f7c01dcedba1f47889ddf20302ed371f3b5039102d26429094e5612a3e90ec" ) TEST_DIAG_UUID = "_APOQ0QPhEeynfbzU12yy7w" @@ -71,7 +71,7 @@ def converter( converter = model_converter.ModelConverter(model, "TEST") converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), + converter_config.CapellaTypeConfig("diagram", {"diagram": {}}, []), diag, ) return converter @@ -368,7 +368,9 @@ def test_add_context_diagram( converter.converter_session[TEST_PHYS_FNC] = data_session.ConverterData( "pa", - converter_config.CapellaTypeConfig("test", "add_context_diagram", []), + converter_config.CapellaTypeConfig( + "test", {"add_context_diagram": {}}, [] + ), model.by_uuid(TEST_PHYS_FNC), ) @@ -417,7 +419,7 @@ def test_update_context_diagram_no_changes( converter = model_converter.ModelConverter(model, "TEST") worker.polarion_data_repo = polarion_repo.PolarionDataRepository( [ - data_model.CapellaWorkItem( + old := data_model.CapellaWorkItem( WORKITEM_ID, uuid_capella=TEST_PHYS_FNC, checksum=json.dumps( @@ -432,16 +434,21 @@ def test_update_context_diagram_no_changes( converter.converter_session[TEST_PHYS_FNC] = data_session.ConverterData( "pa", - converter_config.CapellaTypeConfig("test", "add_context_diagram", []), + converter_config.CapellaTypeConfig( + "test", {"add_context_diagram": {}}, [] + ), model.by_uuid(TEST_PHYS_FNC), ) with mock.patch.object(context.ContextDiagram, "render") as wrapped_render: converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item( - converter.converter_session[TEST_PHYS_FNC] + new := converter.converter_session[TEST_PHYS_FNC] ) + print(old.checksum) + print(new.work_item.checksum) + assert worker.project_client.work_items.update.call_count == 0 assert worker.project_client.work_items.attachments.update.call_count == 0 assert wrapped_render.call_count == 0 @@ -469,7 +476,9 @@ def test_update_context_diagram_with_changes( converter.converter_session[TEST_PHYS_FNC] = data_session.ConverterData( "", - converter_config.CapellaTypeConfig("test", "add_context_diagram", []), + converter_config.CapellaTypeConfig( + "test", {"add_context_diagram": {}}, [] + ), model.by_uuid(TEST_PHYS_FNC), ) worker.project_client.work_items.attachments.get_all.return_value = [ @@ -555,7 +564,7 @@ def test_diagram_delete_attachments( converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( model_converter.get_layer_name(diag), - converter_config.CapellaTypeConfig("diagram", "diagram", []), + converter_config.CapellaTypeConfig("diagram", {"diagram": {}}, []), diag, ) @@ -592,7 +601,7 @@ def test_attached_image_in_description_with_caption( converter.converter_session[uuid] = data_session.ConverterData( "", - converter_config.CapellaTypeConfig("test"), + converter_config.CapellaTypeConfig("test", {}), model.by_uuid(uuid), ) From a49cb47ec397014f1ca5e5170c45170d6a6bafe1 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 6 Jan 2025 10:50:50 +0100 Subject: [PATCH 09/10] chore: Add `uv.lock` to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1799eb0..928f7a2 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,9 @@ target/ profile_default/ ipython_config.py +# uv +uv.lock + # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: From 715690c587f8345ab832e0eec03b08948e383adc Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 6 Jan 2025 11:15:17 +0100 Subject: [PATCH 10/10] ci: Fix pylint --- .../converters/element_converter.py | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index e39808a..5c9d787 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -27,7 +27,9 @@ from capella2polarion.connectors import polarion_repo from capella2polarion.converters import data_session, polarion_html_helper -RE_DESCR_LINK_PATTERN = re.compile(r"
    ([^<]+)<\/a>") +RE_DESCR_LINK_PATTERN = re.compile( + r"([^<]+)<\/a>" +) RE_CAMEL_CASE_2ND_WORD_PATTERN = re.compile(r"([a-z]+)([A-Z][a-z]+)") logger = logging.getLogger(__name__) @@ -40,7 +42,9 @@ def resolve_element_type(type_: str) -> str: return type_[0].lower() + type_[1:] -def _format_texts(type_texts: dict[str, list[str]]) -> dict[str, dict[str, str]]: +def _format_texts( + type_texts: dict[str, list[str]] +) -> dict[str, dict[str, str]]: def _format(texts: list[str]) -> dict[str, str]: if len(texts) > 1: items = "".join(f"
  • {text}
  • " for text in texts) @@ -80,7 +84,9 @@ def _handle_value( if attribute["capella_attr"] == "layer": value = converter_data.layer else: - value = getattr(converter_data.capella_element, attribute["capella_attr"]) + value = getattr( + converter_data.capella_element, attribute["capella_attr"] + ) enum_id = attribute.get("enum_id") if isinstance(value, (str, enum.Enum)): @@ -119,7 +125,9 @@ def serialize(self, uuid: str) -> data_model.CapellaWorkItem | None: """Return a CapellaWorkItem for the given diagram or element.""" converter_data = self.converter_session[uuid] work_item_id = None - if old := self.capella_polarion_mapping.get_work_item_by_capella_uuid(uuid): + if old := self.capella_polarion_mapping.get_work_item_by_capella_uuid( + uuid + ): work_item_id = old.id self.__generic_work_item(converter_data, work_item_id) @@ -134,7 +142,9 @@ def serialize(self, uuid: str) -> data_model.CapellaWorkItem | None: ] = getattr(self, f"_{converter}") serializer(converter_data, **params) except Exception as error: - converter_data.errors.add(", ".join([str(a) for a in error.args])) + converter_data.errors.add( + ", ".join([str(a) for a in error.args]) + ) converter_data.work_item = None if converter_data.errors: @@ -210,7 +220,9 @@ def _render_jinja_template( model=self.model, work_item=converter_data.work_item, ) - _, text, _ = self._sanitize_text(converter_data.capella_element, rendered_jinja) + _, text, _ = self._sanitize_text( + converter_data.capella_element, rendered_jinja + ) return text def setup_env(self, env: jinja2.Environment): @@ -291,9 +303,7 @@ def _draw_additional_attributes_diagram( "value": diagram_html, } - def _sanitize_linked_text( - self, obj: m.ModelElement | m.Diagram - ) -> tuple[ + def _sanitize_linked_text(self, obj: m.ModelElement | m.Diagram) -> tuple[ list[str], markupsafe.Markup, list[data_model.Capella2PolarionAttachment], @@ -319,7 +329,9 @@ def _sanitize_text( ]: referenced_uuids: list[str] = [] replaced_markup = RE_DESCR_LINK_PATTERN.sub( - lambda match: self._replace_markup(obj.uuid, match, referenced_uuids, 2), + lambda match: self._replace_markup( + obj.uuid, match, referenced_uuids, 2 + ), text, ) @@ -338,7 +350,9 @@ def repair_images(node: etree._Element) -> None: file_path = pathlib.PurePosixPath(*file_url.parts[1:]) mime_type, _ = mimetypes.guess_type(file_url) resources = self.model.resources - filehandler = resources[["\x00", workspace][workspace in resources]] + filehandler = resources[ + ["\x00", workspace][workspace in resources] + ] try: with filehandler.open(file_path, "r") as img: content = img.read() @@ -400,7 +414,9 @@ def _replace_markup( self.converter_session[origin_uuid].errors.add( f"Non-existing model element referenced in description: {uuid}" ) - return polarion_html_helper.strike_through(match.group(default_group)) + return polarion_html_helper.strike_through( + match.group(default_group) + ) if pid := self.capella_polarion_mapping.get_work_item_id(uuid): referenced_uuids.append(uuid) return polarion_html_helper.POLARION_WORK_ITEM_URL.format(pid=pid) @@ -422,7 +438,9 @@ def _get_requirement_types_text( continue if not (req.type and req.text): - identifier = req.long_name or req.name or req.summary or req.uuid + identifier = ( + req.long_name or req.name or req.summary or req.uuid + ) self.converter_session[obj.uuid].errors.add( f"Found Requirement without text or type on {identifier!r}" ) @@ -470,7 +488,9 @@ def _add_attributes( for attribute in attributes: try: value = _resolve_capella_attribute(converter_data, attribute) - setattr(converter_data.work_item, attribute["polarion_id"], value) + setattr( + converter_data.work_item, attribute["polarion_id"], value + ) except AttributeError: logger.error( "Attribute %r not found on %r", @@ -540,7 +560,9 @@ def get_condition(cap: m.ModelElement, name: str) -> str: post_condition = get_condition(obj, "postcondition") assert converter_data.work_item, "No work item set yet" - converter_data.work_item.preCondition = polarion_api.HtmlContent(pre_condition) + converter_data.work_item.preCondition = polarion_api.HtmlContent( + pre_condition + ) converter_data.work_item.postCondition = polarion_api.HtmlContent( post_condition ) @@ -639,7 +661,9 @@ def _jinja_as_description( assert ( converter_data.work_item.description ), "Description should already be defined" - converter_data.work_item.description.value = self._render_jinja_template( - template_folder, template_path, converter_data + converter_data.work_item.description.value = ( + self._render_jinja_template( + template_folder, template_path, converter_data + ) ) return converter_data.work_item