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"