diff --git a/src/scwidgets/code/_widget_parameter_panel.py b/src/scwidgets/code/_widget_parameter_panel.py index 29dbebc..a74c52e 100644 --- a/src/scwidgets/code/_widget_parameter_panel.py +++ b/src/scwidgets/code/_widget_parameter_panel.py @@ -1,4 +1,4 @@ -from typing import Callable, Dict, List, Union +from typing import Any, Callable, Dict, List, Union from ipywidgets import Output, VBox, Widget, fixed, interactive from traitlets.utils.sentinel import Sentinel @@ -39,42 +39,61 @@ def dummy_function(**kwargs): "Assumed that interactive returns an output as last child. " "Parameter will be wrongly initialized if this is not True." ) - self._parameters_widget = list(self._interactive_widget.children[:-1]) - super().__init__(self._parameters_widget) + # Because interact only keeps a list of the widgets we build a map + # so the params can be changed in arbitrary order. + # Last widget is an output that interact adds to the widgets. + self._param_to_widget_map = { + key: widget + for key, widget in zip( + parameters.keys(), self._interactive_widget.kwargs_widgets + ) + } + super().__init__(self.panel_params_widget) @property - def parameters_widget(self) -> List[Widget]: - return self._parameters_widget + def param_to_widget_map(self) -> dict[str, Widget]: + return self._param_to_widget_map @property - def parameters_trait(self) -> List[str]: - return ["value"] * len(self._parameters_widget) + def panel_params_trait(self) -> List[str]: + return ["value"] * len(self.panel_params) + + @property + def panel_params_widget(self) -> List[Widget]: + """ + :return: Only parameters that are tunable in the parameter panel are returned. + Fixed parameters are ignored. + """ + return [ + widget + for widget in self._param_to_widget_map.values() + if not (isinstance(widget, fixed)) + ] @property - def params(self) -> dict: + def params(self) -> Dict[str, Any]: """ :return: All parameters that were given on initialization are returned, also including also fixed parameters. """ - return self._interactive_widget.kwargs.copy() - - @params.setter - def params(self, parameters: dict): - for i, key in enumerate(self._interactive_widget.kwargs.keys()): - self._interactive_widget.kwargs_widgets[i].value = parameters[key] + return {key: widget.value for key, widget in self._param_to_widget_map.items()} @property - def panel_parameters(self) -> dict: + def panel_params(self) -> Dict[str, Any]: """ :return: Only parameters that are tunable in the parameter panel are returned. Fixed parameters are ignored. """ return { - key: self._interactive_widget.kwargs_widgets[i].value - for i, key in enumerate(self._interactive_widget.kwargs.keys()) - if not (isinstance(self._interactive_widget.kwargs_widgets[i], fixed)) + key: widget.value + for key, widget in self._param_to_widget_map.items() + if not (isinstance(widget, fixed)) } + def update_params(self, new_params: Dict[str, Any]): + for key, value in new_params.items(): + self.param_to_widget_map[key].value = value + def observe_parameters( self, handler: Callable[[dict], None], @@ -82,7 +101,7 @@ def observe_parameters( notification_type: Union[None, str, Sentinel] = "change", ): """ """ - for widget in self._parameters_widget: + for widget in self.panel_params_widget: widget.observe(handler, trait_name, notification_type) def unobserve_parameters( @@ -91,10 +110,10 @@ def unobserve_parameters( trait_name: Union[str, Sentinel, List[str]], notification_type: Union[None, str, Sentinel] = "change", ): - for widget in self._parameters_widget: + for widget in self.panel_params_widget: widget.unobserve(handler, trait_name, notification_type) def set_parameters_widget_attr(self, name: str, value): - for widget in self._parameters_widget: + for widget in self.panel_params_widget: if hasattr(widget, name): setattr(widget, name, value) diff --git a/src/scwidgets/exercise/_widget_code_exercise.py b/src/scwidgets/exercise/_widget_code_exercise.py index 3159eed..633683e 100644 --- a/src/scwidgets/exercise/_widget_code_exercise.py +++ b/src/scwidgets/exercise/_widget_code_exercise.py @@ -301,8 +301,8 @@ def __init__( update_button_disable_during_action = True self._cue_parameter_panel = UpdateCueBox( - self._parameter_panel.parameters_widget, - self._parameter_panel.parameters_trait, # type: ignore + self._parameter_panel.panel_params_widget, + self._parameter_panel.panel_params_trait, # type: ignore self._parameter_panel, ) @@ -311,14 +311,14 @@ def __init__( # TODO this has to be made public cue_output._widgets_to_observe = [ self._code - ] + self._parameter_panel.parameters_widget + ] + self._parameter_panel.panel_params_widget # fmt: off cue_output._traits_to_observe = ( [ # type: ignore[assignment] "function_body" ] - + self._parameter_panel.parameters_trait + + self._parameter_panel.panel_params_trait ) # fmt: on @@ -326,10 +326,10 @@ def __init__( else: # TODO this has to be made public cue_output._widgets_to_observe = ( - self._parameter_panel.parameters_widget + self._parameter_panel.panel_params_widget ) cue_output._traits_to_observe = ( - self._parameter_panel.parameters_trait # type: ignore[assignment] # noqa: E501 + self._parameter_panel.panel_params_trait # type: ignore[assignment] # noqa: E501 ) cue_output.observe_widgets() elif self._code is not None: @@ -396,8 +396,10 @@ def __init__( save_traits_to_observe.append("function_body") if self._parameter_panel is not None: - save_widgets_to_observe.extend(self._parameter_panel.parameters_widget) - save_traits_to_observe.extend(self._parameter_panel.parameters_trait) + save_widgets_to_observe.extend( + self._parameter_panel.panel_params_widget + ) + save_traits_to_observe.extend(self._parameter_panel.panel_params_trait) if self._cue_code is not None: self._cue_code = SaveCueBox( @@ -535,7 +537,7 @@ def answer(self, answer: dict): if answer["code"] is not None and self._code is not None: self._code.function_body = answer["code"] if answer["parameter_panel"] is not None and self._parameter_panel is not None: - self._parameter_panel.params = answer["parameter_panel"] + self._parameter_panel.update_params(answer["parameter_panel"]) if self._save_cue_box is not None: self._save_cue_box.observe_widgets() @@ -550,10 +552,11 @@ def panel_parameters(self) -> Dict[str, Check.FunInParamT]: :return: Only parameters that are tunable in the parameter panel are returned. Fixed parameters are ignored. """ - if self._parameter_panel is not None: - parameter_panel = self._parameter_panel - return parameter_panel.panel_parameters - return {} + return ( + {} + if self._parameter_panel is None + else self._parameter_panel.panel_parameters + ) @property def params(self) -> Dict[str, Check.FunInParamT]: @@ -561,10 +564,7 @@ def params(self) -> Dict[str, Check.FunInParamT]: :return: All parameters that were given on initialization are returned, also including also fixed parameters. """ - if self._parameter_panel is not None: - parameter_panel = self._parameter_panel - return parameter_panel.params - return {} + return {} if self._parameter_panel is None else self._parameter_panel.params @property def exercise_title(self) -> Union[str, None]: diff --git a/tests/test_code.py b/tests/test_code.py index 9ac1dd2..9d805b0 100644 --- a/tests/test_code.py +++ b/tests/test_code.py @@ -9,13 +9,23 @@ from widget_code_input.utils import CodeValidationError from scwidgets.check import Check, CheckRegistry, CheckResult -from scwidgets.code import CodeInput +from scwidgets.code import CodeInput, ParameterPanel from scwidgets.cue import CueObject from scwidgets.exercise import CodeExercise, ExerciseRegistry from .test_check import multi_param_check, single_param_check +class TestParameterPanel: + + def test_params(self): + from ipywidgets import fixed + + panel = ParameterPanel(**{"x": (0, 1, 0.5), "y": (2, 3, 1), "z": fixed(5)}) + assert panel.params == {"x": 0.0, "y": 2, "z": 5} + assert panel.panel_params == {"x": 0.0, "y": 2} + + class TestCodeInput: # fmt: off @staticmethod