From 93be3efac82c53c520742d217f88bc4638c578c9 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 21 Aug 2024 12:36:32 +0200 Subject: [PATCH 1/9] refactor: Add prefixes in `ConverterConfig` Instead of adding them during the serialization --- capella2polarion/__main__.py | 13 ++- .../converters/converter_config.py | 80 +++++++++++-------- .../converters/element_converter.py | 7 -- capella2polarion/converters/link_converter.py | 6 +- .../converters/model_converter.py | 3 - ci-templates/gitlab/synchronise_elements.yml | 6 +- tests/test_elements.py | 13 +-- 7 files changed, 66 insertions(+), 62 deletions(-) diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index 8f924768..2b0231ee 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -38,6 +38,8 @@ @click.option("--polarion-pat", envvar="POLARION_PAT", type=str) @click.option("--polarion-delete-work-items", is_flag=True, default=False) @click.option("--capella-model", type=cli_helpers.ModelCLI(), default=None) +@click.option("--type-prefix", type=str, default="") +@click.option("--role-prefix", type=str, default="") @click.pass_context def cli( ctx: click.core.Context, @@ -47,6 +49,8 @@ def cli( polarion_pat: str, polarion_delete_work_items: bool, capella_model: capellambse.MelodyModel, + type_prefix: str, + role_prefix: str, ) -> None: """Synchronise data from Capella to Polarion.""" if capella_model.diagram_cache is None: @@ -59,6 +63,8 @@ def cli( polarion_pat, polarion_delete_work_items, capella_model, + type_prefix=type_prefix, + role_prefix=role_prefix, ) capella2polarion_cli.setup_logger() ctx.obj = capella2polarion_cli @@ -79,15 +85,11 @@ def print_cli_state(capella2polarion_cli: Capella2PolarionCli) -> None: default=None, ) @click.option("--force-update", is_flag=True, default=False) -@click.option("--type-prefix", type=str, default="") -@click.option("--role-prefix", type=str, default="") @click.pass_context def synchronize( ctx: click.core.Context, force_update: bool, synchronize_config: typing.TextIO, - type_prefix: str, - role_prefix: str, ) -> None: """Synchronise model elements.""" capella_to_polarion_cli: Capella2PolarionCli = ctx.obj @@ -97,13 +99,10 @@ def synchronize( ) capella_to_polarion_cli.load_synchronize_config(synchronize_config) capella_to_polarion_cli.force_update = force_update - capella_to_polarion_cli.type_prefix = type_prefix - capella_to_polarion_cli.role_prefix = role_prefix converter = model_converter.ModelConverter( capella_to_polarion_cli.capella_model, capella_to_polarion_cli.polarion_params.project_id, - type_prefix=capella_to_polarion_cli.type_prefix, role_prefix=capella_to_polarion_cli.role_prefix, ) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index d81ab52c..57ef7766 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -63,7 +63,10 @@ def _default_type_conversion(c_type: str) -> str: class ConverterConfig: """The overall Config for capella2polarion.""" - def __init__(self): + def __init__(self, type_prefix: str = "", role_prefix: str = ""): + self.type_prefix = type_prefix + self.role_prefix = role_prefix + self._layer_configs: dict[str, dict[str, list[CapellaTypeConfig]]] = {} self._global_configs: dict[str, CapellaTypeConfig] = {} self.polarion_types = set[str]() @@ -77,7 +80,7 @@ def read_config_file(self, synchronize_config: t.TextIO): global_config_dict = config_dict.pop("*", {}) all_type_config = global_config_dict.pop("*", {}) global_links = all_type_config.get("links", []) - self.__global_config.links = _force_link_config(global_links) + self.__global_config.links = self._force_link_config(global_links) if "Diagram" in global_config_dict: diagram_config = global_config_dict.pop("Diagram") or {} @@ -122,21 +125,21 @@ def set_layer_config( # As we set up all types this way, we can expect that all # non-compliant links are coming from global context here closest_links = _filter_links(c_type, closest_config.links, True) - p_type = ( - type_config.get("polarion_type") - or closest_config.p_type - or _default_type_conversion(c_type) + p_type = add_prefix( + ( + type_config.get("polarion_type") + or closest_config.p_type + or _default_type_conversion(c_type) + ), + self.type_prefix, ) self.polarion_types.add(p_type) + links = self._force_link_config(type_config.get("links", [])) self._layer_configs[layer][c_type].append( CapellaTypeConfig( p_type, type_config.get("serializer") or closest_config.converters, - _filter_links( - c_type, - _force_link_config(type_config.get("links", [])), - ) - + closest_links, + _filter_links(c_type, links) + closest_links, type_config.get("is_actor", _C2P_DEFAULT), type_config.get("nature", _C2P_DEFAULT), ) @@ -152,7 +155,7 @@ def set_global_config(self, c_type: str, type_config: dict[str, t.Any]): p_type, type_config.get("serializer"), _filter_links( - c_type, _force_link_config(type_config.get("links", [])) + c_type, self._force_link_config(type_config.get("links", [])) ) + self._get_global_links(c_type), type_config.get("is_actor", _C2P_DEFAULT), @@ -165,14 +168,39 @@ def set_diagram_config(self, diagram_config: dict[str, t.Any]): p_type = diagram_config.get("polarion_type") or "diagram" self.polarion_types.add(p_type) links = _filter_links( - c_type, _force_link_config(diagram_config.get("links", [])) + c_type, self._force_link_config(diagram_config.get("links", [])) ) self.diagram_config = CapellaTypeConfig( - p_type, + add_prefix(p_type, self.type_prefix), diagram_config.get("serializer") or "diagram", links + self._get_global_links(c_type), ) + def _force_link_config(self, links: t.Any) -> list[LinkConfig]: + result: list[LinkConfig] = [] + for link in links: + if isinstance(link, str): + config = LinkConfig( + capella_attr=link, + polarion_role=add_prefix(link, self.role_prefix), + ) + elif isinstance(link, dict): + config = LinkConfig( + capella_attr=(lid := link["capella_attr"]), + polarion_role=add_prefix( + link.get("polarion_role", lid), self.role_prefix + ), + include=link.get("include", {}), + ) + 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: @@ -252,25 +280,11 @@ def _force_dict( raise TypeError("Unsupported Type") -def _force_link_config(links: t.Any) -> list[LinkConfig]: - result: list[LinkConfig] = [] - for link in links: - if isinstance(link, str): - config = LinkConfig(capella_attr=link, polarion_role=link) - elif isinstance(link, dict): - config = LinkConfig( - capella_attr=(lid := link["capella_attr"]), - polarion_role=link.get("polarion_role", lid), - include=link.get("include", {}), - ) - else: - logger.error( - "Link not configured correctly: %r", - link, - ) - continue - result.append(config) - return result +def add_prefix(polarion_type: str, prefix: str) -> str: + """Add a prefix to the given ``polarion_type``.""" + if prefix: + return f"{prefix}_{polarion_type}" + return polarion_type def _filter_links( diff --git a/capella2polarion/converters/element_converter.py b/capella2polarion/converters/element_converter.py index 99c04000..fdf1662b 100644 --- a/capella2polarion/converters/element_converter.py +++ b/capella2polarion/converters/element_converter.py @@ -85,13 +85,11 @@ def __init__( capella_polarion_mapping: polarion_repo.PolarionDataRepository, converter_session: data_session.ConverterSession, generate_attachments: bool, - type_prefix: str = "", ): self.model = model self.capella_polarion_mapping = capella_polarion_mapping self.converter_session = converter_session self.generate_attachments = generate_attachments - self.type_prefix = type_prefix self.jinja_envs: dict[str, jinja2.Environment] = {} def serialize_all(self) -> list[data_models.CapellaWorkItem]: @@ -125,11 +123,6 @@ def serialize(self, uuid: str) -> data_models.CapellaWorkItem | None: ) converter_data.work_item = None - if self.type_prefix and converter_data.work_item is not None: - converter_data.work_item.type = ( - f"{self.type_prefix}_{converter_data.work_item.type}" - ) - if converter_data.errors: log_args = ( converter_data.capella_element._short_repr_(), diff --git a/capella2polarion/converters/link_converter.py b/capella2polarion/converters/link_converter.py index 2632edb5..a7932247 100644 --- a/capella2polarion/converters/link_converter.py +++ b/capella2polarion/converters/link_converter.py @@ -63,8 +63,6 @@ def create_links_for_work_item( for link_config in converter_data.type_config.links: serializer = self.serializers.get(link_config.capella_attr) role_id = link_config.polarion_role - if self.role_prefix: - role_id = f"{self.role_prefix}_{role_id}" try: if serializer: new_links.extend( @@ -225,13 +223,13 @@ def create_grouped_link_fields( key = link.secondary_work_item_id back_links.setdefault(key, []).append(link) - role_id = self._remove_prefix(role) config: converter_config.LinkConfig | None = None for link_config in data.type_config.links: - if link_config.polarion_role == role_id: + if link_config.polarion_role == role: config = link_config break + role_id = self._remove_prefix(role) self._create_link_fields( work_item, role_id, grouped_links, config=config ) diff --git a/capella2polarion/converters/model_converter.py b/capella2polarion/converters/model_converter.py index bc25897c..35bf1828 100644 --- a/capella2polarion/converters/model_converter.py +++ b/capella2polarion/converters/model_converter.py @@ -29,12 +29,10 @@ def __init__( self, model: capellambse.MelodyModel, project_id: str, - type_prefix: str = "", role_prefix: str = "", ): self.model = model self.project_id = project_id - self.type_prefix = type_prefix self.role_prefix = role_prefix self.converter_session: data_session.ConverterSession = {} @@ -110,7 +108,6 @@ def generate_work_items( polarion_data_repo, self.converter_session, generate_attachments, - self.type_prefix, ) work_items = serializer.serialize_all() for work_item in work_items: diff --git a/ci-templates/gitlab/synchronise_elements.yml b/ci-templates/gitlab/synchronise_elements.yml index 7d58264f..e2299051 100644 --- a/ci-templates/gitlab/synchronise_elements.yml +++ b/ci-templates/gitlab/synchronise_elements.yml @@ -17,8 +17,8 @@ capella2polarion_synchronise_elements: $([[ $CAPELLA2POLARION_DEBUG -eq 1 ]] && echo '--debug') \ --polarion-project-id=${CAPELLA2POLARION_PROJECT_ID:?} \ --capella-model="${CAPELLA2POLARION_MODEL_JSON:?}" \ + ${CAPELLA2POLARION_TYPE_PREFIX:+--type-prefix="$CAPELLA2POLARION_TYPE_PREFIX"} \ + ${CAPELLA2POLARION_ROLE_PREFIX:+--role-prefix="$CAPELLA2POLARION_ROLE_PREFIX"} \ synchronize \ --synchronize-config=${CAPELLA2POLARION_CONFIG:?} \ - $([[ $CAPELLA2POLARION_FORCE_UPDATE -eq 1 ]] && echo '--force-update') \ - ${CAPELLA2POLARION_TYPE_PREFIX:+--type-prefix="$CAPELLA2POLARION_TYPE_PREFIX"} \ - ${CAPELLA2POLARION_ROLE_PREFIX:+--role-prefix="$CAPELLA2POLARION_ROLE_PREFIX"} + $([[ $CAPELLA2POLARION_FORCE_UPDATE -eq 1 ]] && echo '--force-update') diff --git a/tests/test_elements.py b/tests/test_elements.py index e46e501c..11d332b2 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -718,8 +718,10 @@ def test_create_link_from_single_attribute_with_role_prefix( ) base_object.pw.polarion_data_repo.update_work_items([work_item_2]) - base_object.mc.converter_session["uuid2"].work_item = work_item_2 - + converter_data = base_object.mc.converter_session["uuid2"] + converter_data.work_item = work_item_2 + link_config = converter_data.type_config.links[0] + link_config.polarion_role = f"_C2P_{link_config.polarion_role}" expected = polarion_api.WorkItemLink( "Obj-2", "Obj-1", @@ -733,6 +735,7 @@ def test_create_link_from_single_attribute_with_role_prefix( base_object.c2pcli.capella_model, role_prefix="_C2P", ) + links = link_serializer.create_links_for_work_item("uuid2") assert links == [expected] @@ -1730,7 +1733,7 @@ def test_add_jinja_to_description(self, model: capellambse.MelodyModel): def test_multiple_serializers(model: capellambse.MelodyModel, prefix: str): cap = model.by_uuid(TEST_OCAP_UUID) type_config = converter_config.CapellaTypeConfig( - "test", + f"{prefix}_test" if prefix else "test", ["include_pre_and_post_condition", "add_context_diagram"], [], ) @@ -1743,12 +1746,12 @@ def test_multiple_serializers(model: capellambse.MelodyModel, prefix: str): ) }, True, - prefix, ) work_item = serializer.serialize(TEST_OCAP_UUID) assert work_item is not None + assert work_item.type is not None assert work_item.type.startswith(prefix) assert "preCondition" in work_item.additional_attributes assert "postCondition" in work_item.additional_attributes @@ -1815,13 +1818,13 @@ def test_generic_work_item_with_type_prefix( } type_config = config.get_type_config(layer, c_type, **attributes) assert type_config is not None + type_config.p_type = f"{prefix}_{type_config.p_type}" ework_item = data_models.CapellaWorkItem(id=f"{prefix}_TEST") serializer = element_converter.CapellaWorkItemSerializer( model, polarion_repo.PolarionDataRepository([ework_item]), {uuid: data_session.ConverterData(layer, type_config, obj)}, False, - prefix, ) work_item = serializer.serialize(uuid) From d82a975850e7eaae9793e73be6d03ffa14a15d87 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 21 Aug 2024 12:40:35 +0200 Subject: [PATCH 2/9] refactor(cli): Move `force_update` to CLI group --- capella2polarion/__main__.py | 10 +++++----- ci-templates/gitlab/synchronise_elements.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index 2b0231ee..d4ce6e42 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -38,6 +38,7 @@ @click.option("--polarion-pat", envvar="POLARION_PAT", type=str) @click.option("--polarion-delete-work-items", is_flag=True, default=False) @click.option("--capella-model", type=cli_helpers.ModelCLI(), default=None) +@click.option("--force-update", is_flag=True, default=False) @click.option("--type-prefix", type=str, default="") @click.option("--role-prefix", type=str, default="") @click.pass_context @@ -49,6 +50,7 @@ def cli( polarion_pat: str, polarion_delete_work_items: bool, capella_model: capellambse.MelodyModel, + force_update: bool, type_prefix: str, role_prefix: str, ) -> None: @@ -63,8 +65,9 @@ def cli( polarion_pat, polarion_delete_work_items, capella_model, - type_prefix=type_prefix, - role_prefix=role_prefix, + force_update, + type_prefix, + role_prefix, ) capella2polarion_cli.setup_logger() ctx.obj = capella2polarion_cli @@ -84,11 +87,9 @@ def print_cli_state(capella2polarion_cli: Capella2PolarionCli) -> None: type=click.File(mode="r", encoding="utf8"), default=None, ) -@click.option("--force-update", is_flag=True, default=False) @click.pass_context def synchronize( ctx: click.core.Context, - force_update: bool, synchronize_config: typing.TextIO, ) -> None: """Synchronise model elements.""" @@ -98,7 +99,6 @@ def synchronize( capella_to_polarion_cli.polarion_params.project_id, ) capella_to_polarion_cli.load_synchronize_config(synchronize_config) - capella_to_polarion_cli.force_update = force_update converter = model_converter.ModelConverter( capella_to_polarion_cli.capella_model, diff --git a/ci-templates/gitlab/synchronise_elements.yml b/ci-templates/gitlab/synchronise_elements.yml index e2299051..76f497ed 100644 --- a/ci-templates/gitlab/synchronise_elements.yml +++ b/ci-templates/gitlab/synchronise_elements.yml @@ -17,8 +17,8 @@ capella2polarion_synchronise_elements: $([[ $CAPELLA2POLARION_DEBUG -eq 1 ]] && echo '--debug') \ --polarion-project-id=${CAPELLA2POLARION_PROJECT_ID:?} \ --capella-model="${CAPELLA2POLARION_MODEL_JSON:?}" \ + $([[ $CAPELLA2POLARION_FORCE_UPDATE -eq 1 ]] && echo '--force-update') \ ${CAPELLA2POLARION_TYPE_PREFIX:+--type-prefix="$CAPELLA2POLARION_TYPE_PREFIX"} \ ${CAPELLA2POLARION_ROLE_PREFIX:+--role-prefix="$CAPELLA2POLARION_ROLE_PREFIX"} \ synchronize \ - --synchronize-config=${CAPELLA2POLARION_CONFIG:?} \ - $([[ $CAPELLA2POLARION_FORCE_UPDATE -eq 1 ]] && echo '--force-update') + --synchronize-config=${CAPELLA2POLARION_CONFIG:?} From 001fa1957e64ff79479b231a591faf21d94ced71 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 22 Aug 2024 14:55:30 +0200 Subject: [PATCH 3/9] feat: Implement `link_field` and `reverse_field` IDs for `LinkConfig` --- capella2polarion/__main__.py | 21 ++-- capella2polarion/cli.py | 13 ++- .../converters/converter_config.py | 35 ++++-- capella2polarion/converters/link_converter.py | 53 +++++---- .../converters/model_converter.py | 6 +- tests/data/model_elements/config.yaml | 4 + tests/test_elements.py | 106 ++++++++++++------ 7 files changed, 151 insertions(+), 87 deletions(-) diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index d4ce6e42..77a136d7 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -38,9 +38,6 @@ @click.option("--polarion-pat", envvar="POLARION_PAT", type=str) @click.option("--polarion-delete-work-items", is_flag=True, default=False) @click.option("--capella-model", type=cli_helpers.ModelCLI(), default=None) -@click.option("--force-update", is_flag=True, default=False) -@click.option("--type-prefix", type=str, default="") -@click.option("--role-prefix", type=str, default="") @click.pass_context def cli( ctx: click.core.Context, @@ -50,9 +47,6 @@ def cli( polarion_pat: str, polarion_delete_work_items: bool, capella_model: capellambse.MelodyModel, - force_update: bool, - type_prefix: str, - role_prefix: str, ) -> None: """Synchronise data from Capella to Polarion.""" if capella_model.diagram_cache is None: @@ -65,9 +59,6 @@ def cli( polarion_pat, polarion_delete_work_items, capella_model, - force_update, - type_prefix, - role_prefix, ) capella2polarion_cli.setup_logger() ctx.obj = capella2polarion_cli @@ -87,10 +78,16 @@ def print_cli_state(capella2polarion_cli: Capella2PolarionCli) -> None: type=click.File(mode="r", encoding="utf8"), default=None, ) +@click.option("--force-update", is_flag=True, default=False) +@click.option("--type-prefix", type=str, default="") +@click.option("--role-prefix", type=str, default="") @click.pass_context def synchronize( ctx: click.core.Context, synchronize_config: typing.TextIO, + force_update: bool, + type_prefix: str, + role_prefix: str, ) -> None: """Synchronise model elements.""" capella_to_polarion_cli: Capella2PolarionCli = ctx.obj @@ -98,12 +95,14 @@ def synchronize( "Synchronising model elements to Polarion project with id %s...", capella_to_polarion_cli.polarion_params.project_id, ) - capella_to_polarion_cli.load_synchronize_config(synchronize_config) + capella_to_polarion_cli.load_synchronize_config( + synchronize_config, type_prefix, role_prefix + ) + capella_to_polarion_cli.force_update = force_update converter = model_converter.ModelConverter( capella_to_polarion_cli.capella_model, capella_to_polarion_cli.polarion_params.project_id, - role_prefix=capella_to_polarion_cli.role_prefix, ) converter.read_model(capella_to_polarion_cli.config) diff --git a/capella2polarion/cli.py b/capella2polarion/cli.py index 038e8d98..f0c0d1e8 100644 --- a/capella2polarion/cli.py +++ b/capella2polarion/cli.py @@ -28,8 +28,6 @@ def __init__( polarion_delete_work_items: bool, capella_model: capellambse.MelodyModel, force_update: bool = False, - type_prefix: str = "", - role_prefix: str = "", ) -> None: self.debug = debug self.polarion_params = pw.PolarionWorkerParams( @@ -42,8 +40,6 @@ def __init__( self.capella_model = capella_model self.config = converter_config.ConverterConfig() self.force_update = force_update - self.type_prefix = type_prefix - self.role_prefix = role_prefix def _none_save_value_string(self, value: str | None) -> str | None: return "None" if value is None else value @@ -97,7 +93,10 @@ def setup_logger(self) -> None: logging.getLogger("httpcore").setLevel("WARNING") def load_synchronize_config( - self, synchronize_config_io: typing.TextIO + self, + synchronize_config_io: typing.TextIO, + type_prefix: str = "", + role_prefix: str = "", ) -> None: """Read the sync config into SynchronizeConfigContent. @@ -107,4 +106,6 @@ def load_synchronize_config( raise RuntimeError("synchronize config io stream is closed ") if not synchronize_config_io.readable(): raise RuntimeError("synchronize config io stream is not readable") - self.config.read_config_file(synchronize_config_io) + self.config.read_config_file( + synchronize_config_io, type_prefix, role_prefix + ) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index 57ef7766..d5c155fa 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -39,6 +39,8 @@ class LinkConfig: capella_attr: str polarion_role: str include: dict[str, str] = dataclasses.field(default_factory=dict) + link_field: str = "" + reverse_field: str = "" @dataclasses.dataclass @@ -63,18 +65,25 @@ def _default_type_conversion(c_type: str) -> str: class ConverterConfig: """The overall Config for capella2polarion.""" - def __init__(self, type_prefix: str = "", role_prefix: str = ""): - self.type_prefix = type_prefix - self.role_prefix = role_prefix - + def __init__(self): self._layer_configs: dict[str, dict[str, list[CapellaTypeConfig]]] = {} self._global_configs: dict[str, CapellaTypeConfig] = {} self.polarion_types = set[str]() self.diagram_config: CapellaTypeConfig | None = None self.__global_config = CapellaTypeConfig() - def read_config_file(self, synchronize_config: t.TextIO): + self._type_prefix: str + self._role_prefix: str + + def read_config_file( + self, + synchronize_config: t.TextIO, + type_prefix: str = "", + role_prefix: str = "", + ): """Read a given yaml file as config.""" + self._type_prefix = type_prefix + self._role_prefix = role_prefix config_dict = yaml.safe_load(synchronize_config) # We handle the cross layer config separately as global_configs global_config_dict = config_dict.pop("*", {}) @@ -131,7 +140,7 @@ def set_layer_config( or closest_config.p_type or _default_type_conversion(c_type) ), - self.type_prefix, + self._type_prefix, ) self.polarion_types.add(p_type) links = self._force_link_config(type_config.get("links", [])) @@ -171,7 +180,7 @@ def set_diagram_config(self, diagram_config: dict[str, t.Any]): c_type, self._force_link_config(diagram_config.get("links", [])) ) self.diagram_config = CapellaTypeConfig( - add_prefix(p_type, self.type_prefix), + add_prefix(p_type, self._type_prefix), diagram_config.get("serializer") or "diagram", links + self._get_global_links(c_type), ) @@ -182,15 +191,21 @@ def _force_link_config(self, links: t.Any) -> list[LinkConfig]: if isinstance(link, str): config = LinkConfig( capella_attr=link, - polarion_role=add_prefix(link, self.role_prefix), + polarion_role=(pid := add_prefix(link, self._role_prefix)), + link_field=pid, + reverse_field=f"{pid}_reverse", ) elif isinstance(link, dict): config = LinkConfig( capella_attr=(lid := link["capella_attr"]), - polarion_role=add_prefix( - link.get("polarion_role", lid), self.role_prefix + polarion_role=( + pid := add_prefix( + link.get("polarion_role", lid), self._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( diff --git a/capella2polarion/converters/link_converter.py b/capella2polarion/converters/link_converter.py index a7932247..d5f185a3 100644 --- a/capella2polarion/converters/link_converter.py +++ b/capella2polarion/converters/link_converter.py @@ -36,13 +36,11 @@ def __init__( converter_session: data_session.ConverterSession, project_id: str, model: capellambse.MelodyModel, - role_prefix: str = "", ): self.capella_polarion_mapping = capella_polarion_mapping self.converter_session = converter_session self.project_id = project_id self.model = model - self.role_prefix = role_prefix self.serializers: dict[str, _Serializer] = { converter_config.DESCRIPTION_REFERENCE_SERIALIZER: self._handle_description_reference_links, # pylint: disable=line-too-long @@ -223,16 +221,11 @@ def create_grouped_link_fields( key = link.secondary_work_item_id back_links.setdefault(key, []).append(link) - config: converter_config.LinkConfig | None = None - for link_config in data.type_config.links: - if link_config.polarion_role == role: - config = link_config - break - - role_id = self._remove_prefix(role) - self._create_link_fields( - work_item, role_id, grouped_links, config=config - ) + config = find_link_config(data, role) + if config is not None and config.link_field: + self._create_link_fields( + work_item, config.link_field, grouped_links, config=config + ) def _create_link_fields( self, @@ -244,7 +237,6 @@ def _create_link_fields( ): link_map: dict[str, dict[str, list[str]]] if reverse: - role = f"{role}_reverse" link_map = {link.primary_work_item_id: {} for link in links} else: link_map = {link.secondary_work_item_id: {} for link in links} @@ -301,26 +293,41 @@ def _create_link_fields( def create_grouped_back_link_fields( self, - work_item: data_models.CapellaWorkItem, + data: data_session.ConverterData, links: list[polarion_api.WorkItemLink], ): """Create fields for the given WorkItem using a list of backlinks. Parameters ---------- - work_item - WorkItem to create the fields for + data + The ConverterData that stores the WorkItem to create the + fields for. links - List of links referencing work_item as secondary + List of links referencing work_item as secondary. """ + work_item = data.work_item + assert work_item is not None + wi = f"[{work_item.id}]({work_item.type} {work_item.title})" + logger.debug("Building grouped back links for work item %r...", wi) for role, grouped_links in _group_by("role", links).items(): - role_id = self._remove_prefix(role) - self._create_link_fields(work_item, role_id, grouped_links, True) + config = find_link_config(data, role) + if config is not None and config.reverse_field: + self._create_link_fields( + work_item, config.reverse_field, grouped_links, True + ) + + +def find_link_config( + data: data_session.ConverterData, role: str +) -> converter_config.LinkConfig | None: + """Search for LinkConfig with matching polarion_role in ``data``.""" + for link_config in data.type_config.links: + if link_config.polarion_role == role: + return link_config - def _remove_prefix(self, role: str) -> str: - if self.role_prefix: - return role.removeprefix(f"{self.role_prefix}_") - return role + logger.error("No LinkConfig found for %r", role) + return None def _group_by( diff --git a/capella2polarion/converters/model_converter.py b/capella2polarion/converters/model_converter.py index 35bf1828..4336966b 100644 --- a/capella2polarion/converters/model_converter.py +++ b/capella2polarion/converters/model_converter.py @@ -29,11 +29,9 @@ def __init__( self, model: capellambse.MelodyModel, project_id: str, - role_prefix: str = "", ): self.model = model self.project_id = project_id - self.role_prefix = role_prefix self.converter_session: data_session.ConverterSession = {} @@ -130,7 +128,6 @@ def generate_work_item_links( self.converter_session, self.project_id, self.model, - self.role_prefix, ) for uuid, converter_data in self.converter_session.items(): if converter_data.work_item is None: @@ -155,7 +152,8 @@ def generate_work_item_links( ) continue + assert converter_data.work_item.id is not None if local_back_links := back_links.get(converter_data.work_item.id): link_serializer.create_grouped_back_link_fields( - converter_data.work_item, local_back_links + converter_data, local_back_links ) diff --git a/tests/data/model_elements/config.yaml b/tests/data/model_elements/config.yaml index 49d426c4..be348f53 100644 --- a/tests/data/model_elements/config.yaml +++ b/tests/data/model_elements/config.yaml @@ -55,10 +55,14 @@ sa: capella_attr: inputs.exchanges include: Exchange Items: exchange_items + link_field: inputExchanges + reverse_field: inputExchangesReverse - polarion_role: output_exchanges capella_attr: outputs.exchanges include: Exchange Items: exchange_items + link_field: output_exchanges + reverse_field: output_exchanges_reverse FunctionalExchange: links: - exchanged_items diff --git a/tests/test_elements.py b/tests/test_elements.py index 11d332b2..97e103ae 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -165,8 +165,6 @@ class GroupedLinksBaseObject(t.TypedDict): link_serializer: link_converter.LinkSerializer work_items: dict[str, data_models.CapellaWorkItem] - back_links: dict[str, list[polarion_api.WorkItemLink]] - reverse_polarion_id_map: dict[str, str] config: converter_config.CapellaTypeConfig @@ -176,13 +174,14 @@ def grouped_links_base_object( base_object: BaseObjectContainer, dummy_work_items: dict[str, data_models.CapellaWorkItem], ) -> GroupedLinksBaseObject: - reverse_polarion_id_map = {v: k for k, v in POLARION_ID_MAP.items()} - back_links: dict[str, list[polarion_api.WorkItemLink]] = {} config = converter_config.CapellaTypeConfig( "fakeModelObject", links=[ converter_config.LinkConfig( - capella_attr="attribute", polarion_role="attribute" + capella_attr="attribute", + polarion_role="attribute", + link_field="attribute", + reverse_field="attribute_reverse", ) ], ) @@ -205,8 +204,6 @@ def grouped_links_base_object( return { "link_serializer": link_serializer, "work_items": dummy_work_items, - "back_links": back_links, - "reverse_polarion_id_map": reverse_polarion_id_map, "config": config, } @@ -733,7 +730,6 @@ def test_create_link_from_single_attribute_with_role_prefix( base_object.mc.converter_session, base_object.pw.polarion_params.project_id, base_object.c2pcli.capella_model, - role_prefix="_C2P", ) links = link_serializer.create_links_for_work_item("uuid2") @@ -1135,7 +1131,10 @@ def test_maintain_grouped_links_attributes( "fakeModelObject", links=[ converter_config.LinkConfig( - capella_attr="attribute", polarion_role="attribute" + capella_attr="attribute", + polarion_role="attribute", + link_field="attribute", + reverse_field="attribute_reverse", ) ], ) @@ -1188,7 +1187,10 @@ def test_maintain_grouped_links_attributes_with_role_prefix( "fakeModelObject", links=[ converter_config.LinkConfig( - capella_attr="attribute", polarion_role="attribute" + capella_attr="attribute", + polarion_role="_C2P_attribute", + link_field="attribute", + reverse_field="attribute_reverse", ) ], ) @@ -1209,7 +1211,6 @@ def test_maintain_grouped_links_attributes_with_role_prefix( base_object.mc.converter_session, base_object.pw.polarion_params.project_id, mock_model, - role_prefix="_C2P", ) for work_item in dummy_work_items.values(): @@ -1219,7 +1220,9 @@ def test_maintain_grouped_links_attributes_with_role_prefix( link_serializer.create_grouped_link_fields(converter_data) assert "attribute" in dummy_work_items["uuid0"].additional_attributes - assert "attribute" in dummy_work_items["uuid1"].additional_attributes + assert ( # Link Role on links were not prefixed + "attribute" not in dummy_work_items["uuid1"].additional_attributes + ) @staticmethod @pytest.mark.parametrize("role_prefix", ["", "_C2P"]) @@ -1234,16 +1237,23 @@ def test_grouped_links_attributes_with_includes( "systemFunction", links=[ converter_config.LinkConfig( - "inputs.exchanges", - "input_exchanges", + capella_attr="inputs.exchanges", + polarion_role="input_exchanges", include={"Exchange Items": "exchange_items"}, + link_field="input_exchanges", + reverse_field="input_exchanges_reverse", ) ], ) ex_config = converter_config.CapellaTypeConfig( "systemFunctionalExchange", links=[ - converter_config.LinkConfig("exchange_items", "exchange_items") + converter_config.LinkConfig( + capella_attr="exchange_items", + polarion_role="exchange_items", + link_field="exchange_items", + reverse_field="exchange_items_reverse", + ) ], ) ex_item_config = converter_config.CapellaTypeConfig("exchangeItem") @@ -1312,22 +1322,21 @@ def test_maintain_reverse_grouped_links_attributes( ): link_serializer = grouped_links_base_object["link_serializer"] dummy_work_items = grouped_links_base_object["work_items"] - reverse_polarion_id_map = grouped_links_base_object[ - "reverse_polarion_id_map" - ] - back_links = grouped_links_base_object["back_links"] config = grouped_links_base_object["config"] + back_links: dict[str, list[polarion_api.WorkItemLink]] = {} + data = {} for work_item in dummy_work_items.values(): - converter_data = data_session.ConverterData( + data[work_item.id] = converter_data = data_session.ConverterData( "test", config, [], work_item ) link_serializer.create_grouped_link_fields( converter_data, back_links ) for work_item_id, links in back_links.items(): - work_item = dummy_work_items[reverse_polarion_id_map[work_item_id]] - link_serializer.create_grouped_back_link_fields(work_item, links) + link_serializer.create_grouped_back_link_fields( + data[work_item_id], links + ) assert ( dummy_work_items["uuid0"].additional_attributes.pop( @@ -1354,25 +1363,27 @@ def test_maintain_reverse_grouped_links_attributes_with_role_prefix( ): link_serializer = grouped_links_base_object["link_serializer"] dummy_work_items = grouped_links_base_object["work_items"] - reverse_polarion_id_map = grouped_links_base_object[ - "reverse_polarion_id_map" - ] - back_links = grouped_links_base_object["back_links"] config = grouped_links_base_object["config"] - for link in dummy_work_items["uuid0"].linked_work_items: + config.links[0].polarion_role = f"_C2P_{config.links[0].polarion_role}" + back_links: dict[str, list[polarion_api.WorkItemLink]] = {} + data = {} + for link in ( + dummy_work_items["uuid0"].linked_work_items + + dummy_work_items["uuid1"].linked_work_items + ): link.role = f"_C2P_{link.role}" - link_serializer.role_prefix = "_C2P" for work_item in dummy_work_items.values(): - converter_data = data_session.ConverterData( + data[work_item.id] = converter_data = data_session.ConverterData( "test", config, [], work_item ) link_serializer.create_grouped_link_fields( converter_data, back_links ) for work_item_id, links in back_links.items(): - work_item = dummy_work_items[reverse_polarion_id_map[work_item_id]] - link_serializer.create_grouped_back_link_fields(work_item, links) + link_serializer.create_grouped_back_link_fields( + data[work_item_id], links + ) assert ( "attribute_reverse" @@ -1393,12 +1404,24 @@ 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", + links=[ + converter_config.LinkConfig( + capella_attr="attribute", + polarion_role="role1", + link_field="attribute1", + reverse_field="attribute_reverse", + ) + ], + ) work_item = data_models.CapellaWorkItem("id", "Dummy") + converter_data = data_session.ConverterData("test", config, [], work_item) links = [ polarion_api.WorkItemLink("prim1", "id", "role1"), polarion_api.WorkItemLink("prim2", "id", "role1"), ] - link_serializer.create_grouped_back_link_fields(work_item, links) + link_serializer.create_grouped_back_link_fields(converter_data, links) check_sum = work_item.calculate_checksum() @@ -1406,7 +1429,7 @@ def test_grouped_linked_work_items_order_consistency( polarion_api.WorkItemLink("prim2", "id", "role1"), polarion_api.WorkItemLink("prim1", "id", "role1"), ] - link_serializer.create_grouped_back_link_fields(work_item, links) + link_serializer.create_grouped_back_link_fields(converter_data, links) assert check_sum == work_item.calculate_checksum() @@ -1889,3 +1912,20 @@ def test_read_config_links(caplog: pytest.LogCaptureFixture): if link.capella_attr == "parent" ) assert caplog.record_tuples[0] + caplog.record_tuples[1] == expected + assert ( + system_fnc_config := config.get_type_config("sa", "SystemFunction") + ) + assert system_fnc_config.links[0] == converter_config.LinkConfig( + capella_attr="inputs.exchanges", + polarion_role="input_exchanges", + include={"Exchange Items": "exchange_items"}, + link_field="inputExchanges", + reverse_field="inputExchangesReverse", + ) + assert system_fnc_config.links[1] == converter_config.LinkConfig( + capella_attr="outputs.exchanges", + polarion_role="output_exchanges", + include={"Exchange Items": "exchange_items"}, + link_field="output_exchanges", + reverse_field="output_exchanges_reverse", + ) From a834934847c289f16b767508bffc552953c8c6c9 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 22 Aug 2024 15:00:59 +0200 Subject: [PATCH 4/9] fix: Reorder flags in ci-template --- ci-templates/gitlab/synchronise_elements.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci-templates/gitlab/synchronise_elements.yml b/ci-templates/gitlab/synchronise_elements.yml index 76f497ed..7d58264f 100644 --- a/ci-templates/gitlab/synchronise_elements.yml +++ b/ci-templates/gitlab/synchronise_elements.yml @@ -17,8 +17,8 @@ capella2polarion_synchronise_elements: $([[ $CAPELLA2POLARION_DEBUG -eq 1 ]] && echo '--debug') \ --polarion-project-id=${CAPELLA2POLARION_PROJECT_ID:?} \ --capella-model="${CAPELLA2POLARION_MODEL_JSON:?}" \ + synchronize \ + --synchronize-config=${CAPELLA2POLARION_CONFIG:?} \ $([[ $CAPELLA2POLARION_FORCE_UPDATE -eq 1 ]] && echo '--force-update') \ ${CAPELLA2POLARION_TYPE_PREFIX:+--type-prefix="$CAPELLA2POLARION_TYPE_PREFIX"} \ - ${CAPELLA2POLARION_ROLE_PREFIX:+--role-prefix="$CAPELLA2POLARION_ROLE_PREFIX"} \ - synchronize \ - --synchronize-config=${CAPELLA2POLARION_CONFIG:?} + ${CAPELLA2POLARION_ROLE_PREFIX:+--role-prefix="$CAPELLA2POLARION_ROLE_PREFIX"} From acae4aca27d2b25d41b438c887d00a9ed56e5856 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 23 Aug 2024 15:20:22 +0200 Subject: [PATCH 5/9] test: Remove obsolete `LinkConfig` --- tests/data/model_elements/config.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/data/model_elements/config.yaml b/tests/data/model_elements/config.yaml index be348f53..eaa72952 100644 --- a/tests/data/model_elements/config.yaml +++ b/tests/data/model_elements/config.yaml @@ -61,8 +61,6 @@ sa: capella_attr: outputs.exchanges include: Exchange Items: exchange_items - link_field: output_exchanges - reverse_field: output_exchanges_reverse FunctionalExchange: links: - exchanged_items From 352466efec53fd48e3125b1bb536bfcd319207fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernst=20W=C3=BCrger?= Date: Fri, 23 Aug 2024 15:31:43 +0200 Subject: [PATCH 6/9] fix: Apply suggestions from code review Co-authored-by: micha91 --- capella2polarion/converters/converter_config.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index d5c155fa..9d24b22c 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -191,17 +191,16 @@ def _force_link_config(self, links: t.Any) -> list[LinkConfig]: if isinstance(link, str): config = LinkConfig( capella_attr=link, - polarion_role=(pid := add_prefix(link, self._role_prefix)), - link_field=pid, - reverse_field=f"{pid}_reverse", + polarion_role=add_prefix(link, self._role_prefix), + link_field=link, + reverse_field=f"{link}_reverse", ) elif isinstance(link, dict): config = LinkConfig( capella_attr=(lid := link["capella_attr"]), - polarion_role=( - pid := add_prefix( - link.get("polarion_role", lid), self._role_prefix - ) + polarion_role=add_prefix( + (pid := link.get("polarion_role", lid)), + self._role_prefix ), include=link.get("include", {}), link_field=(lf := link.get("link_field", pid)), From cae968ba27dd785b37a6fb1295176dc8ac903747 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 23 Aug 2024 15:33:19 +0200 Subject: [PATCH 7/9] ci: Let black reformat --- capella2polarion/converters/converter_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index 9d24b22c..44a6806c 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -199,8 +199,8 @@ def _force_link_config(self, links: t.Any) -> list[LinkConfig]: config = LinkConfig( capella_attr=(lid := link["capella_attr"]), polarion_role=add_prefix( - (pid := link.get("polarion_role", lid)), - self._role_prefix + (pid := link.get("polarion_role", lid)), + self._role_prefix, ), include=link.get("include", {}), link_field=(lf := link.get("link_field", pid)), From d27e28dc15870b63e81519d266e7265969a6fa09 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 23 Aug 2024 16:00:13 +0200 Subject: [PATCH 8/9] test: Remove obsolete test --- tests/test_elements.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_elements.py b/tests/test_elements.py index 97e103ae..602fbd1f 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -1225,11 +1225,8 @@ def test_maintain_grouped_links_attributes_with_role_prefix( ) @staticmethod - @pytest.mark.parametrize("role_prefix", ["", "_C2P"]) def test_grouped_links_attributes_with_includes( - base_object: BaseObjectContainer, - model: capellambse.MelodyModel, - role_prefix: str, + base_object: BaseObjectContainer, model: capellambse.MelodyModel ): fnc = model.by_uuid(TEST_SYS_FNC) ex = model.by_uuid(TEST_SYS_FNC_EX) @@ -1273,7 +1270,6 @@ def test_grouped_links_attributes_with_includes( converter = model_converter.ModelConverter( base_object.c2pcli.capella_model, base_object.c2pcli.polarion_params.project_id, - role_prefix=role_prefix, ) converter.converter_session = base_object.mc.converter_session work_items = converter.generate_work_items( @@ -1291,7 +1287,6 @@ def test_grouped_links_attributes_with_includes( base_object.mc.converter_session, base_object.pw.polarion_params.project_id, base_object.c2pcli.capella_model, - role_prefix=role_prefix, ) backlinks: dict[str, list[polarion_api.WorkItemLink]] = {} work_item = ( From c39b08a5556cfcfc57bc8b3c40a61e599342ad5f Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 26 Aug 2024 04:03:57 +0200 Subject: [PATCH 9/9] refactor: Remove prefix class attributes --- .../converters/converter_config.py | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py index 44a6806c..2725d73e 100644 --- a/capella2polarion/converters/converter_config.py +++ b/capella2polarion/converters/converter_config.py @@ -72,9 +72,6 @@ def __init__(self): self.diagram_config: CapellaTypeConfig | None = None self.__global_config = CapellaTypeConfig() - self._type_prefix: str - self._role_prefix: str - def read_config_file( self, synchronize_config: t.TextIO, @@ -82,18 +79,18 @@ def read_config_file( role_prefix: str = "", ): """Read a given yaml file as config.""" - self._type_prefix = type_prefix - self._role_prefix = role_prefix config_dict = yaml.safe_load(synchronize_config) # We handle the cross layer config separately as global_configs 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) + 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) + self.set_diagram_config(diagram_config, type_prefix) for c_type, type_config in global_config_dict.items(): type_config = type_config or {} @@ -103,7 +100,9 @@ def read_config_file( type_configs = type_configs or {} self.add_layer(layer) for c_type, c_type_config in type_configs.items(): - self.set_layer_config(c_type, c_type_config, layer) + self.set_layer_config( + c_type, c_type_config, layer, type_prefix, role_prefix + ) def add_layer(self, layer: str): """Add a new layer without configuring any types.""" @@ -117,6 +116,8 @@ def set_layer_config( c_type: str, c_type_config: dict[str, t.Any] | list[dict[str, t.Any]] | None, layer: str, + type_prefix: str = "", + role_prefix: str = "", ): """Set one or multiple configs for a type to an existing layer.""" type_configs = _read_capella_type_configs(c_type_config) @@ -140,10 +141,12 @@ def set_layer_config( or closest_config.p_type or _default_type_conversion(c_type) ), - self._type_prefix, + type_prefix, ) self.polarion_types.add(p_type) - links = self._force_link_config(type_config.get("links", [])) + links = self._force_link_config( + type_config.get("links", []), role_prefix + ) self._layer_configs[layer][c_type].append( CapellaTypeConfig( p_type, @@ -171,7 +174,9 @@ def set_global_config(self, c_type: str, type_config: dict[str, t.Any]): type_config.get("nature", _C2P_DEFAULT), ) - def set_diagram_config(self, diagram_config: dict[str, t.Any]): + def set_diagram_config( + self, diagram_config: dict[str, t.Any], type_prefix: str = "" + ): """Set the diagram config.""" c_type = "diagram" p_type = diagram_config.get("polarion_type") or "diagram" @@ -180,18 +185,20 @@ def set_diagram_config(self, diagram_config: dict[str, t.Any]): c_type, self._force_link_config(diagram_config.get("links", [])) ) self.diagram_config = CapellaTypeConfig( - add_prefix(p_type, self._type_prefix), + add_prefix(p_type, type_prefix), diagram_config.get("serializer") or "diagram", links + self._get_global_links(c_type), ) - def _force_link_config(self, links: t.Any) -> list[LinkConfig]: + 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, self._role_prefix), + polarion_role=add_prefix(link, role_prefix), link_field=link, reverse_field=f"{link}_reverse", ) @@ -200,7 +207,7 @@ def _force_link_config(self, links: t.Any) -> list[LinkConfig]: capella_attr=(lid := link["capella_attr"]), polarion_role=add_prefix( (pid := link.get("polarion_role", lid)), - self._role_prefix, + role_prefix, ), include=link.get("include", {}), link_field=(lf := link.get("link_field", pid)),