diff --git a/capellambse_context_diagrams/_elkjs.py b/capellambse_context_diagrams/_elkjs.py index 337588b1..95b1e555 100644 --- a/capellambse_context_diagrams/_elkjs.py +++ b/capellambse_context_diagrams/_elkjs.py @@ -19,7 +19,7 @@ from pathlib import Path import capellambse -import typing_extensions as te +import pydantic __all__ = [ "call_elkjs", @@ -89,87 +89,96 @@ """Options for labels to configure ELK layouting.""" -class ELKInputData(te.TypedDict, total=False): +class BaseELKModel(pydantic.BaseModel): + """Base class for ELK models.""" + + model_config = pydantic.ConfigDict( + extra="allow", + arbitrary_types_allowed=True, + ) + + +class ELKInputData(BaseELKModel): """Data that can be fed to ELK.""" - id: te.Required[str] - layoutOptions: LayoutOptions - children: cabc.MutableSequence[ELKInputChild] # type: ignore - edges: cabc.MutableSequence[ELKInputEdge] + id: str + layoutOptions: LayoutOptions = {} + children: cabc.MutableSequence[ELKInputChild] = [] + edges: cabc.MutableSequence[ELKInputEdge] = [] -class ELKInputChild(ELKInputData, total=False): +class ELKInputChild(ELKInputData): """Children of either `ELKInputData` or `ELKInputChild`.""" - labels: cabc.MutableSequence[ELKInputLabel] - ports: cabc.MutableSequence[ELKInputPort] + labels: cabc.MutableSequence[ELKInputLabel] = [] + ports: cabc.MutableSequence[ELKInputPort] = [] - width: t.Union[int, float] - height: t.Union[int, float] + width: t.Union[int, float] = 0 + height: t.Union[int, float] = 0 -class ELKInputLabel(te.TypedDict, total=False): +class ELKInputLabel(BaseELKModel): """Label data that can be fed to ELK.""" - text: te.Required[str] - layoutOptions: LayoutOptions - width: t.Union[int, float] - height: t.Union[int, float] + text: str + layoutOptions: LayoutOptions = {} + width: t.Union[int, float] = 0 + height: t.Union[int, float] = 0 -class ELKInputPort(t.TypedDict): +class ELKInputPort(BaseELKModel): """Connector data that can be fed to ELK.""" id: str + layoutOptions: LayoutOptions = {} + width: t.Union[int, float] height: t.Union[int, float] - layoutOptions: te.NotRequired[cabc.MutableMapping[str, t.Any]] - -class ELKInputEdge(te.TypedDict): +class ELKInputEdge(BaseELKModel): """Exchange data that can be fed to ELK.""" id: str sources: cabc.MutableSequence[str] targets: cabc.MutableSequence[str] - labels: te.NotRequired[cabc.MutableSequence[ELKInputLabel]] + labels: cabc.MutableSequence[ELKInputLabel] = [] -class ELKPoint(t.TypedDict): +class ELKPoint(BaseELKModel): """Point data in ELK.""" x: t.Union[int, float] y: t.Union[int, float] -class ELKSize(t.TypedDict): +class ELKSize(BaseELKModel): """Size data in ELK.""" width: t.Union[int, float] height: t.Union[int, float] -class ELKOutputElement(t.TypedDict): +class ELKOutputElement(BaseELKModel): """Base class for all elements that comes out of ELK.""" id: str - style: dict[str, t.Any] + style: dict[str, t.Any] = {} class ELKOutputData(ELKOutputElement): """Data that comes from ELK.""" type: t.Literal["graph"] - children: cabc.MutableSequence[ELKOutputChild] # type: ignore + children: cabc.MutableSequence[ELKOutputChild] = [] class ELKOutputNode(ELKOutputElement): """Node that comes out of ELK.""" type: t.Literal["node"] - children: cabc.MutableSequence[ELKOutputChild] # type: ignore + children: cabc.MutableSequence[ELKOutputChild] = [] position: ELKPoint size: ELKSize @@ -179,7 +188,7 @@ class ELKOutputJunction(ELKOutputElement): """Exchange-Junction that comes out of ELK.""" type: t.Literal["junction"] - children: cabc.MutableSequence[ELKOutputLabel] + children: cabc.MutableSequence[ELKOutputLabel] = [] position: ELKPoint size: ELKSize @@ -189,7 +198,7 @@ class ELKOutputPort(ELKOutputElement): """Port that comes out of ELK.""" type: t.Literal["port"] - children: cabc.MutableSequence[ELKOutputLabel] + children: cabc.MutableSequence[ELKOutputLabel] = [] position: ELKPoint size: ELKSize @@ -213,10 +222,10 @@ class ELKOutputEdge(ELKOutputElement): sourceId: str targetId: str routingPoints: cabc.MutableSequence[ELKPoint] - children: cabc.MutableSequence[ELKOutputLabel] + children: cabc.MutableSequence[ELKOutputLabel] = [] -ELKOutputChild = t.Union[ # type: ignore +ELKOutputChild = t.Union[ ELKOutputEdge, ELKOutputJunction, ELKOutputLabel, @@ -344,7 +353,7 @@ def call_elkjs(elk_dict: ELKInputData) -> ELKOutputData: executable=shutil.which("node"), capture_output=True, check=False, - input=json.dumps(elk_dict), + input=elk_dict.model_dump_json(), text=True, env={**os.environ, "NODE_PATH": str(NODE_HOME)}, ) @@ -352,7 +361,7 @@ def call_elkjs(elk_dict: ELKInputData) -> ELKOutputData: log.getChild("node").error("%s", proc.stderr) raise NodeJSError("elk.js process failed") - return json.loads(proc.stdout) + return ELKOutputData.model_validate_json(proc.stdout) def get_global_layered_layout_options() -> LayoutOptions: diff --git a/capellambse_context_diagrams/collectors/dataflow_view.py b/capellambse_context_diagrams/collectors/dataflow_view.py index 906b217f..c52e93e9 100644 --- a/capellambse_context_diagrams/collectors/dataflow_view.py +++ b/capellambse_context_diagrams/collectors/dataflow_view.py @@ -73,7 +73,7 @@ def collector_portless( ) made_edges: set[str] = set() for act in activities: - data["children"].append(act_box := makers.make_box(act)) + data.children.append(act_box := makers.make_box(act)) connections = list(portless.get_exchanges(act, filter=filter)) in_act: dict[str, oa.OperationalActivity] = {} @@ -84,9 +84,9 @@ def collector_portless( else: in_act.setdefault(edge.target.uuid, edge.target) - act_box["height"] += ( - makers.PORT_SIZE + 2 * makers.PORT_PADDING - ) * max(len(in_act), len(out_act)) + act_box.height += (makers.PORT_SIZE + 2 * makers.PORT_PADDING) * max( + len(in_act), len(out_act) + ) ex_datas: list[generic.ExchangeData] = [] for ex in connections: @@ -124,7 +124,7 @@ def collector_default( ) made_edges: set[str] = set() for fnc in functions: - data["children"].append(fnc_box := makers.make_box(fnc)) + data.children.append(fnc_box := makers.make_box(fnc)) _ports = default.port_collector(fnc, diagram.type) connections = default.port_exchange_collector(_ports, filter=filter) in_ports: dict[str, fa.FunctionPort] = {} @@ -135,12 +135,12 @@ def collector_default( else: in_ports.setdefault(edge.target.uuid, edge.target) - fnc_box["ports"] = [ + fnc_box.ports = [ makers.make_port(i.uuid) for i in (in_ports | out_ports).values() ] - fnc_box["height"] += ( - makers.PORT_SIZE + 2 * makers.PORT_PADDING - ) * max(len(in_ports), len(out_ports)) + fnc_box.height += (makers.PORT_SIZE + 2 * makers.PORT_PADDING) * max( + len(in_ports), len(out_ports) + ) ex_datas: list[generic.ExchangeData] = [] for ex in edges: diff --git a/capellambse_context_diagrams/collectors/default.py b/capellambse_context_diagrams/collectors/default.py index b2e49d6a..c587e97d 100644 --- a/capellambse_context_diagrams/collectors/default.py +++ b/capellambse_context_diagrams/collectors/default.py @@ -40,9 +40,9 @@ def collector( ) data = generic.collector(diagram, no_symbol=True) ports = port_collector(diagram.target, diagram.type) - centerbox = data["children"][0] + centerbox = data.children[0] connections = port_exchange_collector(ports) - centerbox["ports"] = [ + centerbox.ports = [ makers.make_port(uuid) for uuid, edges in connections.items() if edges ] ex_datas: list[generic.ExchangeData] = [] @@ -53,9 +53,9 @@ def collector( if is_hierarchical := exchanges.is_hierarchical(ex, centerbox): if not diagram.display_parent_relation: continue - centerbox["labels"][0][ - "layoutOptions" - ] = makers.DEFAULT_LABEL_LAYOUT_OPTIONS + centerbox.labels[0].layoutOptions = ( + makers.DEFAULT_LABEL_LAYOUT_OPTIONS + ) elkdata: _elkjs.ELKInputData = centerbox else: elkdata = data @@ -67,9 +67,9 @@ def collector( ex_datas.append(ex_data) except AttributeError: continue - global_boxes = {centerbox["id"]: centerbox} - made_boxes = {centerbox["id"]: centerbox} - boxes_to_delete = {centerbox["id"]} + global_boxes = {centerbox.id: centerbox} + made_boxes = {centerbox.id: centerbox} + boxes_to_delete = {centerbox.id} def _make_box_and_update_globals( obj: t.Any, @@ -90,8 +90,8 @@ def _make_owner_box(current: t.Any) -> t.Any: no_symbol=diagram.display_symbols_as_boxes, layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, ) - for box in (children := parent_box.setdefault("children", [])): - if box["id"] == current.uuid: + for box in (children := parent_box.children): + if box.id == current.uuid: box = global_boxes.get(current.uuid, current) break else: @@ -107,8 +107,8 @@ def _make_owner_box(current: t.Any) -> t.Any: no_symbol=diagram.display_symbols_as_boxes, layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, ) - box["children"] = [centerbox] - del data["children"][0] + box.children = [centerbox] + del data.children[0] except AttributeError: pass diagram_target_owners = generic.get_all_owners(diagram.target) @@ -128,17 +128,15 @@ def _make_owner_box(current: t.Any) -> t.Any: if box := global_boxes.get(child.uuid): # type: ignore[assignment] if box is centerbox: continue - box.setdefault("ports", []).extend( - [makers.make_port(j.uuid) for j in local_ports] - ) - box["height"] += height + box.ports.extend([makers.make_port(j.uuid) for j in local_ports]) + box.height += height else: box = _make_box_and_update_globals( child, height=height, no_symbol=diagram.display_symbols_as_boxes, ) - box["ports"] = [makers.make_port(j.uuid) for j in local_ports] + box.ports = [makers.make_port(j.uuid) for j in local_ports] if diagram.display_parent_relation: current = child @@ -170,17 +168,15 @@ def _make_owner_box(current: t.Any) -> t.Any: for uuid in boxes_to_delete: del global_boxes[uuid] - data["children"].extend(global_boxes.values()) + data.children.extend(global_boxes.values()) if diagram.display_parent_relation: owner_boxes: dict[str, _elkjs.ELKInputChild] = { - uuid: box - for uuid, box in made_boxes.items() - if box.get("children") + uuid: box for uuid, box in made_boxes.items() if box.children } generic.move_parent_boxes_to_owner(owner_boxes, diagram.target, data) generic.move_edges(owner_boxes, edges, data) - centerbox["height"] = max(centerbox["height"], *stack_heights.values()) + centerbox.height = max(centerbox.height, *stack_heights.values()) derivator = DERIVATORS.get(type(diagram.target)) if diagram.display_derived_interfaces and derivator is not None: derivator(diagram, data, made_boxes[diagram.target.uuid]) @@ -317,7 +313,7 @@ def derive_from_functions( for fnc in diagram.target.allocated_functions: ports.extend(port_collector(fnc, diagram.type)) - context_box_ids = {child["id"] for child in data["children"]} + context_box_ids = {child.id for child in data.children} components: dict[str, cs.Component] = {} for port in ports: for fex in port.exchanges: @@ -349,24 +345,24 @@ def derive_from_functions( no_symbol=diagram.display_symbols_as_boxes, ) class_ = type(derived_comp).__name__ - box["id"] = f"{STYLECLASS_PREFIX}-{class_}:{uuid}" - data["children"].append(box) + box.id = f"{STYLECLASS_PREFIX}-{class_}:{uuid}" + data.children.append(box) source_id = f"{STYLECLASS_PREFIX}-CP_INOUT:{i}" target_id = f"{STYLECLASS_PREFIX}-CP_INOUT:{-i}" - box.setdefault("ports", []).append(makers.make_port(source_id)) - centerbox.setdefault("ports", []).append(makers.make_port(target_id)) + box.ports.append(makers.make_port(source_id)) + centerbox.ports.append(makers.make_port(target_id)) if i % 2 == 0: source_id, target_id = target_id, source_id - data["edges"].append( - { - "id": f"{STYLECLASS_PREFIX}-ComponentExchange:{i}", - "sources": [source_id], - "targets": [target_id], - } + data.edges.append( + _elkjs.ELKInputEdge( + id=f"{STYLECLASS_PREFIX}-ComponentExchange:{i}", + sources=[source_id], + targets=[target_id], + ) ) - data["children"][0]["height"] += ( + data.children[0].height += ( makers.PORT_PADDING + (makers.PORT_SIZE + makers.PORT_PADDING) * len(components) // 2 ) diff --git a/capellambse_context_diagrams/collectors/exchanges.py b/capellambse_context_diagrams/collectors/exchanges.py index 83f64933..cff5941b 100644 --- a/capellambse_context_diagrams/collectors/exchanges.py +++ b/capellambse_context_diagrams/collectors/exchanges.py @@ -127,35 +127,35 @@ def make_ports_and_update_children_size( ) -> None: """Adjust size of functions and make ports.""" stack_height: int | float = -makers.NEIGHBOR_VMARGIN - for child in data["children"]: + for child in data.children: inputs, outputs = [], [] - obj = self.obj._model.by_uuid(child["id"]) + 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] + 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"] = [ + 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, + child.height + 2 * makers.LABEL_VPAD, makers.PORT_PADDING + (makers.PORT_SIZE + makers.PORT_PADDING) * childnum, ) - child["height"] = height + child.height = height stack_height += makers.NEIGHBOR_VMARGIN + height - data["height"] = stack_height + data.height = stack_height @abc.abstractmethod def collect(self) -> None: @@ -174,8 +174,8 @@ def get_elkdata_for_exchanges( data = makers.make_diagram(diagram) collector = collector_type(diagram, data, params) collector.collect() - for comp in data["children"]: - collector.make_ports_and_update_children_size(comp, data["edges"]) + for comp in data.children: + collector.make_ports_and_update_children_size(comp, data.edges) return data @@ -240,7 +240,7 @@ def make_boxes(cntxt: dict[str, t.Any]) -> _elkjs.ELKInputChild | None: box = makers.make_box( comp, no_symbol=True, layout_options=layout_options ) - box["children"] = children + box.children = children made_children.add(comp.uuid) return box return None @@ -273,10 +273,10 @@ def make_boxes(cntxt: dict[str, t.Any]) -> _elkjs.ELKInputChild | None: left_context, right_context = right_context, left_context if left_child := make_boxes(left_context): - self.data["children"].append(left_child) + self.data.children.append(left_child) self.left = left_child if right_child := make_boxes(right_context): - self.data["children"].append(right_child) + self.data.children.append(right_child) self.right = right_child except AttributeError: pass @@ -291,13 +291,13 @@ 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] + 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.setdefault("ports", []).append(makers.make_port(src.uuid)) - self.right.setdefault("ports", []).append(makers.make_port(tgt.uuid)) + self.left.ports.append(makers.make_port(src.uuid)) + self.right.ports.append(makers.make_port(tgt.uuid)) def collect(self) -> None: """Collect all allocated `FunctionalExchange`s in the context.""" @@ -313,10 +313,10 @@ def collect(self) -> None: src, tgt = generic.exchange_data_collector(ex_data) if ex in self.incoming_edges.values(): - self.data["edges"][-1]["sources"] = [tgt.uuid] - self.data["edges"][-1]["targets"] = [src.uuid] + self.data.edges[-1].sources = [tgt.uuid] + self.data.edges[-1].targets = [src.uuid] - if not self.data["edges"]: + if not self.data.edges: logger.warning( "There are no FunctionalExchanges allocated to '%s'.", self.obj.name, @@ -356,14 +356,14 @@ def collect(self) -> None: layout_options = makers.CENTRIC_LABEL_LAYOUT_OPTIONS box = makers.make_box(comp, layout_options=layout_options) - box["children"] = children - self.data["children"].append(box) + box.children = children + self.data.children.append(box) made_children.add(comp.uuid) all_functions.extend(functions) functional_exchanges.extend(inc | outs) - self.data["children"][0]["children"] = [ + self.data.children[0].children = [ makers.make_box(c) for c in all_functions if c in self.obj.functions @@ -386,9 +386,9 @@ def is_hierarchical( ) -> bool: """Check if the exchange is hierarchical (nested) inside ``box``.""" src, trg = generic.collect_exchange_endpoints(ex) - objs = {o["id"] for o in box[key]} + objs = {o.id for o in getattr(box, key)} attr_map = {"children": "parent.uuid", "ports": "parent.parent.uuid"} attr_getter = operator.attrgetter(attr_map[key]) - source_contained = src.uuid in objs or attr_getter(src) == box["id"] - target_contained = trg.uuid in objs or attr_getter(trg) == box["id"] + source_contained = src.uuid in objs or attr_getter(src) == box.id + target_contained = trg.uuid in objs or attr_getter(trg) == box.id return source_contained and target_contained diff --git a/capellambse_context_diagrams/collectors/generic.py b/capellambse_context_diagrams/collectors/generic.py index 51e9f3c3..7ca84d59 100644 --- a/capellambse_context_diagrams/collectors/generic.py +++ b/capellambse_context_diagrams/collectors/generic.py @@ -59,7 +59,7 @@ def collector( ) -> _elkjs.ELKInputData: """Returns ``ELKInputData`` with only centerbox in children and config.""" data = makers.make_diagram(diagram) - data["children"] = [ + data.children = [ makers.make_box( diagram.target, width=width, @@ -108,7 +108,7 @@ def exchange_data_collector( ) -> SourceAndTarget: """Return source and target port from `exchange`. - Additionally inflate `elkdata["children"]` with input data for ELK. + Additionally inflate `elkdata.children` with input data for ELK. You can handover a filter name that corresponds to capellambse filters. This will apply filter functionality from [`filters.FILTER_LABEL_ADJUSTERS`][capellambse_context_diagrams.filters.FILTER_LABEL_ADJUSTERS]. @@ -149,12 +149,12 @@ def exchange_data_collector( name, ) - data.elkdata.setdefault("edges", []).append( - { - "id": render_adj.get("id", data.exchange.uuid), - "sources": [render_adj.get("sources", source.uuid)], - "targets": [render_adj.get("targets", target.uuid)], - }, + data.elkdata.edges.append( + _elkjs.ELKInputEdge( + id=render_adj.get("id", data.exchange.uuid), + sources=[render_adj.get("sources", source.uuid)], + targets=[render_adj.get("targets", target.uuid)], + ) ) label = collect_label(data.exchange) @@ -171,7 +171,7 @@ def exchange_data_collector( ) if label and not no_edgelabels: - data.elkdata["edges"][-1]["labels"] = makers.make_label( + data.elkdata.edges[-1].labels = makers.make_label( render_adj.get("labels_text", label), max_width=makers.MAX_LABEL_WIDTH, ) @@ -201,11 +201,11 @@ def move_parent_boxes_to_owner( ) -> None: """Move boxes to their owner box.""" boxes_to_remove: list[str] = [] - for child in data["children"]: - if not child.get("children"): + for child in data.children: + if not child.children: continue - owner = obj._model.by_uuid(child["id"]) + owner = obj._model.by_uuid(child.id) if ( isinstance(owner, filter_types) or not (oowner := owner.owner) @@ -214,12 +214,10 @@ def move_parent_boxes_to_owner( ): continue - oowner_box.setdefault("children", []).append(child) - boxes_to_remove.append(child["id"]) + oowner_box.children.append(child) + boxes_to_remove.append(child.id) - data["children"] = [ - b for b in data["children"] if b["id"] not in boxes_to_remove - ] + data.children = [b for b in data.children if b.id not in boxes_to_remove] def move_edges( @@ -243,13 +241,11 @@ def move_edges( ): continue - for edge in data["edges"]: - if edge["id"] == c.uuid: - owner_box.setdefault("edges", []).append(edge) - edges_to_remove.append(edge["id"]) - data["edges"] = [ - e for e in data["edges"] if e["id"] not in edges_to_remove - ] + for edge in data.edges: + if edge.id == c.uuid: + owner_box.edges.append(edge) + edges_to_remove.append(edge.id) + data.edges = [e for e in data.edges if e.id not in edges_to_remove] def get_all_owners(obj: common.GenericElement) -> list[str]: diff --git a/capellambse_context_diagrams/collectors/makers.py b/capellambse_context_diagrams/collectors/makers.py index fdea3a12..258a7c23 100644 --- a/capellambse_context_diagrams/collectors/makers.py +++ b/capellambse_context_diagrams/collectors/makers.py @@ -73,12 +73,12 @@ def make_diagram(diagram: context.ContextDiagram) -> _elkjs.ELKInputData: """Return basic skeleton for ``ContextDiagram``s.""" - return { - "id": diagram.uuid, - "layoutOptions": _elkjs.get_global_layered_layout_options(), - "children": [], - "edges": [], - } + return _elkjs.ELKInputData( + id=diagram.uuid, + layoutOptions=_elkjs.get_global_layered_layout_options(), + children=[], + edges=[], + ) def make_label( @@ -106,14 +106,14 @@ def make_label( for line in lines: label_width, label_height = chelpers.get_text_extent(line) labels.append( - { - "text": line, - "width": ( + _elkjs.ELKInputLabel( + text=line, + width=( (icon_width + label_width + 2 * LABEL_HPAD) if line else 0 ), - "height": (label_height + 2 * LABEL_VPAD) if line else 0, - "layoutOptions": layout_options, - } + height=(label_height + 2 * LABEL_VPAD) if line else 0, + layoutOptions=layout_options, + ) ) return labels @@ -141,7 +141,7 @@ def make_box( "icon": (ICON_WIDTH, 0), "layout_options": {}, } - ], # type: ignore + ], max_label_width: int | float = MAX_BOX_WIDTH, layout_options: _elkjs.LayoutOptions | None = None, ) -> _elkjs.ELKInputChild: @@ -168,12 +168,17 @@ def make_box( height = MAX_SYMBOL_HEIGHT width = height * SYMBOL_RATIO for label in labels: - label.setdefault("layoutOptions", {}).update(SYMBOL_LAYOUT_OPTIONS) + label.layoutOptions.update(SYMBOL_LAYOUT_OPTIONS) else: width, height = calculate_height_and_width( labels, width=width, height=height, slim_width=slim_width ) - return {"id": obj.uuid, "labels": labels, "width": width, "height": height} + return _elkjs.ELKInputChild( + id=obj.uuid, + labels=labels, + width=width, + height=height, + ) def calculate_height_and_width( @@ -185,8 +190,8 @@ def calculate_height_and_width( ) -> tuple[int | float, int | float]: """Calculate the size (width and height) from given labels for a box.""" icon = icon_size + icon_padding * 2 - _height = sum(label["height"] + 2 * LABEL_VPAD for label in labels) + icon - min_width = max(label["width"] + 2 * LABEL_HPAD for label in labels) + _height = sum(label.height + 2 * LABEL_VPAD for label in labels) + icon + min_width = max(label.width + 2 * LABEL_HPAD for label in labels) width = min_width if slim_width else max(width, min_width) return width, max(height, _height) @@ -204,9 +209,9 @@ def make_port(uuid: str) -> _elkjs.ELKInputPort: """Return an [`ELKInputPort`][capellambse_context_diagrams._elkjs.ELKInputPort]. """ - return { - "id": uuid, - "width": 10, - "height": 10, - "layoutOptions": {"borderOffset": -8}, - } + return _elkjs.ELKInputPort( + id=uuid, + width=PORT_SIZE, + height=PORT_SIZE, + layoutOptions={"borderOffset": -4 * PORT_PADDING}, + ) diff --git a/capellambse_context_diagrams/collectors/portless.py b/capellambse_context_diagrams/collectors/portless.py index 3d3b1c45..ab876676 100644 --- a/capellambse_context_diagrams/collectors/portless.py +++ b/capellambse_context_diagrams/collectors/portless.py @@ -30,7 +30,7 @@ def collector( via ports/connectors). """ data = generic.collector(diagram, no_symbol=True) - centerbox = data["children"][0] + centerbox = data.children[0] connections = list(get_exchanges(diagram.target)) for ex in connections: try: @@ -42,16 +42,16 @@ def collector( continue contexts = context_collector(connections, diagram.target) - global_boxes = {centerbox["id"]: centerbox} - made_boxes = {centerbox["id"]: centerbox} + global_boxes = {centerbox.id: centerbox} + made_boxes = {centerbox.id: centerbox} 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, layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, ) - box["children"] = [centerbox] - del data["children"][0] + box.children = [centerbox] + del data.children[0] global_boxes[diagram.target.owner.uuid] = box made_boxes[diagram.target.owner.uuid] = box @@ -73,7 +73,7 @@ def collector( if box := global_boxes.get(i.uuid): # type: ignore[assignment] if box is centerbox: continue - box["height"] = height + box.height = height else: box = makers.make_box( i, @@ -92,31 +92,27 @@ def collector( global_boxes[i.owner.uuid] = parent_box made_boxes[i.owner.uuid] = parent_box - parent_box.setdefault("children", []).append( - global_boxes.pop(i.uuid) - ) - for label in parent_box["labels"]: - label["layoutOptions"] = makers.DEFAULT_LABEL_LAYOUT_OPTIONS + parent_box.children.append(global_boxes.pop(i.uuid)) + for label in parent_box.labels: + label.layoutOptions = makers.DEFAULT_LABEL_LAYOUT_OPTIONS stack_heights[side] += makers.NEIGHBOR_VMARGIN + height - del global_boxes[centerbox["id"]] - data["children"].extend(global_boxes.values()) + del global_boxes[centerbox.id] + data.children.extend(global_boxes.values()) if diagram.display_parent_relation: owner_boxes: dict[str, _elkjs.ELKInputChild] = { - uuid: box - for uuid, box in made_boxes.items() - if box.get("children") + uuid: box for uuid, box in made_boxes.items() if box.children } generic.move_parent_boxes_to_owner(owner_boxes, diagram.target, data) generic.move_edges(owner_boxes, connections, data) - centerbox["height"] = max(centerbox["height"], *stack_heights.values()) + centerbox.height = max(centerbox.height, *stack_heights.values()) if not diagram.display_symbols_as_boxes and makers.is_symbol( diagram.target ): - data["layoutOptions"]["spacing.labelNode"] = 5.0 + data.layoutOptions["spacing.labelNode"] = 5.0 return data diff --git a/capellambse_context_diagrams/collectors/realization_view.py b/capellambse_context_diagrams/collectors/realization_view.py index 918687ae..a6bdc0d5 100644 --- a/capellambse_context_diagrams/collectors/realization_view.py +++ b/capellambse_context_diagrams/collectors/realization_view.py @@ -29,7 +29,7 @@ def collector( ) layout_options["elk.contentAlignment"] = "V_CENTER H_CENTER" del layout_options["widthApproximation.targetWidth"] - data["layoutOptions"] = layout_options + data.layoutOptions = layout_options _collector = COLLECTORS[params.get("search_direction", "ALL")] lay_to_els = _collector(diagram.target, params.get("depth", 1)) layer_layout_options: _elkjs.LayoutOptions = layout_options | { # type: ignore[operator] @@ -42,23 +42,23 @@ def collector( labels = makers.make_label(layer) width, height = makers.calculate_height_and_width(labels) - layer_box: _elkjs.ELKInputChild = { - "id": elements[0]["layer"].uuid, - "children": [], - "height": width, - "width": height, - "layoutOptions": layer_layout_options, - } + layer_box = _elkjs.ELKInputChild( + id=elements[0]["layer"].uuid, + children=[], + height=width, + width=height, + layoutOptions=layer_layout_options, + ) children: dict[str, _elkjs.ELKInputChild] = {} for elt in elements: assert elt["element"] is not None if elt["origin"] is not None: edges.append( - { - "id": f'{elt["origin"].uuid}_{elt["element"].uuid}', - "sources": [elt["origin"].uuid], - "targets": [elt["element"].uuid], - } + _elkjs.ELKInputEdge( + id=f'{elt["origin"].uuid}_{elt["element"].uuid}', + sources=[elt["origin"].uuid], + targets=[elt["element"].uuid], + ) ) if elt.get("reverse", False): @@ -71,8 +71,8 @@ def collector( if not (element_box := children.get(target.uuid)): element_box = makers.make_box(target, no_symbol=True) children[target.uuid] = element_box - layer_box["children"].append(element_box) - index = len(layer_box["children"]) - 1 + layer_box.children.append(element_box) + index = len(layer_box.children) - 1 if params.get("show_owners"): owner = target.owner @@ -85,15 +85,15 @@ def collector( no_symbol=True, layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, ) - owner_box["height"] += element_box["height"] + owner_box.height += element_box.height children[owner.uuid] = owner_box - layer_box["children"].append(owner_box) + layer_box.children.append(owner_box) - del layer_box["children"][index] - owner_box.setdefault("children", []).append(element_box) - owner_box["width"] += element_box["width"] - for label in owner_box["labels"]: - label["layoutOptions"].update( + del layer_box.children[index] + owner_box.children.append(element_box) + owner_box.width += element_box.width + for label in owner_box.labels: + label.layoutOptions.update( makers.DEFAULT_LABEL_LAYOUT_OPTIONS ) @@ -104,14 +104,14 @@ def collector( ): eid = f"{source.owner.uuid}_{owner.uuid}" edges.append( - { - "id": eid, - "sources": [source.owner.uuid], - "targets": [owner.uuid], - } + _elkjs.ELKInputEdge( + id=eid, + sources=[source.owner.uuid], + targets=[owner.uuid], + ) ) - data["children"].append(layer_box) + data.children.append(layer_box) return data, edges diff --git a/capellambse_context_diagrams/collectors/tree_view.py b/capellambse_context_diagrams/collectors/tree_view.py index d9d1698c..87638c38 100644 --- a/capellambse_context_diagrams/collectors/tree_view.py +++ b/capellambse_context_diagrams/collectors/tree_view.py @@ -36,7 +36,7 @@ def __init__( all_associations: cabc.Iterable[information.Association], ) -> None: self.data = data - self.made_boxes: set[str] = {data["children"][0]["id"]} + self.made_boxes: set[str] = {data.children[0].id} self.made_edges: set[str] = set() self.data_types: set[str] = set() self.legend_boxes: list[_elkjs.ELKInputChild] = [] @@ -44,8 +44,8 @@ def __init__( self.edge_counter = 0 def __contains__(self, uuid: str) -> bool: - objects = self.data["children"] + self.data["edges"] # type: ignore[operator] - return uuid in {obj["id"] for obj in objects} + objects = self.data.children + self.data.edges # type: ignore[operator] + return uuid in {obj.id for obj in objects} def process_class(self, cls: ClassInfo, params: dict[str, t.Any]): self._process_box(cls.source, cls.partition, params) @@ -72,13 +72,13 @@ def process_class(self, cls: ClassInfo, params: dict[str, t.Any]): if start != "1" or end != "1": text = f"[{start}..{end}] {text}" - self.data["edges"].append( - { - "id": edge_id, - "sources": [cls.source.uuid], - "targets": [cls.target.uuid], - "labels": makers.make_label(text), - } + self.data.edges.append( + _elkjs.ELKInputEdge( + id=edge_id, + sources=[cls.source.uuid], + targets=[cls.target.uuid], + labels=makers.make_label(text), + ) ) if cls.generalizes: @@ -88,12 +88,12 @@ def process_class(self, cls: ClassInfo, params: dict[str, t.Any]): ) if edge.uuid not in self.made_edges: self.made_edges.add(edge.uuid) - self.data["edges"].append( - { - "id": edge.uuid, - "sources": [cls.source.uuid], - "targets": [cls.generalizes.uuid], - } + self.data.edges.append( + _elkjs.ELKInputEdge( + id=edge.uuid, + sources=[cls.source.uuid], + targets=[cls.generalizes.uuid], + ) ) def _process_box( @@ -112,7 +112,7 @@ def _make_box( ) self._set_data_types_and_labels(box, obj) _set_partitioning(box, partition, params) - self.data["children"].append(box) + self.data.children.append(box) return box def _set_data_types_and_labels( @@ -121,12 +121,12 @@ def _set_data_types_and_labels( properties, legends = _get_all_non_edge_properties( target, self.data_types ) - box["labels"].extend(properties) - box["width"], box["height"] = makers.calculate_height_and_width( - list(box["labels"]) + box.labels.extend(properties) + box.width, box.height = makers.calculate_height_and_width( + list(box.labels) ) for legend in legends: - if legend["id"] not in self: + if legend.id not in self: self.legend_boxes.append(legend) @@ -136,7 +136,7 @@ def collector( """Return the class tree data for ELK.""" assert isinstance(diagram.target, information.Class) data = generic.collector(diagram, no_symbol=True) - data["children"][0]["labels"][0].setdefault("layoutOptions", {}).update( + data.children[0].labels[0].layoutOptions.update( makers.DEFAULT_LABEL_LAYOUT_OPTIONS ) all_associations: cabc.Iterable[information.Association] = ( @@ -144,7 +144,7 @@ def collector( ) _set_layout_options(data, params) processor = ClassProcessor(data, all_associations) - processor._set_data_types_and_labels(data["children"][0], diagram.target) + processor._set_data_types_and_labels(data.children[0], diagram.target) for _, cls in get_all_classes( diagram.target, max_partition=params.get("depth"), @@ -154,8 +154,8 @@ def collector( processor.process_class(cls, params) legend = makers.make_diagram(diagram) - legend["layoutOptions"] = copy.deepcopy(_elkjs.RECT_PACKING_LAYOUT_OPTIONS) # type: ignore[arg-type] - legend["children"] = processor.legend_boxes + legend.layoutOptions = copy.deepcopy(_elkjs.RECT_PACKING_LAYOUT_OPTIONS) # type: ignore[arg-type] + legend.children = processor.legend_boxes return data, legend @@ -165,14 +165,16 @@ def _set_layout_options( options = { k: v for k, v in params.items() if k not in ("depth", "super", "sub") } - data["layoutOptions"] = {**DEFAULT_LAYOUT_OPTIONS, **options} - _set_partitioning(data["children"][0], 0, params) + data.layoutOptions = {**DEFAULT_LAYOUT_OPTIONS, **options} + _set_partitioning(data.children[0], 0, params) -def _set_partitioning(box, partition: int, params: dict[str, t.Any]) -> None: +def _set_partitioning( + box: _elkjs.ELKInputChild, partition: int, params: dict[str, t.Any] +) -> None: if params.get("partitioning", False): - box["layoutOptions"] = {} - box["layoutOptions"]["elk.partitioning.partition"] = partition + box.layoutOptions = {} + box.layoutOptions["elk.partitioning.partition"] = partition @dataclasses.dataclass @@ -357,13 +359,13 @@ def _get_all_non_edge_properties( obj: information.Class, data_types: set[str] ) -> tuple[list[_elkjs.ELKInputLabel], list[_elkjs.ELKInputChild]]: layout_options = DATA_TYPE_LABEL_LAYOUT_OPTIONS - properties: list[_elkjs.ELKInputLabel] = [ - { - "text": "", - "layoutOptions": makers.DEFAULT_LABEL_LAYOUT_OPTIONS, - "width": 0, - "height": 0, - } + properties = [ + _elkjs.ELKInputLabel( + text="", + layoutOptions=makers.DEFAULT_LABEL_LAYOUT_OPTIONS, + width=0, + height=0, + ) ] legends: list[_elkjs.ELKInputChild] = [] for prop in obj.properties: @@ -396,8 +398,8 @@ def _get_all_non_edge_properties( label_getter=_get_legend_labels, max_label_width=math.inf, ) - legend["layoutOptions"] = {} - legend["layoutOptions"]["nodeSize.constraints"] = "NODE_LABELS" + legend.layoutOptions = {} + legend.layoutOptions["nodeSize.constraints"] = "NODE_LABELS" legends.append(legend) return properties, legends diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index a5072bdc..4dcf9428 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -455,10 +455,10 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: width, height = class_diagram.viewport.size axis: t.Literal["x", "y"] if params["elk.direction"] in {"DOWN", "UP"}: - legend["layoutOptions"]["aspectRatio"] = width / height + legend.layoutOptions["aspectRatio"] = width / height axis = "x" else: - legend["layoutOptions"]["aspectRatio"] = width + legend.layoutOptions["aspectRatio"] = width axis = "y" params["elkdata"] = legend params["is_legend"] = True @@ -470,9 +470,9 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: def add_context(data: _elkjs.ELKOutputData, is_legend: bool = False) -> None: """Add all connected nodes as context to all elements.""" if is_legend: - for child in data["children"]: - if child["type"] == "node": - child["context"] = [child["id"]] # type: ignore[typeddict-unknown-key] + for child in data.children: + if child.type == "node": + child.context = [child.id] return ids: set[str] = set() @@ -485,27 +485,27 @@ def get_ids( | _elkjs.ELKOutputEdge ), ) -> None: - if obj["id"] and not obj["id"].startswith("g_"): - ids.add(obj["id"]) - for child in obj.get("children", []): - if child["type"] in {"node", "port", "junction", "edge"}: - assert child["type"] != "label" + if obj.id and not obj.id.startswith("g_"): + ids.add(obj.id) + for child in getattr(obj, "children", []): + if child.type in {"node", "port", "junction", "edge"}: + assert child.type != "label" get_ids(child) def set_ids( obj: _elkjs.ELKOutputChild, ids: set[str], ) -> None: - obj["context"] = list(ids) # type: ignore[typeddict-unknown-key] - for child in obj.get("children", []): # type: ignore[attr-defined] + obj.context = list(ids) + for child in getattr(obj, "children", []): set_ids(child, ids) - for child in data["children"]: - if child["type"] in {"node", "port", "junction", "edge"}: - assert child["type"] != "label" + for child in data.children: + if child.type in {"node", "port", "junction", "edge"}: + assert child.type != "label" get_ids(child) - for child in data["children"]: + for child in data.children: set_ids(child, ids) @@ -541,15 +541,15 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: adjust_layer_sizing(data, layout, params["layer_sizing"]) layout = try_to_layout(data) for edge in edges: - layout["children"].append( - { - "id": edge["id"], - "type": "edge", - "sourceId": edge["sources"][0], - "targetId": edge["targets"][0], - "routingPoints": [], - "styleclass": "Realization", - } # type: ignore[arg-type] + layout.children.append( + _elkjs.ELKOutputEdge( + id=edge.id, + type="edge", + sourceId=edge.sources[0], + targetId=edge.targets[0], + routingPoints=[], + styleclass="Realization", + ) ) self._add_layer_labels(layout) return self.serializer.make_diagram( @@ -558,29 +558,29 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: ) def _add_layer_labels(self, layout: _elkjs.ELKOutputData) -> None: - for layer in layout["children"]: - if layer["type"] != "node": + for layer in layout.children: + if layer.type != "node": continue - layer_obj = self.serializer.model.by_uuid(layer["id"]) + layer_obj = self.serializer.model.by_uuid(layer.id) _, layer_name = realization_view.find_layer(layer_obj) - pos = layer["position"]["x"], layer["position"]["y"] - size = layer["size"]["width"], layer["size"]["height"] + pos = layer.position.x, layer.position.y + size = layer.size.width, layer.size.height width, height = helpers.get_text_extent(layer_name) x, y, tspan_y = calculate_label_position(*pos, *size) - label_box: _elkjs.ELKOutputChild = { - "type": "label", - "id": "None", - "text": layer_name, - "position": {"x": x, "y": y}, - "size": {"width": width, "height": height}, - "style": { + label_box = _elkjs.ELKOutputLabel( + type="label", + id="None", + text=layer_name, + position=_elkjs.ELKPoint(x=x, y=y), + size=_elkjs.ELKSize(width=width, height=height), + style={ "text_transform": f"rotate(-90, {x}, {y}) {tspan_y}", "text_fill": "grey", }, - } - layer["children"].insert(0, label_box) - layer["style"] = {"stroke": "grey", "rx": 5, "ry": 5} + ) + layer.children.insert(0, label_box) + layer.style = {"stroke": "grey", "rx": 5, "ry": 5} class DataFlowViewDiagram(ContextDiagram): @@ -621,7 +621,7 @@ def adjust_layer_sizing( """Set `nodeSize.minimum` config in the layoutOptions.""" def calculate_min(key: t.Literal["width", "height"] = "width") -> float: - return max(child["size"][key] for child in layout["children"]) # type: ignore[typeddict-item] + return max(getattr(child.size, key) for child in layout.children) # type: ignore[union-attr] if layer_sizing not in {"UNION", "WIDTH", "HEIGHT", "INDIVIDUAL"}: raise NotImplementedError( @@ -632,8 +632,8 @@ def calculate_min(key: t.Literal["width", "height"] = "width") -> float: min_h = ( calculate_min("height") if layer_sizing in {"UNION", "HEIGHT"} else 0 ) - for layer in data["children"]: - layer["layoutOptions"]["nodeSize.minimum"] = f"({min_w},{min_h})" + for layer in data.children: + layer.layoutOptions["nodeSize.minimum"] = f"({min_w},{min_h})" def stack_diagrams( diff --git a/capellambse_context_diagrams/serializers.py b/capellambse_context_diagrams/serializers.py index 25056a27..8b5e5e0f 100644 --- a/capellambse_context_diagrams/serializers.py +++ b/capellambse_context_diagrams/serializers.py @@ -38,7 +38,7 @@ diagram.Box | diagram.Edge | None, ] -REMAP_STYLECLASS: dict[str, str] = {"Unset": "Association"} +REMAP_STYLECLASS: dict[t.Any, str | None] = {"Unset": "Association"} class DiagramSerializer: @@ -85,7 +85,7 @@ def make_diagram( styleclass=self._diagram.styleclass, params=kwargs, ) - for child in data["children"]: + for child in data.children: self.deserialize_child(child, diagram.Vector2D(), None) for edge, ref, parent in self._edges.values(): @@ -125,24 +125,24 @@ class type that stores all previously named classes. uuid: str styleclass: str | None derived = False - if child["id"].startswith("__"): - if ":" in child["id"]: - styleclass, uuid = child["id"][2:].split(":", 1) + if child.id.startswith("__"): + if ":" in child.id: + styleclass, uuid = child.id[2:].split(":", 1) else: - styleclass = uuid = child["id"][2:] + styleclass = uuid = child.id[2:] if styleclass.startswith("Derived-"): styleclass = styleclass.removeprefix("Derived-") derived = True else: - styleclass = self.get_styleclass(child["id"]) - uuid = child["id"] + styleclass = self.get_styleclass(child.id) + uuid = child.id styleoverrides = self.get_styleoverrides(uuid, child, derived=derived) element: diagram.Box | diagram.Edge | diagram.Circle - if child["type"] in {"node", "port"}: + if child.type in {"node", "port"}: assert parent is None or isinstance(parent, diagram.Box) has_symbol_cls = makers.is_symbol(styleclass) - is_port = child["type"] == "port" + is_port = child.type == "port" box_type = ("box", "symbol")[ is_port or has_symbol_cls @@ -150,11 +150,13 @@ class type that stores all previously named classes. and not self._diagram.display_symbols_as_boxes ] - ref += (child["position"]["x"], child["position"]["y"]) # type: ignore - size = (child["size"]["width"], child["size"]["height"]) # type: ignore + assert not isinstance(child, _elkjs.ELKOutputEdge) + ref += (child.position.x, child.position.y) + size = (child.size.width, child.size.height) features = [] if styleclass in decorations.needs_feature_line: - features = handle_features(child) # type: ignore[arg-type] + assert isinstance(child, _elkjs.ELKOutputNode) + features = handle_features(child) element = diagram.Box( ref, @@ -165,28 +167,27 @@ class type that stores all previously named classes. styleclass=styleclass, styleoverrides=styleoverrides, features=features, - context=child.get("context"), + context=getattr(child, "context", {}), ) element.JSON_TYPE = box_type self.diagram.add_element(element) self._cache[uuid] = element - elif child["type"] == "edge": - styleclass = child.get("styleclass", styleclass) # type: ignore[assignment] - styleclass = REMAP_STYLECLASS.get(styleclass, styleclass) # type: ignore[arg-type] + elif child.type == "edge": + styleclass = getattr(child, "styleClass", styleclass) + styleclass = REMAP_STYLECLASS.get(styleclass, styleclass) EDGE_HANDLER.get(styleclass, lambda c: c)(child) - source_id = child["sourceId"] + source_id = child.sourceId if source_id.startswith("__"): source_id = source_id[2:].split(":", 1)[-1] - target_id = child["targetId"] + target_id = child.targetId if target_id.startswith("__"): target_id = target_id[2:].split(":", 1)[-1] - if child["routingPoints"]: + if child.routingPoints: refpoints = [ - ref + (point["x"], point["y"]) - for point in child["routingPoints"] + ref + (point.x, point.y) for point in child.routingPoints ] else: source = self._cache[source_id] @@ -195,16 +196,16 @@ class type that stores all previously named classes. element = diagram.Edge( refpoints, - uuid=child["id"], + uuid=child.id, source=self.diagram[source_id], target=self.diagram[target_id], styleclass=styleclass, styleoverrides=styleoverrides, - context=child.get("context"), + context=getattr(child, "context", {}), ) self.diagram.add_element(element) self._cache[uuid] = element - elif child["type"] == "label": + elif child.type == "label": assert parent is not None if not parent.port: if parent.JSON_TYPE != "symbol": @@ -217,30 +218,29 @@ class type that stores all previously named classes. if labels := getattr(parent, attr_name): label_box = labels[-1] - label_box.label += " " + child["text"] + label_box.label += " " + child.text label_box.size = diagram.Vector2D( - max(label_box.size.x, child["size"]["width"]), - label_box.size.y + child["size"]["height"], + max(label_box.size.x, child.size.width), + label_box.size.y + child.size.height, ) label_box.pos = diagram.Vector2D( - min(label_box.pos.x, ref.x + child["position"]["x"]), + min(label_box.pos.x, ref.x + child.position.x), label_box.pos.y, ) else: labels.append( diagram.Box( - ref - + (child["position"]["x"], child["position"]["y"]), - (child["size"]["width"], child["size"]["height"]), - label=child["text"], + ref + (child.position.x, child.position.y), + (child.size.width, child.size.height), + label=child.text, styleoverrides=styleoverrides, ) ) element = parent - elif child["type"] == "junction": - uuid = child["id"].rsplit("_", maxsplit=1)[0] - pos = diagram.Vector2D(**child["position"]) + elif child.type == "junction": + uuid = child.id.rsplit("_", maxsplit=1)[0] + pos = diagram.Vector2D(**child.position) if self._is_hierarchical(uuid): # FIXME should this use `parent` instead? pos += self.diagram[self._diagram.target.uuid].pos @@ -248,19 +248,19 @@ class type that stores all previously named classes. element = diagram.Circle( ref + pos, 5, - uuid=child["id"], + uuid=child.id, styleclass=self.get_styleclass(uuid), styleoverrides=styleoverrides, - context=child.get("context"), + context=getattr(child, "context", {}), ) self.diagram.add_element(element) else: - logger.warning("Received unknown type %s", child["type"]) + logger.warning("Received unknown type %s", child.type) return - for i in child.get("children", []): # type: ignore - if i["type"] == "edge": - self._edges.setdefault(i["id"], (i, ref, parent)) + for i in getattr(child, "children", []): + if i.type == "edge": + self._edges.setdefault(i.id, (i, ref, parent)) else: self.deserialize_child(i, ref, element) @@ -297,10 +297,10 @@ def get_styleoverrides( from a given [`_elkjs.ELKOutputChild`][capellambse_context_diagrams._elkjs.ELKOutputChild]. """ - style_condition = self._diagram.render_styles.get(child["type"]) + style_condition = self._diagram.render_styles.get(child.type) styleoverrides: dict[str, t.Any] = {} if style_condition is not None: - if child["type"] != "junction": + if child.type != "junction": obj = self._diagram._model.by_uuid(uuid) else: obj = None @@ -314,7 +314,7 @@ def get_styleoverrides( styleoverrides["stroke-dasharray"] = "4" style: dict[str, t.Any] - if style := child.get("style", {}): + if style := child.style: styleoverrides |= style return styleoverrides @@ -341,13 +341,13 @@ def order_children(self) -> None: def handle_features(child: _elkjs.ELKOutputNode) -> list[str]: """Return all consecutive labels (without first) from the ``child``.""" features: list[str] = [] - if len(child["children"]) <= 1: + if len(child.children) <= 1: return features - all_labels = [i for i in child["children"] if i["type"] == "label"] - labels = list(itertools.takewhile(lambda i: i["text"], all_labels)) - features = [i["text"] for i in all_labels[len(labels) + 1 :]] - child["children"] = labels # type: ignore[typeddict-item] + all_labels = [i for i in child.children if i.type == "label"] + labels = list(itertools.takewhile(lambda i: i.text, all_labels)) + features = [i.text for i in all_labels[len(labels) + 1 :]] + child.children = labels # type: ignore[assignment] return features @@ -373,13 +373,13 @@ def route_shortest_connection( def reverse_edge_refpoints(child: _elkjs.ELKOutputEdge) -> None: - source = child["sourceId"] - target = child["targetId"] - child["targetId"] = source - child["sourceId"] = target - child["routingPoints"] = child["routingPoints"][::-1] + source = child.sourceId + target = child.targetId + child.targetId = source + child.sourceId = target + child.routingPoints = child.routingPoints[::-1] -EDGE_HANDLER: dict[str, cabc.Callable[[_elkjs.ELKOutputEdge], None]] = { +EDGE_HANDLER: dict[str | None, cabc.Callable[[_elkjs.ELKOutputEdge], None]] = { "Generalization": reverse_edge_refpoints } diff --git a/tests/test_capability_diagrams.py b/tests/test_capability_diagrams.py index 2449a0fd..ad8a13a2 100644 --- a/tests/test_capability_diagrams.py +++ b/tests/test_capability_diagrams.py @@ -17,5 +17,6 @@ def test_context_diagrams(model: capellambse.MelodyModel, uuid: str) -> None: assert isinstance(obj, TEST_TYPES), "Precondition failed" diag = obj.context_diagram + diag.render("svgdiagram").save(pretty=True) assert diag.nodes