Skip to content

Commit

Permalink
Only create update button when update_mode manual or code input is …
Browse files Browse the repository at this point in the history
…not `None` (#61)

When no code input exists the update button should be only shown when
`update_mode` is "manual".

Fixes cueing when `CodeExercise` is initiated without a code input. When the
`update_mode` is "manual", a cued button is now shown. The cue was missing
before this commit because the cue was derived from the widgets that the button
is resetting. When When the `update_mode` is "release" or "continuous", no
cueing is shown for the parameter panel, if any is given on init, and the
`update_func` is directly run.
  • Loading branch information
agoscinski authored Nov 29, 2024
1 parent d92c623 commit 86ee1bb
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 106 deletions.
66 changes: 34 additions & 32 deletions src/scwidgets/exercise/_widget_code_exercise.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ def __init__(
[],
[],
self._parameter_panel,
cued=self._code is not None,
)
else:
widgets_to_observe = None
Expand Down Expand Up @@ -341,30 +342,35 @@ def __init__(
if self._cue_outputs is not None:
reset_update_cue_widgets.extend(self._cue_outputs)

if self._code is not None:
description = "Run Code"
button_tooltip = (
"Runs the code and updates outputs with the specified parameters"
if self._code is not None or self._update_mode == "manual":
if self._code is not None:
description = "Run Code"
button_tooltip = (
"Runs the code and updates outputs with the "
"specified parameters"
)
else:
description = "Update"
button_tooltip = "Updates outputs with the specified parameters"

self._update_button = UpdateResetCueButton(
reset_update_cue_widgets, # type: ignore[arg-type]
self._on_click_update_action,
disable_on_successful_action=kwargs.pop(
"disable_update_button_on_successful_action", False
),
disable_during_action=kwargs.pop(
"disable_update_button_during_action",
update_button_disable_during_action,
),
widgets_to_observe=widgets_to_observe,
traits_to_observe=traits_to_observe,
description=description,
button_tooltip=button_tooltip,
cued=True,
)
else:
description = "Update"
button_tooltip = "Updates outputs with the specified parameters"

self._update_button = UpdateResetCueButton(
reset_update_cue_widgets, # type: ignore[arg-type]
self._on_click_update_action,
disable_on_successful_action=kwargs.pop(
"disable_update_button_on_successful_action", False
),
disable_during_action=kwargs.pop(
"disable_update_button_during_action",
update_button_disable_during_action,
),
widgets_to_observe=widgets_to_observe,
traits_to_observe=traits_to_observe,
description=description,
button_tooltip=button_tooltip,
)
self._update_button = None

if self._exercise_registry is None or (
self._code is None and self._parameter_panel is None
Expand Down Expand Up @@ -493,6 +499,11 @@ def __init__(
*args,
**kwargs,
)
# In this case there is no code to be written by the student, so the code
# exercise should work out of the box. Since the cues for the parameters
# are also disabled, we update at the beginning once.
if self._update_mode in ["release", "continuous"] and self._code is None:
self.run_update()

@property
def answer(self) -> dict:
Expand Down Expand Up @@ -555,16 +566,7 @@ def exercise_description(self) -> Union[str, None]:
return self._exercise_description

def _on_trait_parameters_changed(self, change: dict):
if self._update_button is None:
self._output.clear_output(wait=True)
error = ValueError(
"Invalid state: _on_trait_parameters_changed was "
"invoked but no update button was defined"
)
with self._output:
raise error
raise error
self._update_button.click()
self.run_update()

def _on_click_check_action(self) -> bool:
self._output.clear_output(wait=True)
Expand Down
44 changes: 44 additions & 0 deletions tests/notebooks/widget_code_exercise.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,50 @@ def function_to_check():
# tunable_params=True,
# )

# Test 2.4.1 No code
# Test if no update button and no cue is shown because the update_mode is "release"
get_code_exercise(
[],
code=None,
include_checks=False,
include_params=True,
tunable_params=True,
update_mode="release",
)

# Test 2.4.2 No code
# Test if no update button and no cue is shown because the update_mode is "continuous"
get_code_exercise(
[],
code=None,
include_checks=False,
include_params=True,
tunable_params=True,
update_mode="continuous",
)

# Test 2.4.3 No code
# Test if an update button and cue is shown because the update_mode is "manual"
get_code_exercise(
[],
code=None,
include_checks=False,
include_params=False,
tunable_params=False,
update_mode="manual",
)

# Test 2.4.4 No code
# Test if an update button and cue is shown because the update_mode is "manual"
get_code_exercise(
[],
code=None,
include_checks=False,
include_params=True,
tunable_params=True,
update_mode="manual",
)


# Test 3:
# -------
Expand Down
32 changes: 25 additions & 7 deletions tests/test_code.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from typing import Callable, List, Optional
from typing import Callable, List, Literal, Union

import numpy as np
import pytest
Expand Down Expand Up @@ -86,21 +86,31 @@ def test_call(self):

def get_code_exercise(
checks: List[Check],
code: Optional[Callable] = None,
code: Union[None, Literal["from_first_check"], Callable] = "from_first_check",
include_checks=True,
include_params=True,
tunable_params=False,
update_func_argless=False,
update_mode="manual",
):
"""
:param code: "from_first_check" uses the `function_to_check` from the first
check for the construction of a code input
"""
# Important:
# we take the the function_to_check from the first check as code input
if len(checks) == 0 and code is None:
raise ValueError("Either nonempty checks must given or code")
if code is None:
if code == "from_first_check":
if len(checks) == 0:
raise ValueError(
"For option 'from_first_check' either nonempty "
"checks must given or code"
)
code_input = CodeInput(checks[0].function_to_check)
elif code is None:
code_input = None
else:
code_input = CodeInput(code)

if len(checks) > 0 and tunable_params:
# convert single value arrays to tuples
for value in checks[0].inputs_parameters[0].values():
Expand All @@ -116,19 +126,27 @@ def get_code_exercise(
parameters = {
key: fixed(value) for key, value in checks[0].inputs_parameters[0].items()
}
elif tunable_params:
parameters = {"x": (2, 5, 1)}
else:
parameters = None

if update_func_argless:

def update_print():
output = code_ex.run_code(**code_ex.params)
if code_input is None:
output = code_ex.params
else:
output = code_ex.run_code(**code_ex.params)
code_ex.output.display_object = f"Output:\n{output}"

else:

def update_print(code_ex: CodeExercise):
output = code_ex.run_code(**code_ex.params)
if code_input is None:
output = code_ex.params
else:
output = code_ex.run_code(**code_ex.params)
code_ex.output.display_object = f"Output:\n{output}"

code_ex = CodeExercise(
Expand Down
Loading

0 comments on commit 86ee1bb

Please sign in to comment.