diff --git a/capellambse_context_diagrams/_elkjs.py b/capellambse_context_diagrams/_elkjs.py
index 8888c20b..4c672555 100644
--- a/capellambse_context_diagrams/_elkjs.py
+++ b/capellambse_context_diagrams/_elkjs.py
@@ -110,6 +110,16 @@ class ELKInputData(BaseELKModel):
default_factory=list
)
+ def get_children_by_id(self, search_id: str) -> ELKInputChild | None:
+ """Recursively search for a child with the given id."""
+ for child in self.children:
+ if child.id == search_id:
+ return child
+
+ if found_child := child.get_children_by_id(search_id):
+ return found_child
+ return None
+
class ELKInputChild(ELKInputData):
"""Children of either `ELKInputData` or `ELKInputChild`."""
diff --git a/capellambse_context_diagrams/collectors/default.py b/capellambse_context_diagrams/collectors/default.py
index 5f320c6e..57602a88 100644
--- a/capellambse_context_diagrams/collectors/default.py
+++ b/capellambse_context_diagrams/collectors/default.py
@@ -33,8 +33,6 @@
cabc.Iterable[common.GenericElement],
]
-STYLECLASS_PREFIX = "__Derived"
-
class ContextProcessor:
def __init__(
@@ -52,7 +50,7 @@ def __init__(
self.made_boxes = {self.centerbox.id: self.centerbox}
self.boxes_to_delete = {self.centerbox.id}
self.edges: list[fa.AbstractExchange] = []
- if self.diagram.display_parent_relation:
+ if self.diagram._display_parent_relation:
self.diagram_target_owners = generic.get_all_owners(
self.diagram.target
)
@@ -60,13 +58,13 @@ def __init__(
def process_context(self):
if (
- self.diagram.display_parent_relation
+ self.diagram._display_parent_relation
and getattr(self.diagram.target, "owner", None) is not None
and not isinstance(self.diagram.target.owner, generic.PackageTypes)
):
box = self._make_box(
self.diagram.target.owner,
- no_symbol=self.diagram.display_symbols_as_boxes,
+ no_symbol=self.diagram._display_symbols_as_boxes,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
box.children = [self.centerbox]
@@ -78,7 +76,7 @@ def process_context(self):
}
self._process_ports(stack_heights)
- if self.diagram.display_parent_relation and self.diagram.target.owner:
+ if self.diagram._display_parent_relation and self.diagram.target.owner:
current = self.diagram.target.owner
while (
current
@@ -96,7 +94,7 @@ def process_context(self):
del self.global_boxes[uuid]
self.data.children.extend(self.global_boxes.values())
- if self.diagram.display_parent_relation:
+ if self.diagram._display_parent_relation:
owner_boxes: dict[str, _elkjs.ELKInputChild] = {
uuid: box
for uuid, box in self.made_boxes.items()
@@ -124,7 +122,7 @@ def _process_exchanges(self) -> tuple[
if is_hierarchical := exchanges.is_hierarchical(
ex, self.centerbox
):
- if not self.diagram.display_parent_relation:
+ if not self.diagram._display_parent_relation:
continue
self.centerbox.labels[0].layoutOptions = (
makers.DEFAULT_LABEL_LAYOUT_OPTIONS
@@ -167,11 +165,11 @@ def _process_ports(self, stack_heights: dict[str, float | int]) -> None:
box = self._make_box(
port,
height=height,
- no_symbol=self.diagram.display_symbols_as_boxes,
+ no_symbol=self.diagram._display_symbols_as_boxes,
)
box.ports = [makers.make_port(j.uuid) for j in local_ports]
- if self.diagram.display_parent_relation:
+ if self.diagram._display_parent_relation:
current = port
while (
current
@@ -205,7 +203,7 @@ def _make_owner_box(
if not (parent_box := self.global_boxes.get(obj.owner.uuid)):
parent_box = self._make_box(
obj.owner,
- no_symbol=diagram.display_symbols_as_boxes,
+ no_symbol=diagram._display_symbols_as_boxes,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
assert (obj_box := self.global_boxes.get(obj.uuid))
@@ -227,7 +225,7 @@ def collector(
processor = ContextProcessor(diagram, data, params=params)
processor.process_context()
derivator = DERIVATORS.get(type(diagram.target))
- if diagram.display_derived_interfaces and derivator is not None:
+ if diagram._display_derived_interfaces and derivator is not None:
derivator(
diagram,
data,
@@ -403,13 +401,13 @@ def derive_from_functions(
for i, (uuid, derived_component) in enumerate(components.items(), 1):
box = makers.make_box(
derived_component,
- no_symbol=diagram.display_symbols_as_boxes,
+ no_symbol=diagram._display_symbols_as_boxes,
)
class_ = type(derived_comp).__name__
- box.id = f"{STYLECLASS_PREFIX}-{class_}:{uuid}"
+ box.id = f"{makers.STYLECLASS_PREFIX}-{class_}:{uuid}"
data.children.append(box)
- source_id = f"{STYLECLASS_PREFIX}-CP_INOUT:{i}"
- target_id = f"{STYLECLASS_PREFIX}-CP_INOUT:{-i}"
+ source_id = f"{makers.STYLECLASS_PREFIX}-CP_INOUT:{i}"
+ target_id = f"{makers.STYLECLASS_PREFIX}-CP_INOUT:{-i}"
box.ports.append(makers.make_port(source_id))
centerbox.ports.append(makers.make_port(target_id))
if i % 2 == 0:
@@ -417,7 +415,7 @@ def derive_from_functions(
data.edges.append(
_elkjs.ELKInputEdge(
- id=f"{STYLECLASS_PREFIX}-ComponentExchange:{i}",
+ id=f"{makers.STYLECLASS_PREFIX}-ComponentExchange:{i}",
sources=[source_id],
targets=[target_id],
)
diff --git a/capellambse_context_diagrams/collectors/exchanges.py b/capellambse_context_diagrams/collectors/exchanges.py
index a3910685..a97d9e5c 100644
--- a/capellambse_context_diagrams/collectors/exchanges.py
+++ b/capellambse_context_diagrams/collectors/exchanges.py
@@ -24,20 +24,20 @@ class ExchangeCollector(metaclass=abc.ABCMeta):
intermap: dict[str, DT] = {
DT.OAB: ("source", "target", "allocated_interactions", "activities"),
DT.SAB: (
- "source.parent",
- "target.parent",
+ "source.owner",
+ "target.owner",
"allocated_functional_exchanges",
"allocated_functions",
),
DT.LAB: (
- "source.parent",
- "target.parent",
+ "source.owner",
+ "target.owner",
"allocated_functional_exchanges",
"allocated_functions",
),
DT.PAB: (
- "source.parent",
- "target.parent",
+ "source.owner",
+ "target.owner",
"allocated_functional_exchanges",
"allocated_functions",
),
@@ -62,104 +62,14 @@ def __init__(
self.get_alloc_fex = operator.attrgetter(alloc_fex)
self.get_alloc_functions = operator.attrgetter(fncs)
- def get_functions_and_exchanges(
- self, comp: common.GenericElement, interface: common.GenericElement
- ) -> tuple[
- list[common.GenericElement],
- dict[str, common.GenericElement],
- dict[str, common.GenericElement],
- ]:
- """Return `Function`s, incoming and outgoing
- `FunctionalExchange`s for given `Component` and `interface`.
- """
- functions, incomings, outgoings = [], {}, {}
- 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:
- if fex.uuid not in outgoings:
- outgoings[fex.uuid] = fex
- if source not in functions:
- functions.append(source)
-
- target = self.get_target(fex)
- if target in alloc_functions:
- if fex.uuid not in incomings:
- incomings[fex.uuid] = fex
- if target not in functions:
- functions.append(target)
-
- return functions, incomings, outgoings
-
- def collect_context(
- self, comp: common.GenericElement, interface: common.GenericElement
- ) -> tuple[
- dict[str, t.Any],
- dict[str, common.GenericElement],
- dict[str, common.GenericElement],
- ]:
- functions, incomings, outgoings = self.get_functions_and_exchanges(
- comp, interface
- )
- components = []
- for cmp in comp.components:
- fncs, _, _ = self.get_functions_and_exchanges(cmp, interface)
- functions.extend(fncs)
- if fncs:
- c, incs, outs = self.collect_context(cmp, interface)
- components.append(c)
- incomings |= incs
- outgoings |= outs
-
- start = {
- "element": comp,
- "functions": functions,
- "components": components,
- }
- if self.diagram.hide_functions:
- start["functions"] = []
- incomings = {}
- outgoings = {}
-
- return start, incomings, outgoings
-
+ @abc.abstractmethod
def make_ports_and_update_children_size(
self,
data: _elkjs.ELKInputChild,
exchanges: t.Sequence[_elkjs.ELKInputEdge],
) -> None:
- """Adjust size of functions and make ports."""
- stack_height: int | float = -makers.NEIGHBOR_VMARGIN
- for child in data.children:
- inputs, outputs = [], []
- obj = self.obj._model.by_uuid(child.id)
- if isinstance(obj, cs.Component):
- self.make_ports_and_update_children_size(child, exchanges)
- return
- port_ids = {p.uuid for p in obj.inputs + obj.outputs}
- for ex in exchanges:
- source, target = ex.sources[0], ex.targets[0]
- if source in port_ids:
- outputs.append(source)
- elif target in port_ids:
- inputs.append(target)
-
- if generic.DIAGRAM_TYPE_TO_CONNECTOR_NAMES[self.diagram.type]:
- child.ports = [
- makers.make_port(i) for i in set(inputs + outputs)
- ]
-
- childnum = max(len(inputs), len(outputs))
- height = max(
- child.height + 2 * makers.LABEL_VPAD,
- makers.PORT_PADDING
- + (makers.PORT_SIZE + makers.PORT_PADDING) * childnum,
- )
- child.height = height
- stack_height += makers.NEIGHBOR_VMARGIN + height
-
- if stack_height > 0:
- data.height = stack_height
+ """Populate the elkdata container."""
+ raise NotImplementedError
@abc.abstractmethod
def collect(self) -> None:
@@ -203,15 +113,26 @@ def __init__(
data: _elkjs.ELKInputData,
params: dict[str, t.Any],
) -> None:
- self.left = None
- self.right = None
self.incoming_edges = {}
self.outgoing_edges = {}
super().__init__(diagram, data, params)
+ self._functional_exchanges: common.ElementList[
+ common.GenericElement
+ ] = self.get_alloc_fex(diagram.target)
+ self._derived_functional_exchanges: dict[
+ str, common.GenericElement
+ ] = {}
+ self._ex_validity: dict[
+ str, dict[str, common.GenericElement | None]
+ ] = {
+ fex.uuid: {"source": None, "target": None}
+ for fex in self._functional_exchanges
+ }
+
self.get_left_and_right()
- if diagram.include_interface:
+ if diagram._include_interface:
self.add_interface()
def get_left_and_right(self) -> None:
@@ -226,7 +147,7 @@ def get_capella_order(
def make_boxes(cntxt: dict[str, t.Any]) -> _elkjs.ELKInputChild | None:
comp = cntxt["element"]
functions = cntxt["functions"]
- if self.diagram.hide_functions:
+ if self.diagram._hide_functions:
functions = []
components = cntxt["components"]
@@ -254,13 +175,15 @@ def make_boxes(cntxt: dict[str, t.Any]) -> _elkjs.ELKInputChild | None:
try:
comp = self.get_source(self.obj)
- left_context, incs, outs = self.collect_context(comp, self.obj)
+ left_context, incs, outs = self.collect_context(comp)
+ _comp = self.get_target(self.obj)
+ right_context, _, _ = self.collect_context(_comp)
+ self.remove_dangling_functional_exchanges(incs, outs)
+
inc_port_ids = set(ex.target.uuid for ex in incs.values())
out_port_ids = set(ex.source.uuid for ex in outs.values())
port_spread = len(out_port_ids) - len(inc_port_ids)
- _comp = self.get_target(self.obj)
- right_context, _, _ = self.collect_context(_comp, self.obj)
_inc_port_ids = set(ex.target.uuid for ex in outs.values())
_out_port_ids = set(ex.source.uuid for ex in incs.values())
_port_spread = len(_out_port_ids) - len(_inc_port_ids)
@@ -288,6 +211,99 @@ def make_boxes(cntxt: dict[str, t.Any]) -> _elkjs.ELKInputChild | None:
except AttributeError as error:
logger.exception("Interface collection failed: \n%r", str(error))
+ def collect_context(self, comp: common.GenericElement) -> tuple[
+ dict[str, t.Any],
+ dict[str, common.GenericElement],
+ dict[str, common.GenericElement],
+ ]:
+ functions, incomings, outgoings = self.get_functions_and_exchanges(
+ comp
+ )
+ components = []
+ for cmp in comp.components:
+ fncs, _, _ = self.get_functions_and_exchanges(cmp)
+ functions.extend(fncs)
+ if fncs:
+ c, incs, outs = self.collect_context(cmp)
+ components.append(c)
+ incomings |= incs
+ outgoings |= outs
+
+ start = {
+ "element": comp,
+ "functions": functions,
+ "components": components,
+ }
+ if self.diagram._hide_functions:
+ start["functions"] = []
+ incomings = {}
+ outgoings = {}
+
+ return start, incomings, outgoings
+
+ def get_functions_and_exchanges(
+ self, comp: common.GenericElement
+ ) -> tuple[
+ list[common.GenericElement],
+ dict[str, common.GenericElement],
+ dict[str, common.GenericElement],
+ ]:
+ """Return `Function`s, incoming and outgoing
+ `FunctionalExchange`s for given `Component` and `interface`.
+ """
+ functions, incomings, outgoings = [], {}, {}
+ alloc_functions = self.get_alloc_functions(comp)
+ fexes = alloc_functions.map("inputs.exchanges") + alloc_functions.map(
+ "outputs.exchanges"
+ )
+ ex_template = {"source": None, "target": None}
+ for fex in fexes:
+ if fex not in self._functional_exchanges:
+ # fex not allocated to interface
+ if not self.diagram._display_derived_exchanges:
+ continue
+
+ self._derived_functional_exchanges[fex.uuid] = fex
+
+ if (source := self.get_source(fex)) in alloc_functions:
+ self._ex_validity.setdefault(fex.uuid, ex_template.copy())[
+ "source"
+ ] = source
+ outgoings[fex.uuid] = fex
+ if source not in functions:
+ functions.append(source)
+
+ if (target := self.get_target(fex)) in alloc_functions:
+ self._ex_validity.setdefault(fex.uuid, ex_template.copy())[
+ "target"
+ ] = target
+ incomings[fex.uuid] = fex
+ if target not in functions:
+ functions.append(target)
+
+ return functions, incomings, outgoings
+
+ def remove_dangling_functional_exchanges(
+ self,
+ incoming: dict[str, common.GenericElement],
+ outgoing: dict[str, common.GenericElement],
+ ) -> tuple[
+ dict[str, common.GenericElement], dict[str, common.GenericElement]
+ ]:
+ for uuid in self._ex_validity:
+ if not self.found_source_and_target(uuid):
+ incoming.pop(uuid, None)
+ outgoing.pop(uuid, None)
+
+ fex = self.obj._model.by_uuid(uuid)
+ self.diagram.dangling_functional_exchanges.append(fex)
+
+ return incoming, outgoing
+
+ def found_source_and_target(self, uuid: str) -> bool:
+ fex = self._ex_validity[uuid]
+ return None not in (fex["source"], fex["target"])
+
def add_interface(self) -> None:
ex_data = generic.ExchangeData(
self.obj,
@@ -298,10 +314,6 @@ def add_interface(self) -> None:
)
src, tgt = generic.exchange_data_collector(ex_data)
assert self.right is not None
- if self.get_source(self.obj).uuid == self.right.id:
- self.data.edges[-1].sources = [tgt.uuid]
- self.data.edges[-1].targets = [src.uuid]
-
assert self.left is not None
self.left.ports.append(makers.make_port(src.uuid))
self.right.ports.append(makers.make_port(tgt.uuid))
@@ -318,6 +330,11 @@ def collect(self) -> None:
is_hierarchical=False,
)
src, tgt = generic.exchange_data_collector(ex_data)
+ if ex.uuid in self._derived_functional_exchanges:
+ class_ = type(ex).__name__
+ self.data.edges[-1].id = (
+ f"{makers.STYLECLASS_PREFIX}-{class_}:{ex.uuid}"
+ )
if ex in self.incoming_edges.values():
self.data.edges[-1].sources = [tgt.uuid]
@@ -331,7 +348,69 @@ def collect(self) -> None:
except AttributeError:
pass
+ def make_ports_and_update_children_size(
+ self,
+ data: _elkjs.ELKInputChild,
+ exchanges: t.Sequence[_elkjs.ELKInputEdge],
+ ) -> None:
+ """Adjust size of functions and make ports."""
+ stack_height: int | float = -makers.NEIGHBOR_VMARGIN
+ to_remove: list[_elkjs.ELKInputChild] = []
+ for child in data.children:
+ inputs, outputs = [], []
+ obj = self.obj._model.by_uuid(child.id)
+ if isinstance(obj, cs.Component):
+ self.make_ports_and_update_children_size(child, exchanges)
+ return
+
+ port_ids = {p.uuid for p in obj.inputs + obj.outputs}
+ for ex in exchanges:
+ source, target = ex.sources[0], ex.targets[0]
+ if source in port_ids:
+ outputs.append(source)
+ elif target in port_ids:
+ inputs.append(target)
+
+ if generic.DIAGRAM_TYPE_TO_CONNECTOR_NAMES[self.diagram.type]:
+ child.ports = [
+ makers.make_port(i) for i in set(inputs + outputs)
+ ]
+ if not inputs + outputs:
+ to_remove.append(child)
+
+ childnum = max(len(inputs), len(outputs))
+ height = max(
+ child.height + 2 * makers.LABEL_VPAD,
+ makers.PORT_PADDING
+ + (makers.PORT_SIZE + makers.PORT_PADDING) * childnum,
+ )
+ child.height = height
+ stack_height += makers.NEIGHBOR_VMARGIN + height
+
+ if stack_height > 0:
+ data.height = stack_height
+
+ for child in to_remove:
+ data.children.remove(child)
+
+ if not data.children:
+ try:
+ parent_obj = self.obj._model.by_uuid(data.id)
+ assert isinstance(parent_obj, cs.Component)
+ assert parent_obj not in (self.left, self.right)
+ assert parent_obj.owner is not None
+ parent_owner = self.data.get_children_by_id(
+ parent_obj.owner.uuid
+ )
+ assert parent_owner is not None
+ parent_owner.children.remove(data)
+ except (KeyError, AssertionError):
+ pass
+
+
+# pylint: disable
+@t.no_type_check
class FunctionalContextCollector(ExchangeCollector):
def __init__(
self,
@@ -352,8 +431,8 @@ def collect(self) -> None:
else:
comp = self.get_source(interface)
- functions, inc, outs = self.get_functions_and_exchanges(
- self.obj, interface
+ functions, inc, outs = self.get_functions_and_exchanges( # type: ignore
+ interface
)
if comp.uuid not in made_children:
children = [makers.make_box(c) for c in functions]
@@ -385,6 +464,17 @@ def collect(self) -> None:
)
)
+ def get_functions_and_exchanges( # type: ignore
+ self, _: common.GenericElement
+ ) -> tuple[
+ list[common.GenericElement],
+ dict[str, common.GenericElement],
+ dict[str, common.GenericElement],
+ ]: ...
+
+
+# pylint: enable
+
def is_hierarchical(
ex: common.GenericElement,
diff --git a/capellambse_context_diagrams/collectors/generic.py b/capellambse_context_diagrams/collectors/generic.py
index 4e935d64..dc98581e 100644
--- a/capellambse_context_diagrams/collectors/generic.py
+++ b/capellambse_context_diagrams/collectors/generic.py
@@ -64,7 +64,7 @@ def collector(
diagram.target,
width=width,
no_symbol=no_symbol,
- slim_width=diagram.slim_center_box,
+ slim_width=diagram._slim_center_box,
)
]
return data
diff --git a/capellambse_context_diagrams/collectors/makers.py b/capellambse_context_diagrams/collectors/makers.py
index 258a7c23..d05184a3 100644
--- a/capellambse_context_diagrams/collectors/makers.py
+++ b/capellambse_context_diagrams/collectors/makers.py
@@ -70,6 +70,8 @@
}
"""Layout options for a symbol label."""
+STYLECLASS_PREFIX = "__Derived"
+
def make_diagram(diagram: context.ContextDiagram) -> _elkjs.ELKInputData:
"""Return basic skeleton for ``ContextDiagram``s."""
diff --git a/capellambse_context_diagrams/collectors/portless.py b/capellambse_context_diagrams/collectors/portless.py
index ab876676..7e06b857 100644
--- a/capellambse_context_diagrams/collectors/portless.py
+++ b/capellambse_context_diagrams/collectors/portless.py
@@ -44,10 +44,10 @@ def collector(
contexts = context_collector(connections, diagram.target)
global_boxes = {centerbox.id: centerbox}
made_boxes = {centerbox.id: centerbox}
- if diagram.display_parent_relation and diagram.target.owner is not None:
+ if diagram._display_parent_relation and diagram.target.owner is not None:
box = makers.make_box(
diagram.target.owner,
- no_symbol=diagram.display_symbols_as_boxes,
+ no_symbol=diagram._display_symbols_as_boxes,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
box.children = [centerbox]
@@ -63,7 +63,7 @@ def collector(
var_height = generic.MARKER_PADDING + (
generic.MARKER_SIZE + generic.MARKER_PADDING
) * len(exchanges)
- if not diagram.display_symbols_as_boxes and makers.is_symbol(
+ if not diagram._display_symbols_as_boxes and makers.is_symbol(
diagram.target
):
height = makers.MIN_SYMBOL_HEIGHT + var_height
@@ -78,16 +78,16 @@ def collector(
box = makers.make_box(
i,
height=height,
- no_symbol=diagram.display_symbols_as_boxes,
+ no_symbol=diagram._display_symbols_as_boxes,
)
global_boxes[i.uuid] = box
made_boxes[i.uuid] = box
- if diagram.display_parent_relation and i.owner is not None:
+ if diagram._display_parent_relation and i.owner is not None:
if not (parent_box := global_boxes.get(i.owner.uuid)):
parent_box = makers.make_box(
i.owner,
- no_symbol=diagram.display_symbols_as_boxes,
+ no_symbol=diagram._display_symbols_as_boxes,
)
global_boxes[i.owner.uuid] = parent_box
made_boxes[i.owner.uuid] = parent_box
@@ -101,7 +101,7 @@ def collector(
del global_boxes[centerbox.id]
data.children.extend(global_boxes.values())
- if diagram.display_parent_relation:
+ if diagram._display_parent_relation:
owner_boxes: dict[str, _elkjs.ELKInputChild] = {
uuid: box for uuid, box in made_boxes.items() if box.children
}
@@ -109,7 +109,7 @@ def collector(
generic.move_edges(owner_boxes, connections, data)
centerbox.height = max(centerbox.height, *stack_heights.values())
- if not diagram.display_symbols_as_boxes and makers.is_symbol(
+ if not diagram._display_symbols_as_boxes and makers.is_symbol(
diagram.target
):
data.layoutOptions["spacing.labelNode"] = 5.0
diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py
index 8661a37f..1ead23df 100644
--- a/capellambse_context_diagrams/context.py
+++ b/capellambse_context_diagrams/context.py
@@ -15,6 +15,7 @@
from capellambse import diagram as cdiagram
from capellambse import helpers
from capellambse.model import common, diagram, modeltypes
+from capellambse.model.crosslayer import fa
from . import _elkjs, filters, serializers, styling
from .collectors import (
@@ -93,7 +94,9 @@ def _get(
pass
new_diagram = diagram_class(
- self._dgcls, obj, **self._default_render_params
+ self._dgcls,
+ obj,
+ default_render_parameters=self._default_render_params,
)
new_diagram.filters.add(filters.NO_UUID)
cache[diagram_id] = new_diagram
@@ -223,21 +226,6 @@ class ContextDiagram(diagram.AbstractDiagram):
Dictionary with the `ElkChildType` in str format as keys and
`styling.Styler` functions as values. An example is given by:
[`styling.BLUE_ACTOR_FNCS`][capellambse_context_diagrams.styling.BLUE_ACTOR_FNCS]
- display_symbols_as_boxes
- Display objects that are normally displayed as symbol as a
- simple box instead, with the symbol being the box' icon. This
- avoids the object of interest to become one giant, oversized
- symbol in the middle of the diagram, and instead keeps the
- symbol small and only enlarges the surrounding box.
- display_parent_relation
- Display objects with a parent relationship to the object of
- interest as the parent box.
- display_derived_interfaces
- Display derived objects collected from additional collectors
- beside the main collector for building the context.
- slim_center_box
- Minimal width for the center box, containing just the icon and
- the label. This is False if hierarchy was identified.
serializer
The serializer builds a `diagram.Diagram` via
[`serializers.DiagramSerializer.make_diagram`][capellambse_context_diagrams.serializers.DiagramSerializer.make_diagram]
@@ -248,18 +236,37 @@ class ContextDiagram(diagram.AbstractDiagram):
A list of filter names that are applied during collection of
context. Currently this is only done in
[`collectors.exchange_data_collector`][capellambse_context_diagrams.collectors.generic.exchange_data_collector].
+
+ Notes
+ -----
+ * display_symbols_as_boxes — Display objects that are normally
+ displayed as symbol as a simple box instead, with the symbol
+ being the box' icon. This avoids the object of interest to
+ become one giant, oversized symbol in the middle of the diagram,
+ and instead keeps the symbol small and only enlarges the
+ surrounding box.
+ * display_parent_relation — Display objects with a parent
+ relationship to the object of interest as the parent box.
+ * display_derived_interfaces — Display derived objects collected
+ from additional collectors beside the main collector for building
+ the context.
+ * slim_center_box — Minimal width for the center box, containing
+ just the icon and the label. This is False if hierarchy was
+ identified.
"""
+ _display_symbols_as_boxes: bool
+ _display_parent_relation: bool
+ _display_derived_interfaces: bool
+ _slim_center_box: bool
+
def __init__(
self,
class_: str,
obj: common.GenericElement,
*,
render_styles: dict[str, styling.Styler] | None = None,
- display_symbols_as_boxes: bool = False,
- display_parent_relation: bool = False,
- display_derived_interfaces: bool = False,
- slim_center_box: bool = True,
+ default_render_parameters: dict[str, t.Any],
) -> None:
super().__init__(obj._model)
self.target = obj
@@ -268,10 +275,12 @@ def __init__(
self.render_styles = render_styles or {}
self.serializer = serializers.DiagramSerializer(self)
self.__filters: cabc.MutableSet[str] = self.FilterSet(self)
- self.display_symbols_as_boxes = display_symbols_as_boxes
- self.display_parent_relation = display_parent_relation
- self.display_derived_interfaces = display_derived_interfaces
- self.slim_center_box = slim_center_box
+ self._default_render_parameters = {
+ "display_symbols_as_boxes": False,
+ "display_parent_relation": False,
+ "display_derived_interfaces": False,
+ "slim_center_box": True,
+ } | default_render_parameters
if standard_filter := STANDARD_FILTERS.get(class_):
self.filters.add(standard_filter)
@@ -329,32 +338,15 @@ def __iter__(self) -> cabc.Iterator[str]:
def __len__(self) -> int:
return self._set.__len__()
- def render(self, fmt: str | None, /, **params) -> t.Any:
- """Render the diagram in the given format."""
- rparams = params.copy()
- for attr, value in params.items():
- attribute = getattr(self, attr, "NOT_FOUND")
- if attribute not in {"NOT_FOUND", value}:
- self.invalidate_cache()
-
- setattr(self, attr, value)
- del rparams[attr]
- return super().render(fmt, **rparams)
-
def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
+ params = self._default_render_parameters | params
transparent_background = params.pop("transparent_background", False)
- for param_name in [
- "display_parent_relation",
- "display_derived_interfaces",
- "display_symbols_as_boxes",
- "slim_center_box",
- ]:
- if (override := params.pop(param_name, None)) is not None:
- setattr(self, param_name, override)
+ for param_name in self._default_render_parameters:
+ setattr(self, f"_{param_name}", params.pop(param_name))
data = params.get("elkdata") or get_elkdata(self, params)
if has_single_child(data):
- self.display_derived_interfaces = True
+ self._display_derived_interfaces = True
data = get_elkdata(self, params)
layout = try_to_layout(data)
@@ -377,31 +369,68 @@ def filters(self, value: cabc.Iterable[str]) -> None:
class InterfaceContextDiagram(ContextDiagram):
"""An automatically generated Context Diagram exclusively for
``ComponentExchange``s.
+
+ Attributes
+ ----------
+ dangling_functional_exchanges: list[fa.AbstractExchange]
+ A list of ``dangling`` functional exchanges for which either the
+ source or target function were not allocated to a Component,
+ part of the context.
+
+ Notes
+ -----
+ The following render parameters are available:
+
+ * include_interface — Boolean flag to enable inclusion of the
+ context diagram target: The interface ComponentExchange.
+ * hide_functions — Boolean flag to enable white box view: Only
+ displaying Components or Entities.
+ * display_derived_exchanges — Boolean flag to enable inclusion of
+ functional exchanges that are not allocated to the interface but
+ connect allocated functions of collected components.
+
+ In addition to all other render parameters of
+ [`ContextDiagram`][capellambse_context_diagrams.context.ContextDiagram].
"""
+ _include_interface: bool
+ _hide_functions: bool
+ _display_derived_exchanges: bool
+
def __init__(
self,
class_: str,
obj: common.GenericElement,
- include_interface: bool = False,
- hide_functions: bool = False,
- **kw,
+ *,
+ render_styles: dict[str, styling.Styler] | None = None,
+ default_render_parameters: dict[str, t.Any],
) -> None:
- self.include_interface = include_interface
- self.hide_functions = hide_functions
- super().__init__(class_, obj, **kw, display_symbols_as_boxes=True)
+ default_render_parameters = {
+ "include_interface": False,
+ "hide_functions": False,
+ "display_derived_exchanges": False,
+ "display_symbols_as_boxes": True,
+ } | default_render_parameters
+ super().__init__(
+ class_,
+ obj,
+ render_styles=render_styles,
+ default_render_parameters=default_render_parameters,
+ )
+
+ self.dangling_functional_exchanges: list[fa.AbstractExchange] = []
@property
def name(self) -> str: # type: ignore
return f"Interface Context of {self.target.name}"
def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
- for param_name in ("include_interface", "hide_functions"):
- if override := params.pop(param_name, False):
- setattr(self, param_name, override)
+ params = self._default_render_parameters | params
+ for param_name in self._default_render_parameters:
+ setattr(self, f"_{param_name}", params.pop(param_name))
- if self.hide_functions:
- self.include_interface = True
+ if self._hide_functions:
+ self._include_interface = True
params["elkdata"] = exchanges.get_elkdata_for_exchanges(
self, exchanges.InterfaceContextCollector, params
@@ -420,7 +449,7 @@ def name(self) -> str: # type: ignore
def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
params["elkdata"] = exchanges.get_elkdata_for_exchanges(
- self, exchanges.FunctionalContextCollector, params
+ self, exchanges.FunctionalContextCollector, params # type: ignore
)
return super()._create_diagram(params)
@@ -431,8 +460,25 @@ class ClassTreeDiagram(ContextDiagram):
This diagram is exclusively for ``Class``es.
"""
- def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None:
- super().__init__(class_, obj, **kw, display_symbols_as_boxes=True)
+ _display_symbols_as_boxes: bool
+
+ def __init__(
+ self,
+ class_: str,
+ obj: common.GenericElement,
+ *,
+ render_styles: dict[str, styling.Styler] | None = None,
+ default_render_parameters: dict[str, t.Any],
+ ) -> None:
+ default_render_parameters = {
+ "display_symbols_as_boxes": True,
+ } | default_render_parameters
+ super().__init__(
+ class_,
+ obj,
+ render_styles=render_styles,
+ default_render_parameters=default_render_parameters,
+ )
@property
def uuid(self) -> str: # type: ignore
@@ -445,9 +491,16 @@ def name(self) -> str: # type: ignore
return f"Tree view of {self.target.name}"
def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
- params.setdefault("algorithm", params.get("algorithm", "layered"))
+ params = {
+ **self._default_render_parameters,
+ "algorithm": "layered",
+ "edgeRouting": "POLYLINE",
+ **params,
+ }
+ for param_name in self._default_render_parameters:
+ setattr(self, f"_{param_name}", params.pop(param_name))
+
params.setdefault("elk.direction", params.pop("direction", "DOWN"))
- params.setdefault("edgeRouting", params.get("edgeRouting", "POLYLINE"))
params.setdefault(
"nodeSize.constraints",
params.pop("nodeSizeConstraints", "NODE_LABELS"),
@@ -526,8 +579,25 @@ class RealizationViewDiagram(ContextDiagram):
``Entity`` and ``Components`` of all layers.
"""
- def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None:
- super().__init__(class_, obj, **kw, display_symbols_as_boxes=True)
+ _display_symbols_as_boxes: bool
+
+ def __init__(
+ self,
+ class_: str,
+ obj: common.GenericElement,
+ *,
+ render_styles: dict[str, styling.Styler] | None = None,
+ default_render_parameters: dict[str, t.Any],
+ ) -> None:
+ default_render_parameters = {
+ "display_symbols_as_boxes": True,
+ } | default_render_parameters
+ super().__init__(
+ class_,
+ obj,
+ render_styles=render_styles,
+ default_render_parameters=default_render_parameters,
+ )
@property
def uuid(self) -> str: # type: ignore
@@ -540,13 +610,19 @@ def name(self) -> str: # type: ignore
return f"Realization view of {self.target.name}"
def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram:
- params.setdefault("depth", params.get("depth", 1))
- params.setdefault(
- "search_direction", params.get("search_direction", "ALL")
- )
- params.setdefault("show_owners", True)
- params.setdefault("layer_sizing", "WIDTH")
+ params = {
+ **self._default_render_parameters,
+ "depth": 1,
+ "search_direction": "ALL",
+ "show_owners": True,
+ "layer_sizing": "WIDTH",
+ **params,
+ }
+ for param_name in self._default_render_parameters:
+ setattr(self, f"_{param_name}", params.pop(param_name))
+
data, edges = realization_view.collector(self, params)
+
layout = try_to_layout(data)
adjust_layer_sizing(data, layout, params["layer_sizing"])
layout = try_to_layout(data)
@@ -595,8 +671,25 @@ def _add_layer_labels(self, layout: _elkjs.ELKOutputData) -> None:
class DataFlowViewDiagram(ContextDiagram):
"""An automatically generated DataFlowViewDiagram."""
- def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None:
- super().__init__(class_, obj, **kw, display_symbols_as_boxes=True)
+ _display_symbols_as_boxes: bool
+
+ def __init__(
+ self,
+ class_: str,
+ obj: common.GenericElement,
+ *,
+ render_styles: dict[str, styling.Styler] | None = None,
+ default_render_parameters: dict[str, t.Any],
+ ) -> None:
+ default_render_parameters = {
+ "display_symbols_as_boxes": True,
+ } | default_render_parameters
+ super().__init__(
+ class_,
+ obj,
+ render_styles=render_styles,
+ default_render_parameters=default_render_parameters,
+ )
@property
def uuid(self) -> str: # type: ignore
diff --git a/capellambse_context_diagrams/serializers.py b/capellambse_context_diagrams/serializers.py
index a5f9c806..fb3b571c 100644
--- a/capellambse_context_diagrams/serializers.py
+++ b/capellambse_context_diagrams/serializers.py
@@ -61,6 +61,7 @@ def __init__(self, elk_diagram: context.ContextDiagram) -> None:
self._diagram = elk_diagram
self._cache: dict[str, diagram.Box | diagram.Edge] = {}
self._edges: dict[str, EdgeContext] = {}
+ self._junctions: dict[str, EdgeContext] = {}
def make_diagram(
self,
@@ -91,6 +92,9 @@ def make_diagram(
for edge, ref, parent in self._edges.values():
self.deserialize_child(edge, ref, parent)
+ for junction, ref, parent in self._junctions.values():
+ self.deserialize_child(junction, ref, parent)
+
self.diagram.calculate_viewport()
self.order_children()
self._edges.clear()
@@ -148,7 +152,7 @@ class type that stores all previously named classes.
is_port
or has_symbol_cls
and not self._diagram.target.uuid == uuid
- and not self._diagram.display_symbols_as_boxes
+ and not self._diagram._display_symbols_as_boxes
]
assert not isinstance(
@@ -242,7 +246,7 @@ class type that stores all previously named classes.
element = parent
elif child.type == "junction":
- uuid = child.id.rsplit("_", maxsplit=1)[0]
+ uuid = uuid.rsplit("_", maxsplit=1)[0]
pos = diagram.Vector2D(child.position.x, child.position.y)
if self._is_hierarchical(uuid):
# FIXME should this use `parent` instead?
@@ -257,6 +261,7 @@ class type that stores all previously named classes.
context=getattr(child, "context", {}),
)
self.diagram.add_element(element)
+ self._cache[uuid] = element
else:
logger.warning("Received unknown type %s", child.type)
return
@@ -264,6 +269,8 @@ class type that stores all previously named classes.
for i in getattr(child, "children", []):
if i.type == "edge":
self._edges.setdefault(i.id, (i, ref, parent))
+ elif i.type == "junction":
+ self._junctions.setdefault(i.id, (i, ref, parent))
else:
self.deserialize_child(i, ref, element)
diff --git a/docs/assets/images/Context of Left.svg b/docs/assets/images/Context of Left.svg
index 1e6ec85c..5129e11a 100644
--- a/docs/assets/images/Context of Left.svg
+++ b/docs/assets/images/Context of Left.svg
@@ -3,4 +3,124 @@
~ SPDX-License-Identifier: Apache-2.0
-->
-
+
diff --git a/docs/assets/images/Interface Context of Interface.svg b/docs/assets/images/Interface Context of Interface.svg
index 20d877c6..6a5d05be 100644
--- a/docs/assets/images/Interface Context of Interface.svg
+++ b/docs/assets/images/Interface Context of Interface.svg
@@ -3,4 +3,332 @@
~ SPDX-License-Identifier: Apache-2.0
-->
-
+
diff --git a/docs/extras/styling.md b/docs/extras/styling.md
index 8f336d7e..439711be 100644
--- a/docs/extras/styling.md
+++ b/docs/extras/styling.md
@@ -36,9 +36,9 @@ py-capellambse.
# No symbol rendering
There are some ModelObjects that are displayed as symbols in a diagram (e.g.
-Capabilities or Missions). The `.display_symbols_as_boxes` attribute gives you
+Capabilities or Missions). The `.display_symbols_as_boxes` parameter gives you
the control to render these as boxes such that the symbol is displayed as an
-icon beside the box-label. Per default this attribute is set to `True`.
+icon beside the box-label. Per default it is set to `True`.
??? example "Box-only style for Context diagram of Middle OperationalCapability [OCB]"
@@ -46,12 +46,11 @@ icon beside the box-label. Per default this attribute is set to `True`.
from capellambse import aird
diag = model.by_uuid("da08ddb6-92ba-4c3b-956a-017424dbfe85").context_diagram
- diag.display_symbols_as_boxes = True # per default
- diag.render("svgdiagram").save(pretty=True)
+ diag.render("svgdiagram", display_symbols_as_boxes=False).save(pretty=True)
```
produces
@@ -61,12 +60,11 @@ icon beside the box-label. Per default this attribute is set to `True`.
from capellambse import aird
diag = model.by_uuid("9390b7d5-598a-42db-bef8-23677e45ba06").context_diagram
- diag.display_symbols_as_boxes = True # per default
- diag.render("svgdiagram").save(pretty=True)
+ diag.render("svgdiagram", display_symbols_as_boxes=False).save(pretty=True)
```
produces
diff --git a/docs/gen_images.py b/docs/gen_images.py
index 2eb2e2c5..f9338b76 100644
--- a/docs/gen_images.py
+++ b/docs/gen_images.py
@@ -33,10 +33,7 @@
}
interface_context_diagram_uuids: dict[str, tuple[str, dict[str, t.Any]]] = {
"Left to right": ("3ef23099-ce9a-4f7d-812f-935f47e7938d", {}),
- "Interface": (
- "2f8ed849-fbda-4902-82ec-cbf8104ae686",
- {"include_interface": True},
- ),
+ "Interface": ("2f8ed849-fbda-4902-82ec-cbf8104ae686", {}),
}
hierarchy_context = "16b4fcc5-548d-4721-b62a-d3d5b1c1d2eb"
diagram_uuids = general_context_diagram_uuids | interface_context_diagram_uuids
@@ -55,13 +52,13 @@ def generate_index_images() -> None:
print(diag.render("svg", **render_params), file=fd) # type: ignore[arg-type]
-def generate_no_symbol_images() -> None:
+def generate_symbol_images() -> None:
for name in ("Capability", "Middle"):
uuid, _ = general_context_diagram_uuids[name]
diag: context.ContextDiagram = model.by_uuid(uuid).context_diagram
- diag.display_symbols_as_boxes = True
+ diag._display_symbols_as_boxes = True
diag.invalidate_cache()
- filepath = f"{str(dest / diag.name)} no_symbols.svg"
+ filepath = f"{str(dest / diag.name)} symbols.svg"
with mkdocs_gen_files.open(filepath, "w") as fd:
print(diag.render("svg", transparent_background=False), file=fd)
@@ -187,9 +184,29 @@ def generate_interface_with_hide_functions_image():
print(diag.render("svg", **params), file=fd)
+def generate_interface_with_hide_interface_image():
+ uuid = interface_context_diagram_uuids["Interface"][0]
+ diag: context.ContextDiagram = model.by_uuid(uuid).context_diagram
+ params = {"include_interface": False}
+ with mkdocs_gen_files.open(
+ f"{str(dest / diag.name)}-hide-interface.svg", "w"
+ ) as fd:
+ print(diag.render("svg", **params), file=fd)
+
+
+def generate_interface_with_display_derived_exchanges_image():
+ uuid = "86a1afc2-b7fd-4023-bbd5-ab44f5dc2c28"
+ diag: context.ContextDiagram = model.by_uuid(uuid).context_diagram
+ params = {"display_derived_exchanges": True}
+ with mkdocs_gen_files.open(
+ f"{str(dest / diag.name)}-derived-exchanges.svg", "w"
+ ) as fd:
+ print(diag.render("svg", **params), file=fd)
+
+
generate_index_images()
generate_hierarchy_image()
-generate_no_symbol_images()
+generate_symbol_images()
wizard_uuid = general_context_diagram_uuids["educate Wizards"][0]
generate_no_edgelabel_image(wizard_uuid)
@@ -212,4 +229,6 @@ def generate_interface_with_hide_functions_image():
generate_realization_view_images()
generate_data_flow_image()
generate_derived_image()
-generate_interface_with_hide_functions_image()
+# generate_interface_with_hide_functions_image()
+generate_interface_with_hide_interface_image()
+generate_interface_with_display_derived_exchanges_image()
diff --git a/docs/interface.md b/docs/interface.md
index dafe286e..da419fa1 100644
--- a/docs/interface.md
+++ b/docs/interface.md
@@ -20,22 +20,22 @@ diag.render("svgdiagram").save(pretty=True)
-## Include the interface itself in the context
-??? example "Include the interface in the Interface Context"
+## Exclude the interface itself in the context
+??? example "Exclude the interface in the Interface Context"
``` py
import capellambse
model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
diag = model.by_uuid("fbb7f735-3c1f-48de-9791-179d35ca7b98").context_diagram
- diag.render("svgdiagram", include_interface=True).save(pretty=True)
+ diag.render("svgdiagram", include_interface=False).save(pretty=True)
```
## Hide functional model elements from the context
@@ -50,7 +50,22 @@ diag.render("svgdiagram").save(pretty=True)
```
-!!! warning "Interface context only supported for the LogicalComponentExchanges"
+## Display derived functional exchanges in the context
+??? example "Display derived functional exchanges in the Interface Context"
+
+ ``` py
+ import capellambse
+
+ model = capellambse.MelodyModel("tests/data/ContextDiagram.aird")
+ diag = model.by_uuid("fbb7f735-3c1f-48de-9791-179d35ca7b98").context_diagram
+ diag.render("svgdiagram", display_derived_exchanges=True).save(pretty=True)
+ ```
+
+
+!!! warning "Interface context only supported for System and Logical ComponentExchanges"
diff --git a/pyproject.toml b/pyproject.toml
index 45f41615..0436e419 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -169,6 +169,7 @@ addopts = """
--strict-config
--strict-markers
--import-mode=importlib
+ --tb=short
"""
testpaths = ["tests"]
xfail_strict = true
diff --git a/tests/data/ContextDiagram.aird b/tests/data/ContextDiagram.aird
index 772afcb7..83077280 100644
--- a/tests/data/ContextDiagram.aird
+++ b/tests/data/ContextDiagram.aird
@@ -94,7 +94,7 @@
-
+
@@ -134,7 +134,7 @@
-
+
@@ -150,6 +150,10 @@
+
+
+
+
@@ -4603,7 +4607,7 @@
-
+
@@ -4672,7 +4676,7 @@
-
+
@@ -4788,7 +4792,7 @@
-
+
@@ -11967,13 +11971,13 @@
-
+
-
+
@@ -11992,7 +11996,7 @@
-
+
@@ -12003,10 +12007,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -12054,13 +12069,13 @@
-
+
-
+
@@ -12101,27 +12116,49 @@
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
+
+
+
+
@@ -12138,7 +12175,7 @@
-
+
@@ -12165,11 +12202,11 @@
-
+
-
+
@@ -12205,22 +12242,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -12301,35 +12322,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
-
+
@@ -12349,6 +12354,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -12358,7 +12475,7 @@
-
+
@@ -12427,7 +12544,7 @@
-
+
@@ -12436,6 +12553,15 @@
+
+
+
+
+
+
+
+
+
@@ -12446,7 +12572,7 @@
-
+
@@ -12532,20 +12658,42 @@
-
+
-
+
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+ italic
+
@@ -12592,17 +12740,6 @@
-
-
-
-
-
-
-
-
-
-
-
@@ -12648,15 +12785,6 @@
-
-
-
-
-
-
-
-
-
@@ -12675,6 +12803,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -14761,4 +14958,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ bold
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+ bold
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/ContextDiagram.capella b/tests/data/ContextDiagram.capella
index 35ebbc06..94637d48 100644
--- a/tests/data/ContextDiagram.capella
+++ b/tests/data/ContextDiagram.capella
@@ -3194,6 +3194,8 @@ The predator is far away
id="64736cbe-2a66-41af-b300-5174ebeb1da9" name="FOP 1"/>
+
@@ -3211,6 +3213,16 @@ The predator is far away
id="fbfb2b20-b711-4211-9b75-25e38390cdbc" name="RFNC 1">
+
+
+
+
+
+
@@ -3360,6 +3372,12 @@ The predator is far away
+
+
+
+
@@ -4236,6 +4260,9 @@ The predator is far away
+
@@ -4247,7 +4274,10 @@ The predator is far away
+
@@ -4266,6 +4296,12 @@ The predator is far away
+
+
diff --git a/tests/test_interface_diagrams.py b/tests/test_interface_diagrams.py
index 986d0406..d8a608d2 100644
--- a/tests/test_interface_diagrams.py
+++ b/tests/test_interface_diagrams.py
@@ -5,13 +5,14 @@
import pytest
TEST_INTERFACE_UUID = "2f8ed849-fbda-4902-82ec-cbf8104ae686"
+TEST_SA_INTERFACE_UUID = "86a1afc2-b7fd-4023-bbd5-ab44f5dc2c28"
@pytest.mark.parametrize(
"uuid",
[
# pytest.param("3c9764aa-4981-44ef-8463-87a053016635", id="OA"),
- pytest.param("86a1afc2-b7fd-4023-bbd5-ab44f5dc2c28", id="SA"),
+ pytest.param(TEST_SA_INTERFACE_UUID, id="SA"),
pytest.param("3ef23099-ce9a-4f7d-812f-935f47e7938d", id="LA"),
],
)
@@ -25,7 +26,7 @@ def test_interface_diagrams_get_rendered(
assert diag.nodes
-def test_interface_diagrams_with_nested_components(
+def test_interface_diagrams_with_nested_components_and_functions(
model: capellambse.MelodyModel,
) -> None:
obj = model.by_uuid(TEST_INTERFACE_UUID)
@@ -40,9 +41,16 @@ def test_interface_diagram_with_included_interface(
) -> None:
obj = model.by_uuid(TEST_INTERFACE_UUID)
- diag = obj.context_diagram.render(None, include_interface=True)
+ obj.context_diagram.render("svgdiagram", hide_functions=True).save(
+ pretty=True
+ )
+ obj.context_diagram.render("svgdiagram", include_interface=False).save(
+ pretty=True
+ )
+ diag = obj.context_diagram.render(None, include_interface=False)
- assert diag[TEST_INTERFACE_UUID]
+ with pytest.raises(KeyError):
+ diag[TEST_INTERFACE_UUID] # pylint: disable=pointless-statement
def test_interface_diagram_with_hide_functions(
@@ -51,9 +59,6 @@ def test_interface_diagram_with_hide_functions(
obj = model.by_uuid(TEST_INTERFACE_UUID)
diag = obj.context_diagram.render(None, hide_functions=True)
- obj.context_diagram.render("svgdiagram", hide_functions=True).save(
- pretty=True
- )
for uuid in (
"fbfb2b20-b711-4211-9b75-25e38390cdbc", # LogicalFunction
@@ -61,3 +66,18 @@ def test_interface_diagram_with_hide_functions(
):
with pytest.raises(KeyError):
diag[uuid] # pylint: disable=pointless-statement
+
+
+def test_interface_diagram_with_derived_exchanges(
+ model: capellambse.MelodyModel,
+) -> None:
+ obj = model.by_uuid(TEST_SA_INTERFACE_UUID)
+ expected_derived_exchanges = (
+ "d69dcf31-a7d4-40c5-8dd4-b4747aa3ece7",
+ "7a61fcb7-aae5-4698-86de-8b0d70d8c09b",
+ )
+
+ diag = obj.context_diagram.render(None, display_derived_exchanges=True)
+
+ for uuid in expected_derived_exchanges:
+ assert diag[f"__Derived-FunctionalExchange:{uuid}"]