Skip to content

Commit

Permalink
Prevent race condition while loading JS deps; add magic.inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
jcheng5 committed Nov 9, 2023
1 parent 05172bc commit 8ec8f05
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 17 deletions.
38 changes: 29 additions & 9 deletions jupyterlab_extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
'A JupyterLab extension for running interactive Shiny applications directly within notebooks.',
autoStart: true,
requires: [INotebookTracker, IRenderMimeRegistry],
activate: (
activate: async (
app: JupyterFrontEnd,
notebookTracker: INotebookTracker,
renderMimeRegistry: IRenderMimeRegistry
Expand Down Expand Up @@ -90,28 +90,48 @@ const plugin: JupyterFrontEndPlugin<void> = {
return window.Shiny.superclient;
};

injectShinyDependencies();
await injectShinyDependencies();
}
};

function injectShinyDependencies() {
async function injectShinyDependencies() {
document.head.append(
tag('link', { rel: 'stylesheet', href: '/shiny/shared/shiny.min.css' }),
tag('script', { src: '/shiny/shared/jquery/jquery-3.6.0.js' }),
tag('script', { src: '/shiny/shared/shiny.js' }),
tag('link', {
href: '/shiny/shared/bootstrap/bootstrap.min.css',
rel: 'stylesheet'
}),
tag('script', { src: '/shiny/shared/bootstrap/bootstrap.bundle.min.js' }),
tag('script', {
src: '/shiny/shared/ionrangeslider/js/ion.rangeSlider.min.js'
}),
tag('link', {
href: '/shiny/shared/ionrangeslider/css/ion.rangeSlider.css',
rel: 'stylesheet'
})
);

// Dynamically inserted <script> tags aren't guaranteed to load in order, so we need
// to serialize them using each one's load event.
const scripts = [
'/shiny/shared/jquery/jquery-3.6.0.js',
'/shiny/shared/shiny.js',
'/shiny/shared/bootstrap/bootstrap.bundle.min.js',
'/shiny/shared/ionrangeslider/js/ion.rangeSlider.min.js'
];
for (const script of scripts) {
await loadScript(script);
}
}

async function loadScript(src: string): Promise<void> {
let scriptEl = tag('script', { type: 'text/javascript', src });
document.head.appendChild(scriptEl);
return new Promise((resolve, reject) => {
scriptEl.onload = e => {
console.log(`Loaded ${src}`);
resolve();
};
scriptEl.onerror = e => {
reject(new Error(`Failed to load JS script: ${src}`));
};
});
}

export default plugin;
Expand Down
2 changes: 2 additions & 0 deletions shiny/notebook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ async def proceed():

asyncio.create_task(proceed())

ipython.ast_node_interactivity = "all"
print('Setting InteractiveShell.ast_node_interactivity="all"')
print("Shiny is running")


Expand Down
59 changes: 51 additions & 8 deletions shiny/notebook/magic.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,53 @@
from __future__ import annotations

# pyright: reportUnknownVariableType=false, reportUnknownMemberType=false, reportUntypedFunctionDecorator=false
import asyncio
import uuid
from typing import Any, cast

from IPython.core.display_functions import 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 ui

NumberType = int | float
InputTypes = None | str | NumberType | bool | list[str] | tuple[NumberType, NumberType]


def inputs(**kwargs: InputTypes):
return ui.div(*[make_input(k, v) for k, v in kwargs.items()])


def make_input(name: str, value: InputTypes, *, label: str | None = None):
if label is None:
label = name

# bool must be before int, it is one!?
if value is None:
return ui.input_action_button(name, label, class_="btn-primary")
if isinstance(value, bool):
return ui.input_switch(name, label, value)
elif isinstance(value, str):
return ui.input_text(name, label, value)
elif isinstance(value, (int, float)):
return ui.input_numeric(name, label, value)
elif isinstance(value, list):
return ui.input_select(name, label, value, selected=value[0])
elif isinstance(value, tuple):
if len(value) == 2:
return ui.input_slider(name, label, value[0], value[1], value[0])
elif len(value) == 3:
return ui.input_slider(name, label, value[0], value[1], value[2])
else:
raise TypeError(
f"Tuple must have 2 or 3 elements, got {len(value)} elements"
)
else:
raise TypeError(f"Unsupported type {type(value)}")


@magic_arguments()
@argument("name", type=str, nargs="?", help="Name of the reactive calc")
Expand All @@ -33,26 +74,28 @@ def reactive(line: str, cell: str):

@shiny_reactive.Calc
def calc():
res = ipy.run_cell(cell)
res = ipy.run_cell(cell, silent=False)
if res.success:
return res.result
else:
if execution_result.error_before_exec:
raise execution_result.error_before_exec
if execution_result.error_in_exec:
raise execution_result.error_in_exec
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)

try:
update_display(calc(), display_id=display_id)
except Exception as e:
update_display(e, display_id=display_id)

@shiny_reactive.Effect
def _():
from IPython.core.display_functions import update_display

# TODO: Handle errors
try:
update_display(calc(), display_id=display_id)
except Exception as e:
Expand Down

0 comments on commit 8ec8f05

Please sign in to comment.