From abf1288ed54b4f1376ccc166a5e40e135b662e2d Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 12 Dec 2024 09:10:39 +0100 Subject: [PATCH] feat: Add `add_custom_diagrams` serializer --- .../converters/converter_config.py | 56 ++++- .../converters/element_converter.py | 164 ++++++------- docs/source/configuration/sync.rst | 15 +- docs/source/features/sync.rst | 51 ++-- tests/conftest.py | 10 + tests/data/model_elements/config.yaml | 19 ++ tests/test_elements.py | 218 ++++++++++-------- tests/test_workitem_attachments.py | 118 ++++++++-- 8 files changed, 406 insertions(+), 245 deletions(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index bcf40b41..99f90f9d 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -19,6 +19,10 @@ DESCRIPTION_REFERENCE_SERIALIZER = "description_reference" DIAGRAM_ELEMENTS_SERIALIZER = "diagram_elements" +ConverterConfigDict_type: t.TypeAlias = dict[ + str, t.Union[dict[str, t.Any], list[dict[str, t.Any]]] +] + @dataclasses.dataclass class LinkConfig: @@ -46,14 +50,27 @@ 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: str | list[str] | ConverterConfigDict_type | 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 = _force_dict(self.converters) + self.converters = _force_dict( # type: ignore[assignment] + self.converters + ) + + +@dataclasses.dataclass +class CustomDiagramConfig: + """A single Capella Custom Diagram configuration.""" + + capella_attr: str + polarion_id: str + title: str + render_params: dict[str, t.Any] | None = None + filters: list[str] | None = None def _default_type_conversion(c_type: str) -> str: @@ -72,7 +89,7 @@ def __init__(self): def read_config_file( self, - synchronize_config: t.TextIO, + synchronize_config: str | t.TextIO, type_prefix: str = "", role_prefix: str = "", ): @@ -301,7 +318,7 @@ def _read_capella_type_configs( def _force_dict( - config: str | list[str] | dict[str, dict[str, t.Any]] | None, + config: str | list[str] | ConverterConfigDict_type | None, ) -> dict[str, dict[str, t.Any]]: match config: case None: @@ -324,19 +341,18 @@ def add_prefix(polarion_type: str, prefix: str) -> str: def _filter_converter_config( - config: dict[str, dict[str, t.Any]], + config: ConverterConfigDict_type, ) -> dict[str, dict[str, t.Any]]: custom_converters = ( "include_pre_and_post_condition", "linked_text_as_description", - "add_context_diagram", - "add_tree_diagram", - "add_realization_diagram", - "add_cable_tree_diagram", + "add_custom_diagrams", + "add_context_diagram", # TODO: Deprecated, so remove in next release + "add_tree_diagram", # TODO: Deprecated, so remove in next release "add_jinja_fields", "jinja_as_description", ) - filtered_config = {} + filtered_config: dict[str, dict[str, t.Any]] = {} for name, params in config.items(): params = params or {} if name not in custom_converters: @@ -344,8 +360,28 @@ def _filter_converter_config( continue if name in ("add_context_diagram", "add_tree_diagram"): + assert isinstance(params, dict) params = _filter_context_diagram_config(params) + if name in ("add_custom_diagrams",): + if isinstance(params, list): + params = { + "custom_diagrams_configs": [ + CustomDiagramConfig( + **_filter_context_diagram_config(rp) + ) + for rp in params + ] + } + elif isinstance(params, dict): + assert "custom_diagrams_configs" in params + else: + logger.error( # type: ignore[unreachable] + "Unknown 'add_custom_diagrams' config %r", params + ) + continue + + assert isinstance(params, dict) filtered_config[name] = params return filtered_config diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 7db2ebf1..40da2ac7 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -11,6 +11,7 @@ import pathlib import re import typing as t +import warnings from collections import abc as cabc import capellambse @@ -24,7 +25,11 @@ from capella2polarion import data_model from capella2polarion.connectors import polarion_repo -from capella2polarion.converters import data_session, polarion_html_helper +from capella2polarion.converters import ( + converter_config, + data_session, + polarion_html_helper, +) RE_DESCR_LINK_PATTERN = re.compile( r"([^<]+)<\/a>" @@ -101,6 +106,7 @@ def serialize(self, uuid: str) -> data_model.CapellaWorkItem | None: ..., data_model.CapellaWorkItem, ] = getattr(self, f"_{converter}") + assert isinstance(params, dict) serializer(converter_data, **params) except Exception as error: converter_data.errors.add( @@ -236,30 +242,6 @@ def __insert_diagram( return diagram_html - def _draw_additional_attributes_diagram( - self, - work_item: data_model.CapellaWorkItem, - diagram: m.AbstractDiagram, - attribute: str, - title: str, - render_params: dict[str, t.Any] | None = None, - ): - diagram_html, attachment = self._draw_diagram_svg( - diagram, - attribute, - title, - 650, - "additional-attributes-diagram", - render_params, - ) - if attachment: - self._add_attachment(work_item, attachment) - - work_item.additional_attributes[attribute] = { - "type": "text/html", - "value": diagram_html, - } - def _sanitize_linked_text(self, obj: m.ModelElement | m.Diagram) -> tuple[ list[str], markupsafe.Markup, @@ -395,30 +377,6 @@ def _get_requirement_types_text( type_texts[req.type.long_name].append(req.text) return _format_texts(type_texts) - def _add_diagram( - self, - converter_data: data_session.ConverterData, - diagram_attr: str, - diagram_label: str, - render_params: dict[str, t.Any] | None = None, - filters: list[str] | None = None, - ) -> data_model.CapellaWorkItem: - """Add a new custom field diagram based on provided attributes.""" - assert converter_data.work_item, "No work item set yet" - diagram = getattr(converter_data.capella_element, diagram_attr) - - for filter in filters or []: - diagram.filters.add(filter) - - self._draw_additional_attributes_diagram( - converter_data.work_item, - diagram, - diagram_attr, - diagram_label, - render_params, - ) - return converter_data.work_item - # Serializer implementation starts below def __generic_work_item( @@ -523,56 +481,85 @@ def _linked_text_as_description( converter_data.work_item.attachments += attachments return converter_data.work_item - def _add_context_diagram( + def _add_custom_diagrams( self, converter_data: data_session.ConverterData, - render_params: dict[str, t.Any] | None = None, - filters: list[str] | None = None, + custom_diagrams_configs: list[converter_config.CustomDiagramConfig], ) -> data_model.CapellaWorkItem: - return self._add_diagram( - converter_data, - "context_diagram", - "Context Diagram", - render_params, - filters, - ) + """Add new custom field diagrams to the work item.""" + assert converter_data.work_item, "No work item set yet" + for cd_config in custom_diagrams_configs: + diagram = getattr( + converter_data.capella_element, cd_config.capella_attr + ) - def _add_tree_diagram( - self, - converter_data: data_session.ConverterData, - render_params: dict[str, t.Any] | None = None, - filters: list[str] | None = None, - ) -> data_model.CapellaWorkItem: - return self._add_diagram( - converter_data, "tree_view", "Tree View", render_params, filters - ) + for filter in cd_config.filters or []: + diagram.filters.add(filter) - def _add_realization_diagram( + diagram_html, attachment = self._draw_diagram_svg( + diagram, + cd_config.capella_attr, + cd_config.title, + 650, + "additional-attributes-diagram", + cd_config.render_params, + ) + if attachment: + self._add_attachment(converter_data.work_item, attachment) + + converter_data.work_item.additional_attributes[ + cd_config.polarion_id + ] = polarion_api.HtmlContent(diagram_html) + return converter_data.work_item + + def _add_context_diagram( self, converter_data: data_session.ConverterData, render_params: dict[str, t.Any] | None = None, filters: list[str] | None = None, ) -> data_model.CapellaWorkItem: - return self._add_diagram( + warnings.warn( + "Using 'add_context_diagram' is deprecated. " + "Use 'add_custom_diagrams' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self._add_custom_diagrams( converter_data, - "realization_view", - "Realization Diagram", - render_params, - filters, + [ + converter_config.CustomDiagramConfig( + capella_attr="context_diagram", + polarion_id="context_diagram", + title="Context Diagram", + render_params=render_params, + filters=filters, + ) + ], ) - def _add_cable_tree_diagram( + def _add_tree_diagram( self, converter_data: data_session.ConverterData, render_params: dict[str, t.Any] | None = None, filters: list[str] | None = None, ) -> data_model.CapellaWorkItem: - return self._add_diagram( + warnings.warn( + "Using 'add_tree_diagram' is deprecated. " + "Use 'add_custom_diagrams' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self._add_custom_diagrams( converter_data, - "cable_tree", - "Cable Tree Diagram", - render_params, - filters, + [ + converter_config.CustomDiagramConfig( + capella_attr="tree_view", + polarion_id="tree_view", + title="Tree View", + render_params=render_params, + filters=filters, + ) + ], ) def _add_jinja_fields( @@ -583,14 +570,15 @@ def _add_jinja_fields( """Add a new custom field and fill it with rendered jinja content.""" assert converter_data.work_item, "No work item set yet" for field, jinja_properties in fields.items(): - converter_data.work_item.additional_attributes[field] = { - "type": "text/html", - "value": self._render_jinja_template( - jinja_properties.get("template_folder", ""), - jinja_properties["template_path"], - converter_data, - ), - } + converter_data.work_item.additional_attributes[field] = ( + polarion_api.HtmlContent( + self._render_jinja_template( + jinja_properties.get("template_folder", ""), + jinja_properties["template_path"], + converter_data, + ), + ) + ) return converter_data.work_item diff --git a/docs/source/configuration/sync.rst b/docs/source/configuration/sync.rst index 5ce31d3b..ba186ee7 100644 --- a/docs/source/configuration/sync.rst +++ b/docs/source/configuration/sync.rst @@ -45,17 +45,20 @@ serializers in the configs. These will be called in the order provided in the list. Some serializers also support additional configuration. This can be done by providing a dictionary of serializers with the serializer as key and the configuration of the serializer as value. For example ``Class`` using the -``add_tree_diagram`` serializer: +``add_custom_diagrams`` serializer to render the tree view diagram from the +``tree_view`` Capella attribute into a custom field with the ID ``tree_view`` +and title ``Tree View ``: .. literalinclude:: ../../../tests/data/model_elements/config.yaml :language: yaml :lines: 9-13 -or ``SystemFunction`` with the ``add_context_diagram`` serializer using ``filters``: +or ``SystemFunction`` with the ``add_custom_diagrams`` serializer using +``filters``: .. literalinclude:: ../../../tests/data/model_elements/config.yaml :language: yaml - :lines: 64-67 + :lines: 69-72 If a serializer supports additional parameters this will be documented in the supported capella serializers table in :ref:`Model synchronization @@ -73,7 +76,7 @@ to the desired Polarion type. .. literalinclude:: ../../../tests/data/model_elements/config.yaml :language: yaml - :lines: 73-91 + :lines: 84-99 For the ``PhysicalComponent`` you can see this in extreme action, where based on the different permutation of the attributes actor and nature different @@ -90,14 +93,14 @@ Links can be configured by just providing a list of strings: .. literalinclude:: ../../../tests/data/model_elements/config.yaml :language: yaml - :lines: 33-37 + :lines: 36-37 However there is a more verbose way that gives you the option to configure the link further: .. literalinclude:: ../../../tests/data/model_elements/config.yaml :language: yaml - :lines: 52-63 + :lines: 59-68 The links of ``SystemFunction`` are configured such that a ``polarion_role``, a separate ``capella_attr``, an ``include``, ``link_field`` and diff --git a/docs/source/features/sync.rst b/docs/source/features/sync.rst index 35245f00..3aea89af 100644 --- a/docs/source/features/sync.rst +++ b/docs/source/features/sync.rst @@ -59,45 +59,36 @@ specific serializer alone: | linked_text_as_description | A serializer resolving ``Constraint`` s and their | | | linked text. | +--------------------------------------+------------------------------------------------------+ -| add_context_diagram | A serializer adding a context diagram to the work | +| add_context_diagram (Deprecated) | A serializer adding a context diagram to the work | | | item. This requires node.js to be installed. | | | The Capella objects where ``context_diagram`` is | -| | available can be seen in the `context-diagrams | -| | documentation`_. | +| | available can be seen in the `Context Diagram`_ | +| | documentation. | | | You can provide ``render_params`` in the config and | | | these will be passed to the render function of | | | capellambse. | | | You can provide ``filters`` in the config, and these | | | will be passed to the render function of capellambse.| -| | See `context-diagrams filters`_ for documentation. | +| | See `Context Diagram filters`_ for documentation. | +--------------------------------------+------------------------------------------------------+ -| add_tree_diagram | A serializer adding a tree view diagram to the | +| add_tree_diagram (Deprecated) | A serializer adding a tree view diagram to the | | | work item. Same requirements as for | -| | ``add_context_diagram``. `Tree View Documentation`_. | +| | ``add_context_diagram``. `Tree View`_ Documentation. | | | You can provide ``render_params`` in the config and | | | these will be passed to the render function of | | | capellambse. | | | ``filters`` are available here too. | +--------------------------------------+------------------------------------------------------+ -| add_realization_diagram | A serializer adding a realization diagram to the | -| | work item. Requires similar setup as | -| | ``add_context_diagram``. `Realization Diagram | -| | Documentation`_. | -| | You can provide ``render_params`` in the config and | -| | these will be passed to the render function of | -| | capellambse. | -| | ``filters`` can also be provided for additional | -| | customization. | -+--------------------------------------+------------------------------------------------------+ -| add_cable_tree_diagram | A serializer adding a cable tree diagram to the | -| | work item. Requires similar setup as | -| | ``add_context_diagram``. `Cable Tree Diagram | -| | Documentation`_. | -| | You can provide ``render_params`` in the config and | -| | these will be passed to the render function of | -| | capellambse. | -| | ``filters`` are also supported here for additional | -| | customization. | +| add_custom_diagrams | A serializer for adding custom diagrams to work | +| | items. Requires node.js to be installed. Supported | +| | diagrams include `Context Diagram`_, `Tree View`_, | +| | `Realization Diagram`_ and `Cable Tree Diagram`_. | +| | The `capella_attr`, `polarion_id` and `title` need to| +| | provided in the configuration. | +| | You can provide ``render_params`` and ``filters`` in | +| | the config for customization. Documentation for each | +| | diagram type can be found in their respective | +| | sections. | +--------------------------------------+------------------------------------------------------+ | add_jinja_fields | A serializer that allows custom field values to be | | | filled with rendered Jinja2 template content. This | @@ -111,11 +102,11 @@ specific serializer alone: | | description field. | +--------------------------------------+------------------------------------------------------+ -.. _context-diagrams documentation: https://dsd-dbs.github.io/capellambse-context-diagrams/#context-diagram-extension-for-capellambse -.. _Tree View documentation: https://dsd-dbs.github.io/capellambse-context-diagrams/tree_view/ -.. _Realization Diagram documentation: https://dsd-dbs.github.io/capellambse-context-diagrams/realization_view/ -.. _Cable Tree Diagram documentation: https://dsd-dbs.github.io/capellambse-context-diagrams/cable_tree/ -.. _context-diagrams filters: https://dsd-dbs.github.io/capellambse-context-diagrams/extras/filters/ +.. _Context Diagram: https://dsd-dbs.github.io/capellambse-context-diagrams/#context-diagram-extension-for-capellambse +.. _Tree View: https://dsd-dbs.github.io/capellambse-context-diagrams/tree_view/ +.. _Realization Diagram: https://dsd-dbs.github.io/capellambse-context-diagrams/realization_view/ +.. _Cable Tree Diagram: https://dsd-dbs.github.io/capellambse-context-diagrams/cable_tree/ +.. _Context Diagram filters: https://dsd-dbs.github.io/capellambse-context-diagrams/extras/filters/ Links ***** diff --git a/tests/conftest.py b/tests/conftest.py index a4d97bae..c3d603ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,6 +58,16 @@ def model() -> capellambse.MelodyModel: return capellambse.MelodyModel(**TEST_MODEL) +@pytest.fixture +def test_config() -> converter_config.ConverterConfig: + """Return the test config.""" + config = converter_config.ConverterConfig() + config.read_config_file( + TEST_MODEL_ELEMENTS_CONFIG.read_text(encoding="utf8") + ) + return config + + @pytest.fixture def dummy_work_items() -> dict[str, data_model.CapellaWorkItem]: return { diff --git a/tests/data/model_elements/config.yaml b/tests/data/model_elements/config.yaml index fbae34a6..1180b193 100644 --- a/tests/data/model_elements/config.yaml +++ b/tests/data/model_elements/config.yaml @@ -42,6 +42,11 @@ sa: SystemComponent: - links: - allocated_functions + serializer: + add_custom_diagrams: + - capella_attr: realization_view + polarion_id: realization_view + title: Realization View Diagram - is_actor: false polarion_type: systemComponent - is_actor: true @@ -74,6 +79,14 @@ pa: PhysicalComponent: - links: - allocated_functions + serializer: + add_custom_diagrams: + - capella_attr: context_diagram + polarion_id: context_diagram + title: Context Diagram + - capella_attr: realization_view + polarion_id: realization_view + title: Realization View Diagram - is_actor: false nature: UNSET polarion_type: physicalComponent @@ -89,6 +102,12 @@ pa: - is_actor: true nature: BEHAVIOR polarion_type: physicalActorBehavior + PhysicalLink: + serializer: + add_custom_diagrams: + - capella_attr: cable_tree + polarion_id: cable_tree + title: Cable Tree Diagram la: LogicalComponent: diff --git a/tests/test_elements.py b/tests/test_elements.py index b04750ee..982a3b64 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -13,7 +13,7 @@ import polarion_rest_api_client as polarion_api import pytest from capellambse import model as m -from capellambse_context_diagrams import context, filters +from capellambse_context_diagrams import context from capella2polarion import data_model from capella2polarion.connectors import polarion_repo @@ -58,6 +58,7 @@ TEST_SYS_FNC_EX = "1a414995-f4cd-488c-8152-486e459fb9de" TEST_SYS_CMP = "344a405e-c7e5-4367-8a9a-41d3d9a27f81" TEST_PHYS_LINK = "3078ec08-956a-4c61-87ed-0143d1d66715" +TEST_PHYS_CONTEXT_DIAGRAM = "11906f7b-3ae9-4343-b998-95b170be2e2b" TEST_DIAG_DESCR = ( '