diff --git a/src/scwidgets/exercise/_widget_code_exercise.py b/src/scwidgets/exercise/_widget_code_exercise.py index 4825513..71205d3 100644 --- a/src/scwidgets/exercise/_widget_code_exercise.py +++ b/src/scwidgets/exercise/_widget_code_exercise.py @@ -283,6 +283,7 @@ def __init__( [], [], self._parameter_panel, + cued=self._code is not None, ) else: widgets_to_observe = None @@ -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 @@ -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: @@ -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) diff --git a/tests/notebooks/widget_code_exercise.py b/tests/notebooks/widget_code_exercise.py index 2e50b2e..4506200 100644 --- a/tests/notebooks/widget_code_exercise.py +++ b/tests/notebooks/widget_code_exercise.py @@ -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: # ------- diff --git a/tests/test_code.py b/tests/test_code.py index 1b28465..9be0762 100644 --- a/tests/test_code.py +++ b/tests/test_code.py @@ -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 @@ -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(): @@ -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( diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 3a42060..1ba58db 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -1108,6 +1108,7 @@ def test_code_exercise( nb_cell, expected_texts_on_update: List[str], expected_texts_on_check: List[str], + include_code=True, include_checks=True, include_params=True, tunable_params=False, @@ -1148,36 +1149,25 @@ def test_code_exercise( update_boxes = nb_cell.find_elements( By.CLASS_NAME, cue_box_class_name("update", False) ) + assert ( + len(update_boxes) == include_params + include_code + ), f"Text from update boxes {[box.text for box in update_boxes]}" + if include_code: + update_code_input = update_boxes[0] + assert "def function_to_check" in update_code_input.text + assert scwidget_cue_box_class_name( + "update", True + ) in update_code_input.get_attribute("class") if include_params: - assert len(update_boxes) == 2 - else: - assert len(update_boxes) == 1 - update_code_input = update_boxes[0] - assert "def function_to_check" in update_code_input.text - assert scwidget_cue_box_class_name( - "update", True - ) in update_code_input.get_attribute("class") - if include_params: - parameter_panel = update_boxes[1] - # in these tests it should not be contain inything + parameter_panel = update_boxes[1] if include_code else update_boxes[0] + # in these tests it should not be contain anything if tunable_params: assert scwidget_cue_box_class_name( - "update", True + "update", include_code # is cued only if code input present ) in parameter_panel.get_attribute("class") else: assert parameter_panel.size["height"] == 0 - update_buttons = nb_cell.find_elements( - By.CLASS_NAME, reset_cue_button_class_name("update", False) - ) - assert len(update_buttons) == 1 - update_button = update_buttons[0] - assert update_button.text == "Run Code" - assert update_button.is_enabled() - assert scwidget_reset_cue_button_class_name( - "update", True - ) in update_button.get_attribute("class") - ################################################# # asserts for behavior on click of check button # ################################################# @@ -1195,53 +1185,57 @@ def test_code_exercise( assert check_button.is_enabled() outputs = nb_cell.find_elements(By.CLASS_NAME, OUTPUT_CLASS_NAME) for text in expected_texts_on_check: - assert sum([output.text.count(text) for output in outputs]) == 1 + assert sum([output.text.count(text) for output in outputs]) == 1, ( + f"Did not find text {text!r} in outputs " + f"{[output.text for output in outputs]}" + ) ################################################## # asserts for behavior on click of update button # ################################################## - update_button.click() - time.sleep(0.2) - assert not ( - scwidget_cue_box_class_name("update", True) - in update_code_input.get_attribute("class") - ) - assert not ( - scwidget_reset_cue_button_class_name("update", True) - in update_button.get_attribute("class") - ) - assert update_button.is_enabled() - if include_params and tunable_params: + + if include_code or update_mode == "manual": + update_buttons = nb_cell.find_elements( + By.CLASS_NAME, reset_cue_button_class_name("update", False) + ) + assert len(update_buttons) == 1 + update_button = update_buttons[0] + assert update_button.text == "Run Code" if include_code else "Update" + assert update_button.is_enabled() + assert scwidget_reset_cue_button_class_name( + "update", True + ) in update_button.get_attribute("class") + + update_button.click() + time.sleep(0.2) + if include_code: + assert not ( + scwidget_cue_box_class_name("update", True) + in update_code_input.get_attribute("class") + ) assert not ( - scwidget_cue_box_class_name("update", True) - in parameter_panel.get_attribute("class") + scwidget_reset_cue_button_class_name("update", True) + in update_button.get_attribute("class") ) + assert update_button.is_enabled() + if include_params and tunable_params: + assert not ( + scwidget_cue_box_class_name("update", True) + in parameter_panel.get_attribute("class") + ) + outputs = nb_cell.find_elements(By.CLASS_NAME, OUTPUT_CLASS_NAME) for text in expected_texts_on_update: - assert sum([output.text.count(text) for output in outputs]) == 1 - - ##################################### - # asserts on reaction on text input # - ##################################### - # expected_conditions.text_to_be_present_in_element does not work for code input - code_input_lines = nb_cell.find_elements(By.CLASS_NAME, CODE_MIRROR_CLASS_NAME) - assert any(["return" in line.text for line in code_input_lines]) - # Issue #22 - # sending keys to code widget does not work at the moment - # once this works please add this code - # code_input.send_keys("a=5\n") - # time.sleep(0.1) - # assert (scwidget_cue_box_class_name("check", True) in - # check_code_input.get_attribute("class")) - # assert (scwidget_cue_box_class_name("update", True) in - # update_code_input.get_attribute("class")) - # assert check_button.is_enabled() - # assert check_button.is_enabled() + assert sum([output.text.count(text) for output in outputs]) == 1, ( + f"Did not find text {text!r} in outputs " + f"{[output.text for output in outputs]}" + ) if tunable_params: outputs = nb_cell.find_elements(By.CLASS_NAME, OUTPUT_CLASS_NAME) - assert len(outputs) == 2 - before_parameter_change_text = outputs[0].text + outputs[1].text + # In the code we print a text that adds another output + assert len(outputs) == 1 + include_code + before_parameter_change_text = "".join([output.text for output in outputs]) slider_input_box = nb_cell.find_element(By.CLASS_NAME, "widget-readout") slider_input_box.send_keys(Keys.BACKSPACE) @@ -1254,28 +1248,55 @@ def test_code_exercise( assert scwidget_cue_box_class_name( "update", (update_mode == "manual") ) in parameter_panel.get_attribute("class") - assert scwidget_reset_cue_button_class_name( - "update", (update_mode == "manual") - ) in update_button.get_attribute("class") if update_mode == "manual": + assert scwidget_reset_cue_button_class_name( + "update", (update_mode == "manual") + ) in update_button.get_attribute("class") + # Check if output has changed only after click when manual outputs = nb_cell.find_elements(By.CLASS_NAME, OUTPUT_CLASS_NAME) - assert len(outputs) == 2 - after_parameter_change_text = outputs[0].text + outputs[1].text + # In the code we print a text that adds another output + assert len(outputs) == 1 + include_code + after_parameter_change_text = "".join( + [output.text for output in outputs] + ) assert before_parameter_change_text == after_parameter_change_text update_button.click() outputs = nb_cell.find_elements(By.CLASS_NAME, OUTPUT_CLASS_NAME) - assert len(outputs) == 2 - after_parameter_change_text = outputs[0].text + outputs[1].text + assert len(outputs) == 1 + include_code + after_parameter_change_text = "".join([output.text for output in outputs]) assert before_parameter_change_text != after_parameter_change_text + ##################################### + # asserts on reaction on text input # + ##################################### + if include_code: + # expected_conditions.text_to_be_present_in_element does not work + # for code input + code_input_lines = nb_cell.find_elements( + By.CLASS_NAME, CODE_MIRROR_CLASS_NAME + ) + assert any(["return" in line.text for line in code_input_lines]) + # Issue #22 + # sending keys to code widget does not work at the moment + # once this works please add this code + # code_input.send_keys("a=5\n") + # time.sleep(0.1) + # assert (scwidget_cue_box_class_name("check", True) in + # check_code_input.get_attribute("class")) + # assert (scwidget_cue_box_class_name("update", True) in + # update_code_input.get_attribute("class")) + # assert check_button.is_enabled() + # assert check_button.is_enabled() + # Test 1.1 test_code_exercise( nb_cells[1], ["SomeText", "Output"], ["Check was successful"], + include_code=True, include_checks=True, include_params=True, tunable_params=False, @@ -1287,6 +1308,7 @@ def test_code_exercise( nb_cells[2], ["SomeText", "Output"], ["Check failed"], + include_code=True, include_checks=True, include_params=True, tunable_params=False, @@ -1298,6 +1320,7 @@ def test_code_exercise( nb_cells[3], ["SomeText", "NameError: name 'bug' is not defined"], ["NameError: name 'bug' is not defined"], + include_code=True, include_checks=True, include_params=True, tunable_params=False, @@ -1308,6 +1331,7 @@ def test_code_exercise( nb_cells[4], ["SomeText", "Output"], ["Check was successful"], + include_code=True, include_checks=True, include_params=True, tunable_params=True, @@ -1319,6 +1343,7 @@ def test_code_exercise( nb_cells[5], ["SomeText", "Output"], ["Check was successful"], + include_code=True, include_checks=True, include_params=True, tunable_params=True, @@ -1330,6 +1355,7 @@ def test_code_exercise( nb_cells[6], ["SomeText", "Output"], ["Check was successful"], + include_code=True, include_checks=True, include_params=True, tunable_params=True, @@ -1343,14 +1369,66 @@ def test_code_exercise( # Test if update button is shown even if params are None test_code_exercise( nb_cells[7], - ["SomeText", "Output"], + ["SomeText", "Output:"], [], + include_code=True, include_checks=False, include_params=False, tunable_params=False, update_mode="release", ) + # Test 2.2 TODO + # Test 2.3 TODO + + # Test 2.4.1 + test_code_exercise( + nb_cells[8], + ["Output:", "{'x': 3}"], + [], + include_code=False, + include_checks=False, + include_params=True, + tunable_params=True, + update_mode="release", + ) + + # Test 2.4.2 + test_code_exercise( + nb_cells[9], + ["Output:", "{'x': 3}"], + [], + include_code=False, + include_checks=False, + include_params=True, + tunable_params=True, + update_mode="continuous", + ) + + # Test 2.4.3 + test_code_exercise( + nb_cells[10], + ["Output:", "{}"], + [], + include_code=False, + include_checks=False, + include_params=False, + tunable_params=False, + update_mode="manual", + ) + + # Test 2.4.4 + test_code_exercise( + nb_cells[11], + ["Output:", "{'x': 3}"], + [], + include_code=False, + include_checks=False, + include_params=True, + tunable_params=True, + update_mode="manual", + ) + # TODO test only update, no check # Test 3: