Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: Extract-reqtypes-from-generic-serializer #135

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 14 additions & 22 deletions capella2polarion/converters/converter_config.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,6 +17,7 @@

_C2P_DEFAULT = "_C2P_DEFAULT"
DESCRIPTION_REFERENCE_SERIALIZER = "description_reference"
REQUIREMENT_REFERENCE_SERIALIZER = "requirement_reference"
DIAGRAM_ELEMENTS_SERIALIZER = "diagram_elements"


Expand Down Expand Up @@ -81,19 +83,15 @@ 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(
global_links, role_prefix
)
self.__global_config.links = self._force_link_config(global_links, role_prefix)

if "Diagram" in global_config_dict:
diagram_config = global_config_dict.pop("Diagram") or {}
self.set_diagram_config(diagram_config, type_prefix, role_prefix)

for c_type, type_config in global_config_dict.items():
type_config = type_config or {}
self.set_global_config(
c_type, type_config, type_prefix, role_prefix
)
self.set_global_config(c_type, type_config, type_prefix, role_prefix)

for layer, type_configs in config_dict.items():
type_configs = type_configs or {}
Expand Down Expand Up @@ -143,9 +141,7 @@ def set_layer_config(
type_prefix,
)
self.polarion_types.add(p_type)
links = self._force_link_config(
type_config.get("links", []), role_prefix
)
links = self._force_link_config(type_config.get("links", []), role_prefix)
self._layer_configs[layer][c_type].append(
CapellaTypeConfig(
p_type,
Expand All @@ -165,19 +161,15 @@ def set_global_config(
):
"""Set a global config for a specific type."""
p_type = add_prefix(
type_config.get("polarion_type")
or _default_type_conversion(c_type),
type_config.get("polarion_type") or _default_type_conversion(c_type),
type_prefix,
)
self.polarion_types.add(p_type)
link_config = self._force_link_config(
type_config.get("links", []), role_prefix
)
link_config = self._force_link_config(type_config.get("links", []), role_prefix)
self._global_configs[c_type] = CapellaTypeConfig(
p_type,
type_config.get("serializer"),
_filter_links(c_type, link_config)
+ self._get_global_links(c_type),
_filter_links(c_type, link_config) + self._get_global_links(c_type),
type_config.get("is_actor", _C2P_DEFAULT),
type_config.get("nature", _C2P_DEFAULT),
)
Expand Down Expand Up @@ -283,7 +275,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 [{}]
Expand All @@ -300,7 +292,7 @@ def _read_capella_type_configs(


def _force_dict(
config: str | list[str] | dict[str, dict[str, t.Any]] | None
config: str | list[str] | dict[str, dict[str, t.Any]] | None,
) -> dict[str, dict[str, t.Any]]:
match config:
case None:
Expand All @@ -323,11 +315,12 @@ def add_prefix(polarion_type: str, prefix: str) -> str:


def _filter_converter_config(
config: dict[str, dict[str, t.Any]]
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_requirements_text_grouped_by_type",
"add_context_diagram",
"add_tree_diagram",
"add_jinja_fields",
Expand All @@ -348,9 +341,7 @@ def _filter_converter_config(
return filtered_config


def _filter_context_diagram_config(
config: dict[str, t.Any]
) -> dict[str, t.Any]:
def _filter_context_diagram_config(config: dict[str, t.Any]) -> dict[str, t.Any]:
converted_filters = []
for filter_name in config.get("filters", []):
try:
Expand Down Expand Up @@ -381,6 +372,7 @@ def _filter_links(
is_diagram_elements = capella_attr == DIAGRAM_ELEMENTS_SERIALIZER
if (
capella_attr == DESCRIPTION_REFERENCE_SERIALIZER
or capella_attr == REQUIREMENT_REFERENCE_SERIALIZER
or (is_diagram_elements and c_class == m.Diagram)
or hasattr(c_class, capella_attr)
):
Expand Down
2 changes: 2 additions & 0 deletions capella2polarion/converters/data_session.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
"""A module to store data during the conversion process."""

from __future__ import annotations

import dataclasses
Expand All @@ -20,6 +21,7 @@ class ConverterData:
capella_element: m.ModelElement | m.Diagram
work_item: dm.CapellaWorkItem | None = None
description_references: list[str] = dataclasses.field(default_factory=list)
requirement_references: list[str] = dataclasses.field(default_factory=list)
errors: set[str] = dataclasses.field(default_factory=set)


Expand Down
66 changes: 47 additions & 19 deletions capella2polarion/converters/element_converter.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -40,8 +41,16 @@ def resolve_element_type(type_: str) -> str:
return type_[0].lower() + type_[1:]


class SanitizedText(t.NamedTuple):
"""Sanitized text from Capella."""

referenced_uuids: list[str]
text: markupsafe.Markup
attachments: list[data_model.Capella2PolarionAttachment]


def _format_texts(
type_texts: dict[str, list[str]]
type_texts: dict[str, list[SanitizedText]],
) -> dict[str, dict[str, str]]:
def _format(texts: list[str]) -> dict[str, str]:
if len(texts) > 1:
Expand All @@ -52,8 +61,8 @@ def _format(texts: list[str]) -> dict[str, str]:
return {"type": "text/html", "value": text}

requirement_types = {}
for typ, texts in type_texts.items():
requirement_types[typ.lower()] = _format(texts)
for typ, stexts in type_texts.items():
requirement_types[typ.lower()] = _format([s.text for s in stexts])
return requirement_types


Expand Down Expand Up @@ -274,11 +283,7 @@ def _sanitize_linked_text(self, obj: m.ModelElement | m.Diagram) -> tuple[

def _sanitize_text(
self, obj: m.ModelElement | m.Diagram, text: markupsafe.Markup | str
) -> tuple[
list[str],
markupsafe.Markup,
list[data_model.Capella2PolarionAttachment],
]:
) -> SanitizedText:
referenced_uuids: list[str] = []
replaced_markup = RE_DESCR_LINK_PATTERN.sub(
lambda match: self._replace_markup(
Expand Down Expand Up @@ -334,7 +339,7 @@ def repair_images(node: etree._Element) -> None:
repaired_markup = chelpers.process_html_fragments(
replaced_markup, repair_images
)
return referenced_uuids, repaired_markup, attachments
return SanitizedText(referenced_uuids, repaired_markup, attachments)

def _replace_markup(
self,
Expand Down Expand Up @@ -369,7 +374,13 @@ def _replace_markup(

def _get_requirement_types_text(
self, obj: m.ModelElement | m.Diagram
) -> dict[str, dict[str, str]]:
) -> tuple[
list[str],
dict[str, dict[str, str]],
list[data_model.Capella2PolarionAttachment],
]:
referenced_uuids: list[str] = []
attachments: list[data_model.Capella2PolarionAttachment] = []
type_texts = collections.defaultdict(list)
for req in getattr(obj, "requirements", []):
if req is None:
Expand All @@ -387,8 +398,13 @@ def _get_requirement_types_text(
)
continue

type_texts[req.type.long_name].append(req.text)
return _format_texts(type_texts)
sanitized_text = self._sanitize_text(
obj, req.text or markupsafe.Markup("")
)
referenced_uuids.extend(sanitized_text.referenced_uuids)
type_texts[req.type.long_name].append(sanitized_text)
attachments.extend(sanitized_text.attachments)
return referenced_uuids, _format_texts(type_texts), attachments

# Serializer implementation starts below

Expand All @@ -399,27 +415,39 @@ def __generic_work_item(
) -> data_model.CapellaWorkItem:
obj = converter_data.capella_element
raw_description = getattr(obj, "description", None)
uuids, value, attachments = self._sanitize_text(
sanitized_text = self._sanitize_text(
obj, raw_description or markupsafe.Markup("")
)
converter_data.description_references = uuids
requirement_types = self._get_requirement_types_text(obj)

converter_data.description_references = sanitized_text.referenced_uuids
converter_data.work_item = data_model.CapellaWorkItem(
id=work_item_id,
type=converter_data.type_config.p_type,
title=obj.name,
uuid_capella=obj.uuid,
description=polarion_api.HtmlContent(value),
description=polarion_api.HtmlContent(sanitized_text.text),
status="open",
**requirement_types, # type:ignore[arg-type]
)
assert converter_data.work_item is not None
for attachment in attachments:
for attachment in sanitized_text.attachments:
self._add_attachment(converter_data.work_item, attachment)

return converter_data.work_item

def _add_requirements_text_grouped_by_type(
self, converter_data: data_session.ConverterData
):
"""Add requirements custom fields to work item."""
obj = converter_data.capella_element
assert converter_data.work_item is not None
uuids, requirement_types, attachments = (
self._get_requirement_types_text(obj)
)
converter_data.work_item.additional_attributes |= requirement_types
converter_data.requirement_references = uuids
for attachment in attachments:
self._add_attachment(converter_data.work_item, attachment)
return converter_data.work_item

def _diagram(
self,
converter_data: data_session.ConverterData,
Expand Down
25 changes: 24 additions & 1 deletion capella2polarion/converters/link_converter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
"""Objects for synchronization of Capella model objects to Polarion."""

from __future__ import annotations

import collections.abc as cabc
Expand Down Expand Up @@ -46,6 +47,7 @@ def __init__(

self.serializers: dict[str, _Serializer] = {
converter_config.DESCRIPTION_REFERENCE_SERIALIZER: self._handle_description_reference_links, # pylint: disable=line-too-long
converter_config.REQUIREMENT_REFERENCE_SERIALIZER: self._handle_requirement_reference_links, # pylint: disable=line-too-long
converter_config.DIAGRAM_ELEMENTS_SERIALIZER: self._handle_diagram_reference_links, # pylint: disable=line-too-long
}

Expand Down Expand Up @@ -148,7 +150,28 @@ def _handle_description_reference_links(
role_id: str,
links: dict[str, polarion_api.WorkItemLink],
) -> list[polarion_api.WorkItemLink]:
refs = self.converter_session[obj.uuid].description_references
return self._handle_reference_links(obj, work_item_id, role_id, links)

def _handle_requirement_reference_links(
self,
obj: m.ModelElement | m.Diagram,
work_item_id: str,
role_id: str,
links: dict[str, polarion_api.WorkItemLink],
) -> list[polarion_api.WorkItemLink]:
return self._handle_reference_links(
obj, work_item_id, role_id, links, "requirement_references"
)

def _handle_reference_links(
self,
obj: m.ModelElement | m.Diagram,
work_item_id: str,
role_id: str,
links: dict[str, polarion_api.WorkItemLink],
reference_type: str = "description_references",
) -> list[polarion_api.WorkItemLink]:
refs = getattr(self.converter_session[obj.uuid], reference_type)
ref_set = set(self._get_work_item_ids(work_item_id, refs, role_id))
return self._create(work_item_id, role_id, ref_set, links)

Expand Down
7 changes: 7 additions & 0 deletions capella2polarion/data_model/work_items.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
"""Module providing the CapellaWorkItem class."""

from __future__ import annotations

import dataclasses
Expand Down Expand Up @@ -183,3 +184,9 @@ def calculate_checksum(self) -> str:
| dict(sorted(self._attachment_checksums.items()))
)
return self.checksum

def __eq__(self, other: object) -> bool:
"""Compare two CapellaWorkItems."""
if not isinstance(other, CapellaWorkItem):
return False
return self.calculate_checksum() == other.calculate_checksum()
2 changes: 1 addition & 1 deletion tests/data/model/Melody Model Test.capella
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
<ownedRequirements xsi:type="Requirements:Requirement" id="3c2d312c-37c9-41b5-8c32-67578fa52dc3"
ReqIFIdentifier="REQTYPE-1" ReqIFDescription="This is a test requirement of kind 1."
ReqIFLongName="1" ReqIFName="" ReqIFPrefix="3" requirementType="#db47fca9-ddb6-4397-8d4b-e397e53d277e"
ReqIFChapterName="TestReq1" ReqIFForeignID="1" ReqIFText="&lt;p>Test requirement 1 really l o n g text that is&amp;nbsp;way too long to display here as that&lt;/p>&#xA;&#xA;&lt;p>&amp;lt; &amp;gt; &amp;quot; &amp;#39;&lt;/p>&#xA;&#xA;&lt;ul>&#xA;&#x9;&lt;li>This&amp;nbsp;is a list&lt;/li>&#xA;&#x9;&lt;li>an unordered one&lt;/li>&#xA;&lt;/ul>&#xA;&#xA;&lt;ol>&#xA;&#x9;&lt;li>Ordered list&lt;/li>&#xA;&#x9;&lt;li>Ok&lt;/li>&#xA;&lt;/ol>&#xA;"
ReqIFChapterName="TestReq1" ReqIFForeignID="1" ReqIFText="&lt;p>Test requirement 1 really l o n g text that is&amp;nbsp;way too long to display here as that&amp;nbsp;&lt;a href=&quot;hlink://ceffa011-7b66-4b3c-9885-8e075e312ffa&quot;>Important Function&lt;/a>&lt;/p>&#xA;&#xA;&lt;p>&amp;lt; &amp;gt; &amp;quot; &amp;#39;&lt;/p>&#xA;&#xA;&lt;ul>&#xA;&#x9;&lt;li>This&amp;nbsp;is a list&lt;/li>&#xA;&#x9;&lt;li>an unordered one&lt;/li>&#xA;&lt;/ul>&#xA;&#xA;&lt;ol>&#xA;&#x9;&lt;li>Ordered list&lt;/li>&#xA;&#x9;&lt;li>Ok&lt;/li>&#xA;&lt;/ol>&#xA;&#xA;&lt;div>&lt;a href=&quot;hlink://00e7b925-cf4c-4cb0-929e-5409a1cd872b&quot;>Sysexfunc&lt;/a>&lt;/div>&#xA;"
requirementTypeProxy="7">
<ownedAttributes xsi:type="Requirements:BooleanValueAttribute" id="9c692405-b8aa-4caa-b988-51d27db5cd1b"
definition="#682bd51d-5451-4930-a97e-8bfca6c3a127" value="true"/>
Expand Down
Loading
Loading