Skip to content
31 changes: 31 additions & 0 deletions home/node_preview.py
Comment thread
yakutovicha marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Helpers for optional node previews."""

from __future__ import annotations

from aiida import orm

AWB_UNAVAILABLE_MESSAGE = (
"The AiiDAlab widgets are not available. Please install it with "
"`pip install aiidalab-widgets-base` to use this feature."
)


def _load_awb_viewer():
from aiidalab_widgets_base import viewer # noqa: PLC0415

return viewer


def render_unavailable_preview() -> str:
"""Return the fallback message shown when AWB is unavailable."""
return AWB_UNAVAILABLE_MESSAGE


def render_node_preview(node: orm.Node) -> object:
Comment thread
yakutovicha marked this conversation as resolved.
Outdated
"""Render a node preview using AWB when it is available."""
try:
viewer = _load_awb_viewer()
except ImportError:
return render_unavailable_preview()

return viewer(node)
Comment thread
yakutovicha marked this conversation as resolved.
Outdated
48 changes: 16 additions & 32 deletions home/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from aiida.tools.query.calculation import CalculationQueryBuilder
from IPython.display import HTML, Javascript, clear_output, display

from home.node_preview import render_node_preview


class CantRegisterCallbackError(Exception):
"""Exception raised when trying to register a callback after monitoring has started."""
Expand Down Expand Up @@ -304,22 +306,13 @@ def flatten(d, parent_key="", sep="."):

def show_selected_input(self, change=None):
"""Function that displays process inputs selected in the `inputs` Dropdown widget."""
try:
from aiidalab_widgets_base import viewer # noqa PLC0415

with self.output:
self.info.value = ""
clear_output()
if change["new"]:
selected_input = orm.load_node(change["new"])
self.info.value = f"PK: {selected_input.pk}"
display(viewer(selected_input))
except ImportError:
self.output.clear_output()
with self.output:
print(
"The AiiDAlab widgets are not available. Please install it with `pip install aiidalab-widgets-base` to use this feature."
)
with self.output:
self.info.value = ""
clear_output()
if change["new"]:
selected_input = orm.load_node(change["new"])
self.info.value = f"PK: {selected_input.pk}"
display(render_node_preview(selected_input))


class ProcessMonitor(tl.HasTraits):
Expand Down Expand Up @@ -435,22 +428,13 @@ def __init__(self, process=None, **kwargs):

def show_selected_output(self, change=None):
"""Function that displays process output selected in the `outputs` Dropdown widget."""
try:
from aiidalab_widgets_base import viewer # noqa PLC0415

with self.output:
self.info.value = ""
clear_output()
if change["new"]:
selected_output = self.process.outputs[change["new"]]
self.info.value = f"PK: {selected_output.pk}"
display(viewer(selected_output))
except ImportError:
self.output.clear_output()
with self.output:
print(
"The AiiDAlab widgets are not available. Please install it with `pip install aiidalab-widgets-base` to use this feature."
)
with self.output:
self.info.value = ""
clear_output()
if change["new"]:
selected_output = self.process.outputs[change["new"]]
self.info.value = f"PK: {selected_output.pk}"
display(render_node_preview(selected_output))


class ProcessReportWidget(ipw.HTML):
Expand Down
57 changes: 48 additions & 9 deletions tests/test_process.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
import sys
import types
import inspect

import pytest
from aiida import orm

from home import node_preview
from home import process as home_process

pytestmark = pytest.mark.usefixtures("aiida_profile_clean")


@pytest.fixture
def mock_viewer_module(monkeypatch):
"""Provide a lightweight viewer module for optional AWB integration paths."""
module = types.ModuleType("aiidalab_widgets_base")
module.viewer = lambda _: "mock-viewer"
monkeypatch.setitem(sys.modules, "aiidalab_widgets_base", module)
def capture_display(monkeypatch):
displayed = []
monkeypatch.setattr(home_process, "display", displayed.append)
return displayed


def test_process_inputs_widget(generate_calc_job_node, mock_viewer_module):
def test_render_node_preview_uses_awb_when_available(monkeypatch):
node = orm.Int(1)

monkeypatch.setattr(
node_preview, "_load_awb_viewer", lambda: lambda _: "mock-viewer"
)

assert node_preview.render_node_preview(node) == "mock-viewer"


def test_render_node_preview_returns_message_when_awb_is_unavailable(monkeypatch):
def raise_import_error():
raise ImportError

monkeypatch.setattr(node_preview, "_load_awb_viewer", raise_import_error)

assert (
node_preview.render_node_preview(orm.Int(1))
== node_preview.render_unavailable_preview()
)


def test_process_inputs_widget_uses_node_preview_adapter(
generate_calc_job_node, monkeypatch, capture_display
):
process = generate_calc_job_node(
inputs={
"parameters": orm.Int(1),
Expand All @@ -27,6 +50,8 @@ def test_process_inputs_widget(generate_calc_job_node, mock_viewer_module):
}
)

monkeypatch.setattr(home_process, "render_node_preview", lambda _: "mock-viewer")

home_process.ProcessInputsWidget()

widget = home_process.ProcessInputsWidget(process=process)
Expand All @@ -38,16 +63,30 @@ def test_process_inputs_widget(generate_calc_job_node, mock_viewer_module):
widget._inputs.value = nested_uuid
selected_input = orm.load_node(nested_uuid)
assert widget.info.value == f"PK: {selected_input.pk}"
assert capture_display == ["mock-viewer"]


def test_process_outputs_widget(multiply_add_completed_workchain, mock_viewer_module):
def test_process_outputs_widget_shows_unavailable_message(
multiply_add_completed_workchain, monkeypatch, capture_display
):
monkeypatch.setattr(
home_process,
"render_node_preview",
lambda _: node_preview.render_unavailable_preview(),
)

home_process.ProcessOutputsWidget()

widget = home_process.ProcessOutputsWidget(process=multiply_add_completed_workchain)
widget.show_selected_output(change={"new": "result"})

selected_output = multiply_add_completed_workchain.outputs["result"]
assert widget.info.value == f"PK: {selected_output.pk}"
assert capture_display == [node_preview.render_unavailable_preview()]


def test_process_module_does_not_import_awb_directly():
assert "aiidalab_widgets_base" not in inspect.getsource(home_process)
Comment thread
yakutovicha marked this conversation as resolved.
Outdated


def test_process_report_widget(multiply_add_completed_workchain):
Expand Down