diff --git a/.vscode/settings.json b/.vscode/settings.json
index 472078d24..1a74bfc58 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,6 +8,12 @@
"editor.formatOnSave": true,
"editor.tabSize": 4,
},
+ "python.analysis.diagnosticSeverityOverrides": {
+ "reportImportCycles": "none",
+ "reportUnusedFunction": "none",
+ "reportPrivateUsage": "none",
+ "reportUnnecessaryIsInstance": "none"
+ },
"editor.rulers": [
88
],
diff --git a/HISTORY.rst b/HISTORY.rst
index fb803ef3b..6e77dd902 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -5,4 +5,4 @@ History
0.0.0.9000 (2021-08-10)
------------------
-* First release on PyPI.
+* Initial version
diff --git a/examples/bind_event/app.py b/examples/bind_event/app.py
index 3c1d197fc..82fa0c42a 100644
--- a/examples/bind_event/app.py
+++ b/examples/bind_event/app.py
@@ -1,6 +1,9 @@
+import asyncio
+import shiny.ui_toolkit as st
from shiny import *
+from htmltools import tags
-ui = page_fluid(
+ui = st.page_fluid(
tags.p(
"""
The first time you click the button, you should see a 1 appear below the button,
@@ -9,68 +12,72 @@
print the number of clicks in the console twice.
"""
),
- navs_tab_card(
- nav(
+ st.navs_tab_card(
+ st.nav(
"Sync",
- input_action_button("btn", "Click me"),
- output_ui("foo"),
+ st.input_action_button("btn", "Click me"),
+ st.output_ui("btn_value"),
),
- nav(
+ st.nav(
"Async",
- input_action_button("btn_async", "Click me"),
- output_ui("foo_async"),
+ st.input_action_button("btn_async", "Click me"),
+ st.output_ui("btn_async_value"),
),
),
)
-def server(session: ShinySession):
+def server(input: Inputs, output: Outputs, session: Session):
# i.e., observeEvent(once=False)
- @observe()
- @event(lambda: session.input["btn"])
+ @reactive.effect()
+ @event(input.btn)
def _():
- print("@observe() event: ", str(session.input["btn"]))
+ print("@effect() event: ", str(input.btn()))
# i.e., eventReactive()
- @reactive()
- @event(lambda: session.input["btn"])
+ @reactive.calc()
+ @event(input.btn)
def btn() -> int:
- return session.input["btn"]
+ return input.btn()
- @observe()
+ @reactive.effect()
def _():
- print("@reactive() event: ", str(btn()))
+ print("@calc() event: ", str(btn()))
- @session.output("foo")
+ @output()
@render_ui()
- @event(lambda: session.input["btn"])
- def _():
- return session.input["btn"]
+ @event(input.btn)
+ def btn_value():
+ return str(input.btn())
# -----------------------------------------------------------------------------
# Async
# -----------------------------------------------------------------------------
- @observe_async()
- @event(lambda: session.input["btn_async"])
+ @reactive.effect_async()
+ @event(input.btn_async)
async def _():
- print("@observe_async() event: ", str(session.input["btn_async"]))
+ await asyncio.sleep(0)
+ print("@effect_async() event: ", str(input.btn_async()))
- @reactive_async()
- @event(lambda: session.input["btn_async"])
- async def btn_async() -> int:
- return session.input["btn_async"]
+ @reactive.calc_async()
+ @event(input.btn_async)
+ async def btn_async_r() -> int:
+ await asyncio.sleep(0)
+ return input.btn_async()
- @observe_async()
+ @reactive.effect_async()
async def _():
- val = await btn_async()
- print("@reactive_async() event: ", str(val))
+ val = await btn_async_r()
+ print("@calc_async() event: ", str(val))
- @session.output("foo_async")
+ @output()
@render_ui()
- @event(lambda: session.input["btn_async"])
- async def _():
- return session.input["btn_async"]
+ @event(btn_async_r)
+ async def btn_async_value():
+ val = await btn_async_r()
+ print("== " + str(val))
+ return str(val)
-ShinyApp(ui, server).run()
+app = App(ui, server, debug=True)
diff --git a/examples/download/app.py b/examples/download/app.py
index 02171a89f..744157672 100644
--- a/examples/download/app.py
+++ b/examples/download/app.py
@@ -4,28 +4,32 @@
# Then point web browser to:
# http://localhost:8000/
import asyncio
-from datetime import date
import os
import io
-import matplotlib.pyplot as plt
-import numpy as np
+from datetime import date
+from typing import Any
+import shiny.ui_toolkit as st
from shiny import *
+from htmltools import *
+
+import matplotlib.pyplot as plt
+import numpy as np
def make_example(id: str, label: str, title: str, desc: str, extra: Any = None):
- return column(
- 3,
- tags.div(
+ return st.column(
+ 4,
+ div(
class_="card mb-4",
children=[
- tags.div(title, class_="card-header"),
- tags.div(
+ div(title, class_="card-header"),
+ div(
class_="card-body",
children=[
- tags.p(desc, class_="card-text text-muted"),
+ p(desc, class_="card-text text-muted"),
extra,
- download_button(id, label, class_="btn-primary"),
+ st.download_button(id, label, class_="btn-primary"),
],
),
],
@@ -33,9 +37,8 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None):
)
-ui = page_fluid(
- tags.h1("Download examples"),
- row(
+ui = st.page_fluid(
+ st.row(
make_example(
"download1",
label="Download CSV",
@@ -43,19 +46,19 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None):
desc="Downloads a pre-existing file, using its existing name on disk.",
),
),
- row(
+ st.row(
make_example(
"download2",
label="Download plot",
title="Dynamic data generation",
desc="Downloads a PNG that's generated on the fly.",
extra=[
- input_text("title", "Plot title", "Random scatter plot"),
- input_slider("num_points", "Number of data points", 1, 100, 50),
+ st.input_text("title", "Plot title", "Random scatter plot"),
+ st.input_slider("num_points", "Number of data points", 1, 100, 50),
],
),
),
- row(
+ st.row(
make_example(
"download3",
"Download",
@@ -63,7 +66,7 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None):
"Demonstrates that filenames can be generated on the fly (and use Unicode characters!).",
),
),
- row(
+ st.row(
make_example(
"download4",
"Download",
@@ -71,7 +74,7 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None):
"Throws an error in the download handler, download should not succeed.",
),
),
- row(
+ st.row(
make_example(
"download5",
"Download",
@@ -82,7 +85,7 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None):
)
-def server(session: ShinySession):
+def server(input: Inputs, output: Outputs, session: Session):
@session.download()
def download1():
"""
@@ -107,7 +110,7 @@ def download2():
y = np.random.uniform(size=session.input["num_points"])
plt.figure()
plt.scatter(x, y)
- plt.title(session.input["title"])
+ plt.title(input.title())
with io.BytesIO() as buf:
plt.savefig(buf, format="png")
yield buf.getvalue()
@@ -127,9 +130,4 @@ async def _():
raise Exception("This error was caused intentionally")
-app = ShinyApp(ui, server)
-
-if __name__ == "__main__":
- app.run()
- # Alternately, to listen on a TCP port:
- # app.run(conn_type = "tcp")
+app = App(ui, server)
diff --git a/examples/dynamic_ui/app.py b/examples/dynamic_ui/app.py
index cd43e0e5d..bc7db2171 100644
--- a/examples/dynamic_ui/app.py
+++ b/examples/dynamic_ui/app.py
@@ -4,66 +4,66 @@
# Then point web browser to:
# http://localhost:8000/
+import shiny.ui_toolkit as st
from shiny import *
+from htmltools import *
# For plot rendering
import numpy as np
import matplotlib.pyplot as plt
-ui = page_fluid(
- layout_sidebar(
- panel_sidebar(
- h2("Dynamic UI"),
- output_ui("ui"),
- input_action_button("btn", "Trigger insert/remove ui"),
+ui = st.page_fluid(
+ st.layout_sidebar(
+ st.panel_sidebar(
+ st.h2("Dynamic UI"),
+ st.output_ui("ui"),
+ st.input_action_button("btn", "Trigger insert/remove ui"),
),
- panel_main(
- output_text_verbatim("txt"),
- output_plot("plot"),
+ st.panel_main(
+ st.output_text_verbatim("txt"),
+ st.output_plot("plot"),
),
),
)
-def server(session: ShinySession):
- @reactive()
+def server(input: Inputs, output: Outputs, session: Session):
+ @reactive.calc()
def r():
- if session.input["n"] is None:
+ if input.n() is None:
return
- return session.input["n"] * 2
+ return input.n() * 2
- @session.output("txt")
- async def _():
+ @output()
+ @render_text()
+ async def txt():
val = r()
- return f"n*2 is {val}, session id is {get_current_session().id}"
+ return f"n*2 is {val}, session id is {session.id}"
- @session.output("plot")
+ @output()
@render_plot(alt="A histogram")
- def _():
+ def plot():
np.random.seed(19680801)
x = 100 + 15 * np.random.randn(437)
fig, ax = plt.subplots()
- ax.hist(x, session.input["n"], density=True)
+ ax.hist(x, input.n(), density=True)
return fig
- @session.output("ui")
+ @output()
@render_ui()
- def _():
- return input_slider("This slider is rendered via @render_ui()", "N", 0, 100, 20)
+ def ui():
+ return st.input_slider(
+ "This slider is rendered via @render_ui()", "N", 0, 100, 20
+ )
- @observe()
+ @reactive.effect()
def _():
- btn = session.input["btn"]
+ btn = input.btn()
if btn % 2 == 1:
ui_insert(tags.p("Thanks for clicking!", id="thanks"), "body")
elif btn > 0:
ui_remove("#thanks")
-app = ShinyApp(ui, server)
-
-if __name__ == "__main__":
- app.run()
- # Alternately, to listen on a TCP port:
- # app.run(conn_type = "tcp")
+app = App(ui, server)
diff --git a/examples/inputs-update/app.py b/examples/inputs-update/app.py
index 45633c40e..63087f6fd 100644
--- a/examples/inputs-update/app.py
+++ b/examples/inputs-update/app.py
@@ -4,50 +4,54 @@
# Then point web browser to:
# http://localhost:8000/
-from shiny import *
+from datetime import date
-ui = page_fluid(
- panel_title("Changing the values of inputs from the server"),
- row(
- column(
- 3,
- panel_well(
+import shiny.ui_toolkit as st
+from shiny import *
+from htmltools import *
+
+ui = st.page_fluid(
+ st.panel_title("Changing the values of inputs from the server"),
+ st.row(
+ st.column(
+ 4,
+ st.panel_well(
tags.h4("These inputs control the other inputs on the page"),
- input_text(
+ st.input_text(
"control_label", "This controls some of the labels:", "LABEL TEXT"
),
- input_slider(
+ st.input_slider(
"control_num", "This controls values:", min=1, max=20, value=15
),
),
),
- column(
- 3,
- panel_well(
+ st.column(
+ 4,
+ st.panel_well(
tags.h4("These inputs are controlled by the other inputs"),
- input_text("inText", "Text input:", value="start text"),
- input_numeric(
+ st.input_text("inText", "Text input:", value="start text"),
+ st.input_numeric(
"inNumber", "Number input:", min=1, max=20, value=5, step=0.5
),
- input_numeric(
+ st.input_numeric(
"inNumber2", "Number input 2:", min=1, max=20, value=5, step=0.5
),
- input_slider("inSlider", "Slider input:", min=1, max=20, value=15),
- input_slider(
+ st.input_slider("inSlider", "Slider input:", min=1, max=20, value=15),
+ st.input_slider(
"inSlider2", "Slider input 2:", min=1, max=20, value=(5, 15)
),
- input_slider(
+ st.input_slider(
"inSlider3", "Slider input 3:", min=1, max=20, value=(5, 15)
),
- input_date("inDate", "Date input:"),
- input_date_range("inDateRange", "Date range input:"),
+ st.input_date("inDate", "Date input:"),
+ st.input_date_range("inDateRange", "Date range input:"),
),
),
- column(
- 3,
- panel_well(
- input_checkbox("inCheckbox", "Checkbox input", value=False),
- input_checkbox_group(
+ st.column(
+ 4,
+ st.panel_well(
+ st.input_checkbox("inCheckbox", "Checkbox input", value=False),
+ st.input_checkbox_group(
"inCheckboxGroup",
"Checkbox group input:",
{
@@ -55,7 +59,7 @@
"option2": "label 2",
},
),
- input_radio_buttons(
+ st.input_radio_buttons(
"inRadio",
"Radio buttons:",
{
@@ -63,7 +67,7 @@
"option2": "label 2",
},
),
- input_select(
+ st.input_select(
"inSelect",
"Select input:",
{
@@ -71,7 +75,7 @@
"option2": "label 2",
},
),
- input_select(
+ st.input_select(
"inSelect2",
"Select input 2:",
{
@@ -81,9 +85,9 @@
multiple=True,
),
),
- navs_tab(
- nav("panel1", h2("This is the first panel.")),
- nav("panel2", h2("This is the second panel.")),
+ st.navs_tab(
+ st.nav("panel1", h2("This is the first panel.")),
+ st.nav("panel2", h2("This is the second panel.")),
id="inTabset",
),
),
@@ -91,17 +95,17 @@
)
-def server(sess: ShinySession):
- @observe()
+def server(input: Inputs, output: Outputs, session: Session):
+ @reactive.effect()
def _():
# We'll use these multiple times, so use short var names for
# convenience.
- c_label = sess.input["control_label"]
- c_num = sess.input["control_num"]
+ c_label = input.control_label()
+ c_num = input.control_num()
# Text =====================================================
# Change both the label and the text
- update_text(
+ st.update_text(
"inText",
label="New " + c_label,
value="New text " + str(c_num),
@@ -109,10 +113,10 @@ def _():
# Number ===================================================
# Change the value
- update_numeric("inNumber", value=c_num)
+ st.update_numeric("inNumber", value=c_num)
# Change the label, value, min, and max
- update_numeric(
+ st.update_numeric(
"inNumber2",
label="Number " + c_label,
value=c_num,
@@ -123,23 +127,23 @@ def _():
# Slider input =============================================
# Only label and value can be set for slider
- update_slider("inSlider", label="Slider " + c_label, value=c_num)
+ st.update_slider("inSlider", label="Slider " + c_label, value=c_num)
# Slider range input =======================================
# For sliders that pick out a range, pass in a vector of 2
# values.
- update_slider("inSlider2", value=(c_num - 1, c_num + 1))
+ st.update_slider("inSlider2", value=(c_num - 1, c_num + 1))
# Only change the upper handle
- update_slider("inSlider3", value=(sess.input["inSlider3"][0], c_num + 2))
+ st.update_slider("inSlider3", value=(input.inSlider3()[0], c_num + 2))
# Date input ===============================================
# Only label and value can be set for date input
- update_date("inDate", label="Date " + c_label, value=date(2013, 4, c_num))
+ st.update_date("inDate", label="Date " + c_label, value=date(2013, 4, c_num))
# Date range input =========================================
# Only label and value can be set for date range input
- update_date_range(
+ st.update_date_range(
"inDateRange",
label="Date range " + c_label,
start=date(2013, 1, c_num),
@@ -149,7 +153,7 @@ def _():
)
# # Checkbox ===============================================
- update_checkbox("inCheckbox", value=c_num % 2)
+ st.update_checkbox("inCheckbox", value=c_num % 2)
# Checkbox group ===========================================
# Create a list of new options, where the name of the items
@@ -160,7 +164,7 @@ def _():
opts_dict = dict(zip(opt_vals, opt_labels))
# Set the label, choices, and selected item
- update_checkbox_group(
+ st.update_checkbox_group(
"inCheckboxGroup",
label="Checkbox group " + c_label,
choices=opts_dict,
@@ -168,7 +172,7 @@ def _():
)
# Radio group ==============================================
- update_radio_buttons(
+ st.update_radio_buttons(
"inRadio",
label="Radio " + c_label,
choices=opts_dict,
@@ -178,7 +182,7 @@ def _():
# Create a list of new options, where the name of the items
# is something like 'option label x A', and the values are
# 'option-x-A'.
- update_select(
+ st.update_select(
"inSelect",
label="Select " + c_label,
choices=opts_dict,
@@ -187,7 +191,7 @@ def _():
# Can also set the label and select an item (or more than
# one if it's a multi-select)
- update_select(
+ st.update_select(
"inSelect2",
label="Select label " + c_label,
choices=opts_dict,
@@ -197,10 +201,11 @@ def _():
# Tabset input =============================================
# Change the selected tab.
# The tabsetPanel must have been created with an 'id' argument
- update_navs("inTabset", selected="panel2" if c_num % 2 else "panel1")
+ st.update_navs("inTabset", selected="panel2" if c_num % 2 else "panel1")
+
+app = App(ui, server, debug=True)
-app = ShinyApp(ui, server)
if __name__ == "__main__":
app.run()
diff --git a/examples/inputs/app.py b/examples/inputs/app.py
index 2e869643f..9f1308824 100644
--- a/examples/inputs/app.py
+++ b/examples/inputs/app.py
@@ -7,37 +7,38 @@
sys.path.insert(0, shiny_module_dir)
+import shiny.ui_toolkit as st
from shiny import *
-from htmltools import tags, HTML
+from htmltools import tags, HTML, Tag
from fontawesome import icon_svg
-ui = page_fluid(
- panel_title("Hello prism ui"),
- layout_sidebar(
- panel_sidebar(
- input_slider(
+ui = st.page_fluid(
+ st.panel_title("Hello prism ui"),
+ st.layout_sidebar(
+ st.panel_sidebar(
+ st.input_slider(
"n", "input_slider()", min=10, max=100, value=50, step=5, animate=True
),
- input_date("date", "input_date()"),
- input_date_range("date_rng", "input_date_range()"),
- input_text("txt", "input_text()", placeholder="Input some text"),
- input_text_area(
+ st.input_date("date", "input_date()"),
+ st.input_date_range("date_rng", "input_date_range()"),
+ st.input_text("txt", "input_text()", placeholder="Input some text"),
+ st.input_text_area(
"txt_area", "input_text_area()", placeholder="Input some text"
),
- input_numeric("num", "input_numeric()", 20),
- input_password("password", "input_password()"),
- input_checkbox("checkbox", "input_checkbox()"),
- input_checkbox_group(
+ st.input_numeric("num", "input_numeric()", 20),
+ st.input_password("password", "input_password()"),
+ st.input_checkbox("checkbox", "input_checkbox()"),
+ st.input_checkbox_group(
"checkbox_group",
"input_checkbox_group()",
{"a": "Choice 1", "b": "Choice 2"},
selected=["a", "b"],
inline=True,
),
- input_radio_buttons(
+ st.input_radio_buttons(
"radio", "input_radio()", {"a": "Choice 1", "b": "Choice 2"}
),
- input_select(
+ st.input_select(
"select",
"input_select()",
{
@@ -46,33 +47,35 @@
"Group C": {"c1": "c1", "c2": "c2"},
},
),
- input_action_button(
+ st.input_action_button(
"button", "input_action_button()", icon=icon_svg("check")
),
- input_file("file", "File upload"),
+ st.input_file("file", "File upload"),
),
- panel_main(
- output_plot("plot"),
- navs_tab_card(
+ st.panel_main(
+ st.output_plot("plot"),
+ st.navs_tab_card(
# TODO: output_plot() within a tab not working?
- nav("Inputs", output_ui("inputs"), icon=icon_svg("code")),
- nav(
- "Image", output_image("image", inline=True), icon=icon_svg("image")
+ st.nav("Inputs", st.output_ui("inputs"), icon=icon_svg("code")),
+ st.nav(
+ "Image",
+ st.output_image("image", inline=True),
+ icon=icon_svg("image"),
),
- nav(
+ st.nav(
"Misc",
- input_action_link(
+ st.input_action_link(
"link", "Show notification/progress", icon=icon_svg("info")
),
tags.br(),
- input_action_button(
+ st.input_action_button(
"btn", "Show modal", icon=icon_svg("info-circle")
),
- panel_fixed(
- panel_well(
+ st.panel_fixed(
+ st.panel_well(
"A fixed, draggable, panel",
- input_checkbox("checkbox2", "Check me!"),
- panel_conditional(
+ st.input_checkbox("checkbox2", "Check me!"),
+ st.panel_conditional(
"input.checkbox2 == true", "Thanks for checking!"
),
),
@@ -94,52 +97,52 @@
import matplotlib.pyplot as plt
-def server(s: ShinySession):
- @s.output("inputs")
+def server(input: Inputs, output: Outputs, session: Session):
+ @output()
@render_ui()
- def _() -> Tag:
+ def inputs() -> Tag:
vals = [
- f"input_date() {s.input['date']}",
- f"input_date_range(): {s.input['date_rng']}",
- f"input_text(): {s.input['txt']}",
- f"input_text_area(): {s.input['txt_area']}",
- f"input_numeric(): {s.input['num']}",
- f"input_password(): {s.input['password']}",
- f"input_checkbox(): {s.input['checkbox']}",
- f"input_checkbox_group(): {s.input['checkbox_group']}",
- f"input_radio(): {s.input['radio']}",
- f"input_select(): {s.input['select']}",
- f"input_action_button(): {s.input['button']}",
+ f"input_date() {input.date()}",
+ f"input_date_range(): {input.date_rng()}",
+ f"input_text(): {input.txt()}",
+ f"input_text_area(): {input.txt_area()}",
+ f"input_numeric(): {input.num()}",
+ f"input_password(): {input.password()}",
+ f"input_checkbox(): {input.checkbox()}",
+ f"input_checkbox_group(): {input.checkbox_group()}",
+ f"input_radio(): {input.radio()}",
+ f"input_select(): {input.select()}",
+ f"input_action_button(): {input.button()}",
]
return tags.pre(HTML("\n".join(vals)))
np.random.seed(19680801)
x_rand = 100 + 15 * np.random.randn(437)
- @s.output("plot")
+ @output()
@render_plot(alt="A histogram")
- def _():
+ def plot():
fig, ax = plt.subplots()
- ax.hist(x_rand, int(s.input["n"]), density=True)
+ ax.hist(x_rand, int(input.n()), density=True)
return fig
- @s.output("image")
+ @output()
@render_image()
- def _():
+ def image():
from pathlib import Path
dir = Path(__file__).resolve().parent
return {"src": dir / "rstudio-logo.png", "width": "150px"}
- @observe()
+ @reactive.effect()
def _():
- btn = s.input["btn"]
+ btn = input.btn()
if btn and btn > 0:
- modal_show(modal("Hello there!", easy_close=True))
+ st.modal_show(st.modal("Hello there!", easy_close=True))
- @observe()
+ @reactive.effect()
def _():
- link = s.input["link"]
+ link = input.link()
if link and link > 0:
notification_show("A notification!")
p = Progress()
@@ -151,6 +154,6 @@ def _():
p.close()
-app = ShinyApp(ui, server)
+app = App(ui, server)
if __name__ == "__main__":
app.run()
diff --git a/examples/moduleapp/app.py b/examples/moduleapp/app.py
index 9c97ceb05..47b8bc043 100644
--- a/examples/moduleapp/app.py
+++ b/examples/moduleapp/app.py
@@ -28,7 +28,7 @@ def counter_module_ui(
)
-def counter_module_server(session: ShinySessionProxy):
+def counter_module_server(session: SessionProxy):
count: ReactiveVal[int] = ReactiveVal(0)
@observe()
@@ -54,12 +54,12 @@ def _() -> str:
)
-def server(session: ShinySession):
+def server(session: Session):
counter_module.server("counter1")
counter_module.server("counter2")
-app = ShinyApp(ui, server)
+app = App(ui, server)
if __name__ == "__main__":
diff --git a/examples/myapp/app.py b/examples/myapp/app.py
index 4ea868d28..bd794d244 100644
--- a/examples/myapp/app.py
+++ b/examples/myapp/app.py
@@ -4,83 +4,79 @@
# Then point web browser to:
# http://localhost:8000/
-# Add parent directory to path, so we can find the prism module.
-# (This is just a temporary fix)
-import os
-import sys
-
-# This will load the shiny module dynamically, without having to install it.
-# This makes the debug/run cycle quicker.
-shiny_module_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
-sys.path.insert(0, shiny_module_dir)
-
-from shiny import *
-from shiny.fileupload import FileInfo
+import matplotlib.pyplot as plt
# For plot rendering
import numpy as np
-import matplotlib.pyplot as plt
-ui = page_fluid(
- layout_sidebar(
- panel_sidebar(
- input_slider("n", "N", 0, 100, 20),
- input_file("file1", "Choose file", multiple=True),
+import shiny.ui_toolkit as st
+import shiny
+from shiny import Session, Inputs, Outputs, reactive
+from shiny.fileupload import FileInfo
+
+ui = st.page_fluid(
+ st.layout_sidebar(
+ st.panel_sidebar(
+ st.input_slider("n", "N", 0, 100, 20),
+ st.input_file("file1", "Choose file", multiple=True),
),
- panel_main(
- output_text_verbatim("txt"),
- output_text_verbatim("shared_txt"),
- output_plot("plot"),
- output_text_verbatim("file_content"),
+ st.panel_main(
+ st.output_text_verbatim("txt"),
+ st.output_text_verbatim("shared_txt"),
+ st.output_plot("plot"),
+ st.output_text_verbatim("file_content"),
),
),
)
# A ReactiveVal which is shared across all sessions.
-shared_val = ReactiveVal(None)
+shared_val = reactive.Value(None)
-def server(session: ShinySession):
- @reactive()
+def server(input: Inputs, output: Outputs, session: Session):
+ @reactive.calc()
def r():
- if session.input["n"] is None:
+ if input.n() is None:
return
- return session.input["n"] * 2
+ return input.n() * 2
- @session.output("txt")
- async def _():
+ @output()
+ @shiny.render_text()
+ async def txt():
val = r()
- return f"n*2 is {val}, session id is {get_current_session().id}"
+ return f"n*2 is {val}, session id is {session.id}"
# This observer watches n, and changes shared_val, which is shared across
# all running sessions.
- @observe()
+ @reactive.effect()
def _():
- if session.input["n"] is None:
+ if input.n() is None:
return
- shared_val(session.input["n"] * 10)
+ shared_val.set(input.n() * 10)
# Print the value of shared_val(). Changing it in one session should cause
# this to run in all sessions.
- @session.output("shared_txt")
- def _():
+ @output()
+ @shiny.render_text()
+ def shared_txt():
return f"shared_val() is {shared_val()}"
- @session.output("plot")
- @render_plot(alt="A histogram")
- def _():
+ @output()
+ @shiny.render_plot(alt="A histogram")
+ def plot() -> object:
np.random.seed(19680801)
x = 100 + 15 * np.random.randn(437)
fig, ax = plt.subplots()
- ax.hist(x, session.input["n"], density=True)
+ ax.hist(x, input.n(), density=True)
return fig
- @session.output("file_content")
- def _():
- file_infos: list[FileInfo] = session.input["file1"]
+ @output()
+ @shiny.render_text()
+ def file_content():
+ file_infos: list[FileInfo] = input.file1()
if not file_infos:
- return
+ return ""
out_str = ""
for file_info in file_infos:
@@ -91,9 +87,4 @@ def _():
return out_str
-app = ShinyApp(ui, server)
-
-if __name__ == "__main__":
- app.run()
- # Alternately, to listen on a TCP port:
- # app.run(conn_type = "tcp")
+app = shiny.App(ui, server)
diff --git a/examples/req/app.py b/examples/req/app.py
index 2ae8b170a..624753a1a 100644
--- a/examples/req/app.py
+++ b/examples/req/app.py
@@ -1,50 +1,48 @@
+import shiny.ui_toolkit as st
from shiny import *
-ui = page_fluid(
- input_action_button("safe", "Throw a safe error"),
- output_ui("safe"),
- input_action_button("unsafe", "Throw an unsafe error"),
- output_ui("unsafe"),
- input_text(
+ui = st.page_fluid(
+ st.input_action_button("safe", "Throw a safe error"),
+ st.output_ui("safe"),
+ st.input_action_button("unsafe", "Throw an unsafe error"),
+ st.output_ui("unsafe"),
+ st.input_text(
"txt",
"Enter some text below, then remove it. Notice how the text is never fully removed.",
),
- output_ui("txt_out"),
+ st.output_ui("txt_out"),
)
-def server(session: ShinySession):
- @reactive()
+def server(input: Inputs, output: Outputs, session: Session):
+ @reactive.calc()
def safe_click():
- req(session.input["safe"])
- return session.input["safe"]
+ req(input.safe())
+ return input.safe()
- @session.output("safe")
+ @output()
@render_ui()
- def _():
+ def safe():
raise SafeException(f"You've clicked {str(safe_click())} times")
- @session.output("unsafe")
+ @output()
@render_ui()
- def _():
- req(session.input["unsafe"])
- raise Exception(
- f"Super secret number of clicks: {str(session.input['unsafe'])}"
- )
+ def unsafe():
+ req(input.unsafe())
+ raise Exception(f"Super secret number of clicks: {str(input.unsafe())}")
- @observe()
+ @reactive.effect()
def _():
- req(session.input["unsafe"])
- print("unsafe clicks:", session.input["unsafe"])
+ req(input.unsafe())
+ print("unsafe clicks:", input.unsafe())
# raise Exception("Observer exception: this should cause a crash")
- @session.output("txt_out")
+ @output()
@render_ui()
- def _():
- req(session.input["txt"], cancel_output=True)
- return session.input["txt"]
+ def txt_out():
+ req(input.txt(), cancel_output=True)
+ return input.txt()
-app = ShinyApp(ui, server)
+app = App(ui, server)
app.SANITIZE_ERRORS = True
-app.run()
diff --git a/examples/simple/app.py b/examples/simple/app.py
index 74ddce6ed..4704a9d25 100644
--- a/examples/simple/app.py
+++ b/examples/simple/app.py
@@ -4,55 +4,30 @@
# Then point web browser to:
# http://localhost:8000/
-# Add parent directory to path, so we can find the prism module.
-# (This is just a temporary fix)
-import os
-import sys
-
-# This will load the shiny module dynamically, without having to install it.
-# This makes the debug/run cycle quicker.
-shiny_module_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
-sys.path.insert(0, shiny_module_dir)
-
+import shiny.ui_toolkit as st
from shiny import *
-ui = page_fluid(
- layout_sidebar(
- panel_sidebar(
- input_slider("n", "N", 0, 100, 20),
- ),
- panel_main(
- output_text_verbatim("txt", placeholder=True),
- output_plot("plot"),
- ),
- ),
+ui = st.page_fluid(
+ st.input_slider("n", "N", 0, 100, 20),
+ st.output_text_verbatim("txt", placeholder=True),
)
-# from htmltools.core import HTMLDocument
-# from shiny import html_dependencies
-# HTMLDocument(TagList(ui, html_dependencies.shiny_deps())).save_html("temp/app.html")
-
+# A reactive.Value which is exists outside of the session.
+shared_val = reactive.Value(None)
-# A ReactiveVal which is exists outside of the session.
-shared_val = ReactiveVal(None)
-
-def server(session: ShinySession):
- @reactive()
+def server(input: Inputs, output: Outputs, session: Session):
+ @reactive.calc()
def r():
- if session.input["n"] is None:
+ if input.n() is None:
return
- return session.input["n"] * 2
+ return input.n() * 2
- @session.output("txt")
- async def _():
+ @output()
+ @render_text()
+ async def txt():
val = r()
- return f"n*2 is {val}, session id is {get_current_session().id}"
-
+ return f"n*2 is {val}, session id is {session.id}"
-app = ShinyApp(ui, server)
-if __name__ == "__main__":
- app.run()
- # Alternately, to listen on a TCP port:
- # app.run(conn_type = "tcp")
+app = App(ui, server)
diff --git a/pyrightconfig.json b/pyrightconfig.json
deleted file mode 100644
index 9b634b7b6..000000000
--- a/pyrightconfig.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "reportImportCycles": false,
- "reportUnusedFunction": false
-}
diff --git a/setup.cfg b/setup.cfg
index d57a3f120..d5ebf0475 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.0.0.9000
+current_version = 0.0.0.9001
commit = True
tag = True
diff --git a/setup.py b/setup.py
index 8c433f976..8ffcd9804 100644
--- a/setup.py
+++ b/setup.py
@@ -37,6 +37,6 @@
test_suite="tests",
tests_require=test_requirements,
url="https://github.com/rstudio/prism",
- version="0.0.0.9000",
+ version="0.0.0.9001",
zip_safe=False,
)
diff --git a/shiny/__init__.py b/shiny/__init__.py
index 1fde7efce..ce5fb14a7 100644
--- a/shiny/__init__.py
+++ b/shiny/__init__.py
@@ -2,32 +2,17 @@
__author__ = """Winston Chang"""
__email__ = "winston@rstudio.com"
-__version__ = "0.0.0.9000"
+__version__ = "0.0.0.9001"
# All objects imported into this scope will be available as shiny.foo
-from .bootstrap import *
from .decorators import *
-from .download_button import *
from .dynamic_ui import *
-from .input_action_button import *
-from .input_check_radio import *
-from .input_date import *
-from .input_file import *
-from .input_numeric import *
-from .input_password import *
-from .input_select import *
-from .input_slider import *
-from .input_text import *
-from .input_update import *
-from .modal import *
-from .navs import *
from .notifications import *
-from .output import *
-from .page import *
from .progress import *
from .render import *
-from .reactives import *
-from .shinyapp import *
-from .shinysession import *
+from .app import *
+from .session import *
from .shinymodule import *
from .validation import *
+
+from . import reactive
diff --git a/shiny/shinyapp.py b/shiny/app.py
similarity index 91%
rename from shiny/shinyapp.py
rename to shiny/app.py
index 936ce4f9c..c43593bb4 100644
--- a/shiny/shinyapp.py
+++ b/shiny/app.py
@@ -1,4 +1,4 @@
-__all__ = ("ShinyApp",)
+__all__ = ("App",)
import contextlib
from typing import Any, List, Optional, Union, Dict, Callable, cast
@@ -13,7 +13,7 @@
from starlette.responses import Response, HTMLResponse, JSONResponse
from .http_staticfiles import StaticFiles
-from .shinysession import ShinySession, session_context
+from .session import Inputs, Outputs, Session, session_context
from . import reactcore
from .connmanager import (
Connection,
@@ -22,7 +22,7 @@
from .html_dependencies import jquery_deps, shiny_deps
-class ShinyApp:
+class App:
LIB_PREFIX: str = "lib/"
SANITIZE_ERRORS: bool = False
SANITIZE_ERROR_MSG: str = "An error has occurred. Check your logs or contact the app author for clarification."
@@ -30,19 +30,19 @@ class ShinyApp:
def __init__(
self,
ui: Union[Tag, TagList],
- server: Callable[[ShinySession], None],
+ server: Callable[[Inputs, Outputs, Session], None],
*,
debug: bool = False,
) -> None:
self.ui: RenderedHTML = _render_page(ui, lib_prefix=self.LIB_PREFIX)
- self.server: Callable[[ShinySession], None] = server
+ self.server: Callable[[Inputs, Outputs, Session], None] = server
self._debug: bool = debug
- self._sessions: Dict[str, ShinySession] = {}
+ self._sessions: Dict[str, Session] = {}
self._last_session_id: int = 0 # Counter for generating session IDs
- self._sessions_needing_flush: Dict[int, ShinySession] = {}
+ self._sessions_needing_flush: Dict[int, Session] = {}
self._registered_dependencies: Dict[str, HTMLDependency] = {}
self._dependency_handler: Any = starlette.routing.Router()
@@ -58,12 +58,12 @@ def __init__(
),
starlette.routing.Mount("/", app=self._dependency_handler),
],
- lifespan=self.lifespan
+ lifespan=self.lifespan,
)
@contextlib.asynccontextmanager
async def lifespan(self, app: starlette.applications.Starlette):
- unreg = reactcore.on_flushed(self._on_reactive_flushed, once = False)
+ unreg = reactcore.on_flushed(self._on_reactive_flushed, once=False)
try:
yield
finally:
@@ -72,15 +72,15 @@ async def lifespan(self, app: starlette.applications.Starlette):
async def _on_reactive_flushed(self):
await self.flush_pending_sessions()
- def create_session(self, conn: Connection) -> ShinySession:
+ def create_session(self, conn: Connection) -> Session:
self._last_session_id += 1
id = str(self._last_session_id)
- session = ShinySession(self, id, conn, debug=self._debug)
+ session = Session(self, id, conn, debug=self._debug)
self._sessions[id] = session
return session
- def remove_session(self, session: Union[ShinySession, str]) -> None:
- if isinstance(session, ShinySession):
+ def remove_session(self, session: Union[Session, str]) -> None:
+ if isinstance(session, Session):
session = session.id
if self._debug:
@@ -162,7 +162,7 @@ async def _on_session_request_cb(self, request: Request) -> ASGIApp:
subpath: str = request.path_params["subpath"] # type: ignore
if session_id in self._sessions:
- session: ShinySession = self._sessions[session_id]
+ session: Session = self._sessions[session_id]
with session_context(session):
return await session.handle_request(request, action, subpath)
@@ -171,7 +171,7 @@ async def _on_session_request_cb(self, request: Request) -> ASGIApp:
# ==========================================================================
# Flush
# ==========================================================================
- def request_flush(self, session: ShinySession) -> None:
+ def request_flush(self, session: Session) -> None:
# TODO: Until we have reactive domains, because we can't yet keep track
# of which sessions need a flush.
pass
diff --git a/shiny/decorators.py b/shiny/decorators.py
index 4122cc88f..03698cb51 100644
--- a/shiny/decorators.py
+++ b/shiny/decorators.py
@@ -2,7 +2,7 @@
from typing import TypeVar, Callable, List, Awaitable, Union, cast
from .input_handlers import ActionButtonValue
-from .reactives import isolate
+from .reactive import isolate
from .validation import req
from .utils import is_async_callable, run_coro_sync
diff --git a/shiny/dynamic_ui.py b/shiny/dynamic_ui.py
index 10c0238d8..5e87c0089 100644
--- a/shiny/dynamic_ui.py
+++ b/shiny/dynamic_ui.py
@@ -8,7 +8,7 @@
from htmltools import TagChildArg
-from .shinysession import ShinySession, _require_active_session, _process_deps
+from .session import Session, _require_active_session, _process_deps
def ui_insert(
@@ -17,7 +17,7 @@ def ui_insert(
where: Literal["beforeBegin", "afterBegin", "beforeEnd", "afterEnd"] = "beforeEnd",
multiple: bool = False,
immediate: bool = False,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
@@ -40,7 +40,7 @@ def ui_remove(
selector: str,
multiple: bool = False,
immediate: bool = False,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
diff --git a/shiny/html_dependencies.py b/shiny/html_dependencies.py
index d003d4209..63b88e09a 100644
--- a/shiny/html_dependencies.py
+++ b/shiny/html_dependencies.py
@@ -1,5 +1,4 @@
-from htmltools import HTMLDependency, HTML
-from typing import List, Union
+from htmltools import HTMLDependency
def shiny_deps() -> HTMLDependency:
@@ -12,30 +11,6 @@ def shiny_deps() -> HTMLDependency:
)
-def bootstrap_deps(bs3compat: bool = True) -> List[HTMLDependency]:
- dep = HTMLDependency(
- name="bootstrap",
- version="5.0.1",
- source={"package": "shiny", "subdir": "www/shared/bootstrap/"},
- script={"src": "bootstrap.bundle.min.js"},
- stylesheet={"href": "bootstrap.min.css"},
- )
- deps = [jquery_deps(), dep]
- if bs3compat:
- deps.append(bs3compat_deps())
- return deps
-
-
-# TODO: if we want to support glyphicons we'll need to bundle font files, too
-def bs3compat_deps() -> HTMLDependency:
- return HTMLDependency(
- name="bs3-compat",
- version="1.0",
- source={"package": "shiny", "subdir": "www/shared/bs3compat/"},
- script=[{"src": "transition.js"}, {"src": "tabs.js"}, {"src": "bs3compat.js"}],
- )
-
-
def jquery_deps() -> HTMLDependency:
return HTMLDependency(
name="jquery",
@@ -43,72 +18,3 @@ def jquery_deps() -> HTMLDependency:
source={"package": "shiny", "subdir": "www/shared/jquery/"},
script={"src": "jquery-3.6.0.min.js"},
)
-
-
-def nav_deps(
- include_bootstrap: bool = True,
-) -> Union[HTMLDependency, List[HTMLDependency]]:
- dep = HTMLDependency(
- name="bslib-navs",
- version="1.0",
- source={"package": "shiny", "subdir": "www/shared/bslib/dist/"},
- script={"src": "navs.min.js"},
- )
- return [dep, *bootstrap_deps()] if include_bootstrap else dep
-
-
-def ionrangeslider_deps() -> List[HTMLDependency]:
- return [
- HTMLDependency(
- name="ionrangeslider",
- version="2.3.1",
- source={"package": "shiny", "subdir": "www/shared/ionrangeslider/"},
- script={"src": "js/ion.rangeSlider.min.js"},
- stylesheet={"href": "css/ion.rangeSlider.css"},
- ),
- HTMLDependency(
- name="strftime",
- version="0.9.2",
- source={"package": "shiny", "subdir": "www/shared/strftime/"},
- script={"src": "strftime-min.js"},
- ),
- ]
-
-
-def datepicker_deps() -> HTMLDependency:
- return HTMLDependency(
- name="bootstrap-datepicker",
- version="1.9.0",
- source={"package": "shiny", "subdir": "www/shared/datepicker/"},
- # TODO: pre-compile the Bootstrap 5 version?
- stylesheet={"href": "css/bootstrap-datepicker3.min.css"},
- script={"src": "js/bootstrap-datepicker.min.js"},
- # Need to enable noConflict mode. See #1346.
- head=HTML(
- ""
- ),
- )
-
-
-def selectize_deps() -> HTMLDependency:
- return HTMLDependency(
- name="selectize",
- version="0.12.6",
- source={"package": "shiny", "subdir": "www/shared/selectize/"},
- script=[
- {"src": "js/selectize.min.js"},
- {"src": "accessibility/js/selectize-plugin-a11y.min.js"},
- ],
- # TODO: pre-compile the Bootstrap 5 version?
- stylesheet={"href": "css/selectize.bootstrap3.css"},
- )
-
-
-def jqui_deps() -> HTMLDependency:
- return HTMLDependency(
- name="jquery-ui",
- version="1.12.1",
- source={"package": "shiny", "subdir": "www/shared/jqueryui/"},
- script={"src": "jquery-ui.min.js"},
- stylesheet={"href": "jquery-ui.min.css"},
- )
diff --git a/shiny/input_handlers.py b/shiny/input_handlers.py
index 95f587176..2a745ef64 100644
--- a/shiny/input_handlers.py
+++ b/shiny/input_handlers.py
@@ -2,9 +2,9 @@
from typing import TYPE_CHECKING, Callable, Dict, Union, List, Any, TypeVar
if TYPE_CHECKING:
- from .shinysession import ShinySession
+ from .session import Session
-InputHandlerType = Callable[[Any, str, "ShinySession"], Any]
+InputHandlerType = Callable[[Any, str, "Session"], Any]
class _InputHandlers(Dict[str, InputHandlerType]):
@@ -24,7 +24,7 @@ def remove(self, name: str):
del self[name]
def process_value(
- self, type: str, value: Any, name: str, session: "ShinySession"
+ self, type: str, value: Any, name: str, session: "Session"
) -> Any:
handler = self.get(type)
if handler is None:
@@ -39,19 +39,19 @@ def process_value(
# Doesn't do anything since it seems weird to coerce None into some sort of NA (like we do in R)?
@input_handlers.add("shiny.number")
-def _(value: _NumberType, name: str, session: "ShinySession") -> _NumberType:
+def _(value: _NumberType, name: str, session: "Session") -> _NumberType:
return value
# TODO: implement when we have bookmarking
@input_handlers.add("shiny.password")
-def _(value: str, name: str, session: "ShinySession") -> str:
+def _(value: str, name: str, session: "Session") -> str:
return value
@input_handlers.add("shiny.date")
def _(
- value: Union[str, List[str]], name: str, session: "ShinySession"
+ value: Union[str, List[str]], name: str, session: "Session"
) -> Union[date, List[date]]:
if isinstance(value, str):
return datetime.strptime(value, "%Y-%m-%d").date()
@@ -60,7 +60,7 @@ def _(
@input_handlers.add("shiny.datetime")
def _(
- value: Union[int, float, List[int], List[float]], name: str, session: "ShinySession"
+ value: Union[int, float, List[int], List[float]], name: str, session: "Session"
) -> Union[datetime, List[datetime]]:
if isinstance(value, (int, float)):
return datetime.utcfromtimestamp(value)
@@ -72,11 +72,11 @@ class ActionButtonValue(int):
@input_handlers.add("shiny.action")
-def _(value: int, name: str, session: "ShinySession") -> ActionButtonValue:
+def _(value: int, name: str, session: "Session") -> ActionButtonValue:
return ActionButtonValue(value)
# TODO: implement when we have bookmarking
@input_handlers.add("shiny.file")
-def _(value: Any, name: str, session: "ShinySession") -> Any:
+def _(value: Any, name: str, session: "Session") -> Any:
return value
diff --git a/shiny/notifications.py b/shiny/notifications.py
index 1d535082e..b250a6116 100644
--- a/shiny/notifications.py
+++ b/shiny/notifications.py
@@ -9,7 +9,7 @@
from htmltools import TagList, TagChildArg
from .utils import run_coro_sync, rand_hex
-from .shinysession import ShinySession, _require_active_session, _process_deps
+from .session import Session, _require_active_session, _process_deps
def notification_show(
@@ -19,7 +19,7 @@ def notification_show(
close_button: bool = True,
id: Optional[str] = None,
type: Literal["default", "message", "warning", "error"] = "default",
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
@@ -43,7 +43,7 @@ def notification_show(
)
-def notification_remove(id: str, session: Optional[ShinySession] = None) -> str:
+def notification_remove(id: str, session: Optional[Session] = None) -> str:
session = _require_active_session(session)
run_coro_sync(
session.send_message({"notification": {"type": "remove", "message": None}})
diff --git a/shiny/progress.py b/shiny/progress.py
index ec0d0ae5d..80ac3b961 100644
--- a/shiny/progress.py
+++ b/shiny/progress.py
@@ -1,15 +1,13 @@
from typing import Optional, Dict, Any
from warnings import warn
from .utils import run_coro_sync, rand_hex
-from .shinysession import ShinySession, _require_active_session
+from .session import Session, _require_active_session
class Progress:
_style = "notification"
- def __init__(
- self, min: int = 0, max: int = 1, session: Optional[ShinySession] = None
- ):
+ def __init__(self, min: int = 0, max: int = 1, session: Optional[Session] = None):
self.min = min
self.max = max
self.value = None
diff --git a/shiny/reactcore.py b/shiny/reactcore.py
index a1545d4fd..1170f3af0 100644
--- a/shiny/reactcore.py
+++ b/shiny/reactcore.py
@@ -142,9 +142,8 @@ async def flush(self) -> None:
await self._flushed_callbacks.invoke()
async def _flush_sequential(self) -> None:
- # Sequential flush: instead of storing the tasks in a list and
- # calling gather() on them later, just run each observer in
- # sequence.
+ # Sequential flush: instead of storing the tasks in a list and calling gather()
+ # on them later, just run each effect in sequence.
while not self._pending_flush_queue.empty():
ctx = self._pending_flush_queue.get()
await ctx.execute_flush_callbacks()
diff --git a/shiny/reactives.py b/shiny/reactive.py
similarity index 70%
rename from shiny/reactives.py
rename to shiny/reactive.py
index 73da6c947..c90f0db9f 100644
--- a/shiny/reactives.py
+++ b/shiny/reactive.py
@@ -1,22 +1,20 @@
"""Reactive components"""
__all__ = (
- "ReactiveVal",
- "ReactiveValues",
- "Reactive",
- "ReactiveAsync",
- "reactive",
- "reactive_async",
- "Observer",
- "ObserverAsync",
- "observe",
- "observe_async",
+ "Value",
+ "Calc",
+ "CalcAsync",
+ "calc",
+ "calc_async",
+ "Effect",
+ "EffectAsync",
+ "effect",
+ "effect_async",
"isolate",
"invalidate_later",
)
import asyncio
-import sys
import time
import traceback
from typing import (
@@ -27,8 +25,6 @@
TypeVar,
Union,
Generic,
- Any,
- overload,
)
import typing
import inspect
@@ -41,31 +37,22 @@
from .validation import SilentException
if TYPE_CHECKING:
- from .shinysession import ShinySession
+ from .session import Session
T = TypeVar("T")
# ==============================================================================
-# ReactiveVal and ReactiveValues
+# Value
# ==============================================================================
-class ReactiveVal(Generic[T]):
+class Value(Generic[T]):
def __init__(self, value: T) -> None:
self._value: T = value
self._dependents: Dependents = Dependents()
- @overload
+ # Calling the object is equivalent to `.get()`
def __call__(self) -> T:
- ...
-
- @overload
- def __call__(self, value: T) -> bool:
- ...
-
- def __call__(self, value: Union[MISSING_TYPE, T] = MISSING) -> Union[T, bool]:
- if isinstance(value, MISSING_TYPE):
- return self.get()
- else:
- return self.set(value)
+ self._dependents.register()
+ return self._value
def get(self) -> T:
self._dependents.register()
@@ -80,40 +67,15 @@ def set(self, value: T) -> bool:
return True
-class ReactiveValues:
- def __init__(self, **kwargs: object) -> None:
- self._map: dict[str, ReactiveVal[Any]] = {}
- for key, value in kwargs.items():
- self._map[key] = ReactiveVal(value)
-
- def __setitem__(self, key: str, value: object) -> None:
- if key in self._map:
- self._map[key](value)
- else:
- self._map[key] = ReactiveVal(value)
-
- def __getitem__(self, key: str) -> Any:
- # Auto-populate key if accessed but not yet set. Needed to take reactive
- # dependencies on input values that haven't been received from client
- # yet.
- if key not in self._map:
- self._map[key] = ReactiveVal(None)
-
- return self._map[key]()
-
- def __delitem__(self, key: str) -> None:
- del self._map[key]
-
-
# ==============================================================================
-# Reactive
+# Calc
# ==============================================================================
-class Reactive(Generic[T]):
+class Calc(Generic[T]):
def __init__(
self,
func: Callable[[], T],
*,
- session: Union[MISSING_TYPE, "ShinySession", None] = MISSING,
+ session: Union[MISSING_TYPE, "Session", None] = MISSING,
) -> None:
if inspect.iscoroutinefunction(func):
raise TypeError("Reactive requires a non-async function")
@@ -128,14 +90,14 @@ def __init__(
self._ctx: Optional[Context] = None
self._exec_count: int = 0
- self._session: Optional[ShinySession]
+ self._session: Optional[Session]
# Use `isinstance(x, MISSING_TYPE)`` instead of `x is MISSING` because
# the type checker doesn't know that MISSING is the only instance of
# MISSING_TYPE; this saves us from casting later on.
if isinstance(session, MISSING_TYPE):
# If no session is provided, autodetect the current session (this
# could be None if outside of a session).
- session = shinysession.get_current_session()
+ session = shiny_session.get_current_session()
self._session = session
# Use lists to hold (optional) value and error, instead of Optional[T],
@@ -174,7 +136,7 @@ async def update_value(self) -> None:
was_running = self._running
self._running = True
- with shinysession.session_context(self._session):
+ with shiny_session.session_context(self._session):
try:
with self._ctx():
await self._run_func()
@@ -195,19 +157,19 @@ async def _run_func(self) -> None:
self._error.append(err)
-class ReactiveAsync(Reactive[T]):
+class CalcAsync(Calc[T]):
def __init__(
self,
func: Callable[[], Awaitable[T]],
*,
- session: Union[MISSING_TYPE, "ShinySession", None] = MISSING,
+ session: Union[MISSING_TYPE, "Session", None] = MISSING,
) -> None:
if not inspect.iscoroutinefunction(func):
- raise TypeError("ReactiveAsync requires an async function")
+ raise TypeError("CalcAsync requires an async function")
- # Init the Reactive base class with a placeholder synchronous function
- # so it won't throw an error, then replace it with the async function.
- # Need the `cast` to satisfy the type checker.
+ # Init the Calc base class with a placeholder synchronous function so it won't
+ # throw an error, then replace it with the async function. Need the `cast` to
+ # satisfy the type checker.
super().__init__(lambda: typing.cast(T, None), session=session)
self._func: Callable[[], Awaitable[T]] = func
self._is_async = True
@@ -216,37 +178,37 @@ async def __call__(self) -> T:
return await self.get_value()
-def reactive(
- *, session: Union[MISSING_TYPE, "ShinySession", None] = MISSING
-) -> Callable[[Callable[[], T]], Reactive[T]]:
- def create_reactive(fn: Callable[[], T]) -> Reactive[T]:
- return Reactive(fn, session=session)
+def calc(
+ *, session: Union[MISSING_TYPE, "Session", None] = MISSING
+) -> Callable[[Callable[[], T]], Calc[T]]:
+ def create_calc(fn: Callable[[], T]) -> Calc[T]:
+ return Calc(fn, session=session)
- return create_reactive
+ return create_calc
-def reactive_async(
- *, session: Union[MISSING_TYPE, "ShinySession", None] = MISSING
-) -> Callable[[Callable[[], Awaitable[T]]], ReactiveAsync[T]]:
- def create_reactive_async(fn: Callable[[], Awaitable[T]]) -> ReactiveAsync[T]:
- return ReactiveAsync(fn, session=session)
+def calc_async(
+ *, session: Union[MISSING_TYPE, "Session", None] = MISSING
+) -> Callable[[Callable[[], Awaitable[T]]], CalcAsync[T]]:
+ def create_calc_async(fn: Callable[[], Awaitable[T]]) -> CalcAsync[T]:
+ return CalcAsync(fn, session=session)
- return create_reactive_async
+ return create_calc_async
# ==============================================================================
-# Observer
+# Effect
# ==============================================================================
-class Observer:
+class Effect:
def __init__(
self,
func: Callable[[], None],
*,
priority: int = 0,
- session: Union[MISSING_TYPE, "ShinySession", None] = MISSING,
+ session: Union[MISSING_TYPE, "Session", None] = MISSING,
) -> None:
if inspect.iscoroutinefunction(func):
- raise TypeError("Observer requires a non-async function")
+ raise TypeError("Effect requires a non-async function")
self._func: Callable[[], Awaitable[None]] = utils.wrap_async(func)
self._is_async: bool = False
@@ -258,14 +220,14 @@ def __init__(
self._ctx: Optional[Context] = None
self._exec_count: int = 0
- self._session: Optional[ShinySession]
+ self._session: Optional[Session]
# Use `isinstance(x, MISSING_TYPE)`` instead of `x is MISSING` because
# the type checker doesn't know that MISSING is the only instance of
# MISSING_TYPE; this saves us from casting later on.
if isinstance(session, MISSING_TYPE):
# If no session is provided, autodetect the current session (this
# could be None if outside of a session).
- session = shinysession.get_current_session()
+ session = shiny_session.get_current_session()
self._session = session
if self._session is not None:
@@ -277,7 +239,7 @@ def __init__(
def _create_context(self) -> Context:
ctx = Context()
- # Store the context explicitly in Observer object
+ # Store the context explicitly in Effect object
# TODO: More explanation here
self._ctx = ctx
@@ -305,17 +267,17 @@ async def run(self) -> None:
ctx = self._create_context()
self._exec_count += 1
- with shinysession.session_context(self._session):
+ with shiny_session.session_context(self._session):
try:
with ctx():
await self._func()
except SilentException:
- # It's OK for SilentException to cause an observer to stop running
+ # It's OK for SilentException to cause an Effect to stop running
pass
except Exception as e:
traceback.print_exc()
- warnings.warn("Error in observer: " + str(e), ReactiveWarning)
+ warnings.warn("Error in Effect: " + str(e), ReactiveWarning)
if self._session:
await self._session.unhandled_error(e)
@@ -332,27 +294,27 @@ def _on_session_ended_cb(self) -> None:
self.destroy()
-class ObserverAsync(Observer):
+class EffectAsync(Effect):
def __init__(
self,
func: Callable[[], Awaitable[None]],
*,
priority: int = 0,
- session: Union[MISSING_TYPE, "ShinySession", None] = MISSING,
+ session: Union[MISSING_TYPE, "Session", None] = MISSING,
) -> None:
if not inspect.iscoroutinefunction(func):
- raise TypeError("ObserverAsync requires an async function")
+ raise TypeError("EffectAsync requires an async function")
- # Init the Observer base class with a placeholder synchronous function
+ # Init the Efect base class with a placeholder synchronous function
# so it won't throw an error, then replace it with the async function.
super().__init__(lambda: None, session=session, priority=priority)
self._func: Callable[[], Awaitable[None]] = func
self._is_async = True
-def observe(
- *, priority: int = 0, session: Union[MISSING_TYPE, "ShinySession", None] = MISSING
-) -> Callable[[Callable[[], None]], Observer]:
+def effect(
+ *, priority: int = 0, session: Union[MISSING_TYPE, "Session", None] = MISSING
+) -> Callable[[Callable[[], None]], Effect]:
"""[summary]
Args:
@@ -363,21 +325,21 @@ def observe(
[description]
"""
- def create_observer(fn: Callable[[], None]) -> Observer:
- return Observer(fn, priority=priority, session=session)
+ def create_effect(fn: Callable[[], None]) -> Effect:
+ return Effect(fn, priority=priority, session=session)
- return create_observer
+ return create_effect
-def observe_async(
+def effect_async(
*,
priority: int = 0,
- session: Union[MISSING_TYPE, "ShinySession", None] = MISSING,
-) -> Callable[[Callable[[], Awaitable[None]]], ObserverAsync]:
- def create_observer_async(fn: Callable[[], Awaitable[None]]) -> ObserverAsync:
- return ObserverAsync(fn, priority=priority, session=session)
+ session: Union[MISSING_TYPE, "Session", None] = MISSING,
+) -> Callable[[Callable[[], Awaitable[None]]], EffectAsync]:
+ def create_effect_async(fn: Callable[[], Awaitable[None]]) -> EffectAsync:
+ return EffectAsync(fn, priority=priority, session=session)
- return create_observer_async
+ return create_effect_async
# ==============================================================================
@@ -438,4 +400,6 @@ def cancel_task():
# Import here at the bottom seems to fix a circular dependency problem.
-from . import shinysession
+# Need to import as shiny_session to avoid naming conflicts with function params named
+# `session`.
+from . import session as shiny_session
diff --git a/shiny/render.py b/shiny/render.py
index 077d628b5..a327aaaae 100644
--- a/shiny/render.py
+++ b/shiny/render.py
@@ -21,61 +21,116 @@
from htmltools import TagChildArg
if TYPE_CHECKING:
- from .shinysession import ShinySession
+ from .session import Session
from . import utils
__all__ = (
+ "render_text",
"render_plot",
"render_image",
"render_ui",
)
-# It would be nice to specify the return type of RenderPlotFunc to be something like:
-# Union[matplotlib.figure.Figure, PIL.Image.Image]
-# However, if we did that, we'd have to import those modules at load time, which adds
-# a nontrivial amount of overhead. So for now, we're just using `object`.
-RenderPlotFunc = Callable[[], object]
-RenderPlotFuncAsync = Callable[[], Awaitable[object]]
-
-
-class ImgData(TypedDict):
- src: str
- width: Union[str, float]
- height: Union[str, float]
- alt: Optional[str]
-
-
-RenderImageFunc = Callable[[], ImgData]
-RenderImageFuncAsync = Callable[[], Awaitable[ImgData]]
-
-
+# ======================================================================================
+# RenderFunction/RenderFunctionAsync base class
+# ======================================================================================
class RenderFunction:
def __init__(self, fn: Callable[[], object]) -> None:
- raise NotImplementedError
+ self.__name__ = fn.__name__
+ self.__doc__ = fn.__doc__
def __call__(self) -> object:
raise NotImplementedError
- def set_metadata(self, session: "ShinySession", name: str) -> None:
+ def set_metadata(self, session: "Session", name: str) -> None:
"""When RenderFunctions are assigned to Output object slots, this method
is used to pass along session and name information.
"""
- self._session: ShinySession = session
+ self._session: Session = session
self._name: str = name
+# The reason for having a separate RenderFunctionAsync class is because the __call__
+# method is marked here as async; you can't have a single class where one method could
+# be either sync or async.
class RenderFunctionAsync(RenderFunction):
async def __call__(self) -> object:
raise NotImplementedError
+# ======================================================================================
+# RenderText
+# ======================================================================================
+RenderTextFunc = Callable[[], Union[str, None]]
+RenderTextFuncAsync = Callable[[], Awaitable[Union[str, None]]]
+
+
+class RenderText(RenderFunction):
+ def __init__(self, fn: RenderTextFunc) -> None:
+ super().__init__(fn)
+ # The Render*Async subclass will pass in an async function, but it tells the
+ # static type checker that it's synchronous. wrap_async() is smart -- if is
+ # passed an async function, it will not change it.
+ self._fn: RenderTextFuncAsync = utils.wrap_async(fn)
+
+ def __call__(self) -> Union[str, None]:
+ return utils.run_coro_sync(self.run())
+
+ async def run(self) -> Union[str, None]:
+ return await self._fn()
+
+
+class RenderTextAsync(RenderText, RenderFunctionAsync):
+ def __init__(self, fn: RenderTextFuncAsync) -> None:
+ if not inspect.iscoroutinefunction(fn):
+ raise TypeError(self.__class__.__name__ + " requires an async function")
+ super().__init__(typing.cast(RenderTextFunc, fn))
+
+ async def __call__(self) -> Union[str, None]: # type: ignore
+ return await self.run()
+
+
+def render_text() -> Callable[[Union[RenderTextFunc, RenderTextFuncAsync]], RenderText]:
+ def wrapper(fn: Union[RenderTextFunc, RenderTextFuncAsync]) -> RenderText:
+ if inspect.iscoroutinefunction(fn):
+ fn = typing.cast(RenderTextFuncAsync, fn)
+ return RenderTextAsync(fn)
+ else:
+ fn = typing.cast(RenderTextFunc, fn)
+ return RenderText(fn)
+
+ return wrapper
+
+
+# ======================================================================================
+# RenderPlot
+# ======================================================================================
+# It would be nice to specify the return type of RenderPlotFunc to be something like:
+# Union[matplotlib.figure.Figure, PIL.Image.Image]
+# However, if we did that, we'd have to import those modules at load time, which adds
+# a nontrivial amount of overhead. So for now, we're just using `object`.
+RenderPlotFunc = Callable[[], object]
+RenderPlotFuncAsync = Callable[[], Awaitable[object]]
+
+
+class ImgData(TypedDict):
+ src: str
+ width: Union[str, float]
+ height: Union[str, float]
+ alt: Optional[str]
+
+
class RenderPlot(RenderFunction):
_ppi: float = 96
- def __init__(self, fn: RenderPlotFunc, alt: Optional[str] = None) -> None:
- self._fn: RenderPlotFuncAsync = utils.wrap_async(fn)
+ def __init__(self, fn: RenderPlotFunc, *, alt: Optional[str] = None) -> None:
+ super().__init__(fn)
self._alt: Optional[str] = alt
+ # The Render*Async subclass will pass in an async function, but it tells the
+ # static type checker that it's synchronous. wrap_async() is smart -- if is
+ # passed an async function, it will not change it.
+ self._fn: RenderPlotFuncAsync = utils.wrap_async(fn)
def __call__(self) -> object:
return utils.run_coro_sync(self.run())
@@ -83,13 +138,13 @@ def __call__(self) -> object:
async def run(self) -> object:
# Reactively read some information about the plot.
pixelratio: float = typing.cast(
- float, self._session.input[".clientdata_pixelratio"]
+ float, self._session.input[".clientdata_pixelratio"]()
)
width: float = typing.cast(
- float, self._session.input[f".clientdata_output_{self._name}_width"]
+ float, self._session.input[f".clientdata_output_{self._name}_width"]()
)
height: float = typing.cast(
- float, self._session.input[f".clientdata_output_{self._name}_height"]
+ float, self._session.input[f".clientdata_output_{self._name}_height"]()
)
fig = await self._fn()
@@ -125,12 +180,8 @@ async def run(self) -> object:
class RenderPlotAsync(RenderPlot, RenderFunctionAsync):
def __init__(self, fn: RenderPlotFuncAsync, alt: Optional[str] = None) -> None:
if not inspect.iscoroutinefunction(fn):
- raise TypeError("PlotAsync requires an async function")
-
- # Init the Plot base class with a placeholder synchronous function so it
- # won't throw an error, then replace it with the async function.
- super().__init__(lambda: None, alt)
- self._fn: RenderPlotFuncAsync = fn
+ raise TypeError(self.__class__.__name__ + " requires an async function")
+ super().__init__(typing.cast(RenderPlotFunc, fn), alt=alt)
async def __call__(self) -> object:
return await self.run()
@@ -234,10 +285,26 @@ def try_render_plot_pil(
return "TYPE_MISMATCH"
+# ======================================================================================
+# RenderImage
+# ======================================================================================
+RenderImageFunc = Callable[[], ImgData]
+RenderImageFuncAsync = Callable[[], Awaitable[ImgData]]
+
+
class RenderImage(RenderFunction):
- def __init__(self, fn: RenderImageFunc, delete_file: bool = False) -> None:
- self._fn: RenderImageFuncAsync = utils.wrap_async(fn)
+ def __init__(
+ self,
+ fn: RenderImageFunc,
+ *,
+ delete_file: bool = False,
+ ) -> None:
+ super().__init__(fn)
self._delete_file: bool = delete_file
+ # The Render*Async subclass will pass in an async function, but it tells the
+ # static type checker that it's synchronous. wrap_async() is smart -- if is
+ # passed an async function, it will not change it.
+ self._fn: RenderImageFuncAsync = utils.wrap_async(fn)
def __call__(self) -> object:
return utils.run_coro_sync(self.run())
@@ -260,11 +327,8 @@ async def run(self) -> object:
class RenderImageAsync(RenderImage, RenderFunctionAsync):
def __init__(self, fn: RenderImageFuncAsync, delete_file: bool = False) -> None:
if not inspect.iscoroutinefunction(fn):
- raise TypeError("ImageAsync requires an async function")
- # Init the Image base class with a placeholder synchronous function so it
- # won't throw an error, then replace it with the async function.
- super().__init__(lambda: None, delete_file)
- self._fn: RenderImageFuncAsync = fn
+ raise TypeError(self.__class__.__name__ + " requires an async function")
+ super().__init__(typing.cast(RenderImageFunc, fn), delete_file=delete_file)
async def __call__(self) -> object:
return await self.run()
@@ -284,12 +348,19 @@ def wrapper(fn: Union[RenderImageFunc, RenderImageFuncAsync]) -> RenderImage:
return wrapper
+# ======================================================================================
+# RenderUI
+# ======================================================================================
RenderUIFunc = Callable[[], TagChildArg]
RenderUIFuncAsync = Callable[[], Awaitable[TagChildArg]]
class RenderUI(RenderFunction):
def __init__(self, fn: RenderUIFunc) -> None:
+ super().__init__(fn)
+ # The Render*Async subclass will pass in an async function, but it tells the
+ # static type checker that it's synchronous. wrap_async() is smart -- if is
+ # passed an async function, it will not change it.
self._fn: RenderUIFuncAsync = utils.wrap_async(fn)
def __call__(self) -> object:
@@ -300,7 +371,7 @@ async def run(self) -> object:
if ui is None:
return None
# TODO: better a better workaround for the circular dependency
- from .shinysession import _process_deps
+ from .session import _process_deps
return _process_deps(ui, self._session)
@@ -308,10 +379,8 @@ async def run(self) -> object:
class RenderUIAsync(RenderUI, RenderFunctionAsync):
def __init__(self, fn: RenderUIFuncAsync) -> None:
if not inspect.iscoroutinefunction(fn):
- raise TypeError("PlotAsync requires an async function")
-
- super().__init__(lambda: None)
- self._fn: RenderUIFuncAsync = fn
+ raise TypeError(self.__class__.__name__ + " requires an async function")
+ super().__init__(typing.cast(RenderUIFunc, fn))
async def __call__(self) -> object:
return await self.run()
diff --git a/shiny/shinysession.py b/shiny/session.py
similarity index 87%
rename from shiny/shinysession.py
rename to shiny/session.py
index 5d249b930..03e82d9fa 100644
--- a/shiny/shinysession.py
+++ b/shiny/session.py
@@ -1,5 +1,6 @@
__all__ = (
- "ShinySession",
+ "Session",
+ "Inputs",
"Outputs",
"get_current_session",
"session_context",
@@ -48,11 +49,11 @@
from typing_extensions import TypedDict
if TYPE_CHECKING:
- from .shinyapp import ShinyApp
+ from .app import App
from htmltools import TagChildArg, TagList
-from .reactives import ReactiveValues, Observer, ObserverAsync, isolate
+from .reactive import Value, Effect, EffectAsync, isolate
from .http_staticfiles import FileResponse
from .connmanager import Connection, ConnectionClosed
from . import reactcore
@@ -114,19 +115,19 @@ def _empty_outbound_message_queues() -> _OutBoundMessageQueues:
return {"values": [], "input_messages": [], "errors": []}
-class ShinySession:
+class Session:
# ==========================================================================
# Initialization
# ==========================================================================
def __init__(
- self, app: "ShinyApp", id: str, conn: Connection, debug: bool = False
+ self, app: "App", id: str, conn: Connection, debug: bool = False
) -> None:
- self.app: ShinyApp = app
+ self.app: App = app
self.id: str = id
self._conn: Connection = conn
self._debug: bool = debug
- self.input: ReactiveValues = ReactiveValues()
+ self.input: Inputs = Inputs()
self.output: Outputs = Outputs(self)
self._outbound_message_queues = _empty_outbound_message_queues()
@@ -145,7 +146,7 @@ def __init__(
self._flushed_callbacks = utils.Callbacks()
with session_context(self):
- self.app.server(self)
+ self.app.server(self.input, self.output, self)
def _register_session_end_callbacks(self) -> None:
# This is to be called from the initialization. It registers functions
@@ -237,7 +238,7 @@ def _manage_inputs(self, data: Dict[str, object]) -> None:
if len(keys) == 2:
val = input_handlers.process_value(keys[1], val, keys[0], self)
- self.input[keys[0]] = val
+ self.input[keys[0]].set(val)
# ==========================================================================
# Message handlers
@@ -290,7 +291,7 @@ async def uploadEnd(job_id: str, input_id: str) -> None:
)
return None
file_data = upload_op.finish()
- self.input[input_id] = file_data
+ self.input[input_id].set(file_data)
# Explicitly return None to signal that the message was handled.
return None
@@ -505,7 +506,7 @@ def download(
filename: Optional[Union[str, Callable[[], str]]] = None,
media_type: Union[None, str, Callable[[], str]] = None,
encoding: str = "utf-8",
- ):
+ ) -> Callable[[_DownloadHandler], None]:
def wrapper(fn: _DownloadHandler):
if name is None:
effective_name = fn.__name__
@@ -519,7 +520,8 @@ def wrapper(fn: _DownloadHandler):
encoding=encoding,
)
- @self.output(effective_name)
+ @self.output(name=effective_name)
+ @render.render_text()
@functools.wraps(fn)
def _():
# TODO: the `w=` parameter should eventually be a worker ID, if we add those
@@ -528,38 +530,86 @@ def _():
return wrapper
+# ======================================================================================
+# Inputs
+# ======================================================================================
+class Inputs:
+ def __init__(self, **kwargs: object) -> None:
+ self._map: dict[str, Value[Any]] = {}
+ for key, value in kwargs.items():
+ self._map[key] = Value(value)
+
+ def __setitem__(self, key: str, value: Value[Any]) -> None:
+ if not isinstance(value, Value):
+ raise TypeError("`value` must be a shiny.reactive.Value object.")
+
+ self._map[key] = value
+
+ def __getitem__(self, key: str) -> Value[Any]:
+ # Auto-populate key if accessed but not yet set. Needed to take reactive
+ # dependencies on input values that haven't been received from client
+ # yet.
+ if key not in self._map:
+ self._map[key] = Value(None)
+
+ return self._map[key]
+
+ def __delitem__(self, key: str) -> None:
+ del self._map[key]
+
+ # Allow access of values as attributes.
+ def __setattr__(self, attr: str, value: Value[Any]) -> None:
+ # Need special handling of "_map".
+ if attr == "_map":
+ super().__setattr__(attr, value)
+ return
+
+ self.__setitem__(attr, value)
+
+ def __getattr__(self, attr: str) -> Value[Any]:
+ if attr == "_map":
+ return object.__getattribute__(self, attr)
+ return self.__getitem__(attr)
+
+ def __delattr__(self, key: str) -> None:
+ self.__delitem__(key)
+
+
+# ======================================================================================
+# Outputs
+# ======================================================================================
class Outputs:
- def __init__(self, session: ShinySession) -> None:
- self._output_obervers: Dict[str, Observer] = {}
- self._session: ShinySession = session
+ def __init__(self, session: Session) -> None:
+ self._output_obervers: Dict[str, Effect] = {}
+ self._session: Session = session
def __call__(
- self, name: str
- ) -> Callable[[Union[Callable[[], object], render.RenderFunction]], None]:
- def set_fn(fn: Union[Callable[[], object], render.RenderFunction]) -> None:
-
+ self, *, name: Optional[str] = None
+ ) -> Callable[[render.RenderFunction], None]:
+ def set_fn(fn: render.RenderFunction) -> None:
+ fn_name = name or fn.__name__
# fn is either a regular function or a RenderFunction object. If
# it's the latter, we can give it a bit of metadata, which can be
# used by the
if isinstance(fn, render.RenderFunction):
- fn.set_metadata(self._session, name)
+ fn.set_metadata(self._session, fn_name)
- if name in self._output_obervers:
- self._output_obervers[name].destroy()
+ if fn_name in self._output_obervers:
+ self._output_obervers[fn_name].destroy()
- @ObserverAsync
+ @EffectAsync
async def output_obs():
await self._session.send_message(
- {"recalculating": {"name": name, "status": "recalculating"}}
+ {"recalculating": {"name": fn_name, "status": "recalculating"}}
)
message: Dict[str, object] = {}
try:
if utils.is_async_callable(fn):
fn2 = typing.cast(Callable[[], Awaitable[object]], fn)
- message[name] = await fn2()
+ message[fn_name] = await fn2()
else:
- message[name] = fn()
+ message[fn_name] = fn()
except SilentCancelOutputException:
return
except SilentException:
@@ -576,7 +626,7 @@ async def output_obs():
err_msg = str(e)
# Register the outbound error message
msg: Dict[str, object] = {
- name: {
+ fn_name: {
"message": err_msg,
# TODO: is it possible to get the call?
"call": None,
@@ -589,10 +639,10 @@ async def output_obs():
self._session._outbound_message_queues["values"].append(message)
await self._session.send_message(
- {"recalculating": {"name": name, "status": "recalculated"}}
+ {"recalculating": {"name": fn_name, "status": "recalculated"}}
)
- self._output_obervers[name] = output_obs
+ self._output_obervers[fn_name] = output_obs
return None
@@ -602,25 +652,25 @@ async def output_obs():
# ==============================================================================
# Context manager for current session (AKA current reactive domain)
# ==============================================================================
-_current_session: ContextVar[Optional[ShinySession]] = ContextVar(
+_current_session: ContextVar[Optional[Session]] = ContextVar(
"current_session", default=None
)
-def get_current_session() -> Optional[ShinySession]:
+def get_current_session() -> Optional[Session]:
return _current_session.get()
@contextmanager
-def session_context(session: Optional[ShinySession]):
- token: Token[Union[ShinySession, None]] = _current_session.set(session)
+def session_context(session: Optional[Session]):
+ token: Token[Union[Session, None]] = _current_session.set(session)
try:
yield
finally:
_current_session.reset(token)
-def _require_active_session(session: Optional[ShinySession]) -> ShinySession:
+def _require_active_session(session: Optional[Session]) -> Session:
if session is None:
session = get_current_session()
if session is None:
@@ -656,9 +706,7 @@ class _RenderedDeps(TypedDict):
html: str
-def _process_deps(
- ui: TagChildArg, session: Optional[ShinySession] = None
-) -> _RenderedDeps:
+def _process_deps(ui: TagChildArg, session: Optional[Session] = None) -> _RenderedDeps:
session = _require_active_session(session)
diff --git a/shiny/shinymodule.py b/shiny/shinymodule.py
index 0f12bc088..417f947c6 100644
--- a/shiny/shinymodule.py
+++ b/shiny/shinymodule.py
@@ -1,7 +1,8 @@
__all__ = (
- "ReactiveValuesProxy",
+ "InputsProxy",
+ "InputsProxy",
"OutputsProxy",
- "ShinySessionProxy",
+ "SessionProxy",
"ShinyModule",
)
@@ -9,28 +10,45 @@
from htmltools.core import TagChildArg
-from .shinysession import ShinySession, Outputs, _require_active_session
-from .reactives import ReactiveValues
+from .session import Session, Inputs, Outputs, _require_active_session
+from .reactive import Value
from .render import RenderFunction
-class ReactiveValuesProxy(ReactiveValues):
- def __init__(self, ns: str, values: ReactiveValues):
+class InputsProxy(Inputs):
+ def __init__(self, ns: str, values: Inputs):
self._ns: str = ns
- self._values: ReactiveValues = values
+ self._values: Inputs = values
def _ns_key(self, key: str) -> str:
return self._ns + "-" + key
- def __setitem__(self, key: str, value: object) -> None:
- self._values[self._ns_key(key)] = value
+ def __setitem__(self, key: str, value: Value[Any]) -> None:
+ self._values[self._ns_key(key)].set(value)
- def __getitem__(self, key: str) -> object:
+ def __getitem__(self, key: str) -> Value[Any]:
return self._values[self._ns_key(key)]
def __delitem__(self, key: str) -> None:
del self._values[self._ns_key(key)]
+ # Allow access of values as attributes.
+ def __setattr__(self, attr: str, value: Value[Any]) -> None:
+ if attr in ("_values", "_ns"):
+ super().__setattr__(attr, value)
+ return
+ else:
+ self.__setitem__(attr, value)
+
+ def __getattr__(self, attr: str) -> Value[Any]:
+ if attr in ("_values", "_ns"):
+ return object.__getattribute__(self, attr)
+ else:
+ return self.__getitem__(attr)
+
+ def __delattr__(self, key: str) -> None:
+ self.__delitem__(key)
+
class OutputsProxy(Outputs):
def __init__(self, ns: str, outputs: Outputs):
@@ -41,16 +59,16 @@ def _ns_key(self, key: str) -> str:
return self._ns + "-" + key
def __call__(
- self, name: str
- ) -> Callable[[Union[Callable[[], object], RenderFunction]], None]:
- return self._outputs(self._ns_key(name))
+ self, *, name: Optional[str] = None
+ ) -> Callable[[RenderFunction], None]:
+ return self._outputs(name=self._ns_key(name))
-class ShinySessionProxy(ShinySession):
- def __init__(self, ns: str, parent_session: ShinySession) -> None:
+class SessionProxy(Session):
+ def __init__(self, ns: str, parent_session: Session) -> None:
self._ns: str = ns
- self._parent: ShinySession = parent_session
- self.input: ReactiveValuesProxy = ReactiveValuesProxy(ns, parent_session.input)
+ self._parent: Session = parent_session
+ self.input: InputsProxy = InputsProxy(ns, parent_session.input)
self.output: OutputsProxy = OutputsProxy(ns, parent_session.output)
@@ -58,20 +76,20 @@ class ShinyModule:
def __init__(
self,
ui: Callable[..., TagChildArg],
- server: Callable[[ShinySessionProxy], None],
+ server: Callable[[InputsProxy, OutputsProxy, SessionProxy], None],
) -> None:
self._ui: Callable[..., TagChildArg] = ui
- self._server: Callable[[ShinySessionProxy], None] = server
+ self._server: Callable[[InputsProxy, OutputsProxy, SessionProxy], None] = server
def ui(self, namespace: str, *args: Any) -> TagChildArg:
ns = ShinyModule._make_ns_fn(namespace)
return self._ui(ns, *args)
- def server(self, ns: str, *, session: Optional[ShinySession] = None) -> None:
+ def server(self, ns: str, *, session: Optional[Session] = None) -> None:
self.ns: str = ns
session = _require_active_session(session)
- session_proxy = ShinySessionProxy(ns, session)
- self._server(session_proxy)
+ session_proxy = SessionProxy(ns, session)
+ self._server(session_proxy.input, session_proxy.output, session_proxy)
@staticmethod
def _make_ns_fn(namespace: str) -> Callable[[str], str]:
diff --git a/shiny/ui_toolkit/__init__.py b/shiny/ui_toolkit/__init__.py
new file mode 100644
index 000000000..ad89ff38b
--- /dev/null
+++ b/shiny/ui_toolkit/__init__.py
@@ -0,0 +1,20 @@
+"""UI Toolkit for Shiny."""
+
+# All objects imported into this scope will be available as shiny.ui_toolkit.foo
+from .bootstrap import *
+from .download_button import *
+from .html_dependencies import *
+from .input_action_button import *
+from .input_check_radio import *
+from .input_date import *
+from .input_file import *
+from .input_numeric import *
+from .input_password import *
+from .input_select import *
+from .input_slider import *
+from .input_text import *
+from .input_update import *
+from .modal import *
+from .navs import *
+from .output import *
+from .page import *
diff --git a/shiny/bootstrap.py b/shiny/ui_toolkit/bootstrap.py
similarity index 100%
rename from shiny/bootstrap.py
rename to shiny/ui_toolkit/bootstrap.py
diff --git a/shiny/download_button.py b/shiny/ui_toolkit/download_button.py
similarity index 97%
rename from shiny/download_button.py
rename to shiny/ui_toolkit/download_button.py
index 2ce52fa5b..a907306eb 100644
--- a/shiny/download_button.py
+++ b/shiny/ui_toolkit/download_button.py
@@ -2,7 +2,7 @@
from htmltools import tags, Tag, TagChildArg, TagAttrArg, css
-from .shinyenv import is_pyodide
+from ..shinyenv import is_pyodide
# TODO: implement icon
diff --git a/shiny/ui_toolkit/html_dependencies.py b/shiny/ui_toolkit/html_dependencies.py
new file mode 100644
index 000000000..60c98e8df
--- /dev/null
+++ b/shiny/ui_toolkit/html_dependencies.py
@@ -0,0 +1,98 @@
+from typing import List, Union
+
+from htmltools import HTML, HTMLDependency
+
+from ..html_dependencies import jquery_deps
+
+
+def bootstrap_deps(bs3compat: bool = True) -> List[HTMLDependency]:
+ dep = HTMLDependency(
+ name="bootstrap",
+ version="5.0.1",
+ source={"package": "shiny", "subdir": "www/shared/bootstrap/"},
+ script={"src": "bootstrap.bundle.min.js"},
+ stylesheet={"href": "bootstrap.min.css"},
+ )
+ deps = [jquery_deps(), dep]
+ if bs3compat:
+ deps.append(bs3compat_deps())
+ return deps
+
+
+# TODO: if we want to support glyphicons we'll need to bundle font files, too
+def bs3compat_deps() -> HTMLDependency:
+ return HTMLDependency(
+ name="bs3-compat",
+ version="1.0",
+ source={"package": "shiny", "subdir": "www/shared/bs3compat/"},
+ script=[{"src": "transition.js"}, {"src": "tabs.js"}, {"src": "bs3compat.js"}],
+ )
+
+
+def nav_deps(
+ include_bootstrap: bool = True,
+) -> Union[HTMLDependency, List[HTMLDependency]]:
+ dep = HTMLDependency(
+ name="bslib-navs",
+ version="1.0",
+ source={"package": "shiny", "subdir": "www/shared/bslib/dist/"},
+ script={"src": "navs.min.js"},
+ )
+ return [dep, *bootstrap_deps()] if include_bootstrap else dep
+
+
+def ionrangeslider_deps() -> List[HTMLDependency]:
+ return [
+ HTMLDependency(
+ name="ionrangeslider",
+ version="2.3.1",
+ source={"package": "shiny", "subdir": "www/shared/ionrangeslider/"},
+ script={"src": "js/ion.rangeSlider.min.js"},
+ stylesheet={"href": "css/ion.rangeSlider.css"},
+ ),
+ HTMLDependency(
+ name="strftime",
+ version="0.9.2",
+ source={"package": "shiny", "subdir": "www/shared/strftime/"},
+ script={"src": "strftime-min.js"},
+ ),
+ ]
+
+
+def datepicker_deps() -> HTMLDependency:
+ return HTMLDependency(
+ name="bootstrap-datepicker",
+ version="1.9.0",
+ source={"package": "shiny", "subdir": "www/shared/datepicker/"},
+ # TODO: pre-compile the Bootstrap 5 version?
+ stylesheet={"href": "css/bootstrap-datepicker3.min.css"},
+ script={"src": "js/bootstrap-datepicker.min.js"},
+ # Need to enable noConflict mode. See #1346.
+ head=HTML(
+ ""
+ ),
+ )
+
+
+def selectize_deps() -> HTMLDependency:
+ return HTMLDependency(
+ name="selectize",
+ version="0.12.6",
+ source={"package": "shiny", "subdir": "www/shared/selectize/"},
+ script=[
+ {"src": "js/selectize.min.js"},
+ {"src": "accessibility/js/selectize-plugin-a11y.min.js"},
+ ],
+ # TODO: pre-compile the Bootstrap 5 version?
+ stylesheet={"href": "css/selectize.bootstrap3.css"},
+ )
+
+
+def jqui_deps() -> HTMLDependency:
+ return HTMLDependency(
+ name="jquery-ui",
+ version="1.12.1",
+ source={"package": "shiny", "subdir": "www/shared/jqueryui/"},
+ script={"src": "jquery-ui.min.js"},
+ stylesheet={"href": "jquery-ui.min.css"},
+ )
diff --git a/shiny/input_action_button.py b/shiny/ui_toolkit/input_action_button.py
similarity index 100%
rename from shiny/input_action_button.py
rename to shiny/ui_toolkit/input_action_button.py
diff --git a/shiny/input_check_radio.py b/shiny/ui_toolkit/input_check_radio.py
similarity index 100%
rename from shiny/input_check_radio.py
rename to shiny/ui_toolkit/input_check_radio.py
diff --git a/shiny/input_date.py b/shiny/ui_toolkit/input_date.py
similarity index 100%
rename from shiny/input_date.py
rename to shiny/ui_toolkit/input_date.py
diff --git a/shiny/input_file.py b/shiny/ui_toolkit/input_file.py
similarity index 100%
rename from shiny/input_file.py
rename to shiny/ui_toolkit/input_file.py
diff --git a/shiny/input_numeric.py b/shiny/ui_toolkit/input_numeric.py
similarity index 100%
rename from shiny/input_numeric.py
rename to shiny/ui_toolkit/input_numeric.py
diff --git a/shiny/input_password.py b/shiny/ui_toolkit/input_password.py
similarity index 100%
rename from shiny/input_password.py
rename to shiny/ui_toolkit/input_password.py
diff --git a/shiny/input_select.py b/shiny/ui_toolkit/input_select.py
similarity index 100%
rename from shiny/input_select.py
rename to shiny/ui_toolkit/input_select.py
diff --git a/shiny/input_slider.py b/shiny/ui_toolkit/input_slider.py
similarity index 100%
rename from shiny/input_slider.py
rename to shiny/ui_toolkit/input_slider.py
diff --git a/shiny/input_text.py b/shiny/ui_toolkit/input_text.py
similarity index 100%
rename from shiny/input_text.py
rename to shiny/ui_toolkit/input_text.py
diff --git a/shiny/input_update.py b/shiny/ui_toolkit/input_update.py
similarity index 92%
rename from shiny/input_update.py
rename to shiny/ui_toolkit/input_update.py
index f584b230d..9b2a44ae0 100644
--- a/shiny/input_update.py
+++ b/shiny/ui_toolkit/input_update.py
@@ -13,8 +13,8 @@
from .input_check_radio import ChoicesArg, _generate_options
from .input_select import SelectChoicesArg, _normalize_choices, _render_choices
from .input_slider import SliderValueArg, SliderStepArg, _slider_type, _as_numeric
-from .utils import drop_none
-from .shinysession import ShinySession, _require_active_session, _process_deps
+from ..utils import drop_none
+from ..session import Session, _require_active_session, _process_deps
# -----------------------------------------------------------------------------
# input_action_button.py
@@ -24,7 +24,7 @@ def update_action_button(
*,
label: Optional[str] = None,
icon: TagChildArg = None,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
# TODO: supporting a TagChildArg for label would require changes to shiny.js
@@ -43,7 +43,7 @@ def update_checkbox(
*,
label: Optional[str] = None,
value: Optional[bool] = None,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
msg = {"label": label, "value": value}
@@ -57,7 +57,7 @@ def update_checkbox_group(
choices: Optional[ChoicesArg] = None,
selected: Optional[Union[str, List[str]]] = None,
inline: bool = False,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
_update_choice_input(
id=id,
@@ -77,7 +77,7 @@ def update_radio_buttons(
choices: Optional[ChoicesArg] = None,
selected: Optional[str] = None,
inline: bool = False,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
_update_choice_input(
id=id,
@@ -98,7 +98,7 @@ def _update_choice_input(
choices: Optional[ChoicesArg] = None,
selected: Optional[Union[str, List[str]]] = None,
inline: bool = False,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
options = None
@@ -121,7 +121,7 @@ def update_date(
value: Optional[date] = None,
min: Optional[date] = None,
max: Optional[date] = None,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
@@ -142,7 +142,7 @@ def update_date_range(
end: Optional[date] = None,
min: Optional[date] = None,
max: Optional[date] = None,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
value = {"start": str(start), "end": str(end)}
@@ -166,7 +166,7 @@ def update_numeric(
min: Optional[float] = None,
max: Optional[float] = None,
step: Optional[float] = None,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
msg = {
@@ -188,7 +188,7 @@ def update_select(
label: Optional[str] = None,
choices: Optional[SelectChoicesArg] = None,
selected: Optional[str] = None,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
@@ -221,7 +221,7 @@ def update_slider(
step: Optional[SliderStepArg] = None,
time_format: Optional[str] = None,
timezone: Optional[str] = None,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
@@ -259,7 +259,7 @@ def update_text(
label: Optional[str] = None,
value: Optional[str] = None,
placeholder: Optional[str] = None,
- session: Optional[ShinySession] = None,
+ session: Optional[Session] = None,
) -> None:
session = _require_active_session(session)
msg = {"label": label, "value": value, "placeholder": placeholder}
@@ -275,7 +275,7 @@ def update_text(
# TODO: we should probably provide a nav_select() alias for this as well
def update_navs(
- id: str, selected: Optional[str] = None, session: Optional[ShinySession] = None
+ id: str, selected: Optional[str] = None, session: Optional[Session] = None
) -> None:
session = _require_active_session(session)
msg = {"value": selected}
diff --git a/shiny/input_utils.py b/shiny/ui_toolkit/input_utils.py
similarity index 100%
rename from shiny/input_utils.py
rename to shiny/ui_toolkit/input_utils.py
diff --git a/shiny/modal.py b/shiny/ui_toolkit/modal.py
similarity index 90%
rename from shiny/modal.py
rename to shiny/ui_toolkit/modal.py
index 7f455906a..e7e523371 100644
--- a/shiny/modal.py
+++ b/shiny/ui_toolkit/modal.py
@@ -8,8 +8,8 @@
from htmltools import tags, Tag, div, HTML, TagChildArg, TagAttrArg
-from .utils import run_coro_sync
-from .shinysession import ShinySession, _require_active_session, _process_deps
+from ..utils import run_coro_sync
+from ..session import Session, _require_active_session, _process_deps
def modal_button(label: str, icon: TagChildArg = None) -> Tag:
@@ -79,12 +79,12 @@ def modal(
)
-def modal_show(modal: Tag, session: Optional[ShinySession] = None) -> None:
+def modal_show(modal: Tag, session: Optional[Session] = None) -> None:
session = _require_active_session(session)
msg = _process_deps(modal)
run_coro_sync(session.send_message({"modal": {"type": "show", "message": msg}}))
-def modal_remove(session: Optional[ShinySession] = None) -> None:
+def modal_remove(session: Optional[Session] = None) -> None:
session = _require_active_session(session)
run_coro_sync(session.send_message({"modal": {"type": "remove", "message": None}}))
diff --git a/shiny/navs.py b/shiny/ui_toolkit/navs.py
similarity index 100%
rename from shiny/navs.py
rename to shiny/ui_toolkit/navs.py
diff --git a/shiny/output.py b/shiny/ui_toolkit/output.py
similarity index 100%
rename from shiny/output.py
rename to shiny/ui_toolkit/output.py
diff --git a/shiny/page.py b/shiny/ui_toolkit/page.py
similarity index 100%
rename from shiny/page.py
rename to shiny/ui_toolkit/page.py
diff --git a/shiny/utils.py b/shiny/utils.py
index 83f7f3062..a1e1ebe4a 100644
--- a/shiny/utils.py
+++ b/shiny/utils.py
@@ -1,11 +1,14 @@
from typing import (
Callable,
Awaitable,
+ Union,
Tuple,
TypeVar,
Dict,
Any,
+ cast,
)
+import functools
import os
import tempfile
import importlib
@@ -31,12 +34,20 @@ def rand_hex(bytes: int) -> str:
T = TypeVar("T")
-def wrap_async(fn: Callable[[], T]) -> Callable[[], Awaitable[T]]:
+def wrap_async(
+ fn: Union[Callable[[], T], Callable[[], Awaitable[T]]]
+) -> Callable[[], Awaitable[T]]:
"""
- Wrap a synchronous function that returns T, and return an async function
- that wraps the original function.
+ Given a synchronous function that returns T, return an async function that wraps the
+ original function. If the input function is already async, then return it unchanged.
"""
+ if is_async_callable(fn):
+ return cast(Callable[[], Awaitable[T]], fn)
+
+ fn = cast(Callable[[], T], fn)
+
+ @functools.wraps(fn)
async def fn_async() -> T:
return fn()
@@ -45,8 +56,8 @@ async def fn_async() -> T:
def is_async_callable(obj: object) -> bool:
"""
- Returns True if `obj` is an `async def` function, or if it's an object with
- a `__call__` method which is an `async def` function.
+ Returns True if `obj` is an `async def` function, or if it's an object with a
+ `__call__` method which is an `async def` function.
"""
if inspect.iscoroutinefunction(obj):
return True
diff --git a/tests/test_reactives.py b/tests/test_reactives.py
index 4b09ffddf..50277264b 100644
--- a/tests/test_reactives.py
+++ b/tests/test_reactives.py
@@ -1,14 +1,13 @@
-"""Tests for `shiny.reactives` and `shiny.reactcore`."""
+"""Tests for `shiny.reactive` and `shiny.reactcore`."""
import pytest
import asyncio
from typing import List
-from shiny import reactives
from shiny.input_handlers import ActionButtonValue
import shiny.reactcore as reactcore
from shiny.decorators import *
-from shiny.reactives import *
+from shiny.reactive import *
from shiny.validation import req
from .mocktime import MockTime
@@ -17,25 +16,25 @@
@pytest.mark.asyncio
async def test_flush_runs_newly_invalidated():
"""
- Make sure that a flush will also run any reactives that were invalidated
- during the flush.
+ Make sure that a flush will also run any calcs that were invalidated during the
+ flush.
"""
- v1 = ReactiveVal(1)
- v2 = ReactiveVal(2)
+ v1 = Value(1)
+ v2 = Value(2)
v2_result = None
- # In practice, on the first flush, Observers run in the order that they were
- # created. Our test checks that o2 runs _after_ o1.
- @observe()
+ # In practice, on the first flush, Effects run in the order that they were created.
+ # Our test checks that o2 runs _after_ o1.
+ @effect()
def o2():
nonlocal v2_result
v2_result = v2()
- @observe()
+ @effect()
def o1():
- v2(v1())
+ v2.set(v1())
await reactcore.flush()
assert v2_result == 1
@@ -46,25 +45,25 @@ def o1():
@pytest.mark.asyncio
async def test_flush_runs_newly_invalidated_async():
"""
- Make sure that a flush will also run any reactives that were invalidated
- during the flush. (Same as previous test, but async.)
+ Make sure that a flush will also run any calcs that were invalidated during the
+ flush. (Same as previous test, but async.)
"""
- v1 = ReactiveVal(1)
- v2 = ReactiveVal(2)
+ v1 = Value(1)
+ v2 = Value(2)
v2_result = None
- # In practice, on the first flush, Observers run in the order that they were
+ # In practice, on the first flush, Effects run in the order that they were
# created. Our test checks that o2 runs _after_ o1.
- @observe_async()
+ @effect_async()
async def o2():
nonlocal v2_result
v2_result = v2()
- @observe_async()
+ @effect_async()
async def o1():
- v2(v1())
+ v2.set(v1())
await reactcore.flush()
assert v2_result == 1
@@ -73,39 +72,39 @@ async def o1():
# ======================================================================
-# Setting ReactiveVal to same value doesn't invalidate downstream
+# Setting reactive.Value to same value doesn't invalidate downstream
# ======================================================================
@pytest.mark.asyncio
-async def test_reactive_val_same_no_invalidate():
- v = ReactiveVal(1)
+async def test_reactive_value_same_no_invalidate():
+ v = Value(1)
- @observe()
+ @effect()
def o():
v()
await reactcore.flush()
assert o._exec_count == 1
- v(1)
+ v.set(1)
await reactcore.flush()
assert o._exec_count == 1
# ======================================================================
-# Recursive calls to reactives
+# Recursive calls to calcs
# ======================================================================
@pytest.mark.asyncio
-async def test_recursive_reactive():
- v = ReactiveVal(5)
+async def test_recursive_calc():
+ v = Value(5)
- @reactive()
+ @calc()
def r():
if v() == 0:
return 0
- v(v() - 1)
+ v.set(v() - 1)
r()
- @observe()
+ @effect()
def o():
r()
@@ -117,17 +116,17 @@ def o():
@pytest.mark.asyncio
-async def test_recursive_reactive_async():
- v = ReactiveVal(5)
+async def test_recursive_calc_async():
+ v = Value(5)
- @reactive_async()
+ @calc_async()
async def r():
if v() == 0:
return 0
- v(v() - 1)
+ v.set(v() - 1)
await r()
- @observe_async()
+ @effect_async()
async def o():
await r()
@@ -145,12 +144,12 @@ async def o():
@pytest.mark.asyncio
async def test_async_sequential():
- x: ReactiveVal[int] = ReactiveVal(1)
+ x: Value[int] = Value(1)
results: list[int] = []
exec_order: list[str] = []
async def react_chain(n: int):
- @reactive_async()
+ @calc_async()
async def r():
nonlocal exec_order
exec_order.append(f"r{n}-1")
@@ -158,7 +157,7 @@ async def r():
exec_order.append(f"r{n}-2")
return x() + 10
- @observe_async()
+ @effect_async()
async def _():
nonlocal exec_order
exec_order.append(f"o{n}-1")
@@ -170,14 +169,14 @@ async def _():
await asyncio.gather(react_chain(1), react_chain(2))
await reactcore.flush()
- x(5)
+ x.set(5)
await reactcore.flush()
assert results == [111, 211, 115, 215]
- # This is the order of execution if the async observers are run
- # sequentially. The `asyncio.sleep(0)` still yields control, but since there
- # are no other observers scheduled, it will simply resume at the same point.
+ # This is the order of execution if the async effects are run sequentially. The
+ # `asyncio.sleep(0)` still yields control, but since there are no other effects
+ # scheduled, it will simply resume at the same point.
# fmt: off
assert exec_order == [
'o1-1', 'o1-2', 'r1-1', 'r1-2', 'o1-3',
@@ -193,11 +192,10 @@ async def _():
# ======================================================================
@pytest.mark.asyncio
async def test_isolate_basic_without_context():
- # isolate() works with Reactive and ReactiveVal; allows executing without a
- # reactive context.
- v = ReactiveVal(1)
+ # isolate() works with calc and Value; allows executing without a reactive context.
+ v = Value(1)
- @reactive()
+ @calc()
def r():
return v() + 10
@@ -214,16 +212,16 @@ def get_r():
@pytest.mark.asyncio
async def test_isolate_prevents_dependency():
- v = ReactiveVal(1)
+ v = Value(1)
- @reactive()
+ @calc()
def r():
return v() + 10
- v_dep = ReactiveVal(1) # Use this only for invalidating the observer
+ v_dep = Value(1) # Use this only for invalidating the effect
o_val = None
- @observe()
+ @effect()
def o():
nonlocal o_val
v_dep()
@@ -234,13 +232,13 @@ def o():
assert o_val == 11
# Changing v() shouldn't invalidate o
- v(2)
+ v.set(2)
await reactcore.flush()
assert o_val == 11
assert o._exec_count == 1
- # v_dep() should invalidate the observer
- v_dep(2)
+ # v_dep() should invalidate the effect
+ v_dep.set(2)
await reactcore.flush()
assert o_val == 12
assert o._exec_count == 2
@@ -260,11 +258,11 @@ async def f():
@pytest.mark.asyncio
async def test_isolate_async_basic_without_context():
- # async isolate works with Reactive and ReactiveVal; allows executing
- # without a reactive context.
- v = ReactiveVal(1)
+ # async isolate works with calc and Value; allows executing without a reactive
+ # context.
+ v = Value(1)
- @reactive_async()
+ @calc_async()
async def r():
return v() + 10
@@ -278,16 +276,16 @@ async def get_r():
@pytest.mark.asyncio
async def test_isolate_async_prevents_dependency():
- v = ReactiveVal(1)
+ v = Value(1)
- @reactive_async()
+ @calc_async()
async def r():
return v() + 10
- v_dep = ReactiveVal(1) # Use this only for invalidating the observer
+ v_dep = Value(1) # Use this only for invalidating the effect
o_val = None
- @observe_async()
+ @effect_async()
async def o():
nonlocal o_val
v_dep()
@@ -298,39 +296,39 @@ async def o():
assert o_val == 11
# Changing v() shouldn't invalidate o
- v(2)
+ v.set(2)
await reactcore.flush()
assert o_val == 11
assert o._exec_count == 1
- # v_dep() should invalidate the observer
- v_dep(2)
+ # v_dep() should invalidate the effect
+ v_dep.set(2)
await reactcore.flush()
assert o_val == 12
assert o._exec_count == 2
# ======================================================================
-# Priority for observers
+# Priority for effects
# ======================================================================
@pytest.mark.asyncio
-async def test_observer_priority():
- v = ReactiveVal(1)
+async def test_effect_priority():
+ v = Value(1)
results: list[int] = []
- @observe(priority=1)
+ @effect(priority=1)
def o1():
nonlocal results
v()
results.append(1)
- @observe(priority=2)
+ @effect(priority=2)
def o2():
nonlocal results
v()
results.append(2)
- @observe(priority=1)
+ @effect(priority=1)
def o3():
nonlocal results
v()
@@ -339,9 +337,9 @@ def o3():
await reactcore.flush()
assert results == [2, 1, 3]
- # Add another observer with priority 2. Only this one will run (until we
+ # Add another effect with priority 2. Only this one will run (until we
# invalidate others by changing v).
- @observe(priority=2)
+ @effect(priority=2)
def o4():
nonlocal results
v()
@@ -353,35 +351,35 @@ def o4():
# Change v and run again, to make sure results are stable
results.clear()
- v(2)
+ v.set(2)
await reactcore.flush()
assert results == [2, 4, 1, 3]
results.clear()
- v(3)
+ v.set(3)
await reactcore.flush()
assert results == [2, 4, 1, 3]
# Same as previous, but with async
@pytest.mark.asyncio
-async def test_observer_async_priority():
- v = ReactiveVal(1)
+async def test_effect_async_priority():
+ v = Value(1)
results: list[int] = []
- @observe_async(priority=1)
+ @effect_async(priority=1)
async def o1():
nonlocal results
v()
results.append(1)
- @observe_async(priority=2)
+ @effect_async(priority=2)
async def o2():
nonlocal results
v()
results.append(2)
- @observe_async(priority=1)
+ @effect_async(priority=1)
async def o3():
nonlocal results
v()
@@ -390,9 +388,9 @@ async def o3():
await reactcore.flush()
assert results == [2, 1, 3]
- # Add another observer with priority 2. Only this one will run (until we
+ # Add another effect with priority 2. Only this one will run (until we
# invalidate others by changing v).
- @observe_async(priority=2)
+ @effect_async(priority=2)
async def o4():
nonlocal results
v()
@@ -404,25 +402,25 @@ async def o4():
# Change v and run again, to make sure results are stable
results.clear()
- v(2)
+ v.set(2)
await reactcore.flush()
assert results == [2, 4, 1, 3]
results.clear()
- v(3)
+ v.set(3)
await reactcore.flush()
assert results == [2, 4, 1, 3]
# ======================================================================
-# Destroying observers
+# Destroying effects
# ======================================================================
@pytest.mark.asyncio
-async def test_observer_destroy():
- v = ReactiveVal(1)
+async def test_effect_destroy():
+ v = Value(1)
results: list[int] = []
- @observe()
+ @effect()
def o1():
nonlocal results
v()
@@ -431,16 +429,16 @@ def o1():
await reactcore.flush()
assert results == [1]
- v(2)
+ v.set(2)
o1.destroy()
await reactcore.flush()
assert results == [1]
# Same as above, but destroy before running first time
- v = ReactiveVal(1)
+ v = Value(1)
results: list[int] = []
- @observe()
+ @effect()
def o2():
nonlocal results
v()
@@ -458,68 +456,68 @@ def o2():
async def test_error_handling():
vals: List[str] = []
- @observe()
+ @effect()
def _():
vals.append("o1")
- @observe()
+ @effect()
def _():
vals.append("o2-1")
raise Exception("Error here!")
vals.append("o2-2")
- @observe()
+ @effect()
def _():
vals.append("o3")
- # Error in observer should get converted to warning.
+ # Error in effect should get converted to warning.
with pytest.warns(reactcore.ReactiveWarning):
await reactcore.flush()
- # All observers should have executed.
+ # All effects should have executed.
assert vals == ["o1", "o2-1", "o3"]
vals: List[str] = []
- @reactive()
+ @calc()
def r():
vals.append("r")
raise Exception("Error here!")
- @observe()
+ @effect()
def _():
vals.append("o1-1")
r()
vals.append("o1-2")
- @observe()
+ @effect()
def _():
vals.append("o2")
- # Error in observer should get converted to warning.
+ # Error in effect should get converted to warning.
with pytest.warns(reactcore.ReactiveWarning):
await reactcore.flush()
assert vals == ["o1-1", "r", "o2"]
@pytest.mark.asyncio
-async def test_reactive_error_rethrow():
- # Make sure reactives re-throw errors.
+async def test_calc_error_rethrow():
+ # Make sure calcs re-throw errors.
vals: List[str] = []
- v = ReactiveVal(1)
+ v = Value(1)
- @reactive()
+ @calc()
def r():
vals.append("r")
raise Exception("Error here!")
- @observe()
+ @effect()
def _():
v()
vals.append("o1-1")
r()
vals.append("o1-2")
- @observe()
+ @effect()
def _():
v()
vals.append("o2-2")
@@ -530,7 +528,7 @@ def _():
await reactcore.flush()
assert vals == ["o1-1", "r", "o2-2"]
- v(2)
+ v.set(2)
with pytest.warns(reactcore.ReactiveWarning):
await reactcore.flush()
assert vals == ["o1-1", "r", "o2-2", "o1-1", "o2-2"]
@@ -542,11 +540,11 @@ def _():
# For https://github.com/rstudio/prism/issues/26
@pytest.mark.asyncio
async def test_dependent_invalidation():
- trigger = ReactiveVal(0)
- v = ReactiveVal(0)
+ trigger = Value(0)
+ v = Value(0)
error_occurred = False
- @observe()
+ @effect()
def _():
trigger()
@@ -554,21 +552,21 @@ def _():
with isolate():
r()
val = v()
- v(val + 1)
+ v.set(val + 1)
except Exception:
nonlocal error_occurred
error_occurred = True
- @observe()
+ @effect()
def _():
r()
- @reactive()
+ @calc()
def r():
return v()
await reactcore.flush()
- trigger(1)
+ trigger.set(1)
await reactcore.flush()
with isolate():
@@ -579,13 +577,13 @@ def r():
# ------------------------------------------------------------
-# req() pauses execution in @observe() and @reactive()
+# req() pauses execution in @effect() and @calc()
# ------------------------------------------------------------
@pytest.mark.asyncio
async def test_req():
n_times = 0
- @observe()
+ @effect()
def _():
req(False)
nonlocal n_times
@@ -594,7 +592,7 @@ def _():
await reactcore.flush()
assert n_times == 0
- @observe()
+ @effect()
def _():
req(True)
nonlocal n_times
@@ -603,14 +601,14 @@ def _():
await reactcore.flush()
assert n_times == 1
- @reactive()
+ @calc()
def r():
req(False)
return 1
val = None
- @observe()
+ @effect()
def _():
nonlocal val
val = r()
@@ -618,12 +616,12 @@ def _():
await reactcore.flush()
assert val is None
- @reactive()
+ @calc()
def r2():
req(True)
return 1
- @observe()
+ @effect()
def _():
nonlocal val
val = r2()
@@ -637,7 +635,7 @@ async def test_invalidate_later():
mock_time = MockTime()
with mock_time():
- @observe()
+ @effect()
def obs1():
invalidate_later(1)
@@ -667,9 +665,9 @@ def obs1():
async def test_invalidate_later_invalidation():
mock_time = MockTime()
with mock_time():
- rv = ReactiveVal(0)
+ rv = Value(0)
- @observe()
+ @effect()
def obs1():
if rv() == 0:
invalidate_later(1)
@@ -679,7 +677,7 @@ def obs1():
# Change rv, triggering invalidation of obs1. The expected behavior is that
# the invalidation causes the invalidate_later call to be cancelled.
- rv(1)
+ rv.set(1)
await reactcore.flush()
assert obs1._exec_count == 2
@@ -717,218 +715,227 @@ async def add_result_later(delay: float, msg: str):
# ------------------------------------------------------------
# @event() works as expected
# ------------------------------------------------------------
-def test_event_decorator():
+@pytest.mark.asyncio
+async def test_event_decorator():
n_times = 0
# By default, runs every time that event expression is _not_ None (ignore_none=True)
- @observe()
+ @effect()
@event(lambda: None, lambda: ActionButtonValue(0))
def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 0
# Unless ignore_none=False
- @observe()
+ @effect()
@event(lambda: None, lambda: ActionButtonValue(0), ignore_none=False)
def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 1
# Or if one of the args is not None
- @observe()
+ @effect()
@event(lambda: None, lambda: ActionButtonValue(0), lambda: True)
def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 2
- # Is invalidated properly by reactive vals
- r = ReactiveVal(1)
+ # Is invalidated properly by reactive values
+ v = Value(1)
- @observe()
- @event(r)
+ @effect()
+ @event(v)
def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 3
- r(1)
- asyncio.run(reactcore.flush())
+ v.set(1)
+ await reactcore.flush()
assert n_times == 3
- r(2)
- asyncio.run(reactcore.flush())
+ v.set(2)
+ await reactcore.flush()
assert n_times == 4
# Doesn't run on init
- r = ReactiveVal(1)
+ v = Value(1)
- @observe()
- @event(r, ignore_init=True)
+ @effect()
+ @event(v, ignore_init=True)
def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 4
- r(2)
- asyncio.run(reactcore.flush())
+ v.set(2)
+ await reactcore.flush()
assert n_times == 5
# Isolates properly
- r = ReactiveVal(1)
- r2 = ReactiveVal(1)
+ v = Value(1)
+ v2 = Value(1)
- @observe()
- @event(r)
+ @effect()
+ @event(v)
def _():
nonlocal n_times
- n_times += r2()
+ n_times += v2()
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 6
- r2(2)
- asyncio.run(reactcore.flush())
+ v2.set(2)
+ await reactcore.flush()
assert n_times == 6
- # works with @reactive()
- r2 = ReactiveVal(1)
+ # works with @calc()
+ v2 = Value(1)
- @reactive()
- @event(lambda: r2(), ignore_init=True)
+ @calc()
+ @event(lambda: v2(), ignore_init=True)
def r2b():
return 1
- @observe()
+ @effect()
def _():
nonlocal n_times
n_times += r2b()
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 6
- r2(2)
- asyncio.run(reactcore.flush())
+ v2.set(2)
+ await reactcore.flush()
assert n_times == 7
# ------------------------------------------------------------
# @event() works as expected with async
# ------------------------------------------------------------
-def test_event_async_decorator():
+@pytest.mark.asyncio
+async def test_event_async_decorator():
n_times = 0
# By default, runs every time that event expression is _not_ None (ignore_none=True)
- @observe_async()
+ @effect_async()
@event(lambda: None, lambda: ActionButtonValue(0))
async def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 0
# Unless ignore_none=False
- @observe_async()
+ @effect_async()
@event(lambda: None, lambda: ActionButtonValue(0), ignore_none=False)
async def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 1
# Or if one of the args is not None
- @observe_async()
+ @effect_async()
@event(lambda: None, lambda: ActionButtonValue(0), lambda: True)
async def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 2
- # Is invalidated properly by reactive vals
- r = ReactiveVal(1)
+ # Is invalidated properly by reactive values
+ v = Value(1)
- @observe_async()
- @event(r)
+ @effect_async()
+ @event(v)
async def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 3
- r(1)
- asyncio.run(reactcore.flush())
+ v.set(1)
+ await reactcore.flush()
assert n_times == 3
- r(2)
- asyncio.run(reactcore.flush())
+ v.set(2)
+ await reactcore.flush()
assert n_times == 4
# Doesn't run on init
- r = ReactiveVal(1)
+ v = Value(1)
- @observe_async()
- @event(r, ignore_init=True)
+ @effect_async()
+ @event(v, ignore_init=True)
async def _():
nonlocal n_times
n_times += 1
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 4
- r(2)
- asyncio.run(reactcore.flush())
+ v.set(2)
+ await reactcore.flush()
assert n_times == 5
# Isolates properly
- r = ReactiveVal(1)
- r2 = ReactiveVal(1)
+ v = Value(1)
+ v2 = Value(1)
- @observe_async()
- @event(r)
+ @effect_async()
+ @event(v)
async def _():
nonlocal n_times
- n_times += r2()
+ n_times += v2()
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 6
- r2(2)
- asyncio.run(reactcore.flush())
+ v2.set(2)
+ await reactcore.flush()
assert n_times == 6
- # works with @reactive()
- r2 = ReactiveVal(1)
+ # works with @calc()
+ v2 = Value(1)
- @reactive_async()
- @event(lambda: r2(), ignore_init=True)
+ @calc_async()
+ async def r_a():
+ await asyncio.sleep(0) # Make sure the async function yields control
+ return 1
+
+ @calc_async()
+ @event(lambda: v2(), r_a, ignore_init=True)
async def r2b():
+ await asyncio.sleep(0) # Make sure the async function yields control
return 1
- @observe_async()
+ @effect_async()
async def _():
nonlocal n_times
+ await asyncio.sleep(0)
n_times += await r2b()
- asyncio.run(reactcore.flush())
+ await reactcore.flush()
assert n_times == 6
- r2(2)
- asyncio.run(reactcore.flush())
+ v2.set(2)
+ await reactcore.flush()
assert n_times == 7
diff --git a/tests/test_shinysession.py b/tests/test_shinysession.py
index d31ee70be..09740d881 100644
--- a/tests/test_shinysession.py
+++ b/tests/test_shinysession.py
@@ -1,4 +1,4 @@
-"""Tests for `shiny.shinysession`."""
+"""Tests for `shiny.Session`."""
import pytest