diff --git a/capella2polarion/elements/__init__.py b/capella2polarion/elements/__init__.py index 35980484..ba119130 100644 --- a/capella2polarion/elements/__init__.py +++ b/capella2polarion/elements/__init__.py @@ -33,7 +33,9 @@ } PHYSICAL_COMPONENT_TYPES = { "PhysicalComponentNode": "PhysicalComponent", + "PhysicalActorNode": "PhysicalComponent", "PhysicalComponentBehavior": "PhysicalComponent", + "PhysicalActorBehavior": "PhysicalComponent", } POL2CAPELLA_TYPES: dict[str, str] = ( { @@ -163,13 +165,7 @@ def patch_work_items(ctx: dict[str, t.Any]) -> None: new_work_item, back_links[old_work_item.id] ) - api_helper.patch_work_item( - ctx["API"], - new_work_item, - old_work_item, - old_work_item.title, - "element", - ) + api_helper.patch_work_item(ctx["API"], new_work_item, old_work_item) def get_types(ctx: dict[str, t.Any]) -> set[str]: @@ -234,25 +230,22 @@ def _fix_components( elements[typ] = actors elements[xtype] = components - nodes: list[common.GenericElement] = [] - behaviors: list[common.GenericElement] = [] - components = [] + nature_mapping: dict[str, tuple[list[common.GenericElement], str]] = { + "UNSET": ([], "PhysicalComponent"), + "NODE": ([], "PhysicalComponentNode"), + "BEHAVIOR": ([], "PhysicalComponentBehavior"), + "NODE_actor": ([], "PhysicalActorNode"), + "BEHAVIOR_actor": ([], "PhysicalActorBehavior"), + } for obj in elements.get("PhysicalComponent", []): - if obj.nature is not None and obj.nature.name == "NODE": - nodes.append(obj) - type_map[obj.uuid] = "PhysicalComponentNode" - elif obj.nature is not None and obj.nature.name == "BEHAVIOR": - behaviors.append(obj) - type_map[obj.uuid] = "PhysicalComponentBehavior" - else: - components.append(obj) - - if nodes: - elements["PhysicalComponentNode"] = nodes - if behaviors: - elements["PhysicalComponentBehavior"] = behaviors - if components: - elements["PhysicalComponent"] = components + postfix = "_actor" if obj.is_actor else "" + container, xtype = nature_mapping[f"{str(obj.nature)}{postfix}"] + container.append(obj) + type_map[obj.uuid] = xtype + + for container, xtype in nature_mapping.values(): + if container: + elements[xtype] = container def make_model_elements_index(ctx: dict[str, t.Any]) -> None: diff --git a/capella2polarion/elements/api_helper.py b/capella2polarion/elements/api_helper.py index b3fef82b..241aaba3 100644 --- a/capella2polarion/elements/api_helper.py +++ b/capella2polarion/elements/api_helper.py @@ -16,8 +16,6 @@ def patch_work_item( api: polarion_api.OpenAPIPolarionProjectClient, new: serialize.CapellaWorkItem, old: serialize.CapellaWorkItem, - name: str, - _type: str, ): """Patch a given WorkItem. @@ -29,16 +27,11 @@ def patch_work_item( The updated CapellaWorkItem old The CapellaWorkItem currently present on polarion - name - The name of the object, which should be displayed in log - messages. - _type - The type of element, which should be shown in log messages. """ if new == old: return - log_args = (old.id, _type, name) + log_args = (old.id, new.type, new.title) logger.info("Update work item %r for model %s %r...", *log_args) if "uuid_capella" in new.additional_attributes: del new.additional_attributes["uuid_capella"] @@ -52,18 +45,21 @@ def patch_work_item( handle_links( old.linked_work_items, new.linked_work_items, - ("Delete", _type, name), + ("Delete", *log_args[1:]), api.delete_work_item_links, ) handle_links( new.linked_work_items, old.linked_work_items, - ("Create", _type, name), + ("Create", *log_args[1:]), api.create_work_item_links, ) except polarion_api.PolarionApiException as error: - wi = f"{old.id}({_type} {name})" - logger.error("Updating work item %r failed. %s", wi, error.args[0]) + logger.error( + "Updating work item %r (%s %s) failed. %s", + *log_args, + error.args[0], + ) def handle_links( diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index e533f8f1..58611857 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -6,6 +6,7 @@ import collections.abc as cabc import functools import logging +import types import typing as t from collections import defaultdict from itertools import chain @@ -50,6 +51,8 @@ def create_work_items( missing_types: set[str] = set() for work_item in _work_items: assert work_item is not None + assert work_item.title is not None + assert work_item.type is not None if work_item.type in valid_types: work_items.append(work_item) else: @@ -60,7 +63,9 @@ def create_work_items( "%r are missing in the capella2polarion configuration", ", ".join(missing_types), ) - ctx["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} + ctx["WORK_ITEMS"] = types.MappingProxyType( + {wi.uuid_capella: wi for wi in work_items} + ) return work_items diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index c8a1c232..286e3946 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -23,7 +23,9 @@ from capella2polarion.elements import helpers -RE_DESCR_LINK_PATTERN = re.compile(r"[^<]+<\/a>") +RE_DESCR_LINK_PATTERN = re.compile( + r"([^<]+)<\/a>" +) RE_DESCR_DELETED_PATTERN = re.compile( f"" ) @@ -143,8 +145,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 logger.warning( - "Requirement without text or type found %r", req.name + "Requirement without text or type found %r", identifier ) continue @@ -174,7 +177,7 @@ def _sanitize_description( ) -> tuple[list[str], markupsafe.Markup]: referenced_uuids: list[str] = [] replaced_markup = RE_DESCR_LINK_PATTERN.sub( - lambda match: replace_markup(match, ctx, referenced_uuids), descr + lambda match: replace_markup(match, ctx, referenced_uuids, 2), descr ) def repair_images(node: etree._Element) -> None: @@ -208,7 +211,7 @@ def replace_markup( match: re.Match, ctx: dict[str, t.Any], referenced_uuids: list[str], - non_matcher: cabc.Callable[[str], str] = lambda i: i, + default_group: int = 1, ) -> str: """Replace UUID references in a ``match`` with a work item link. @@ -216,10 +219,16 @@ def replace_markup( text is returned. """ uuid = match.group(1) + try: + ctx["MODEL"].by_uuid(uuid) + except KeyError: + logger.error("Found link to non-existing model element: %r", uuid) + return strike_through(match.group(default_group)) if pid := ctx["POLARION_ID_MAP"].get(uuid): referenced_uuids.append(uuid) return POLARION_WORK_ITEM_URL.format(pid=pid) - return non_matcher(match.group(0)) + logger.warning("Found reference to non-existing work item: %r", uuid) + return match.group(default_group) def include_pre_and_post_condition( @@ -232,11 +241,6 @@ def get_condition(cap: PrePostConditionElement, name: str) -> str: return "" return condition.specification["capella:linkedText"].striptags() - def strike_through(string: str) -> str: - if match := RE_DESCR_DELETED_PATTERN.match(string): - string = match.group(1) - return f'{string}' - def matcher(match: re.Match) -> str: return strike_through(replace_markup(match, ctx, [])) @@ -254,6 +258,13 @@ def matcher(match: re.Match) -> str: return work_item +def strike_through(string: str) -> str: + """Return a striked-through html span from given ``string``.""" + if match := RE_DESCR_DELETED_PATTERN.match(string): + string = match.group(1) + return f'{string}' + + def get_linked_text( obj: capellacore.Constraint, ctx: dict[str, t.Any] ) -> markupsafe.Markup: @@ -280,7 +291,7 @@ def _condition(html: bool, value: str) -> CapellaWorkItem.Condition: return {"type": _type, "value": value} -def component_or_actor( +def _include_actor_in_type( obj: cs.Component, ctx: dict[str, t.Any] ) -> CapellaWorkItem: """Return attributes for a ``Component``.""" @@ -294,15 +305,15 @@ def component_or_actor( return work_item -def physical_component( +def _include_nature_in_type( obj: pa.PhysicalComponent, ctx: dict[str, t.Any] ) -> CapellaWorkItem: """Return attributes for a ``PhysicalComponent``.""" - work_item = component_or_actor(obj, ctx) + work_item = _include_actor_in_type(obj, ctx) xtype = work_item.type - if obj.nature is not None: - # pylint: disable-next=attribute-defined-outside-init - work_item.type = f"{xtype}{obj.nature.name.capitalize()}" + nature = [obj.nature.name, ""][obj.nature == "UNSET"] + # pylint: disable-next=attribute-defined-outside-init + work_item.type = f"{xtype}{nature.capitalize()}" return work_item @@ -311,11 +322,11 @@ def physical_component( ] SERIALIZERS: dict[str, Serializer] = { "CapabilityRealization": include_pre_and_post_condition, - "LogicalComponent": component_or_actor, + "LogicalComponent": _include_actor_in_type, "OperationalCapability": include_pre_and_post_condition, - "PhysicalComponent": physical_component, + "PhysicalComponent": _include_nature_in_type, "SystemCapability": include_pre_and_post_condition, - "SystemComponent": component_or_actor, + "SystemComponent": _include_actor_in_type, "Scenario": include_pre_and_post_condition, "Constraint": constraint, } diff --git a/tests/test_elements.py b/tests/test_elements.py index c9ef64e8..58f1697d 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -31,8 +31,7 @@ TEST_WE_UUID = "e37510b9-3166-4f80-a919-dfaac9b696c7" TEST_E_UUID = "4bf0356c-89dd-45e9-b8a6-e0332c026d33" TEST_WE_DESCR = ( - '

\n' + '

Weather

\n' ) TEST_ACTOR_UUID = "08e02248-504d-4ed8-a295-c7682a614f66" TEST_PHYS_COMP = "b9f9a83c-fb02-44f7-9123-9d86326de5f1"