Skip to content

Commit

Permalink
Merge pull request #67 from scipp/switchable-widget
Browse files Browse the repository at this point in the history
Switchable widget.
  • Loading branch information
YooSunYoung authored Aug 9, 2024
2 parents 8482131 + 67295da commit 70bf1fa
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 20 deletions.
1 change: 1 addition & 0 deletions requirements/basetest.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

pytest
numpy
ipywidgets
52 changes: 50 additions & 2 deletions requirements/basetest.txt
Original file line number Diff line number Diff line change
@@ -1,21 +1,69 @@
# SHA1:43a90d54540ba9fdbb2c21a785a9d5d7483af53b
# SHA1:8c92c02ac11274f9d1f3b5b2fa648275b8e32140
#
# This file is autogenerated by pip-compile-multi
# To update, run:
#
# pip-compile-multi
#
asttokens==2.4.1
# via stack-data
comm==0.2.2
# via ipywidgets
decorator==5.1.1
# via ipython
exceptiongroup==1.2.2
# via pytest
# via
# ipython
# pytest
executing==2.0.1
# via stack-data
iniconfig==2.0.0
# via pytest
ipython==8.26.0
# via ipywidgets
ipywidgets==8.1.3
# via -r basetest.in
jedi==0.19.1
# via ipython
jupyterlab-widgets==3.0.11
# via ipywidgets
matplotlib-inline==0.1.7
# via ipython
numpy==2.0.1
# via -r basetest.in
packaging==24.1
# via pytest
parso==0.8.4
# via jedi
pexpect==4.9.0
# via ipython
pluggy==1.5.0
# via pytest
prompt-toolkit==3.0.47
# via ipython
ptyprocess==0.7.0
# via pexpect
pure-eval==0.2.3
# via stack-data
pygments==2.18.0
# via ipython
pytest==8.3.2
# via -r basetest.in
six==1.16.0
# via asttokens
stack-data==0.6.3
# via ipython
tomli==2.0.1
# via pytest
traitlets==5.14.3
# via
# comm
# ipython
# ipywidgets
# matplotlib-inline
typing-extensions==4.12.2
# via ipython
wcwidth==0.2.13
# via prompt-toolkit
widgetsnbextension==4.0.11
# via ipywidgets
34 changes: 21 additions & 13 deletions src/ess/reduce/ui.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
from collections.abc import Callable
from typing import cast
from collections.abc import Callable, Iterable
from typing import Any, cast

import ipywidgets as widgets
import sciline
from IPython import display
from ipywidgets import Layout, TwoByTwoLayout

from .widgets import create_parameter_widget
from .widgets import SwitchWidget, create_parameter_widget, default_style
from .workflow import (
Key,
assign_parameter_values,
get_parameters,
get_possible_outputs,
get_typical_outputs,
workflow_registry,
)

