diff --git a/shiny/notebook/__init__.py b/shiny/notebook/__init__.py index 790b0b49d..f664b4e61 100644 --- a/shiny/notebook/__init__.py +++ b/shiny/notebook/__init__.py @@ -72,7 +72,11 @@ async def proceed(): asyncio.create_task(proceed()) ipython.ast_node_interactivity = "all" + ipython.run_cell("from shiny import ui, render, reactive") + ipython.run_cell("from shiny.notebook.magic import inputs") print('Setting InteractiveShell.ast_node_interactivity="all"') + print("from shiny import ui, render, reactive") + print("from shiny.notebook.magic import inputs") print("Shiny is running") diff --git a/shiny/notebook/magic.py b/shiny/notebook/magic.py index c25c16f9b..a898e5b3d 100644 --- a/shiny/notebook/magic.py +++ b/shiny/notebook/magic.py @@ -3,16 +3,20 @@ # pyright: reportUnknownVariableType=false, reportUnknownMemberType=false, reportUntypedFunctionDecorator=false import asyncio import uuid +from contextlib import contextmanager from typing import Any, cast -from IPython.core.display_functions import update_display +from IPython.core.display_functions import display, update_display from IPython.core.getipython import get_ipython from IPython.core.magic import register_cell_magic from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring from IPython.display import HTML, clear_output +from shiny import reactive as shiny_reactive from shiny import ui +from .log import logger + NumberType = int | float InputTypes = None | str | NumberType | bool | list[str] | tuple[NumberType, NumberType] @@ -60,46 +64,49 @@ def make_input(name: str, value: InputTypes, *, label: str | None = None): ) @register_cell_magic def reactive(line: str, cell: str): - clear_output() - args = parse_argstring(reactive, line) - reactive_name = args.name or f"__anonymous_{uuid.uuid4().hex}" + reactive_name: str | None = args.name # TODO: If line/cell magics are still in the cell, error. - # indented = re.sub(r"(^|\r?\n)", r"\1 ", cell) ipy = get_ipython() - from shiny import reactive as shiny_reactive + # This basically captures a reference to "the cell that's executing us" while we're + # still in the main IPython event loop. We need to re-install this whenever we + # re-execute, so that a reactive's output always goes to its originating cell. + parent_msg = ipy.kernel.get_parent() @shiny_reactive.Calc def calc(): - res = ipy.run_cell(cell, silent=False) - if res.success: - return res.result - else: - if res.error_before_exec: - raise res.error_before_exec - if res.error_in_exec: - raise res.error_in_exec - - ipy.push({reactive_name: calc}) - - if not args.no_echo: - display_id = f"__{reactive_name}_output_display__" - display(HTML(""), display_id=display_id) - + # Temporarily install the parent message so that the cell's output goes to the + # right place. + with set_parent(ipy.kernel.shell, parent_msg): + clear_output(wait=True) + res = ipy.run_cell(cell) + + if reactive_name is not None: + # The output of the cell would've been displayed by now. The purpose of the rest + # of this is in case the magic was given a name, so that other code can consume + # the return value. + if res.success: + return res.result + else: + if res.error_before_exec: + raise res.error_before_exec + if res.error_in_exec: + raise res.error_in_exec + + if reactive_name is not None: + ipy.push({reactive_name: calc}) + + @shiny_reactive.Effect + def _(): + # TODO: destroy this reactive if this cell is ever re-executed try: - update_display(calc(), display_id=display_id) + calc() except Exception as e: - update_display(e, display_id=display_id) - - @shiny_reactive.Effect - def _(): - try: - update_display(calc(), display_id=display_id) - except Exception as e: - update_display(e, display_id=display_id) + # TODO: Where to log!?!? + logger.exception(e) async def flush(): async with shiny_reactive.lock(): @@ -149,3 +156,13 @@ async def flush(): # code = code.replace("{reactive_name}", reactive_name).replace("{cell}", cell) # ipy.run_cell(code) + + +@contextmanager +def set_parent(shell, parent_msg): + old_parent = shell.get_parent() + shell.set_parent(parent_msg) + try: + yield + finally: + shell.set_parent(old_parent)