Skip to content

Commit

Permalink
Restructure storage of paramters and widgets in ParameterPanel
Browse files Browse the repository at this point in the history
Introduce `param_to_widget_map` to access the widgets in form of a dict
for more convenient handling.

To more robustly set parameter values the setter has been replaced by
the `update_params` method. The setter introduced behavior that is
unintuitive for python as it behaves like an update function while using
assign syntax.

Alternatively a design implementing methods `__{get,set}item__`,
`values` and `keys` was considered, but `keys` was clashing with the
trait variable of `Widget` which would have required a different naming.
This would have been more confusing.

```python
code_ex.params # returns parameter_panel
code_ex.params['x'] = 5 # works, updates the panel
code_ex.params['x']
code_ex.params.values() # works
code_ex.params.params_keys() # cannot be keys because it is already used by Widget
```
  • Loading branch information
agoscinski committed Dec 19, 2024
1 parent 0c95cbd commit 0830925
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 35 deletions.
61 changes: 40 additions & 21 deletions src/scwidgets/code/_widget_parameter_panel.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -39,50 +39,69 @@ 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 it can the params can be changed in arbitrary order.
# Last widget is an output 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],
trait_name: Union[str, Sentinel, List[str]],
notification_type: Union[None, str, Sentinel] = "change",
):
""" """
for widget in self._parameters_widget:
for widget in self._parameters_widget.values():
widget.observe(handler, trait_name, notification_type)

def unobserve_parameters(
Expand All @@ -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._parameters_widget.values():
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._parameters_widget.values():
if hasattr(widget, name):
setattr(widget, name, value)
25 changes: 12 additions & 13 deletions src/scwidgets/exercise/_widget_code_exercise.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand All @@ -311,25 +311,25 @@ 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

cue_output.observe_widgets()
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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand All @@ -561,10 +563,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]:
Expand Down
32 changes: 31 additions & 1 deletion tests/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -221,6 +231,26 @@ def update_print(code_ex: CodeExercise):


class TestCodeExercise:
@pytest.mark.parametrize(
"code_ex",
[
get_code_exercise(
[single_param_check(use_fingerprint=False, failing=False, buggy=False)],
),
get_code_exercise(
[multi_param_check(use_fingerprint=False, failing=False)]
),
get_code_exercise(
[single_param_check(use_fingerprint=False, failing=False, buggy=False)],
include_params=True,
tunable_params=True,
),
],
)
def test_run_code2(self, code_ex):
output = code_ex.run_code(**code_ex.params)
assert np.allclose((output,), code_ex.checks[0].outputs_references[0])

@pytest.mark.parametrize(
"code_ex",
[
Expand Down

0 comments on commit 0830925

Please sign in to comment.