Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
e85d8cc
Add basic flat app support
wch Oct 17, 2023
5297c58
Add @suspend_display and @output_args decorators
jcheng5 Oct 17, 2023
50d6d7f
Functions within with, while, try, etc. are displayed
jcheng5 Oct 17, 2023
9d5d70a
Call repr() on objects
wch Oct 17, 2023
3cd256d
Replace Any with object
wch Oct 18, 2023
03fb00d
Ignore type checking for line
wch Oct 18, 2023
9ab79d3
Don't print None
wch Oct 18, 2023
e09ea98
Fix path for flat apps
wch Oct 18, 2023
59f04c4
Move flat.py into flat/
wch Oct 18, 2023
cb16657
Refactor flat app wrapping code
wch Oct 18, 2023
391c8c6
Reorganize
wch Oct 19, 2023
5736c93
Merge remote-tracking branch 'origin/main' into flat-mode
wch Oct 19, 2023
7197199
Restructure files
wch Oct 20, 2023
b5fc438
Add flat.ui module
wch Oct 20, 2023
84bd0b2
Add flat.open
wch Oct 21, 2023
b1f4ea3
Code cleanup
wch Oct 21, 2023
d98986f
Update exports
wch Oct 23, 2023
090c93d
Remove unused import
wch Oct 23, 2023
d6b9933
Remove .open code
wch Oct 25, 2023
5aaaa53
Refactor RecallContextManager and components
wch Oct 25, 2023
8982f02
Rename shiny.flat.ui to shiny.flat.layout
wch Oct 25, 2023
105dca4
Better typing for RecallContextManager; add some components
wch Oct 25, 2023
9301477
Rename shiny.flat to shiny.express
wch Oct 25, 2023
016901f
Use release version of htmltools
wch Oct 25, 2023
068765b
Simplify use of __getattr__
wch Oct 25, 2023
5e856dc
Fix use of shiny.express
wch Oct 25, 2023
2adaba9
Allow importing session from shiny.express otside of app
wch Oct 25, 2023
ddf96f9
Bump version to 0.5.1.9004
wch Oct 25, 2023
297fbd9
Add layout.set_page()
wch Oct 26, 2023
2b9665f
Allow layout functions to set the page
wch Oct 26, 2023
ef0ebae
Make body tag into dynamic ui output
wch Oct 26, 2023
b6f4fc8
Undo change in pyrightconfig
wch Oct 27, 2023
8bacc1a
Make 'shiny run' work with express apps when no filename is provided
wch Oct 27, 2023
323c19c
More robust express-mode detection
wch Oct 27, 2023
526d521
Merge remote-tracking branch 'origin/main' into flat-mode
wch Oct 27, 2023
89fa03a
Add layout.layout_column_wrap and layout.page_fillable
wch Oct 28, 2023
8cc81d5
Merge remote-tracking branch 'origin/main' into flat-mode
wch Oct 28, 2023
baacb10
Add accordions to express.layout
wch Oct 29, 2023
27d041d
Merge remote-tracking branch 'origin/main' into flat-mode
wch Oct 30, 2023
48206d8
Merge branch 'main' into flat-mode
wch Oct 31, 2023
e1ad116
Bump version to 0.6.0.9001
wch Oct 31, 2023
2917d6e
Add examples
wch Oct 31, 2023
56dfcbb
Fix filtering of def'ed functions to display
wch Oct 31, 2023
4a45a1e
Set default page more clearly
wch Oct 31, 2023
4e3e22e
Add shared example app
wch Oct 31, 2023
e76d233
Fix function def
wch Nov 2, 2023
b444b91
Return function object
wch Nov 2, 2023
adeb691
Merge remote-tracking branch 'origin/main' into flat-mode
wch Nov 4, 2023
1b51001
Copy title and lang
wch Nov 4, 2023
e60a77d
Code cleanup
wch Nov 4, 2023
f9d16dd
Output decorator should return decorated function
jcheng5 Nov 7, 2023
688cb1d
Try to fix pyright errors
jcheng5 Nov 7, 2023
10cd0ec
Add @display_body decorator
jcheng5 Nov 8, 2023
3d58b5c
Fix pyright
jcheng5 Nov 8, 2023
5542ba6
Disable flake8 for test_display_decorator
jcheng5 Nov 8, 2023
7f54dac
Fix type error in Python <3.10
jcheng5 Nov 8, 2023
cd6b4bb
pyright py3.8
jcheng5 Nov 8, 2023
e8cdc87
Pyright
jcheng5 Nov 8, 2023
ea2bd07
Simplify basic express app
wch Nov 7, 2023
31a344c
App updates
wch Nov 8, 2023
a25abe6
Add render.display decorator
jcheng5 Nov 9, 2023
f114dc1
Fix pyright
jcheng5 Nov 9, 2023
eb3668e
pyright again
jcheng5 Nov 9, 2023
9f3a470
Generate static UI
wch Nov 9, 2023
5f53029
Remove unused import
wch Nov 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions examples/express/accordion_app.py
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)
9 changes: 9 additions & 0 deletions examples/express/basic_app.py
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()}"
25 changes: 25 additions & 0 deletions examples/express/column_wrap_app.py
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")
32 changes: 32 additions & 0 deletions examples/express/nav_app.py
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)
14 changes: 14 additions & 0 deletions examples/express/plot_app.py
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)
19 changes: 19 additions & 0 deletions examples/express/shared.py
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)
35 changes: 35 additions & 0 deletions examples/express/shared_app.py
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)
15 changes: 15 additions & 0 deletions examples/express/sidebar_app.py
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)
7 changes: 7 additions & 0 deletions js/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ const opts: Array<BuildOptions> = [
minify: false,
sourcemap: false,
},
{
entryPoints: {
"page-output/page-output": "page-output/page-output.ts",
},
minify: false,
sourcemap: false,
},
];

