Skip to content

Commit

Permalink
Treat value as one of fields.
Browse files Browse the repository at this point in the history
YooSunYoung committed Dec 11, 2024
1 parent 3507b6e commit 27b7b8a
Showing 3 changed files with 61 additions and 37 deletions.
34 changes: 25 additions & 9 deletions src/ess/reduce/widgets/_base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
import warnings
from collections.abc import Iterable
from typing import Any, Protocol, runtime_checkable

from ipywidgets import Widget
@@ -13,18 +14,22 @@ def set_fields(self, new_values: dict[str, Any]) -> None: ...
def get_fields(self) -> dict[str, Any]: ...


def _warn_invalid_field(invalid_fields: Iterable[str]) -> None:
for field_name in invalid_fields:
warning_msg = f"Cannot set field '{field_name}'."
" The field does not exist in the widget."
"The field value will be ignored."
warnings.warn(warning_msg, UserWarning, stacklevel=2)


class WidgetWithFieldsMixin:
def set_fields(self, new_values: dict[str, Any]) -> None:
# Extract valid fields
new_field_names = set(new_values.keys())
valid_field_names = new_field_names & set(self.fields.keys())
# Warn for invalid fields
invalid_field_names = new_field_names - valid_field_names
for field_name in invalid_field_names:
warning_msg = f"Cannot set field '{field_name}'."
" The field does not exist in the widget."
"The field value will be ignored."
warnings.warn(warning_msg, UserWarning, stacklevel=1)
_warn_invalid_field(invalid_field_names)
# Set valid fields
for field_name in valid_field_names:
self.fields[field_name].value = new_values[field_name]
@@ -36,12 +41,16 @@ def get_fields(self) -> dict[str, Any]:
}


def set_fields(widget: Widget, new_values: Any) -> None:
def set_fields(widget: Widget, new_values: dict[str, Any]) -> None:
if isinstance(widget, WidgetWithFieldsProtocol) and isinstance(new_values, dict):
widget.set_fields(new_values)
else:
try:
widget.value = new_values
# Use value property setter if ``new_values`` contains 'value'
if 'value' in new_values:
widget.value = new_values['value']
# Warn if there is any other fields in new_values
_warn_invalid_field(set(new_values.keys()) - {'value'})
except AttributeError as error:
# Checking if the widget value property has a setter in advance, i.e.
# ```python
@@ -63,7 +72,14 @@ def set_fields(widget: Widget, new_values: Any) -> None:
)


def get_fields(widget: Widget) -> Any:
def get_fields(widget: Widget) -> dict[str, Any] | None:
if isinstance(widget, WidgetWithFieldsProtocol):
return widget.get_fields()
return widget.value
try:
return {'value': widget.value}
except AttributeError:
warnings.warn(
f"Cannot get value or fields for widget of type {type(widget)}.",
UserWarning,
stacklevel=1,
)
38 changes: 20 additions & 18 deletions src/ess/reduce/widgets/_optional_widget.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@

from ipywidgets import HTML, HBox, Layout, RadioButtons, Widget

from ._base import WidgetWithFieldsProtocol
from ._base import get_fields, set_fields
from ._config import default_style


@@ -66,24 +66,26 @@ def value(self, value: Any) -> None:
self._option_box.value = self.name
self.wrapped.value = value

def set_fields(self, new_values: Any) -> None:
def set_fields(self, new_values: dict[str, Any]) -> None:
new_values = dict(new_values)
# Set the value of the option box
if new_values is not None:
self._option_box.value = self.name
else:
self._option_box.value = None
opted_out_flag = (
new_values.pop( # We assume ``opted-out`` is not used in any wrapped widget
'opted-out',
self._option_box.value is None,
)
)
if not isinstance(opted_out_flag, bool):
raise ValueError(
f"Invalid value for 'opted-out' field: {opted_out_flag}."
" The value should be a boolean."
)
self._option_box.value = None if opted_out_flag else self.name
# Set the value of the wrapped widget
if isinstance(self.wrapped, WidgetWithFieldsProtocol):
self.wrapped.set_fields(new_values)
elif new_values is not None:
self.wrapped.value = new_values
else:
... # Do not set the value of the wrapped widget
set_fields(self.wrapped, new_values)

def get_fields(self) -> dict[str, Any] | None:
if self._option_box.value is None:
return None
elif isinstance(self.wrapped, WidgetWithFieldsProtocol):
return self.wrapped.get_fields()
else:
return self.wrapped.value
return {
**(get_fields(self.wrapped) or {}),
'opted-out': self._option_box.value is None,
}
26 changes: 16 additions & 10 deletions tests/widget_test.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
)
from ess.reduce.ui import WorkflowWidget, workflow_widget
from ess.reduce.widgets import OptionalWidget, SwitchWidget, create_parameter_widget
from ess.reduce.widgets._base import WidgetWithFieldsProtocol
from ess.reduce.widgets._base import WidgetWithFieldsProtocol, get_fields, set_fields
from ess.reduce.workflow import register_workflow, workflow_registry

SwitchableInt = NewType('SwitchableInt', int)
@@ -173,17 +173,19 @@ def test_switchable_optional_parameter_switchable_first() -> None:


def test_optional_widget_set_value_get_fields() -> None:
optional_param = Parameter('a', 'a', 1, optional=True)
optional_param = IntParam('a', 'a', 1, optional=True)
optional_widget = create_parameter_widget(optional_param)
assert isinstance(optional_widget, WidgetWithFieldsProtocol)
assert isinstance(optional_widget, OptionalWidget)
# Check initial state
assert optional_widget._option_box.value is None
assert optional_widget.get_fields() is None
# Update the value of the wrapped widget
optional_widget.value = 'test'
# Check the fields
assert optional_widget.get_fields() == 'test'
assert get_fields(optional_widget) == {'opted-out': True, 'value': 1}
# Update the value of the wrapped widget and check the fields
set_fields(optional_widget, {'value': 2})
assert optional_widget.value is None # Opted-out is not changed
assert get_fields(optional_widget) == {'opted-out': True, 'value': 2}
optional_widget.value = 3
assert get_fields(optional_widget) == {'opted-out': False, 'value': 3}


def test_optional_widget_set_fields_get_fields() -> None:
@@ -195,12 +197,16 @@ def test_optional_widget_set_fields_get_fields() -> None:
assert isinstance(optional_widget, OptionalWidget)
# Check initial state
assert optional_widget._option_box.value is None
assert optional_widget.get_fields() is None
expected = {'opted-out': True, 'x': 1, 'y': 2, 'z': 3, 'unit': 'm'}
assert optional_widget.get_fields() == expected
# Update the value of the wrapped widget
optional_widget.set_fields({'x': 4, 'y': 5, 'z': 6, 'unit': 'm'})
optional_widget.set_fields({'opted-out': True, 'x': 4, 'y': 5, 'z': 6, 'unit': 'm'})
assert optional_widget.value is None # Opted-out is not changed
optional_widget.set_fields({'opted-out': False})
assert optional_widget.value == sc.vector([4, 5, 6], unit='m')
# Check the fields and the option box value
assert optional_widget.get_fields() == {'x': 4, 'y': 5, 'z': 6, 'unit': 'm'}
expected = {'opted-out': False, 'x': 4, 'y': 5, 'z': 6, 'unit': 'm'}
assert optional_widget.get_fields() == expected
assert optional_widget._option_box.value == optional_param.name


0 comments on commit 27b7b8a

Please sign in to comment.