diff --git a/tutorial/tests/testsuite/helpers.py b/tutorial/tests/testsuite/helpers.py index 5c7f17c5..4dddf1e1 100644 --- a/tutorial/tests/testsuite/helpers.py +++ b/tutorial/tests/testsuite/helpers.py @@ -11,6 +11,7 @@ from IPython.display import Code from IPython.display import display as ipython_display from ipywidgets import HTML +from testsuite import iPythonGenerator from .ai_helpers import AIExplanation, OpenAIWrapper @@ -377,6 +378,9 @@ class IPytestResult: class TestResultOutput: """Class to prepare and display test results in a Jupyter notebook""" + def __init__(self, output_generator=iPythonGenerator()) -> None: + self.output_generator = output_generator + ipytest_result: IPytestResult solution: Optional[str] = None MAX_ATTEMPTS: ClassVar[int] = 3 @@ -409,29 +413,12 @@ def display_results(self) -> None: self.MAX_ATTEMPTS - self.ipytest_result.test_attempts ) cells.append( - HTML( - '
' - f'
' - '📝' - 'Solution will be available after ' - f'{attempts_remaining} more failed attempt{"s" if attempts_remaining > 1 else ""}' - "
" - "
" + self.output_generator.attempts_remaining_explanation( + attempts_remaining ) ) - ipython_display( - ipywidgets.VBox( - children=cells, - layout={ - "border": "1px solid #e5e7eb", - "background-color": "#ffffff", - "margin": "5px", - "padding": "0.75rem", - "border-radius": "0.5rem", - }, - ) - ) + iPythonGenerator.display_cells(cells) # TODO: This is left for reference if we ever want to bring back this styling # Perhaps we should remove it if it's unnecessary diff --git a/tutorial/tests/testsuite/testsuite.py b/tutorial/tests/testsuite/testsuite.py index 2bb8398f..b18b5240 100644 --- a/tutorial/tests/testsuite/testsuite.py +++ b/tutorial/tests/testsuite/testsuite.py @@ -6,6 +6,7 @@ import io import os import pathlib +from abc import ABC, abstractmethod from collections import defaultdict from contextlib import contextmanager, redirect_stderr, redirect_stdout from queue import Queue @@ -13,11 +14,13 @@ from typing import Dict, List, Optional import ipynbname +import ipywidgets import pytest from dotenv import find_dotenv, load_dotenv from IPython.core.interactiveshell import InteractiveShell from IPython.core.magic import Magics, cell_magic, magics_class -from IPython.display import HTML, display +from IPython.display import HTML +from IPython.display import display as ipython_display from .ai_helpers import OpenAIWrapper from .ast_parser import AstParser @@ -143,11 +146,58 @@ def get_module_name(line: str, globals_dict: Dict) -> str | None: return module_name +class OutputGenerator(ABC): + @abstractmethod + def display(self, content: str) -> None: + pass + + @abstractmethod + def attempts_remaining_explanation(attempts_remaining: int) -> str: + pass + + +class iPythonGenerator(OutputGenerator): + def attempts_remaining_explanation(attempts_remaining: int) -> str: + return ( + '
' + f'
' + '📝' + 'Solution will be available after ' + f'{attempts_remaining} more failed attempt{"s" if attempts_remaining > 1 else ""}' + "
" + "
" + ) + + def display_cells(cells): + html_cells = [HTML(cell) for cell in cells] + ipython_display( + ipywidgets.VBox( + children=html_cells, + layout={ + "border": "1px solid #e5e7eb", + "background-color": "#ffffff", + "margin": "5px", + "padding": "0.75rem", + "border-radius": "0.5rem", + }, + ) + ) + return None + + +class DebugGenerator(OutputGenerator): + def attempts_remaining_explanation(attempts_remaining: int) -> str: + return f"{attempts_remaining}" + + def display_cells(cells): + return cells + + @magics_class class TestMagic(Magics): """Class to add the test cell magic""" - def __init__(self, shell): + def __init__(self, shell, output_generator=iPythonGenerator()): super().__init__(shell) self.shell: InteractiveShell = shell self.cell: str = "" @@ -160,6 +210,7 @@ def __init__(self, shell): ) self._orig_traceback = self.shell._showtraceback # type: ignore # This is monkey-patching suppress printing any exception or traceback + self.output_generator = output_generator def extract_functions_to_test(self) -> List[AFunction]: """Retrieve the functions names and implementations defined in the current cell""" @@ -311,7 +362,7 @@ def ipytest(self, line: str, cell: str): module_file=self.module_file, results=results, ) - display(HTML(debug_output.to_html())) + output = self.output_generator.display(debug_output) # Parse the AST of the test module to retrieve the solution code ast_parser = AstParser(self.module_file) @@ -329,7 +380,7 @@ def ipytest(self, line: str, cell: str): ).display_results() -def load_ipython_extension(ipython): +def load_ipython_extension(ipython, output_generator): """ Any module file that define a function named `load_ipython_extension` can be loaded via `%load_ext module.path` or be configured to be @@ -377,13 +428,11 @@ def load_ipython_extension(ipython): ) message_color = "#ffebee" - display( - HTML( - "
" - f"{message}" - "
" - ) + output_generator.display( + "
" + f"{message}" + "
" ) # Register the magic @@ -393,4 +442,4 @@ def load_ipython_extension(ipython): "
" "🔄 IPytest extension (re)loaded.
" ) - display(HTML(message)) + output_generator.display(message)