Skip to content

Support update_func without arguments in CodeExercise #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions src/scwidgets/exercise/_widget_code_exercise.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# see https://stackoverflow.com/a/33533514
from __future__ import annotations

import inspect
import types
from platform import python_version
from typing import Any, Callable, Dict, List, Optional, Union
Expand Down Expand Up @@ -67,7 +68,10 @@ def __init__(
update_mode: str = "release",
cue_outputs: Union[None, CueOutput, List[CueOutput]] = None,
update_func: Optional[
Callable[[CodeExercise], Union[Any, Check.FunOutParamsT]]
Union[
Callable[[CodeExercise], Union[Any, Check.FunOutParamsT]],
Callable[[], Union[Any, Check.FunOutParamsT]],
]
] = None,
exercise_description: Optional[str] = None,
exercise_title: Optional[str] = None,
Expand All @@ -82,7 +86,30 @@ def __init__(
)
self._update_mode = update_mode

self._update_func = update_func
self._update_func: Optional[
Union[
Callable[[CodeExercise], Union[Any, Check.FunOutParamsT]],
Callable[[], Union[Any, Check.FunOutParamsT]],
]
] = update_func

self._update_func_nb_nondefault_args: Optional[int]
if update_func is not None:
self._update_func_nb_nondefault_args = len(
[
value
for value in inspect.signature(update_func).parameters.values()
if not isinstance(value.default, inspect._empty)
]
)
if self._update_func_nb_nondefault_args > 1:
raise ValueError(
f"The given update_func has "
f"{self._update_func_nb_nondefault_args} parameters without "
"defaults, but only zero or one are supported."
)
else:
self._update_func_nb_nondefault_args = None

self._exercise_description = exercise_description
if exercise_description is None:
Expand Down Expand Up @@ -639,7 +666,7 @@ def cue_outputs(self):

def _on_click_update_action(self) -> bool:
self._output.clear_output(wait=True)
raised_error = False
self._raised_error = False
# runs code and displays output
with self._output:
try:
Expand All @@ -648,7 +675,10 @@ def _on_click_update_action(self) -> bool:
cue_output.clear_display(wait=True)

if self._update_func is not None:
self._update_func(self)
if self._update_func_nb_nondefault_args == 0:
self._update_func() # type: ignore[call-arg]
else:
self._update_func(self) # type: ignore[call-arg]
elif self._code is not None:
self.run_code(**self.parameters)

Expand All @@ -657,18 +687,18 @@ def _on_click_update_action(self) -> bool:
cue_output.draw_display()

except CodeValidationError as e:
raised_error = True
self._raised_error = True
raise e
except Exception as e:
raised_error = True
self._raised_error = True
raise e

# The clear_output command at the beginning of the function waits till
# something is printed. If nothing is printed, it is not cleared. We
# enforce it to be invoked by printing an empty string
print("", end="")

return not (raised_error)
return not (self._raised_error)

def run_update(self):
"""
Expand Down
70 changes: 66 additions & 4 deletions tests/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

class TestCodeInput:
# fmt: off
@staticmethod
def mock_function_0():
return 0

@staticmethod
def mock_function_1(x, y):
"""
Expand Down Expand Up @@ -81,6 +85,7 @@ def get_code_exercise(
include_checks=True,
include_params=True,
tunable_params=False,
update_func_argless=False,
update_mode="manual",
):
# Important:
Expand Down Expand Up @@ -109,9 +114,17 @@ def get_code_exercise(
else:
parameters = None

def update_print(code_ex: CodeExercise):
output = code_ex.run_code(**code_ex.parameters)
code_ex.cue_outputs[0].display_object = f"Output:\n{output}"
if update_func_argless:

def update_print():
output = code_ex.run_code(**code_ex.parameters)
code_ex.cue_outputs[0].display_object = f"Output:\n{output}"

else:

def update_print(code_ex: CodeExercise):
output = code_ex.run_code(**code_ex.parameters)
code_ex.cue_outputs[0].display_object = f"Output:\n{output}"

code_ex = CodeExercise(
code=code_input,
Expand Down Expand Up @@ -198,7 +211,7 @@ def test_compute_and_set_references(self, code_ex):
"code_ex",
[
get_code_exercise(
[single_param_check(use_fingerprint=False, failing=False, buggy=False)]
[single_param_check(use_fingerprint=False, failing=False, buggy=False)],
),
get_code_exercise(
[multi_param_check(use_fingerprint=False, failing=False)]
Expand Down Expand Up @@ -260,3 +273,52 @@ def print_success(code_ex: CodeExercise | None):
exercise_registry.create_new_file()
code_ex._save_button.click()
os.remove("test_save_registry-student_name.json")

@pytest.mark.parametrize(
"code_ex",
[
get_code_exercise(
[],
code=TestCodeInput.mock_function_0,
update_func_argless=False,
),
get_code_exercise(
[],
code=TestCodeInput.mock_function_0,
update_func_argless=True,
),
],
)
def test_run_update(self, code_ex):
"""Test run_update"""
import io
from contextlib import redirect_stdout

buffer = io.StringIO()
code_ex._output = redirect_stdout(buffer)

def mock_clear_output(wait):
pass

code_ex._output.clear_output = mock_clear_output

# Be aware that the raised error is captured in the code_ex._output
# To debug failures in the test you have to manually run it in debug
# mode and execute `code_ex._update_button.click()` Redirecting stderr
# does des not workc
code_ex.run_update()
assert "Output:\n0" in buffer.getvalue()

def test_invalid_update_func(self):
"""Test run_update for wrong input"""

def failing_update(a, b):
pass

with pytest.raises(
ValueError, match=r".*The given update_func has 2 parameters .*"
):
CodeExercise(
code=TestCodeInput.mock_function_0,
update_func=failing_update,
)
Loading