From df012d2d655c3b4f6d18ee6e85d55f356d97c0c4 Mon Sep 17 00:00:00 2001 From: DivyaSuman14 Date: Fri, 1 Sep 2023 17:20:04 +0200 Subject: [PATCH 1/2] make check outputs visually more appealing * implement AssertResult that allows formatting of assert results * use answer_key as CheckableWidget name * handle error during check more transparent to student by catching errors during fingerprint function and asserts and embedding them into the assert message * implement input parameters for Checks - suppress_fingerprint_asserts: specifies if the assert messages that use the fingerprint function result are suppressed - stop_on_assert_error_raised: Specifies if running the asserts is stopped as soon as an error is raised in an assert * format check results more structured --- src/scwidgets/_utils.py | 48 +++- src/scwidgets/check/__init__.py | 3 +- src/scwidgets/check/_asserts.py | 96 ++++++-- src/scwidgets/check/_check.py | 229 ++++++++++++++++-- src/scwidgets/check/_widget_check_registry.py | 36 ++- src/scwidgets/code/_widget_code_demo.py | 9 +- tests/test_check.py | 15 +- tests/test_widgets.py | 20 +- 8 files changed, 386 insertions(+), 70 deletions(-) diff --git a/src/scwidgets/_utils.py b/src/scwidgets/_utils.py index 48a8af6..4757716 100644 --- a/src/scwidgets/_utils.py +++ b/src/scwidgets/_utils.py @@ -1,16 +1,56 @@ +import re + from termcolor import colored class Printer: - # move to output + # TODO rename to Formatter + # remove print funcs + LINE_LENGTH = 120 + INFO_COLOR = "blue" + ERROR_COLOR = "red" + SUCCESS_COLOR = "green" + + @staticmethod + def format_title_message(message: str) -> str: + return message.center(Printer.LINE_LENGTH - len(message) // 2, "-") + + @staticmethod + def break_lines(message: str) -> str: + return "\n ".join(re.findall(r".{1," + str(Printer.LINE_LENGTH) + "}", message)) + + @staticmethod + def color_error_message(message: str) -> str: + return colored(message, Printer.ERROR_COLOR, attrs=["bold"]) + @staticmethod def print_error_message(message: str): - print(colored(message, "red", attrs=["bold"])) + print(Printer.color_error_message(message)) + + @staticmethod + def color_success_message(message: str) -> str: + return colored(message, Printer.SUCCESS_COLOR, attrs=["bold"]) @staticmethod def print_success_message(message: str): - print(colored(message, "green", attrs=["bold"])) + print(Printer.color_success_message(message)) + + @staticmethod + def color_info_message(message: str): + return colored(message, Printer.INFO_COLOR, attrs=["bold"]) @staticmethod def print_info_message(message: str): - print(colored(message, "blue", attrs=["bold"])) + print(Printer.color_info_message(message)) + + @staticmethod + def color_assert_failed(message: str) -> str: + return colored(message, "light_" + Printer.ERROR_COLOR) + + @staticmethod + def color_assert_info(message: str) -> str: + return colored(message, "light_" + Printer.INFO_COLOR) + + @staticmethod + def color_assert_success(message: str) -> str: + return colored(message, "light_" + Printer.SUCCESS_COLOR) diff --git a/src/scwidgets/check/__init__.py b/src/scwidgets/check/__init__.py index 257028c..c501ab2 100644 --- a/src/scwidgets/check/__init__.py +++ b/src/scwidgets/check/__init__.py @@ -5,12 +5,13 @@ assert_shape, assert_type, ) -from ._check import Check, ChecksLog +from ._check import AssertResult, Check, ChecksLog from ._widget_check_registry import CheckableWidget, CheckRegistry __all__ = [ "Check", "ChecksLog", + "AssertResult", "CheckRegistry", "CheckableWidget", "assert_shape", diff --git a/src/scwidgets/check/_asserts.py b/src/scwidgets/check/_asserts.py index bb2121d..6a1d563 100644 --- a/src/scwidgets/check/_asserts.py +++ b/src/scwidgets/check/_asserts.py @@ -1,19 +1,19 @@ import functools from collections import abc -from typing import Iterable, TypeVar, Union +from typing import Iterable, Union import numpy as np -from ._check import Check +from ._check import AssertResult, Check -AssertResultT = TypeVar("AssertResultT", bound="str") +AssertFunctionOutputT = Union[str, AssertResult] def assert_shape( output_parameters: Check.FunOutParamsT, output_references: Check.FunOutParamsT, parameters_to_check: Union[Iterable[int], str] = "auto", -) -> str: +) -> AssertResult: assert len(output_parameters) == len( output_references ), "output_parameters and output_references have to have the same length" @@ -22,7 +22,7 @@ def assert_shape( if isinstance(parameters_to_check, str): if parameters_to_check == "auto": parameter_indices = [] - for i in range(len(output_parameters)): + for i in range(len(output_references)): if hasattr(output_references[i], "shape"): parameter_indices.append(i) elif parameters_to_check == "all": @@ -40,13 +40,25 @@ def assert_shape( f"but got type {type(parameters_to_check)}." ) + failed_parameter_indices = [] + failed_parameter_values = [] + messages = [] for i in parameter_indices: if output_parameters[i].shape != output_references[i].shape: - return ( - f"For parameter {i} expected shape {output_references[i].shape} " + message = ( + f"Expected shape {output_references[i].shape})" f"but got {output_parameters[i].shape}." ) - return "" + failed_parameter_indices.append(i) + failed_parameter_values.append(output_parameters[i]) + messages.append(message) + + return AssertResult( + assert_name="assert_shape", + parameter_indices=failed_parameter_indices, + parameter_values=failed_parameter_values, + messages=messages, + ) def assert_numpy_allclose( @@ -56,7 +68,7 @@ def assert_numpy_allclose( rtol=1e-05, atol=1e-08, equal_nan=False, -) -> str: +) -> AssertResult: assert len(output_parameters) == len( output_references ), "output_parameters and output_references have to have the same length" @@ -86,6 +98,9 @@ def assert_numpy_allclose( f"but got type {type(parameters_to_check)}." ) + failed_parameter_indices = [] + failed_parameter_values = [] + messages = [] for i in parameter_indices: is_allclose = np.allclose( output_parameters[i], @@ -101,18 +116,28 @@ def assert_numpy_allclose( ) abs_diff = np.sum(diff) rel_diff = np.sum(diff / np.abs(output_references[i])) - return ( - f"Output parameter {i} is not close to reference absolute difference " + + message = ( + f"Output is not close to reference absolute difference " f"is {abs_diff}, relative difference is {rel_diff}." ) - return "" + failed_parameter_indices.append(i) + failed_parameter_values.append(output_parameters[i]) + messages.append(message) + + return AssertResult( + assert_name="assert_numpy_allclose", + parameter_indices=failed_parameter_indices, + parameter_values=failed_parameter_values, + messages=messages, + ) def assert_type( output_parameters: Check.FunOutParamsT, output_references: Check.FunOutParamsT, parameters_to_check: Union[Iterable[int], str] = "all", -) -> str: +) -> AssertResult: assert len(output_parameters) == len( output_references ), "output_parameters and output_references have to have the same length" @@ -134,20 +159,31 @@ def assert_type( f"but got type {type(parameters_to_check)}." ) + failed_parameter_indices = [] + failed_parameter_values = [] + messages = [] for i in parameter_indices: if not (isinstance(output_parameters[i], type(output_references[i]))): - return ( + message = ( f"Expected type {type(output_references[i])} " f"but got {type(output_parameters[i])}." ) - return "" + failed_parameter_indices.append(i) + failed_parameter_values.append(output_parameters[i]) + messages.append(message) + return AssertResult( + assert_name="assert_type", + parameter_indices=failed_parameter_indices, + parameter_values=failed_parameter_values, + messages=messages, + ) def assert_numpy_sub_dtype( output_parameters: Union[Check.FunOutParamsT, tuple[Check.FingerprintT]], numpy_type: Union[np.dtype, type], parameters_to_check: Union[Iterable[int], str] = "all", -) -> str: +) -> AssertResult: if parameters_to_check == "all": parameter_indices = range(len(output_parameters)) elif isinstance(parameters_to_check, abc.Iterable): @@ -158,23 +194,41 @@ def assert_numpy_sub_dtype( f"but got type {type(parameters_to_check)}." ) + failed_parameter_indices = [] + failed_parameter_values = [] + messages = [] for i in parameter_indices: if not (isinstance(output_parameters[i], np.ndarray)): - return ( - f"Output parameter {i} expected to be numpy array " + failed_parameter_indices.append(i) + failed_parameter_values.append(output_parameters[i]) + message = ( + f"Output expected to be numpy array " f"but got {type(output_parameters[i])}." ) + messages.append(message) if not (np.issubdtype(output_parameters[i].dtype, numpy_type)): if isinstance(numpy_type, np.dtype): type_name = numpy_type.type.__name__ else: type_name = numpy_type.__name__ - return ( - f"Output parameter {i} expected to be sub dtype " + failed_parameter_indices.append(i) + failed_parameter_values.append(output_parameters[i]) + message = ( + f"Output expected to be sub dtype " f"numpy.{type_name} but got " f"numpy.{output_parameters[i].dtype.type.__name__}." ) - return "" + messages.append(message) + if isinstance(numpy_type, np.dtype): + type_name = numpy_type.type.__name__ + else: + type_name = numpy_type.__name__ + return AssertResult( + assert_name=f"assert_numpy_{type_name}_sub_dtype", + parameter_indices=failed_parameter_indices, + parameter_values=failed_parameter_values, + messages=messages, + ) assert_numpy_floating_sub_dtype = functools.partial( diff --git a/src/scwidgets/check/_check.py b/src/scwidgets/check/_check.py index 68b9588..cec2870 100644 --- a/src/scwidgets/check/_check.py +++ b/src/scwidgets/check/_check.py @@ -4,10 +4,24 @@ import functools import inspect +import re +import sys import types from copy import deepcopy +from platform import python_version +from types import TracebackType from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union +import IPython.core.ultratb + +from .._utils import Printer + +ExecutionInfo = Tuple[ + Union[None, type], # BaseException type + Union[None, BaseException], + Union[None, TracebackType], +] + class Check: """ @@ -35,6 +49,14 @@ class Check: :param fingerprint: A one-way function that takes as input the output parameters of function :param function_to_check: and obscures the :param output_references:. + :param suppress_fingerprint_asserts: + Specifies if the assert messages that use the fingerprint function output for + tests are surpressed. The message might be confusing to a student as the output + is converted by the fingerprint function. + :param stop_on_assert_error_raised: + Specifies if running the asserts is stopped as soon as an error is raised in an + assert. If a lot of asserts are specified the printing of a lot of error + tracebacks might make debugging harder. """ FunInParamT = TypeVar("FunInParamT", bound=Any) @@ -56,6 +78,8 @@ def __init__( fingerprint: Optional[ Callable[[Check.FunOutParamsT], Check.FingerprintT] ] = None, + suppress_fingerprint_asserts: bool = True, + stop_on_assert_error_raised: bool = True, ): self._function_to_check = function_to_check self._asserts = [] @@ -102,6 +126,8 @@ def __init__( self._inputs_parameters = inputs_parameters self._outputs_references = outputs_references self._fingerprint = fingerprint + self._suppress_fingerprint_asserts = suppress_fingerprint_asserts + self._stop_on_assert_error_raised = stop_on_assert_error_raised @property def function_to_check(self): @@ -180,11 +206,28 @@ def check_function(self) -> ChecksLog: output = (output,) for assert_f in self._univariate_asserts: - assert_result = assert_f(output) + try: + assert_result = assert_f(output) + except Exception: + excution_info = sys.exc_info() + check_result.append(excution_info, assert_f, input_parameters) + if self._stop_on_assert_error_raised: + return check_result check_result.append(assert_result, assert_f, input_parameters) if self._fingerprint is not None: - output = self._fingerprint(*output) + try: + output = self._fingerprint(*output) + except Exception as exception: + if python_version() >= "3.11": + exception.add_note( + "An error was raised in fingerprint function, " + " most likely because your output type is wrong." + ) + excution_info = sys.exc_info() + check_result.append(excution_info, assert_f, input_parameters) + return check_result + if not (isinstance(output, tuple)): output = (output,) @@ -199,10 +242,24 @@ def check_function(self) -> ChecksLog: f"{len(self._outputs_references[i])}]." # type: ignore[index] ) - assert_result = assert_f( - output, self._outputs_references[i] # type: ignore[index, call-arg] + try: + assert_result = assert_f( + output, + self._outputs_references[i], # type: ignore[index, call-arg] + ) + except Exception: + excution_info = sys.exc_info() + check_result.append(excution_info, assert_f, input_parameters) + if self._stop_on_assert_error_raised: + return check_result + check_result.append( + assert_result, + assert_f, + input_parameters, + self._suppress_fingerprint_asserts + and self._fingerprint is not None, ) - check_result.append(assert_result, assert_f, input_parameters) + return check_result @@ -211,34 +268,105 @@ def __init__(self): self._assert_results = [] self._assert_names = [] self._inputs_parameters = [] + self._suppress_assert_messages = [] def append( self, - assert_result: str, + assert_result: Union[str, AssertResult, ExecutionInfo], assert_f: Optional[Check.AssertFunT] = None, input_parameters: Optional[dict] = None, + suppress_assert_message: Optional[bool] = False, ): self._assert_results.append(assert_result) - self._assert_names.append(self._get_name_from_assert(assert_f)) + if isinstance(assert_result, AssertResult): + self._assert_names.append(assert_result.assert_name) + else: + self._assert_names.append(self._get_name_from_assert(assert_f)) + self._inputs_parameters.append(input_parameters) + self._suppress_assert_messages.append(suppress_assert_message) def extend(self, check_results: ChecksLog): self._assert_results.extend(check_results._assert_results) self._assert_names.extend(check_results._assert_names) self._inputs_parameters.extend(check_results._inputs_parameters) + self._suppress_assert_messages.extend(check_results._suppress_assert_messages) @property def successful(self): - return len([result for result in self._assert_results if result != ""]) == 0 + return ( + len( + [ + result + for result in self._assert_results + if (isinstance(result, str) and result != "") + or (isinstance(result, AssertResult) and not (result.successful)) + ] + ) + == 0 + ) def message(self) -> str: - messages = [ - f"Test failed for input:\n" - f" {self._inputs_parameters[i]}\n" - f" {self._assert_names[i]} failed: {result}\n" - for i, result in enumerate(self._assert_results) - if result != "" - ] + messages = [] + for i, result in enumerate(self._assert_results): + if (isinstance(result, str) and result == "") or ( + isinstance(result, AssertResult) and result.successful + ): + message = Printer.color_assert_success( + f"{self._assert_names[i]} passed", + ) + else: + message = Printer.color_assert_failed( + f"{self._assert_names[i]} failed", + ) + if len(self._inputs_parameters[i]) > 0: + message += Printer.color_error_message(" for input\n") + elif (isinstance(result, tuple) and len(result) == 3) or not ( + self._suppress_assert_messages[i] + ): + message += Printer.color_error_message("\n") + + message += "\n".join( + [ + f" {Printer.color_info_message(param_name)}: {param_value!r}" + for param_name, param_value in self._inputs_parameters[ + i + ].items() + ] + ) + + if isinstance(result, tuple) and len(result) == 3: + if len(self._inputs_parameters[i]) > 0: + message += "\n" + tb = IPython.core.ultratb.VerboseTB() + message = Printer.color_assert_failed( + f"{self._assert_names[i]} failed\n" + ) + assert_result = tb.text(*result) + assert_result = re.sub( + r"(^)", + r"\1" + f"{Printer.color_assert_failed('|')} ", + assert_result, + flags=re.M, + ) + message += assert_result + elif not (self._suppress_assert_messages[i]): + if len(self._inputs_parameters[i]) > 0: + message += "\n" + if hasattr(result, "message"): + assert_result = f"{result.message()}" + else: + assert_result = f"{Printer.color_assert_failed(result)}" + # adds "| " to the beginning of each line + assert_result = re.sub( + r"(^)", + r"\1" + f"{Printer.color_assert_failed('|')} ", + assert_result, + flags=re.M, + ) + message += f"{assert_result}" + messages.append(message) + return "\n".join(messages) def _get_name_from_assert(self, assert_f: Any) -> str: @@ -260,3 +388,74 @@ def assert_names(self): @property def inputs_parameters(self): return deepcopy(self._inputs_parameters) + + +class AssertResult: + """ + :param assert_name: + TODO + :param parameter_indices: + TODO + TODO... + """ + + def __init__( + self, + assert_name: str, + parameter_indices: Union[int, List[int]], + parameter_values: Union[Any, List[Any]], + messages: Union[str, List[str]], + ): + self._assert_name = assert_name + + # we do not include parameter_values in the check because it can be a list + # by type definition + if isinstance(parameter_indices, list) or isinstance(messages, list): + if ( + not (isinstance(parameter_indices, list)) + or not (isinstance(parameter_values, list)) + or not (isinstance(messages, list)) + ): + raise ValueError( + "If one of the inputs parameter_indices, paramater_values or " + "messages is a list, then all must be lists of the same size." + ) + elif len(parameter_indices) != len(parameter_values) or len( + parameter_indices + ) != len(messages): + raise ValueError( + "If one of the inputs parameter_indices, paramater_values or " + "messages is a list, then all must be lists of the same size, " + "but got len(parameter_indices), len(parameter_values), " + f"len(messages) [{len(parameter_indices)}, " + f"{len(parameter_values)}, {len(messages)}]" + ) + if not (isinstance(parameter_indices, list)): + parameter_indices = [parameter_indices] + self._parameter_indices = parameter_indices + + if not (isinstance(parameter_values, list)): + parameter_values = [parameter_values] + self._parameter_values = parameter_values + + if not (isinstance(messages, list)): + messages = [messages] + self._messages = messages + + def message(self) -> str: + message = "" + for i in range(len(self._parameter_indices)): + message += ( + Printer.color_assert_failed(f"output {self._parameter_indices[i]}: ") + + Printer.color_assert_failed(f"{self._parameter_values[i]}\n") + + Printer.color_assert_failed(self._messages[i]) + ) + return message + + @property + def assert_name(self) -> str: + return self._assert_name + + @property + def successful(self): + return len(self._parameter_indices) == 0 diff --git a/src/scwidgets/check/_widget_check_registry.py b/src/scwidgets/check/_widget_check_registry.py index 4e36448..48d222f 100644 --- a/src/scwidgets/check/_widget_check_registry.py +++ b/src/scwidgets/check/_widget_check_registry.py @@ -18,12 +18,17 @@ class CheckableWidget: :param check_registry: the check registry that registers the checks for this widget + + :param name: + Optional name of the widget that is shown in the messages of the checks """ - def __init__(self, check_registry: Optional[CheckRegistry]): + def __init__( + self, check_registry: Optional[CheckRegistry], name: Optional[str] = None + ): self._check_registry = check_registry if self._check_registry is not None: - self._check_registry.register_widget(self) + self._check_registry.register_widget(self, name) def compute_output_to_check( self, *input_args: Check.FunInParamT @@ -72,6 +77,7 @@ def _add_check_from_check_parameters( fingerprint: Optional[ Callable[[Check.FunOutParamsT], Check.FingerprintT] ] = None, + suppress_fingerprint_asserts: bool = True, ): if self._check_registry is None: raise ValueError( @@ -79,7 +85,12 @@ def _add_check_from_check_parameters( ) self._check_registry.add_check( - self, asserts, inputs_parameters, outputs_references, fingerprint + self, + asserts, + inputs_parameters, + outputs_references, + fingerprint, + suppress_fingerprint_asserts, ) def compute_and_set_references(self): @@ -186,6 +197,7 @@ def add_check( fingerprint: Optional[ Callable[[Check.FunOutParamsT], Check.FingerprintT] ] = None, + suppress_fingerprint_asserts: bool = True, ): if not (issubclass(type(widget), CheckableWidget)): raise ValueError("Argument widget must be subclass of CheckableWidget") @@ -199,6 +211,7 @@ def add_check( inputs_parameters, outputs_references, fingerprint, + suppress_fingerprint_asserts, ) self._checks[widget].append(check) @@ -265,19 +278,26 @@ def _on_click_check_all_widgets_button(self, change: dict): with self._output: if isinstance(widget_results, Exception): Printer.print_error_message( - f"Widget {self._names[widget]} " f"raised error:" + Printer.format_title_message( + f"Widget {self._names[widget]} " f"raised error:" + ) ) raise widget_results elif isinstance(widget_results, ChecksLog): if widget_results.successful: Printer.print_success_message( - f"Widget {self._names[widget]} all checks " - f"were successful." + Printer.format_title_message( + f"Widget {self._names[widget]} all checks " + f"were successful" + ) ) + print(widget_results.message()) else: Printer.print_error_message( - f"Widget {self._names[widget]} not all checks were " - "successful:" + Printer.format_title_message( + f"Widget {self._names[widget]} not all checks were " + "successful:" + ) ) print(widget_results.message()) else: diff --git a/src/scwidgets/code/_widget_code_demo.py b/src/scwidgets/code/_widget_code_demo.py index fe28198..3425e30 100644 --- a/src/scwidgets/code/_widget_code_demo.py +++ b/src/scwidgets/code/_widget_code_demo.py @@ -142,7 +142,7 @@ def __init__( elif not (isinstance(cue_outputs, list)): cue_outputs = [cue_outputs] - CheckableWidget.__init__(self, check_registry) + CheckableWidget.__init__(self, check_registry, answer_key) AnswerWidget.__init__(self, answer_registry, answer_key) self._code = code @@ -564,9 +564,12 @@ def handle_checks_result(self, result: Union[ChecksLog, Exception]): raise result elif isinstance(result, ChecksLog): if result.successful: - Printer.print_success_message("Check was successful.") + Printer.print_success_message("Check was successful") + Printer.print_success_message("--------------------") + print(result.message()) else: - Printer.print_error_message("Check failed:") + Printer.print_error_message("Check failed") + Printer.print_error_message("------------") print(result.message()) else: print(result) diff --git a/tests/test_check.py b/tests/test_check.py index de284df..ed13d98 100644 --- a/tests/test_check.py +++ b/tests/test_check.py @@ -19,7 +19,7 @@ def test_assert_shape(): output_parameters = (np.array([1, 2, 3]),) output_references = (np.array([1, 2, 3]),) result = assert_shape(output_parameters, output_references) - assert result == "" + assert result.successful def test_assert_invalid_parameters_to_check(): @@ -33,29 +33,28 @@ def test_assert_numpy_allclose(): output_parameters = (np.array([1.0, 2.0]),) output_references = (np.array([1.1, 2.2]),) result = assert_numpy_allclose(output_parameters, output_references) - assert "Output parameter 0 is not close to reference" in result + assert "Output is not close to reference" in result.message() def test_assert_type(): output_parameters = (42,) output_references = (42,) result = assert_type(output_parameters, output_references) - assert result == "" + assert result.successful def test_assert_numpy_floating_sub_dtype(): output_parameters = (np.array([1.0, 2.0]),) result = assert_numpy_floating_sub_dtype(output_parameters) - assert result == "" + assert result.successful def test_assert_invalid_output_parameter_dtype(): output_parameters = (np.array([1, 2]),) result = assert_numpy_floating_sub_dtype(output_parameters) - assert result == ( - "Output parameter 0 expected to be sub dtype numpy.floating " - "but got numpy.int64." - ) + assert ( + "Output expected to be sub dtype numpy.floating " "but got numpy.int64." + ) in result.message() def single_param_check(use_fingerprint=False, failing=False, buggy=False): diff --git a/tests/test_widgets.py b/tests/test_widgets.py index e6497c0..a2acd34 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -934,33 +934,33 @@ def test_button_clicks( # Test 1.1 use_fingerprint=False, failing=False, buggy=False test_button_clicks( nb_cells[3], - "Widget 1 all checks were successful.", - "Successfully set all references.", - "Widget 1 all checks were successful.", + "Widget 1 all checks were successful", + "Successfully set all references", + "Widget 1 all checks were successful", ) # Test 1.2 use_fingerprint=True, failing=False, buggy=False test_button_clicks( nb_cells[4], - "Widget 1 all checks were successful.", - "Successfully set all references.", - "Widget 1 all checks were successful.", + "Widget 1 all checks were successful", + "Successfully set all references", + "Widget 1 all checks were successful", ) # Test 1.3 use_fingerprint=False, failing=False, buggy=False test_button_clicks( nb_cells[5], "Widget 1 not all checks were successful", - "Successfully set all references.", - "Widget 1 all checks were successful.", + "Successfully set all references", + "Widget 1 all checks were successful", ) # Test 1.4 use_fingerprint=False, failing=False, buggy=False test_button_clicks( nb_cells[6], "Widget 1 not all checks were successful", - "Successfully set all references.", - "Widget 1 all checks were successful.", + "Successfully set all references", + "Widget 1 all checks were successful", ) # Test 1.5 use_fingerprint=False, failing=False, buggy=True From 266c5a7f44e270c7fa7a5c01214555ff11c587bb Mon Sep 17 00:00:00 2001 From: Alexander Goscinski Date: Mon, 25 Dec 2023 16:04:48 +0100 Subject: [PATCH 2/2] CueObjects now accepts None as default parameter --- src/scwidgets/cue/_widget_cue_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scwidgets/cue/_widget_cue_object.py b/src/scwidgets/cue/_widget_cue_object.py index 8f3cfca..35f8022 100644 --- a/src/scwidgets/cue/_widget_cue_object.py +++ b/src/scwidgets/cue/_widget_cue_object.py @@ -35,7 +35,7 @@ class CueObject(CueOutput): def __init__( self, - display_object: Any, + display_object: Any = None, widgets_to_observe: Union[None, List[Widget], Widget] = None, traits_to_observe: Union[ None, str, List[str], List[List[str]], Sentinel