_style = {
'description_width': 'auto',
'value_width': 'auto',
'button_width': 'auto',
}
workflow_select = widgets.Dropdown(
options=[(workflow.__name__, workflow) for workflow in workflow_registry],
description='Workflow:',
Expand All @@ -34,7 +30,7 @@

possible_outputs_widget = widgets.SelectMultiple(
description='Extended Outputs:',
style=_style,
style=default_style,
layout=Layout(width='80%', height='150px'),
)

Expand Down Expand Up @@ -111,16 +107,28 @@ def reset_button_clicked(b):
output = widgets.Output()


def collect_values(
parameter_box: widgets.VBox, param_keys: Iterable[Key]
) -> dict[Key, Any]:
return {
node: parameter_box.children[i].children[0].value
for i, node in enumerate(param_keys)
if (
not isinstance(
widget := parameter_box.children[i].children[0], SwitchWidget
)
)
or widget.enabled
}


def run_workflow(_: widgets.Button) -> None:
workflow_constructor = cast(Callable[[], sciline.Pipeline], workflow_select.value)
selected_workflow = workflow_constructor()
outputs = possible_outputs_widget.value + typical_outputs_widget.value
registry = get_parameters(selected_workflow, outputs)

values = {
node: parameter_box.children[i].children[0].value
for i, node in enumerate(registry.keys())
}
values = collect_values(parameter_box, registry.keys())

workflow = assign_parameter_values(selected_workflow, values)

Expand Down
22 changes: 22 additions & 0 deletions src/ess/reduce/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ._linspace_widget import LinspaceWidget
from ._vector_widget import VectorWidget
from ._bounds_widget import BoundsWidget
from ._switchable_widget import SwitchWidget


class EssWidget(Protocol):
Expand All @@ -36,6 +37,26 @@ class EssWidget(Protocol):
def value(self) -> Any: ...


from collections.abc import Callable
from functools import wraps


def switchable_widget(
func: Callable[[Parameter], widgets.Widget],
) -> Callable[[Parameter], widgets.Widget]:
"""Wrap a widget in a switchable widget."""

@wraps(func)
def wrapper(param: Parameter) -> widgets.Widget:
widget = func(param)
if param.switchable:
return SwitchWidget(widget, name=param.name)
return widget

return wrapper


@switchable_widget
@singledispatch
def create_parameter_widget(param: Parameter) -> widgets.Widget:
"""Create a widget for a parameter depending on the ``param`` type.
Expand Down Expand Up @@ -133,5 +154,6 @@ def vector_parameter_widget(param: VectorParameter):
'LinspaceWidget',
'EssWidget',
'VectorWidget',
'SwitchWidget',
'create_parameter_widget',
]
6 changes: 1 addition & 5 deletions src/ess/reduce/widgets/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,4 @@
from ipywidgets import Layout

default_layout = Layout(width='80%')
default_style = {
'description_width': 'auto',
'value_width': 'auto',
'button_width': 'auto',
}
default_style = {'description_width': 'auto'}
47 changes: 47 additions & 0 deletions src/ess/reduce/widgets/_switchable_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
from typing import Any

from ipywidgets import Checkbox, HBox, Label, Stack, Widget

from ._config import default_style


class SwitchWidget(HBox):
"""Wrapper widget to handle switchable widgets.
When you retrieve the value of this widget,
it will return the value of the wrapped widget.
It is expected not to be set in the workflow if ``enabled`` is False.
"""

def __init__(self, wrapped: Widget, name: str = '') -> None:
super().__init__()
self._enable_box = Checkbox(description='', style=default_style)
# The layout is not applied if they are set in the constructor
self._enable_box.layout.description_width = '0px'
self._enable_box.layout.width = 'auto'

self.wrapped = wrapped
wrapped_stack = Stack([Label(name, style=default_style), self.wrapped])
# We wanted to implement this by greying out the widget when disabled
# but ``disabled`` is not a common property of all widgets
wrapped_stack.selected_index = 0

def handle_checkbox(change) -> None:
wrapped_stack.selected_index = 1 if change.new else 0

self._enable_box.observe(handle_checkbox, names='value')
self.children = [self._enable_box, wrapped_stack]

@property
def enabled(self) -> bool:
return self._enable_box.value

@enabled.setter
def enabled(self, value: bool) -> None:
self._enable_box.value = value

@property
def value(self) -> Any:
return self.wrapped.value
32 changes: 32 additions & 0 deletions tests/widget_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
from ess.reduce.parameter import Parameter
from ess.reduce.widgets import SwitchWidget, create_parameter_widget


def test_switchable_widget_dispatch() -> None:
switchable_param = Parameter('a', 'a', 1, switchable=True)
assert isinstance(create_parameter_widget(switchable_param), SwitchWidget)
non_switchable_param = Parameter('b', 'b', 2, switchable=False)
assert not isinstance(create_parameter_widget(non_switchable_param), SwitchWidget)


def test_collect_values_from_disabled_switchable_widget() -> None:
from ess.reduce.ui import collect_values
from ipywidgets import Box, Text, VBox

enabled_switch_widget = SwitchWidget(Text('int'), name='int')
enabled_switch_widget.enabled = True
disabled_switch_widget = SwitchWidget(Text('float'), name='float')
disabled_switch_widget.enabled = False
non_switch_widget = Text('str')
test_box = VBox(
[
Box([enabled_switch_widget]),
Box([disabled_switch_widget]),
Box([non_switch_widget]),
]
)

values = collect_values(test_box, (int, float, str))
assert values == {int: 'int', str: 'str'}

0 comments on commit 70bf1fa

Please sign in to comment.