-
Notifications
You must be signed in to change notification settings - Fork 114
Add support for express mode apps #767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e85d8cc
5297c58
50d6d7f
9d5d70a
3cd256d
03fb00d
9ab79d3
e09ea98
59f04c4
cb16657
391c8c6
5736c93
7197199
b5fc438
84bd0b2
b1f4ea3
d98986f
090c93d
d6b9933
5aaaa53
8982f02
105dca4
9301477
016901f
068765b
5e856dc
2adaba9
ddf96f9
297fbd9
2b9665f
ef0ebae
b6f4fc8
8bacc1a
323c19c
526d521
89fa03a
8cc81d5
baacb10
27d041d
48206d8
e1ad116
2917d6e
56dfcbb
4a45a1e
4e3e22e
e76d233
b444b91
adeb691
1b51001
e60a77d
f9d16dd
688cb1d
10cd0ec
3d58b5c
5542ba6
7f54dac
cd6b4bb
e8cdc87
ea2bd07
31a344c
a25abe6
f114dc1
eb3668e
9f3a470
5f53029
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
|
||
from shiny import render, ui | ||
from shiny.express import input, layout | ||
|
||
with layout.accordion(open=["Panel 1", "Panel 2"]): | ||
with layout.accordion_panel("Panel 1"): | ||
ui.input_slider("n", "N", 1, 100, 50) | ||
|
||
with layout.accordion_panel("Panel 2"): | ||
|
||
@render.text | ||
def txt(): | ||
return f"n = {input.n()}" | ||
|
||
|
||
@render.plot | ||
def histogram(): | ||
np.random.seed(19680801) | ||
x = 100 + 15 * np.random.randn(437) | ||
plt.hist(x, input.n(), density=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from shiny import render, ui | ||
from shiny.express import input | ||
|
||
ui.input_slider("n", "N", 1, 100, 50) | ||
|
||
|
||
@render.text() | ||
def txt(): | ||
return f"n = {input.n()}" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
|
||
from shiny import render, ui | ||
from shiny.express import input, layout | ||
|
||
with layout.layout_column_wrap(width=1 / 2): | ||
with layout.card(): | ||
ui.input_slider("n", "N", 1, 100, 50) | ||
|
||
with layout.card(): | ||
|
||
@render.plot | ||
def histogram(): | ||
np.random.seed(19680801) | ||
x = 100 + 15 * np.random.randn(437) | ||
plt.hist(x, input.n(), density=True) | ||
|
||
with layout.card(): | ||
|
||
@render.plot | ||
def histogram2(): | ||
np.random.seed(19680801) | ||
x = 100 + 15 * np.random.randn(437) | ||
plt.hist(x, input.n(), density=True, color="red") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
|
||
from shiny import render, ui | ||
from shiny.express import input, layout | ||
|
||
with layout.column(width=6): | ||
with layout.navset_tab(): | ||
with layout.nav(title="One"): | ||
ui.input_slider("n", "N", 1, 100, 50) | ||
|
||
with layout.nav(title="Two"): | ||
|
||
@render.plot | ||
def histogram(): | ||
np.random.seed(19680801) | ||
x = 100 + 15 * np.random.randn(437) | ||
plt.hist(x, input.n(), density=True) | ||
|
||
|
||
with layout.column(width=6): | ||
with layout.navset_card_tab(): | ||
with layout.nav(title="One"): | ||
ui.input_slider("n2", "N", 1, 100, 50) | ||
|
||
with layout.nav(title="Two"): | ||
|
||
@render.plot | ||
def histogram2(): | ||
np.random.seed(19680801) | ||
x = 100 + 15 * np.random.randn(437) | ||
plt.hist(x, input.n2(), density=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
|
||
from shiny import render, ui | ||
from shiny.express import input | ||
|
||
ui.input_slider("n", "N", 1, 100, 50) | ||
|
||
|
||
@render.plot | ||
def histogram(): | ||
np.random.seed(19680801) | ||
x = 100 + 15 * np.random.randn(437) | ||
plt.hist(x, input.n(), density=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# This is not a Shiny application; it is meant to be imported by shared_app.py. | ||
|
||
from shiny import reactive, session | ||
|
||
# Print this at the console to make it clear that shared.py is loaded just once per run | ||
# of the app; each additional session does not result in this file loading again. | ||
print("Loading shared.py!") | ||
|
||
# This is a variable that can be used and shared across multiple sessions. This can be | ||
# useful for large data, or values that are expensive to compute. It can also be useful | ||
# for mutable objects that you want to share across sessions. | ||
data = ["This", "is", "a", "list", "of", "words"] | ||
|
||
# Any reactive objects should be created without a session context. | ||
with session.session_context(None): | ||
# This reactive value can be used by multiple sessions; if it is invalidated (in | ||
# other words, if the value is changed), it will trigger invalidations in all of | ||
# those sessions. | ||
rv = reactive.Value(-1) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# This app demonstrates how to use "global" variables that are shared across sessions. | ||
# This is useful if you want to load data just once and use it in multiple apps, or if | ||
# you want to share data or reactives among apps. | ||
|
||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
import shared | ||
|
||
from shiny import reactive, render, ui | ||
from shiny.express import input | ||
|
||
|
||
@render.plot | ||
def histogram(): | ||
np.random.seed(19680801) | ||
x = 100 + 15 * np.random.randn(437) | ||
plt.hist(x, shared.rv(), density=True) | ||
|
||
|
||
ui.input_slider("n", "N", 1, 100, 50) | ||
|
||
|
||
@reactive.Effect | ||
def _(): | ||
shared.rv.set(input.n()) | ||
|
||
|
||
@render.text | ||
def rv_value(): | ||
return f"shared.rv() = {shared.rv()}" | ||
|
||
|
||
@render.text | ||
def text_data(): | ||
return "shared.data = " + str(shared.data) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import matplotlib.pyplot as plt | ||
import numpy as np | ||
|
||
from shiny import render, ui | ||
from shiny.express import input, layout | ||
|
||
with layout.sidebar(): | ||
ui.input_slider("n", "N", 1, 100, 50) | ||
|
||
|
||
@render.plot | ||
def histogram(): | ||
np.random.seed(19680801) | ||
x = 100 + 15 * np.random.randn(437) | ||
plt.hist(x, input.n(), density=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import type { ErrorsMessageValue } from "rstudio-shiny/srcts/types/src/shiny/shinyapp"; | ||
|
||
class PageOutputBinding extends Shiny.OutputBinding { | ||
originalBodyTagAttrs: Array<Attr> | null = null; | ||
|
||
find(scope: HTMLElement | JQuery<HTMLElement>): JQuery<HTMLElement> { | ||
return $(scope).find(".shiny-page-output"); | ||
} | ||
|
||
onValueError(el: HTMLElement, err: ErrorsMessageValue): void { | ||
Shiny.unbindAll(el); | ||
this.renderError(el, err); | ||
} | ||
|
||
async renderValue( | ||
el: HTMLElement, | ||
data: Parameters<typeof Shiny.renderContent>[1] | ||
): Promise<void> { | ||
if (el !== document.body) { | ||
throw new Error( | ||
'Output with class "shiny-page-output" must be a <body> tag.' | ||
); | ||
} | ||
|
||
if (this.originalBodyTagAttrs === null) { | ||
// On the first run, store el's attributes so that on later runs we can clear | ||
// any added attributes and reset this element to its original state. | ||
this.originalBodyTagAttrs = Array.from(el.attributes); | ||
} else { | ||
// This is a later run. Reset attributes to their inital state. | ||
for (const attr of this.originalBodyTagAttrs) { | ||
el.setAttribute(attr.name, attr.value); | ||
} | ||
} | ||
|
||
let content = typeof data === "string" ? data : data.html; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. data will never be a string |
||
|
||
// Parse the HTML | ||
const parser = new DOMParser(); | ||
const doc = parser.parseFromString(content, "text/html"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might be a little slow but whatever |
||
|
||
// Copy the <html> tag's lang attribute, if present. | ||
if (doc.documentElement.lang) { | ||
document.documentElement.lang = doc.documentElement.lang; | ||
} | ||
|
||
// Copy the <title>, if present. | ||
if (doc.title) { | ||
document.title = doc.title; | ||
} | ||
|
||
// Copy attributes from parsed <body> to the output element (which should be a | ||
// <body>) | ||
for (const attr of Array.from(doc.body.attributes)) { | ||
if (attr.name === "class") el.classList.add(...attr.value.split(" ")); | ||
else el.setAttribute(attr.name, attr.value); | ||
} | ||
|
||
content = content | ||
.replace(/<html>.*<body[^>]*>/gis, "") | ||
.replace(/<\/body>.*<\/html>/gis, ""); | ||
|
||
if (typeof data === "string") { | ||
data = content; | ||
} else { | ||
data.html = content; | ||
} | ||
|
||
await Shiny.renderContent(el, data); | ||
} | ||
} | ||
|
||
Shiny.outputBindings.register( | ||
new PageOutputBinding(), | ||
"shinyPageOutputBinding" | ||
); | ||
|
||
export { PageOutputBinding }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,74 @@ | ||
from __future__ import annotations | ||
|
||
from ..session import Inputs, Outputs, Session | ||
from ..session import _utils as _session_utils | ||
from . import app, layout | ||
from ._output import output_args, suspend_display | ||
from ._run import is_express_app, wrap_express_app | ||
from .display_decorator import display_body | ||
|
||
__all__ = ( | ||
"input", | ||
"output", | ||
"session", | ||
"is_express_app", | ||
"output_args", | ||
"suspend_display", | ||
"wrap_express_app", | ||
"app", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems private |
||
"layout", | ||
"display_body", | ||
) | ||
|
||
# Add types to help type checkers | ||
input: Inputs | ||
output: Outputs | ||
session: Session | ||
|
||
|
||
# Note that users should use `from shiny.express import input` instead of `from shiny | ||
# import express` and acces via `express.input`. The former provides a static value for | ||
# `input`, but the latter is dynamic -- every time `express.input` is accessed, it | ||
# returns the input for the current session. This will work in the vast majority of | ||
# cases, but when it fails, it will be very confusing. | ||
def __getattr__(name: str) -> object: | ||
if name == "input": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider detecting if input, output, or session are requested more than once from the same scope. (using |
||
return _get_current_session_or_mock().input | ||
elif name == "output": | ||
return _get_current_session_or_mock().output | ||
elif name == "session": | ||
return _get_current_session_or_mock() | ||
|
||
raise AttributeError(f"Module 'shiny.express' has no attribute '{name}'") | ||
|
||
|
||
# A very bare-bones mock session class that is used only in shiny.express. | ||
class _MockSession: | ||
def __init__(self): | ||
from typing import cast | ||
|
||
from .._namespaces import Root | ||
|
||
self.input = Inputs({}) | ||
self.output = Outputs(cast(Session, self), Root, {}, {}) | ||
|
||
# This is needed so that Outputs don't throw an error. | ||
def _is_hidden(self, name: str) -> bool: | ||
return False | ||
|
||
|
||
_current_mock_session: _MockSession | None = None | ||
|
||
|
||
def _get_current_session_or_mock() -> Session: | ||
from typing import cast | ||
|
||
session = _session_utils.get_current_session() | ||
if session is None: | ||
global _current_mock_session | ||
if _current_mock_session is None: | ||
_current_mock_session = _MockSession() | ||
return cast(Session, _current_mock_session) | ||
|
||
else: | ||
return session |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to also clear added attributes