Skip to content

Commit

Permalink
integrate cue outputs into code demo
Browse files Browse the repository at this point in the history
* code can be now None in CodeDemo
* add cue_outputs and update_func as input
* allows ParameterPanel as input for parameters
* ValueError -> TypeError at the beginning
  • Loading branch information
agoscinski committed Dec 21, 2023
1 parent bb2aeb0 commit da055dc
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 48 deletions.
238 changes: 190 additions & 48 deletions src/scwidgets/code/_widget_code_demo.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# postpones evaluation of annotations
# see https://stackoverflow.com/a/33533514
from __future__ import annotations

import types
from copy import deepcopy
from platform import python_version
from typing import Dict, List, Optional, Union
from typing import Any, Callable, Dict, List, Optional, Union

from ipywidgets import HBox, Layout, Output, VBox, Widget
from ipywidgets import HBox, Layout, VBox, Widget
from widget_code_input.utils import CodeValidationError

from ..check import Check, CheckableWidget, CheckRegistry, ChecksLog
from ..cue import CheckCueBox, CheckResetCueButton, UpdateCueBox, UpdateResetCueButton
from ..cue import (
CheckCueBox,
CheckResetCueButton,
CueOutput,
UpdateCueBox,
UpdateResetCueButton,
)
from ._widget_code_input import CodeInput
from ._widget_parameter_panel import ParameterPanel

