diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py index a981dc46..54217868 100644 --- a/capellambse_context_diagrams/__init__.py +++ b/capellambse_context_diagrams/__init__.py @@ -18,9 +18,17 @@ """ from __future__ import annotations +import collections.abc as cabc import logging +import typing as t from importlib import metadata +from capellambse.aird import COLORS, CSSdef, capstyle +from capellambse.model import common +from capellambse.model.crosslayer import fa +from capellambse.model.layers import ctx, la, oa, pa +from capellambse.model.modeltypes import DiagramType + from . import context try: @@ -29,6 +37,8 @@ __version__ = "0.0.0+unknown" del metadata +ClassPair = tuple[type[common.GenericElement], DiagramType] + logger = logging.getLogger(__name__) ATTR_NAME = "context_diagram" @@ -43,13 +53,7 @@ def init() -> None: def register_classes() -> None: """Add the `context_diagram` property to the relevant model objects.""" - from capellambse.model.layers import ctx, la, oa, pa - from capellambse.model.modeltypes import DiagramType - - patch_styles() - supported_classes: list[ - tuple[type[common.GenericElement], DiagramType] - ] = [ + supported_classes: list[ClassPair] = [ (oa.Entity, DiagramType.OAB), (oa.OperationalActivity, DiagramType.OAIB), (oa.OperationalCapability, DiagramType.OCB), @@ -62,6 +66,7 @@ def register_classes() -> None: (pa.PhysicalComponent, DiagramType.PAB), (pa.PhysicalFunction, DiagramType.PDFB), ] + patch_styles(supported_classes) class_: type[common.GenericElement] for class_, dgcls in supported_classes: common.set_accessor( @@ -69,7 +74,7 @@ def register_classes() -> None: ) -def patch_styles() -> None: +def patch_styles(classes: cabc.Iterable[ClassPair]) -> None: """Add missing default styling to default styles. See Also @@ -77,8 +82,6 @@ def patch_styles() -> None: [capstyle.get_style][capellambse.aird.capstyle.get_style] : Default style getter. """ - from capellambse.aird import COLORS, CSSdef, capstyle - cap: dict[str, CSSdef] = { "fill": [COLORS["_CAP_Entity_Gray_min"], COLORS["_CAP_Entity_Gray"]], "stroke": COLORS["dark_gray"], @@ -90,17 +93,13 @@ def patch_styles() -> None: capstyle.STYLES["Operational Capabilities Blank"][ "Box.OperationalCapability" ] = cap - capstyle.STYLES["System Data Flow Blank"]["Circle.FunctionalExchange"] = { - "fill": COLORS["_CAP_xAB_Function_Border_Green"] - } + circle_style = {"fill": COLORS["_CAP_xAB_Function_Border_Green"]} + for _, dt in classes: + capstyle.STYLES[dt.value]["Circle.FunctionalExchange"] = circle_style def register_interface_context() -> None: """Add the `context_diagram` property to interface model objects.""" - from capellambse.model.crosslayer import fa - from capellambse.model.layers import ctx, la, oa, pa - from capellambse.model.modeltypes import DiagramType - common.set_accessor( oa.CommunicationMean, ATTR_NAME, @@ -134,9 +133,7 @@ def register_functional_context() -> None: The functional context diagrams will be available soon. """ - from capellambse.model.layers import ctx, la, oa, pa - from capellambse.model.modeltypes import DiagramType - + attr_name = f"functional_{ATTR_NAME}" supported_classes: list[ tuple[type[common.GenericElement], DiagramType] ] = [ @@ -149,9 +146,6 @@ def register_functional_context() -> None: for class_, dgcls in supported_classes: common.set_accessor( class_, - f"functional_{ATTR_NAME}", + attr_name, context.FunctionalContextAccessor(dgcls.value), ) - - -from capellambse.model import common diff --git a/capellambse_context_diagrams/collectors/exchanges.py b/capellambse_context_diagrams/collectors/exchanges.py index f576a7f5..b4d57f34 100644 --- a/capellambse_context_diagrams/collectors/exchanges.py +++ b/capellambse_context_diagrams/collectors/exchanges.py @@ -22,7 +22,7 @@ class ExchangeCollector(metaclass=abc.ABCMeta): """Base class for context collection on Exchanges.""" - intermap = { + intermap: dict[str, DT] = { DT.OAB: ("source", "target", "allocated_interactions", "activities"), DT.SAB: ( "source.parent", @@ -34,13 +34,13 @@ class ExchangeCollector(metaclass=abc.ABCMeta): "source.parent", "target.parent", "allocated_functional_exchanges", - "functions", + "allocated_functions", ), DT.PAB: ( "source.parent", "target.parent", "allocated_functional_exchanges", - "functions", + "allocated_functions", ), } @@ -57,7 +57,7 @@ def __init__( self.get_source = operator.attrgetter(src) self.get_target = operator.attrgetter(trg) self.get_alloc_fex = operator.attrgetter(alloc_fex) - self.get_functions = operator.attrgetter(fncs) + self.get_alloc_functions = operator.attrgetter(fncs) def get_functions_and_exchanges( self, comp: common.GenericElement, interface: common.GenericElement @@ -70,7 +70,7 @@ def get_functions_and_exchanges( `FunctionalExchange`s for given `Component` and `interface`. """ functions, outgoings, incomings = [], [], [] - alloc_functions = self.get_functions(comp) + alloc_functions = self.get_alloc_functions(comp) for fex in self.get_alloc_fex(interface): source = self.get_source(fex) if source in alloc_functions: @@ -170,7 +170,8 @@ def get_left_and_right(self) -> None: def get_capella_order( comp: common.GenericElement, functions: list[common.GenericElement] ) -> list[common.GenericElement]: - return [fnc for fnc in comp.functions if fnc in functions] + alloc_functions = self.get_alloc_functions(comp) + return [fnc for fnc in alloc_functions if fnc in functions] def make_boxes( comp: common.GenericElement, functions: list[common.GenericElement] @@ -180,7 +181,7 @@ def make_boxes( box["children"] = [ makers.make_box(c) for c in functions - if c in self.get_functions(comp) + if c in self.get_alloc_functions(comp) ] self.data["children"].append(box) made_children.add(comp.uuid) diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 630a6a76..8ac1285b 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -120,8 +120,7 @@ def __get__( # type: ignore class ContextDiagram(diagram.AbstractDiagram): - """ - An automatically generated context diagram. + """An automatically generated context diagram. Attributes ---------- @@ -152,6 +151,23 @@ class ContextDiagram(diagram.AbstractDiagram): [`collectors.exchange_data_collector`][capellambse_context_diagrams.collectors.generic.exchange_data_collector]. """ + def __init__( + self, + class_: str, + obj: common.GenericElement, + *, + render_styles: dict[str, styling.Styler] | None = None, + display_symbols_as_boxes: bool = False, + ) -> None: + super().__init__(obj._model) + self.target = obj + self.styleclass = class_ + + self.render_styles = render_styles or styling.BLUE_ACTOR_FNCS + self.serializer = serializers.DiagramSerializer(self) + self.__filters: cabc.MutableSet[str] = self.FilterSet(self) + self.display_symbols_as_boxes = display_symbols_as_boxes + @property def uuid(self) -> str: # type: ignore """Returns diagram UUID.""" @@ -228,23 +244,6 @@ def __iter__(self) -> cabc.Iterator[str]: def __len__(self) -> int: return self._set.__len__() - def __init__( - self, - class_: str, - obj: common.GenericElement, - *, - render_styles: dict[str, styling.Styler] | None = None, - display_symbols_as_boxes: bool = False, - ) -> None: - super().__init__(obj._model) - self.target = obj - self.styleclass = class_ - - self.render_styles = render_styles or styling.BLUE_ACTOR_FNCS - self.serializer = serializers.DiagramSerializer(self) - self.__filters: cabc.MutableSet[str] = self.FilterSet(self) - self.display_symbols_as_boxes = display_symbols_as_boxes - def _create_diagram( self, params: dict[str, t.Any], @@ -270,9 +269,12 @@ def filters(self, value: cabc.Iterable[str]) -> None: class InterfaceContextDiagram(ContextDiagram): """An automatically generated Context Diagram exclusively for - ComponentExchanges. + ``ComponentExchange``s. """ + def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None: + super().__init__(class_, obj, **kw, display_symbols_as_boxes=True) + @property def name(self) -> str: # type: ignore return f"Interface Context of {self.target.name}" diff --git a/pyproject.toml b/pyproject.toml index bd31cc50..e306a5b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "capellambse", + "capellambse>=0.4.19.dev12", "typing_extensions", ] diff --git a/tests/data/ContextDiagram.aird b/tests/data/ContextDiagram.aird index fae318fc..be98facb 100644 --- a/tests/data/ContextDiagram.aird +++ b/tests/data/ContextDiagram.aird @@ -17,7 +17,7 @@ - + @@ -318,6 +318,17 @@ + + + + + + + + + + + @@ -393,6 +404,17 @@ + + + + + + + + + + + @@ -706,6 +728,22 @@ + + + + + + + + + + + + + + + + @@ -910,6 +948,15 @@ + + + + + + + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO @@ -960,6 +1007,15 @@ + + + + + + + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO @@ -1220,6 +1276,15 @@ + + + + + + + + + diff --git a/tests/data/ContextDiagram.capella b/tests/data/ContextDiagram.capella index 666d6be3..7645b936 100644 --- a/tests/data/ContextDiagram.capella +++ b/tests/data/ContextDiagram.capella @@ -1945,6 +1945,9 @@ The predator is far away + + @@ -2349,6 +2355,9 @@ The predator is far away id="b5aaa4ee-c96b-48f3-be37-36b596eed46d" targetElement="#91fd613c-4cea-4c2b-8acb-0c8c27f5f0da" sourceElement="#b2d2a4f1-0c21-45f4-918c-3d3c5e8af104"/> + diff --git a/tests/test_interface_diagrams.py b/tests/test_interface_diagrams.py index dd363962..ff1a9190 100644 --- a/tests/test_interface_diagrams.py +++ b/tests/test_interface_diagrams.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2022 Copyright DB Netz AG and the capellambse-context-diagrams contributors # SPDX-License-Identifier: Apache-2.0 +import pathlib + import capellambse import pytest @@ -9,14 +11,17 @@ "uuid", [ # pytest.param("3c9764aa-4981-44ef-8463-87a053016635", id="OA"), - # pytest.param("86a1afc2-b7fd-4023-bbd5-ab44f5dc2c28", id="SA"), + pytest.param("86a1afc2-b7fd-4023-bbd5-ab44f5dc2c28", id="SA"), pytest.param("3ef23099-ce9a-4f7d-812f-935f47e7938d", id="LA"), ], ) def test_interface_diagrams_get_rendered( - model: capellambse.MelodyModel, uuid: str + model: capellambse.MelodyModel, uuid: str, tmp_path: pathlib.Path ) -> None: obj = model.by_uuid(uuid) + filename = tmp_path / "tmp.svg" + diag = obj.context_diagram + diag.render("svgdiagram").save_drawing(filename=filename) assert diag.nodes