From 9288132e9a518030989b01e6667f097f855565e7 Mon Sep 17 00:00:00 2001 From: YooSunyoung Date: Tue, 10 Dec 2024 16:31:19 +0100 Subject: [PATCH 1/8] Optional widget fields setter/getter --- src/ess/reduce/widgets/_optional_widget.py | 23 +++++++++++++ tests/widget_test.py | 40 +++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/ess/reduce/widgets/_optional_widget.py b/src/ess/reduce/widgets/_optional_widget.py index 26f5dd7e..6a33abf2 100644 --- a/src/ess/reduce/widgets/_optional_widget.py +++ b/src/ess/reduce/widgets/_optional_widget.py @@ -4,6 +4,7 @@ from ipywidgets import HTML, HBox, Layout, RadioButtons, Widget +from ._base import WidgetWithFieldsProtocol from ._config import default_style @@ -64,3 +65,25 @@ def value(self, value: Any) -> None: else: self._option_box.value = self.name self.wrapped.value = value + + def set_fields(self, new_values: Any) -> None: + # 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 + # 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 + + 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 diff --git a/tests/widget_test.py b/tests/widget_test.py index 793c4e9b..91e5959f 100644 --- a/tests/widget_test.py +++ b/tests/widget_test.py @@ -8,9 +8,15 @@ import scipp as sc from ipywidgets import FloatText, IntText -from ess.reduce.parameter import BinEdgesParameter, Parameter, parameter_registry +from ess.reduce.parameter import ( + BinEdgesParameter, + Parameter, + Vector3dParameter, + parameter_registry, +) 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.workflow import register_workflow, workflow_registry SwitchableInt = NewType('SwitchableInt', int) @@ -166,6 +172,38 @@ def test_switchable_optional_parameter_switchable_first() -> None: assert isinstance(dummy_widget.wrapped, OptionalWidget) +def test_optional_widget_set_value_get_fields() -> None: + optional_param = Parameter('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' + + +def test_optional_widget_set_fields_get_fields() -> None: + optional_param = Vector3dParameter( + 'a', 'a', sc.vector([1, 2, 3], unit='m'), 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.set_fields({'x': 4, 'y': 5, 'z': 6, 'unit': 'm'}) + 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'} + assert optional_widget._option_box.value == optional_param.name + + def test_optional_widget_dispatch() -> None: optional_param = Parameter('a', 'a', 1, optional=True) assert isinstance(create_parameter_widget(optional_param), OptionalWidget) From 3507b6ea9227f5235bbbb938f6f30e432331212a Mon Sep 17 00:00:00 2001 From: YooSunyoung Date: Wed, 11 Dec 2024 13:21:21 +0100 Subject: [PATCH 2/8] Handle value setter with try-except instead of a condition --- src/ess/reduce/widgets/_base.py | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/ess/reduce/widgets/_base.py b/src/ess/reduce/widgets/_base.py index 3ae851c0..4224d648 100644 --- a/src/ess/reduce/widgets/_base.py +++ b/src/ess/reduce/widgets/_base.py @@ -36,25 +36,31 @@ def get_fields(self) -> dict[str, Any]: } -def _has_widget_value_setter(widget: Widget) -> bool: - widget_type = type(widget) - return ( - widget_property := getattr(widget_type, 'value', None) - ) is not None and getattr(widget_property, 'fset', None) is not None - - def set_fields(widget: Widget, new_values: Any) -> None: if isinstance(widget, WidgetWithFieldsProtocol) and isinstance(new_values, dict): widget.set_fields(new_values) - elif _has_widget_value_setter(widget): - widget.value = new_values else: - warnings.warn( - f"Cannot set value or fields for widget of type {type(widget)}." - " The new_value(s) will be ignored.", - UserWarning, - stacklevel=1, - ) + try: + widget.value = new_values + except AttributeError as error: + # Checking if the widget value property has a setter in advance, i.e. + # ```python + # (widget_property := getattr(type(widget), 'value', None)) is not None + # and getattr(widget_property, 'fset', None) is not None + # ``` + # does not work with a class that inherits Traitlets class. + # In those classes, even if a property has a setter, + # it may not have `fset` attribute. + # It is not really feasible to check all possible cases of value setters. + # Instead, we try setting the value and catch the AttributeError. + # to determine if the widget has a value setter. + warnings.warn( + f"Cannot set value for widget of type {type(widget)}." + " The new_value(s) will be ignored." + f" Setting value caused the following error: {error}", + UserWarning, + stacklevel=1, + ) def get_fields(widget: Widget) -> Any: From 7db8ba91639d14d0e5a11f5d1f74c82c7f479707 Mon Sep 17 00:00:00 2001 From: YooSunyoung Date: Wed, 11 Dec 2024 14:43:25 +0100 Subject: [PATCH 3/8] Switchable fields set/getter. --- src/ess/reduce/widgets/_switchable_widget.py | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/ess/reduce/widgets/_switchable_widget.py b/src/ess/reduce/widgets/_switchable_widget.py index 2231d16f..3aa2cd4a 100644 --- a/src/ess/reduce/widgets/_switchable_widget.py +++ b/src/ess/reduce/widgets/_switchable_widget.py @@ -4,6 +4,7 @@ from ipywidgets import Checkbox, HBox, Label, Stack, Widget +from ._base import WidgetWithFieldsProtocol, get_fields, set_fields from ._config import default_style @@ -49,3 +50,38 @@ def value(self) -> Any: @value.setter def value(self, value: Any) -> None: self.wrapped.value = value + + def set_fields(self, new_values: dict[str, Any]) -> None: + # Check if the new values are a dictionary + if not isinstance(new_values, dict): + raise ValueError(f"Expected a dictionary, got {new_values}.") + # Retrieve and set the enabled flag first + new_values = dict(new_values) + enabled_flag = new_values.pop('enabled', self.enabled) + if not isinstance(enabled_flag, bool): + raise ValueError(f"`enabled` must be a boolean, got {enabled_flag}") + self.enabled = enabled_flag + # Set fields or value of the wrapped widget + if isinstance(self.wrapped, WidgetWithFieldsProtocol): + # Expecting {'enabled': True/False, **wrapped_fields} + self.wrapped.set_fields(new_values) + elif 'value' in new_values: # Skip if 'value' is not in new_values + # i.e. User might only want to change the enabled flag + # We use ``set_fields`` to set the value of the wrapped widget + # so that it handles availability of a value setter. + # Expecting {'enabled': True/False, 'value': value} + set_fields(self.wrapped, new_values.pop('value')) + if new_values: + # Check if there are any fields left except for 'enabled' and 'value' + raise ValueError( + f"Unexpected field(s) {new_values.keys()} for widget {self.wrapped}" + ) + + def get_fields(self) -> dict[str, Any]: + wrapped_fields = get_fields(self.wrapped) + if isinstance(self.wrapped, WidgetWithFieldsProtocol) and isinstance( + wrapped_fields, dict + ): + return {'enabled': self.enabled, **wrapped_fields} + else: + return {'enabled': self.enabled, 'value': wrapped_fields} From 81fd146c71d027d1ea9c40a42b42908537d0a877 Mon Sep 17 00:00:00 2001 From: YooSunyoung Date: Wed, 11 Dec 2024 14:43:50 +0100 Subject: [PATCH 4/8] Test switchable widget. --- tests/widget_test.py | 70 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/widget_test.py b/tests/widget_test.py index 91e5959f..d88f92ac 100644 --- a/tests/widget_test.py +++ b/tests/widget_test.py @@ -440,3 +440,73 @@ def test_bin_edges_widget_with_default_values() -> None: assert param_widget.fields['stop'].value == 0.6 assert param_widget.fields['nbins'].value == 150 assert param_widget.fields['spacing'].value == 'linear' + + +def test_switchable_widget_set_values() -> None: + param = IntParam('a', 'a', 1, switchable=True) + widget = create_parameter_widget(param) + assert isinstance(widget, SwitchWidget) + assert not widget.enabled + widget.set_fields({'enabled': True}) + assert widget.enabled + widget.set_fields({'enabled': False}) + assert not widget.enabled + widget.set_fields({'enabled': True, 'value': 2}) + assert widget.enabled + assert widget.value == 2 + widget.set_fields({'enabled': False, 'value': 3}) + assert not widget.enabled + assert widget.value == 3 + widget.set_fields({'value': 4}) + assert not widget.enabled + assert widget.value == 4 + + +def test_switchable_widget_get_fields_only_value() -> None: + param = IntParam('a', 'a', 1, switchable=True) + widget = create_parameter_widget(param) + assert isinstance(widget, SwitchWidget) + assert widget.get_fields() == {'enabled': False, 'value': 1} + widget.enabled = True + assert widget.get_fields() == {'enabled': True, 'value': 1} + widget.value = 2 + assert widget.get_fields() == {'enabled': True, 'value': 2} + widget.enabled = False + assert widget.get_fields() == {'enabled': False, 'value': 2} + + +def test_switchable_widget_set_fields() -> None: + param = Vector3dParameter('a', 'a', sc.vector([1, 2, 3], unit='m'), switchable=True) + widget = create_parameter_widget(param) + assert isinstance(widget, SwitchWidget) + assert not widget.enabled + assert widget.value == sc.vector([1, 2, 3], unit='m') + widget.set_fields({'enabled': True, 'x': 4, 'y': 5, 'z': 6, 'unit': 'm'}) + assert widget.enabled + assert widget.value == sc.vector([4, 5, 6], unit='m') + widget.set_fields({'x': 7, 'y': 8}) + assert widget.enabled + assert widget.value == sc.vector([7, 8, 6], unit='m') + + +def test_switchable_widget_get_fields_sub_fields() -> None: + param = Vector3dParameter('a', 'a', sc.vector([1, 2, 3], unit='m'), switchable=True) + widget = create_parameter_widget(param) + assert isinstance(widget, SwitchWidget) + assert widget.get_fields() == { + 'enabled': False, + 'x': 1, + 'y': 2, + 'z': 3, + 'unit': 'm', + } + widget.enabled = True + assert widget.get_fields() == {'enabled': True, 'x': 1, 'y': 2, 'z': 3, 'unit': 'm'} + widget.set_fields({'enabled': False, 'x': 4, 'y': 5, 'unit': 'mm'}) + assert widget.get_fields() == { + 'enabled': False, + 'x': 4, + 'y': 5, + 'z': 3, + 'unit': 'mm', + } From 27b7b8af52e7f2f70b8994a944714cd1aede916f Mon Sep 17 00:00:00 2001 From: YooSunyoung Date: Wed, 11 Dec 2024 16:52:16 +0100 Subject: [PATCH 5/8] Treat value as one of fields. --- src/ess/reduce/widgets/_base.py | 34 ++++++++++++++----- src/ess/reduce/widgets/_optional_widget.py | 38 ++++++++++++---------- tests/widget_test.py | 26 +++++++++------ 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/ess/reduce/widgets/_base.py b/src/ess/reduce/widgets/_base.py index 4224d648..69bd6ff0 100644 --- a/src/ess/reduce/widgets/_base.py +++ b/src/ess/reduce/widgets/_base.py @@ -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,6 +14,14 @@ 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 @@ -20,11 +29,7 @@ def set_fields(self, new_values: dict[str, Any]) -> None: 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, + ) diff --git a/src/ess/reduce/widgets/_optional_widget.py b/src/ess/reduce/widgets/_optional_widget.py index 6a33abf2..81079f9f 100644 --- a/src/ess/reduce/widgets/_optional_widget.py +++ b/src/ess/reduce/widgets/_optional_widget.py @@ -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, + } diff --git a/tests/widget_test.py b/tests/widget_test.py index 91e5959f..c94d4f11 100644 --- a/tests/widget_test.py +++ b/tests/widget_test.py @@ -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 From 8970630b4bd98427e1404ff932f9b1b2fbb26a44 Mon Sep 17 00:00:00 2001 From: YooSunyoung Date: Wed, 11 Dec 2024 13:21:21 +0100 Subject: [PATCH 6/8] Handle value setter with try-except instead of a condition --- src/ess/reduce/widgets/_base.py | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/ess/reduce/widgets/_base.py b/src/ess/reduce/widgets/_base.py index 3ae851c0..4224d648 100644 --- a/src/ess/reduce/widgets/_base.py +++ b/src/ess/reduce/widgets/_base.py @@ -36,25 +36,31 @@ def get_fields(self) -> dict[str, Any]: } -def _has_widget_value_setter(widget: Widget) -> bool: - widget_type = type(widget) - return ( - widget_property := getattr(widget_type, 'value', None) - ) is not None and getattr(widget_property, 'fset', None) is not None - - def set_fields(widget: Widget, new_values: Any) -> None: if isinstance(widget, WidgetWithFieldsProtocol) and isinstance(new_values, dict): widget.set_fields(new_values) - elif _has_widget_value_setter(widget): - widget.value = new_values else: - warnings.warn( - f"Cannot set value or fields for widget of type {type(widget)}." - " The new_value(s) will be ignored.", - UserWarning, - stacklevel=1, - ) + try: + widget.value = new_values + except AttributeError as error: + # Checking if the widget value property has a setter in advance, i.e. + # ```python + # (widget_property := getattr(type(widget), 'value', None)) is not None + # and getattr(widget_property, 'fset', None) is not None + # ``` + # does not work with a class that inherits Traitlets class. + # In those classes, even if a property has a setter, + # it may not have `fset` attribute. + # It is not really feasible to check all possible cases of value setters. + # Instead, we try setting the value and catch the AttributeError. + # to determine if the widget has a value setter. + warnings.warn( + f"Cannot set value for widget of type {type(widget)}." + " The new_value(s) will be ignored." + f" Setting value caused the following error: {error}", + UserWarning, + stacklevel=1, + ) def get_fields(widget: Widget) -> Any: From 4d3efe9d346c09b46bf62a0311232b7b4d5fd627 Mon Sep 17 00:00:00 2001 From: YooSunyoung Date: Thu, 12 Dec 2024 11:09:05 +0100 Subject: [PATCH 7/8] Fix switchable widget fields setter/getter and add docstring to the set/get fields helper. --- src/ess/reduce/widgets/_base.py | 42 ++++++++++++++++++++ src/ess/reduce/widgets/_switchable_widget.py | 29 ++------------ 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/ess/reduce/widgets/_base.py b/src/ess/reduce/widgets/_base.py index 69bd6ff0..195c7eb8 100644 --- a/src/ess/reduce/widgets/_base.py +++ b/src/ess/reduce/widgets/_base.py @@ -42,6 +42,32 @@ def get_fields(self) -> dict[str, Any]: def set_fields(widget: Widget, new_values: dict[str, Any]) -> None: + """Set the fields of a widget with the given values. + + Parameters + ---------- + widget: + The widget to set the fields. It should either be an instance of + ``WidgetWithFieldsProtocol`` or have a value property setter. + new_values: + The new values to set for the fields. + i.e. ``{'field_name': field_value}`` + If the widget does not have a ``set_fields/get_fields`` method, + (e.g. it is not an instance of ``WidgetWithFieldsProtocol``), + it will try to set the value of the widget directly. + The value of the widget should be available in the ``new_values`` dictionary + with the key 'value'. + i.e. ``{'value': widget_value}`` + + Raises + ------ + TypeError: + If ``new_values`` is not a dictionary. + + """ + if not isinstance(new_values, dict): + raise TypeError(f"new_values must be a dictionary, got {type(new_values)}") + if isinstance(widget, WidgetWithFieldsProtocol) and isinstance(new_values, dict): widget.set_fields(new_values) else: @@ -73,6 +99,22 @@ def set_fields(widget: Widget, new_values: dict[str, Any]) -> None: def get_fields(widget: Widget) -> dict[str, Any] | None: + """Get the fields of a widget. + + If the widget is an instance of ``WidgetWithFieldsProtocol``, + it will return the fields of the widget. + i.e. ``{'field_name': field_value}`` + Otherwise, it will try to get the value of the widget and return a dictionary + with the key 'value' and the value of the widget. + i.e. ``{'value': widget_value}`` + + Parameters + ---------- + widget: + The widget to get the fields. It should either be an instance of + ``WidgetWithFieldsProtocol`` or have a value property. + + """ if isinstance(widget, WidgetWithFieldsProtocol): return widget.get_fields() try: diff --git a/src/ess/reduce/widgets/_switchable_widget.py b/src/ess/reduce/widgets/_switchable_widget.py index 3aa2cd4a..85924b64 100644 --- a/src/ess/reduce/widgets/_switchable_widget.py +++ b/src/ess/reduce/widgets/_switchable_widget.py @@ -4,7 +4,7 @@ from ipywidgets import Checkbox, HBox, Label, Stack, Widget -from ._base import WidgetWithFieldsProtocol, get_fields, set_fields +from ._base import get_fields, set_fields from ._config import default_style @@ -52,36 +52,15 @@ def value(self, value: Any) -> None: self.wrapped.value = value def set_fields(self, new_values: dict[str, Any]) -> None: - # Check if the new values are a dictionary - if not isinstance(new_values, dict): - raise ValueError(f"Expected a dictionary, got {new_values}.") # Retrieve and set the enabled flag first new_values = dict(new_values) enabled_flag = new_values.pop('enabled', self.enabled) if not isinstance(enabled_flag, bool): raise ValueError(f"`enabled` must be a boolean, got {enabled_flag}") self.enabled = enabled_flag - # Set fields or value of the wrapped widget - if isinstance(self.wrapped, WidgetWithFieldsProtocol): - # Expecting {'enabled': True/False, **wrapped_fields} - self.wrapped.set_fields(new_values) - elif 'value' in new_values: # Skip if 'value' is not in new_values - # i.e. User might only want to change the enabled flag - # We use ``set_fields`` to set the value of the wrapped widget - # so that it handles availability of a value setter. - # Expecting {'enabled': True/False, 'value': value} - set_fields(self.wrapped, new_values.pop('value')) - if new_values: - # Check if there are any fields left except for 'enabled' and 'value' - raise ValueError( - f"Unexpected field(s) {new_values.keys()} for widget {self.wrapped}" - ) + # Set the rest of the fields + set_fields(self.wrapped, new_values) def get_fields(self) -> dict[str, Any]: wrapped_fields = get_fields(self.wrapped) - if isinstance(self.wrapped, WidgetWithFieldsProtocol) and isinstance( - wrapped_fields, dict - ): - return {'enabled': self.enabled, **wrapped_fields} - else: - return {'enabled': self.enabled, 'value': wrapped_fields} + return {'enabled': self.enabled, **(wrapped_fields or {})} From 48077166a58d0704b311060b4aba116b86536eb6 Mon Sep 17 00:00:00 2001 From: YooSunyoung Date: Fri, 20 Dec 2024 13:41:26 +0100 Subject: [PATCH 8/8] Update opted-out name to have essreduce prefix. --- src/ess/reduce/widgets/_optional_widget.py | 13 ++++++------- tests/widget_test.py | 16 +++++++++------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/ess/reduce/widgets/_optional_widget.py b/src/ess/reduce/widgets/_optional_widget.py index 81079f9f..73c72abe 100644 --- a/src/ess/reduce/widgets/_optional_widget.py +++ b/src/ess/reduce/widgets/_optional_widget.py @@ -69,15 +69,14 @@ def value(self, value: Any) -> None: def set_fields(self, new_values: dict[str, Any]) -> None: new_values = dict(new_values) # Set the value of the option box - 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, - ) + opted_out_flag = new_values.pop( + # We assume ``essreduce-opted-out`` is not used in any wrapped widget + 'essreduce-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}." + f"Invalid value for 'essreduce-opted-out' field: {opted_out_flag}." " The value should be a boolean." ) self._option_box.value = None if opted_out_flag else self.name @@ -87,5 +86,5 @@ def set_fields(self, new_values: dict[str, Any]) -> None: def get_fields(self) -> dict[str, Any] | None: return { **(get_fields(self.wrapped) or {}), - 'opted-out': self._option_box.value is None, + 'essreduce-opted-out': self._option_box.value is None, } diff --git a/tests/widget_test.py b/tests/widget_test.py index 76f80e9a..42d3e603 100644 --- a/tests/widget_test.py +++ b/tests/widget_test.py @@ -180,13 +180,13 @@ def test_optional_widget_set_value_get_fields() -> None: assert isinstance(optional_widget, OptionalWidget) # Check initial state assert optional_widget._option_box.value is None - assert get_fields(optional_widget) == {'opted-out': True, 'value': 1} + assert get_fields(optional_widget) == {'essreduce-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} + assert get_fields(optional_widget) == {'essreduce-opted-out': True, 'value': 2} optional_widget.value = 3 - assert get_fields(optional_widget) == {'opted-out': False, 'value': 3} + assert get_fields(optional_widget) == {'essreduce-opted-out': False, 'value': 3} def test_optional_widget_set_fields_get_fields() -> None: @@ -198,15 +198,17 @@ 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 - expected = {'opted-out': True, 'x': 1, 'y': 2, 'z': 3, 'unit': 'm'} + expected = {'essreduce-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({'opted-out': True, 'x': 4, 'y': 5, 'z': 6, 'unit': 'm'}) + optional_widget.set_fields( + {'essreduce-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}) + optional_widget.set_fields({'essreduce-opted-out': False}) assert optional_widget.value == sc.vector([4, 5, 6], unit='m') # Check the fields and the option box value - expected = {'opted-out': False, 'x': 4, 'y': 5, 'z': 6, 'unit': 'm'} + expected = {'essreduce-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