Skip to content

Commit

Permalink
Prevent duplicate cell output
Browse files Browse the repository at this point in the history
  • Loading branch information
jcheng5 committed Oct 10, 2023
1 parent e540258 commit d1cd2fe
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 30 deletions.
4 changes: 4 additions & 0 deletions shiny/notebook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Expand Down
77 changes: 47 additions & 30 deletions shiny/notebook/magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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)

0 comments on commit d1cd2fe

Please sign in to comment.