From 03b4cbaf0ce49539a7470093f0a1dc73fa1d9b8a Mon Sep 17 00:00:00 2001 From: ruyer Date: Wed, 14 Oct 2026 17:12:50 +0200 Subject: [PATCH 1/2] feat[canvas]: Patch the orange-canvas to add an export and import menus as ewoks json workflow --- CHANGELOG.md | 1 + src/ewoksorange/gui/canvas/main.py | 2 + src/ewoksorange/gui/canvas/menu.py | 104 ++++++++++++++++++++++++ src/ewoksorange/tests/test_canvas_io.py | 61 ++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 src/ewoksorange/gui/canvas/menu.py create mode 100644 src/ewoksorange/tests/test_canvas_io.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ba78c673..ec9182ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Two new "Import Ewoks workflow..." and "Export Ewoks workflow..." menus in the orange main canvas. - New property `auto_execute` in `ewoksorange.gui.owwidgets.OWEwoksBaseWidget`. Allow to propagate outputs downstream without triggering execution. - Support OASYS2 (`pip install ewoksorange[oasys2]`) next to the standard `orange-canvas-core` + `orange-widget-base` and the optional Orange3. diff --git a/src/ewoksorange/gui/canvas/main.py b/src/ewoksorange/gui/canvas/main.py index 4b2b479b..24c7dc30 100644 --- a/src/ewoksorange/gui/canvas/main.py +++ b/src/ewoksorange/gui/canvas/main.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from ...orange_version import ORANGE_VERSION +from .menu import add_ewoks_import_export_actions if ORANGE_VERSION == ORANGE_VERSION.latest_oasys: from oasys2.canvas import config as _oasys_config_module @@ -98,6 +99,7 @@ def _temporary_log_handlers(log_level): def main(argv=None): + add_ewoks_import_export_actions() parser = arg_parser() parser.add_argument( diff --git a/src/ewoksorange/gui/canvas/menu.py b/src/ewoksorange/gui/canvas/menu.py new file mode 100644 index 00000000..08b9ffda --- /dev/null +++ b/src/ewoksorange/gui/canvas/menu.py @@ -0,0 +1,104 @@ +import io +import logging +import os +import tempfile +from pathlib import Path + +from AnyQt.QtWidgets import QAction +from AnyQt.QtWidgets import QFileDialog +from AnyQt.QtWidgets import QMenu +from orangecanvas.application.canvasmain import CanvasMainWindow + +from ..workflows.owscheme import ewoks_to_ows +from ..workflows.owscheme import ows_to_ewoks + +_logger = logging.getLogger(__name__) +_FILTER = "Ewoks workflow (*.json *.yml *.yaml)" +_patched = False + + +def _import_ewoks_workflow(window: CanvasMainWindow) -> None: + + from ...bindings import load_graph # Lazy import to avoid circular import + + filename, _ = QFileDialog.getOpenFileName( + window, "Import Ewoks workflow", "", _FILTER + ) + if not filename: + return + try: + graph = load_graph(filename) + with tempfile.TemporaryDirectory() as tmpdir: + ows = os.path.join(tmpdir, "imported.ows") + ewoks_to_ows(graph, ows, error_on_duplicates=False) + # Opens in this window if it is empty, else in a new one, + # without associating the temporary path with the document. + window.open_example_scheme(ows) + except Exception as e: + _logger.exception("Ewoks import failed") + window.output_view().write(f"Ewoks import failed: {e}\n") + + +def _export_ewoks_workflow(window: CanvasMainWindow) -> None: + + from ...bindings import save_graph # Lazy import to avoid circular import + + filename, _ = QFileDialog.getSaveFileName( + window, "Export Ewoks workflow", "", _FILTER + ) + if not filename: + return + if not Path(filename).suffix: + filename += ".json" + try: + scheme = window.current_document().scheme() + buffer = io.BytesIO() + scheme.save_to(buffer, pretty=True, pickle_fallback=True) + buffer.seek(0) + graph = ows_to_ewoks(buffer) + save_graph(graph, filename) + except Exception as e: + _logger.exception("Ewoks export failed") + window.output_view().write(f"Ewoks export failed: {e}\n") + + +def _add_actions(window: CanvasMainWindow) -> None: + menu = window.menuBar().findChild(QMenu, "file-menu") + if menu is None: + return + import_action = QAction( + "Import Ewoks workflow...", window, objectName="action-import-ewoks" + ) + import_action.triggered.connect(lambda: _import_ewoks_workflow(window)) + export_action = QAction( + "Export Ewoks workflow...", window, objectName="action-export-ewoks" + ) + export_action.triggered.connect(lambda: _export_ewoks_workflow(window)) + + before = menu.findChild(QAction, "open-actions-separator") + for action in (import_action, export_action): + if before is not None: + menu.insertAction(before, action) + else: + menu.addAction(action) + + +def add_ewoks_import_export_actions() -> None: + """ + Add Ewoks import/export to the File menu of every canvas window. + """ + global _patched + if _patched: + return + _patched = True + + # NOTE: There is no other way than a patch here because + # - We need a solution that works also with Oasys2 fork + # - Main code where the CanvasMainWindow is instantiated is in Orange3 or in Oasys2 and there is no public interface given. + original_init = CanvasMainWindow.__init__ + + def __init__(self, *args, **kwargs): + original_init(self, *args, **kwargs) + _add_actions(self) + + CanvasMainWindow.__init__ = __init__ diff --git a/src/ewoksorange/tests/test_canvas_io.py b/src/ewoksorange/tests/test_canvas_io.py new file mode 100644 index 00000000..750d3dfc --- /dev/null +++ b/src/ewoksorange/tests/test_canvas_io.py @@ -0,0 +1,61 @@ +import json + +from ewoksorange.gui.canvas import menu +from ewoksorange.gui.canvas.handler import OrangeCanvasHandler + + +def test_exported_json_matches_manual_workflow( + tmp_path, monkeypatch, ewoksorange_qtapp +): + destination_path = tmp_path / "exported_workflow.json" + graph = { + "nodes": [ + { + "id": "source", + "task_type": "class", + "task_identifier": "ewokscore.tests.examples.tasks.sumtask.SumTask", + }, + { + "id": "sink", + "task_type": "class", + "task_identifier": "ewokscore.tests.examples.tasks.sumtask.SumTask", + }, + ], + "links": [ + { + "source": "source", + "target": "sink", + "data_mapping": [{"source_output": "result", "target_input": "a"}], + } + ], + } + + source_path = tmp_path / "source_workflow.json" + + with open(source_path, "w") as f: + json.dump(graph, f) + + # Here i import to the canvas then re export it and verify the exported json. + + with OrangeCanvasHandler() as handler: + monkeypatch.setattr( + menu.QFileDialog, + "getOpenFileName", + lambda *args, **kwargs: (str(source_path), ""), + ) + + menu._import_ewoks_workflow(handler.canvas) + + monkeypatch.setattr( + menu.QFileDialog, + "getSaveFileName", + lambda *_args, **_kwargs: (str(destination_path), ""), + ) + menu._export_ewoks_workflow(handler.canvas) + + with open(destination_path) as f: + exported_workflow = json.load(f) + + assert exported_workflow["nodes"][0]["id"] == "source" + assert exported_workflow["nodes"][1]["id"] == "sink" + assert exported_workflow["links"] == graph["links"] From a7ef1f959935665cf2563ae9d1ab029c1cff6ea0 Mon Sep 17 00:00:00 2001 From: maxenceprog <66560010+maxenceprog@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:44:52 +0200 Subject: [PATCH 2/2] Update src/ewoksorange/gui/canvas/menu.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Loïc Huder <42204205+loichuder@users.noreply.github.com> --- src/ewoksorange/gui/canvas/menu.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ewoksorange/gui/canvas/menu.py b/src/ewoksorange/gui/canvas/menu.py index 08b9ffda..e31ba20b 100644 --- a/src/ewoksorange/gui/canvas/menu.py +++ b/src/ewoksorange/gui/canvas/menu.py @@ -92,9 +92,7 @@ def add_ewoks_import_export_actions() -> None: return _patched = True - # NOTE: There is no other way than a patch here because - # - We need a solution that works also with Oasys2 fork - # - Main code where the CanvasMainWindow is instantiated is in Orange3 or in Oasys2 and there is no public interface given. + # CanvasMainWindow is instantiated both in Orange3 or in Oasys2 and there is no public interface given. original_init = CanvasMainWindow.__init__ def __init__(self, *args, **kwargs):