From ecae9f4dd178104b86f9652f488b3191b271a18f Mon Sep 17 00:00:00 2001 From: Loic Huder Date: Mon, 14 Sep 2026 17:00:10 +0200 Subject: [PATCH 1/3] Fill in missing task input/output names from edge data mappings Co-Authored-By: Claude Sonnet 5 --- src/ewoksdraw/graph_to_svg.py | 48 +++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/ewoksdraw/graph_to_svg.py b/src/ewoksdraw/graph_to_svg.py index e91a3b6..b9402e1 100644 --- a/src/ewoksdraw/graph_to_svg.py +++ b/src/ewoksdraw/graph_to_svg.py @@ -1,3 +1,5 @@ +import warnings +from collections import defaultdict from pathlib import Path from ewokscore.graph import TaskGraph @@ -15,15 +17,57 @@ from .svg.svg_task_group import SvgTaskGroup +def retrieve_edge_sources_and_targets( + graph: TaskGraph, +) -> tuple[defaultdict[str, list[str]], defaultdict[str, list[str]]]: + sources: dict[str, list[str]] = defaultdict(list) + targets: dict[str, list[str]] = defaultdict(list) + for source_node_id, target_node_id, link_attrs in graph.graph.edges(data=True): + if link_attrs.get("map_all_data", False): + warnings.warn( + f"Ewoks link {source_node_id!r} -> {target_node_id!r} uses 'map_all_data', which " + "is not yet supported.", + UserWarning, + stacklevel=2, + ) + + for mapping in link_attrs.get("data_mapping", []): + source_output = mapping.get("source_output") + if source_output is None: + warnings.warn( + f"Data mapping on Ewoks link {source_node_id!r} -> {target_node_id!r} has no " + "'source_output', which is not yet supported.", + UserWarning, + stacklevel=2, + ) + continue + + sources[source_node_id].append(source_output) + targets[target_node_id].append(mapping["target_input"]) + + return sources, targets + + def build_svg_task_group(graph: TaskGraph) -> SvgTaskGroup: """Build an SVG task group from an Ewoks task graph.""" + source_outputs_per_node, target_inputs_per_node = retrieve_edge_sources_and_targets( + graph + ) + svg_tasks = {} for node_id, node_attrs in graph.graph.nodes.items(): signature = node_signature(node_id, node_attrs) + + inputs = set(node_input.name for node_input in signature.inputs) + # Add eventual missing target inputs + inputs |= set(target_inputs_per_node[node_id]) + outputs = set(node_output.name for node_output in signature.outputs) + # Add eventual missing source outputs + outputs |= set(source_outputs_per_node[node_id]) svg_tasks[node_id] = SvgTask( task_name=node_id, - input_names=[node_input.name for node_input in signature.inputs], - output_names=[node_output.name for node_output in signature.outputs], + input_names=list(inputs), + output_names=list(outputs), import_error=bool(signature.import_error), ) return SvgTaskGroup( From b3d0dec8229695774d16638117af56c851b355d2 Mon Sep 17 00:00:00 2001 From: Loic Huder Date: Mon, 14 Sep 2026 17:37:21 +0200 Subject: [PATCH 2/3] Deduplicate warning generation for edges --- src/ewoksdraw/graph_to_svg.py | 36 +----------------- src/ewoksdraw/layout/elk_converter.py | 48 ++++++++---------------- src/ewoksdraw/models.py | 8 ++++ src/ewoksdraw/utils.py | 53 +++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 67 deletions(-) create mode 100644 src/ewoksdraw/models.py create mode 100644 src/ewoksdraw/utils.py diff --git a/src/ewoksdraw/graph_to_svg.py b/src/ewoksdraw/graph_to_svg.py index b9402e1..6ef8ae4 100644 --- a/src/ewoksdraw/graph_to_svg.py +++ b/src/ewoksdraw/graph_to_svg.py @@ -1,5 +1,3 @@ -import warnings -from collections import defaultdict from pathlib import Path from ewokscore.graph import TaskGraph @@ -15,42 +13,12 @@ from .svg.svg_canvas import SvgCanvas from .svg.svg_task import SvgTask from .svg.svg_task_group import SvgTaskGroup - - -def retrieve_edge_sources_and_targets( - graph: TaskGraph, -) -> tuple[defaultdict[str, list[str]], defaultdict[str, list[str]]]: - sources: dict[str, list[str]] = defaultdict(list) - targets: dict[str, list[str]] = defaultdict(list) - for source_node_id, target_node_id, link_attrs in graph.graph.edges(data=True): - if link_attrs.get("map_all_data", False): - warnings.warn( - f"Ewoks link {source_node_id!r} -> {target_node_id!r} uses 'map_all_data', which " - "is not yet supported.", - UserWarning, - stacklevel=2, - ) - - for mapping in link_attrs.get("data_mapping", []): - source_output = mapping.get("source_output") - if source_output is None: - warnings.warn( - f"Data mapping on Ewoks link {source_node_id!r} -> {target_node_id!r} has no " - "'source_output', which is not yet supported.", - UserWarning, - stacklevel=2, - ) - continue - - sources[source_node_id].append(source_output) - targets[target_node_id].append(mapping["target_input"]) - - return sources, targets +from .utils import get_edge_sources_and_targets def build_svg_task_group(graph: TaskGraph) -> SvgTaskGroup: """Build an SVG task group from an Ewoks task graph.""" - source_outputs_per_node, target_inputs_per_node = retrieve_edge_sources_and_targets( + source_outputs_per_node, target_inputs_per_node = get_edge_sources_and_targets( graph ) diff --git a/src/ewoksdraw/layout/elk_converter.py b/src/ewoksdraw/layout/elk_converter.py index 78cf9db..4c4b2ca 100644 --- a/src/ewoksdraw/layout/elk_converter.py +++ b/src/ewoksdraw/layout/elk_converter.py @@ -1,6 +1,7 @@ -import warnings from typing import Any +from ..utils import get_valid_data_mappings + try: from typing import NotRequired except ImportError: @@ -152,38 +153,19 @@ def convert_ewoks_to_elk_graph( used_ids.add(root_id) edges: list[ElkEdgeBeforeLayout] = [] - for source, target, link_attrs in ewoks_graph.graph.edges(data=True): - if link_attrs.get("map_all_data", False): - warnings.warn( - f"Ewoks link {source!r} -> {target!r} uses 'map_all_data', which " - "is not yet supported.", - UserWarning, - stacklevel=2, - ) - - for mapping in link_attrs.get("data_mapping", []): - source_output = mapping.get("source_output") - if source_output is None: - warnings.warn( - f"Data mapping on Ewoks link {source!r} -> {target!r} has no " - "'source_output', which is not yet supported.", - UserWarning, - stacklevel=2, - ) - continue - - edge_id = _available_elk_id( - f"edge_{len(edges)}_{source}_{target}", used_ids - ) - - edges.append( - { - "id": edge_id, - "sources": [f"{source}.output.{source_output}"], - "targets": [f"{target}.input.{mapping['target_input']}"], - } - ) - used_ids.add(edge_id) + for mapping in get_valid_data_mappings(ewoks_graph): + edge_id = _available_elk_id( + f"edge_{len(edges)}_{mapping.source}_{mapping.target}", used_ids + ) + + edges.append( + { + "id": edge_id, + "sources": [f"{mapping.source}.output.{mapping.source_output}"], + "targets": [f"{mapping.target}.input.{mapping.target_input}"], + } + ) + used_ids.add(edge_id) return { "id": root_id, diff --git a/src/ewoksdraw/models.py b/src/ewoksdraw/models.py new file mode 100644 index 0000000..9c2cd3d --- /dev/null +++ b/src/ewoksdraw/models.py @@ -0,0 +1,8 @@ +from typing import NamedTuple + + +class DataMapping(NamedTuple): + source: str + target: str + source_output: str + target_input: str diff --git a/src/ewoksdraw/utils.py b/src/ewoksdraw/utils.py new file mode 100644 index 0000000..8dbe678 --- /dev/null +++ b/src/ewoksdraw/utils.py @@ -0,0 +1,53 @@ +import warnings +from collections import defaultdict + +from ewokscore.graph import TaskGraph + +from .models import DataMapping + + +def get_valid_data_mappings(ewoks_graph: TaskGraph) -> list[DataMapping]: + data_mappings: list[DataMapping] = [] + for source, target, link_attrs in ewoks_graph.graph.edges(data=True): + if link_attrs.get("map_all_data", False): + warnings.warn( + f"Ewoks link {source!r} -> {target!r} uses 'map_all_data', which " + "is not yet supported.", + UserWarning, + stacklevel=2, + ) + + for mapping in link_attrs.get("data_mapping", []): + source_output = mapping.get("source_output") + if source_output is None: + warnings.warn( + f"Data mapping on Ewoks link {source!r} -> {target!r} has no " + "'source_output', which is not yet supported.", + UserWarning, + stacklevel=2, + ) + continue + + data_mappings.append( + DataMapping( + source=source, + target=target, + source_output=source_output, + target_input=mapping["target_input"], + ) + ) + + return data_mappings + + +def get_edge_sources_and_targets( + graph: TaskGraph, +) -> tuple[dict[str, list[str]], dict[str, list[str]]]: + data_mappings = get_valid_data_mappings(graph) + sources: dict[str, list[str]] = defaultdict(list[str]) + targets: dict[str, list[str]] = defaultdict(list[str]) + for mapping in data_mappings: + sources[mapping.source].append(mapping.source_output) + targets[mapping.target].append(mapping.target_input) + + return sources, targets From 4b1732eba694e2e7287c8f89e0e50b8141b56d7c Mon Sep 17 00:00:00 2001 From: Loic Huder Date: Wed, 7 Oct 2026 17:04:19 +0200 Subject: [PATCH 3/3] Use dict instead of set to remove duplicate while keeping order --- src/ewoksdraw/graph_to_svg.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ewoksdraw/graph_to_svg.py b/src/ewoksdraw/graph_to_svg.py index 6ef8ae4..bd49bc5 100644 --- a/src/ewoksdraw/graph_to_svg.py +++ b/src/ewoksdraw/graph_to_svg.py @@ -26,16 +26,19 @@ def build_svg_task_group(graph: TaskGraph) -> SvgTaskGroup: for node_id, node_attrs in graph.graph.nodes.items(): signature = node_signature(node_id, node_attrs) - inputs = set(node_input.name for node_input in signature.inputs) + # Use dict instead of set to remove duplicate while keeping order + inputs = dict.fromkeys(node_input.name for node_input in signature.inputs) # Add eventual missing target inputs - inputs |= set(target_inputs_per_node[node_id]) - outputs = set(node_output.name for node_output in signature.outputs) + inputs.update(dict.fromkeys(target_inputs_per_node[node_id])) + + outputs = dict.fromkeys(node_output.name for node_output in signature.outputs) # Add eventual missing source outputs - outputs |= set(source_outputs_per_node[node_id]) + outputs.update(dict.fromkeys(source_outputs_per_node[node_id])) + svg_tasks[node_id] = SvgTask( task_name=node_id, - input_names=list(inputs), - output_names=list(outputs), + input_names=list(inputs.keys()), + output_names=list(outputs.keys()), import_error=bool(signature.import_error), ) return SvgTaskGroup(