Expand All @@ -18,52 +27,109 @@ class CodeDemo(VBox, CheckableWidget):
of the several widgets that allow to check check, run and visualize code.
:param code:
An or function or CodeInput that is usually the input of code for a student to
fill in a solution
A function or CodeInput that is the input of code
:param check_registry:
a check registry that is used to register checks
:param parameters:
Can be any input that is allowed as keyword arguments in ipywidgets.interactive
for the parameters. _options and other widget layout parameter are controlled
by CodeDemo.
Input parameters for the :py:class:`ParameterPanel` class or an initialized
:py:class:`ParameterPanel` object. Specifies the arguments in the parameter
panel.
:param update_mode:
determines how the parameters are refreshed on change
Determines how the parameters are refreshed on changes of the code input
or parameters
:param cue_outputs:
List of CueOuputs that are drawn an refreshed
:param update_func:
A function that is run during the update process. The function takes as argument
the CodeDemo, so it can update all cue_ouputs
"""

def __init__(
self,
code: Union[CodeInput, types.FunctionType],
code: Union[None, CodeInput, types.FunctionType] = None,
check_registry: Optional[CheckRegistry] = None,
parameters: Optional[Dict[str, Union[Check.FunInParamT, Widget]]] = None,
parameters: Optional[
Union[Dict[str, Union[Check.FunInParamT, Widget]], ParameterPanel]
] = None,
update_mode: str = "release",
cue_outputs: Union[None, CueOutput, List[CueOutput]] = None,
update_func: Optional[
Callable[[CodeDemo], Union[Any, Check.FunOutParamsT]]
] = None,
*args,
**kwargs,
):
allowed_update_modes = ["manual", "continuous", "release"]
if update_mode not in allowed_update_modes:
raise ValueError(
raise TypeError(
f"Got update mode {update_mode!r} but only "
f"{allowed_update_modes} are allowed."
)
self._update_mode = update_mode

self._update_func = update_func

# verify if input argument `parameter` is valid
if parameters is not None:
allowed_parameter_types = [dict, ParameterPanel]
parameter_type_allowed = False
for allowed_parameter_type in allowed_parameter_types:
if isinstance(parameters, allowed_parameter_type):
parameter_type_allowed = True
if not (parameter_type_allowed):
raise TypeError(
f"Got parameter {type(parameters)!r} but only "
f"{allowed_parameter_types} are allowed."
)

# verify if input argument `parameter` is valid
if isinstance(code, types.FunctionType):
code = CodeInput(function=code)

# check compability between code and parameters, can only be checked if
# update_func is not used because we cannot know how the code input is used
if update_func is None and code is not None and parameters is not None:
if isinstance(parameters, dict):
compatibility_result = code.compatible_with_signature(
list(parameters.keys())
)
elif isinstance(parameters, ParameterPanel):
compatibility_result = code.compatible_with_signature(
list(parameters.parameters.keys())
)
if compatibility_result != "":
raise ValueError(
"Code and parameters do no match: " + compatibility_result
)

if cue_outputs is None:
cue_outputs = []
elif not (isinstance(cue_outputs, list)):
cue_outputs = [cue_outputs]

CheckableWidget.__init__(self, check_registry)

self._code = code
self._output = Output()
self._parameters = deepcopy(parameters)
# TODO this needs to be settable
self._output = CueOutput()
if isinstance(parameters, dict):
self._parameter_panel = ParameterPanel(**parameters)
elif isinstance(parameters, ParameterPanel):
self._parameter_panel = parameters
parameters = self._parameter_panel.parameters
self._parameters = parameters
self._cue_code = self._code
self._cue_outputs = cue_outputs

if self._check_registry is None:
self._check_button = None
else:
elif self._code is not None:
self._cue_code = CheckCueBox(
self._code, "function_body", self._cue_code, cued=True
)
Expand All @@ -84,26 +150,21 @@ def __init__(
self._update_button = None
self._parameter_panel = VBox([])
else:
compatibility_result = self._code.compatible_with_signature(
list(self._parameters.keys())
)
if compatibility_result != "":
raise ValueError(compatibility_result)

# set up update button and cueing
# -------------------------------
self._cue_code = UpdateCueBox(
self._code,
"function_body",
self._cue_code,
cued=True,
layout=Layout(width="98%", height="auto"),
)

if self._code is not None:
self._cue_code = UpdateCueBox(
self._code,
"function_body",
self._cue_code,
cued=True,
layout=Layout(width="98%", height="auto"),
)

# set up parameter panel
# ----------------------

self._parameter_panel = ParameterPanel(**self._parameters)
if self._update_mode == "continuous":
self._parameter_panel.set_parameters_widget_attr(
"continuous_update", True
Expand All @@ -117,12 +178,28 @@ def __init__(
self._parameter_panel.observe_parameters(
self._on_trait_parameters_changed, "value"
)
# the button only cues on cue_code change
widgets_to_observe = [self._code]
traits_to_observe = ["function_body"]

if self._code is not None:
# the button only cues on cue_code change
widgets_to_observe = [self._code]
traits_to_observe = ["function_body"]
else:
widgets_to_observe = None
traits_to_observe = None
# assume when continuous that the function is fast
# and that disabling causes flicker
disable_during_action = False
if self._code is not None:
for cue_output in self._cue_outputs:
# TODO this has to be made public
cue_output._widgets_to_observe = [self._code]
cue_output._traits_to_observe = ["function_body"]
cue_output.observe_widgets()

# TODO set this
self._output._widgets_to_observe = [self._code]
self._output._traits_to_observe = ["function_body"]
self._output.observe_widgets()

self._cue_parameter_panel = UpdateCueBox(
[],
Expand All @@ -140,8 +217,43 @@ def __init__(
self._parameter_panel,
)

for cue_output in self._cue_outputs:
if self._code is not None:
# TODO this has to be made public
cue_output._widgets_to_observe = [
self._code
] + self._parameter_panel.parameters_widget
cue_output._traits_to_observe = [
"function_body"
] + self._parameter_panel.parameters_trait
cue_output.observe_widgets()
else:
# TODO this has to be made public
cue_output._widgets_to_observe = (
self._parameter_panel.parameters_widget
)
cue_output._traits_to_observe = (
self._parameter_panel.parameters_trait
)
cue_output.observe_widgets()

reset_update_cue_widgets = []
if self._code is not None:
reset_update_cue_widgets.append(self._cue_code)
reset_update_cue_widgets.append(self._cue_parameter_panel)
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"
)
else:
description = "Update"
button_tooltip = "Updates outputs with the specified parameters"

self._update_button = UpdateResetCueButton(
[self._cue_code, self._cue_parameter_panel], # type: ignore[arg-type]
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
Expand All @@ -151,8 +263,8 @@ def __init__(
),
widgets_to_observe=widgets_to_observe,
traits_to_observe=traits_to_observe,
description="Run Code",
button_tooltip="Runs the code with the specified parameters",
description=description,
button_tooltip=button_tooltip,
)

if self._check_button is None and self._update_button is None:
Expand All @@ -164,14 +276,21 @@ def __init__(
else:
self._buttons_panel = HBox([self._check_button, self._update_button])

VBox.__init__(
self,
demo_children = []
if self._code is not None:
demo_children.append(self._cue_code)
demo_children.extend(
[
self._cue_code,
self._cue_parameter_panel,
self._buttons_panel,
self._output,
],
]
)
demo_children.extend(self._cue_outputs)

VBox.__init__(
self,
demo_children,
*args,
**kwargs,
)
Expand Down Expand Up @@ -227,35 +346,58 @@ def _output_results(self, results: List[Union[str, ChecksLog, Exception]]):
else:
print(result)

@property
def code(self):
return self._code

@property
def cue_outputs(self):
return self._cue_outputs

def _on_click_update_action(self) -> bool:
self._output.clear_output(wait=True)
raised_error = False
# runs code and displays output
with self._output:
try:
result = self._code.run(**self.panel_parameters)
print("Output:")
print(result)
for cue_output in self.cue_outputs:
if hasattr(cue_output, "clear_display"):
cue_output.clear_display(wait=True)

if self._update_func is not None:
self._update_func(self)
elif self._code is not None:
self.run_code(**self.panel_parameters)

for cue_output in self.cue_outputs:
if hasattr(cue_output, "draw_display"):
cue_output.draw_display()

except CodeValidationError as e:
raised_error = True
raise e
except Exception as e:
raised_error = True
# we give the student the additional information that this is most
# likely not because of his code
if python_version() >= "3.11":
e.add_note("This is most likely not related to your code input.")
raise e

return not (raised_error)

def run_code(self, *args, **kwargs) -> Check.FunOutParamsT:
"""
Runs the `code` with the given (keyword) arguments and returns the output of the
`code`. If no `code` was given on intialization, then a `ValueError` is raised.
"""
try:
if self._code is None:
raise ValueError(
"run_code was invoked, but no code was given on initializaion"
)
return self._code.run(*args, **kwargs)
except CodeValidationError as e:
raise e
except Exception as e:
# we give the student the additional information that this is most likely
# not because of his code
if python_version() >= "3.11":
e.add_note("This might might be not related to your code input.")
e.add_note("This might be not related to your code input.")
raise e
8 changes: 8 additions & 0 deletions tests/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from scwidgets.check import Check, CheckRegistry, ChecksLog
from scwidgets.code import CodeDemo, CodeInput
from scwidgets.cue import CueObject

from .test_check import multi_param_check, single_param_check

Expand Down Expand Up @@ -91,10 +92,17 @@ def get_code_demo(
parameters = {
key: fixed(value) for key, value in checks[0].inputs_parameters[0].items()
}

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

code_demo = CodeDemo(
code=code_input,
check_registry=CheckRegistry() if include_checks is True else None,
parameters=parameters if include_params is True else None,
cue_outputs=[CueObject("Not initialized")],
update_func=update_print,
update_mode=update_mode,
)

Expand Down

0 comments on commit da055dc

Please sign in to comment.