// Run function to avoid top level await
Expand Down
78 changes: 78 additions & 0 deletions js/page-output/page-output.ts
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);
Copy link
Collaborator

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

}
}

let content = typeof data === "string" ? data : data.html;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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");
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 };
2 changes: 1 addition & 1 deletion shiny/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""A package for building reactive web applications."""

__version__ = "0.6.0.9000"
__version__ = "0.6.0.9001"

from ._shinyenv import is_pyodide as _is_pyodide

Expand Down
16 changes: 14 additions & 2 deletions shiny/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import uvicorn.config

import shiny
from shiny.express import is_express_app

from . import _autoreload, _hostenv, _static, _utils
from ._typing_extensions import NotRequired, TypedDict
Expand Down Expand Up @@ -275,7 +276,18 @@ def run_app(
os.environ["SHINY_PORT"] = str(port)

if isinstance(app, str):
app, app_dir = resolve_app(app, app_dir)
# Remove ":app" suffix if present. Normally users would just pass in the
# filename without the trailing ":app", as in `shiny run app.py`, but the
# default value for `shiny run` is "app.py:app", so we need to handle it.
app_no_suffix = re.sub(r":app$", "", app)
if is_express_app(app_no_suffix, app_dir):
app_path = Path(app_no_suffix).resolve()
# Set this so shiny.express.app.wrap_express_app() can find the app file.
os.environ["SHINY_EXPRESS_APP_FILE"] = str(app_path)
app = "shiny.express.app:app"
app_dir = str(app_path.parent)
else:
app, app_dir = resolve_app(app, app_dir)

if app_dir:
app_dir = os.path.realpath(app_dir)
Expand Down Expand Up @@ -381,7 +393,7 @@ def is_file(app: str) -> bool:
return "/" in app or app.endswith(".py")


def resolve_app(app: str, app_dir: Optional[str]) -> tuple[str, Optional[str]]:
def resolve_app(app: str, app_dir: str | None) -> tuple[str, str | None]:
# The `app` parameter can be:
#
# - A module:attribute name
Expand Down
66 changes: 66 additions & 0 deletions shiny/express/__init__.py
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",
Copy link
Collaborator

Choose a reason for hiding this comment

The 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":
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 inspect?)

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
Loading