diff --git a/docs/Makefile b/docs/Makefile index c81892065..47a6af731 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -53,10 +53,14 @@ quartodoc_impl: $(PYBIN) ## Build qmd files for API docs $(eval export IN_QUARTODOC=true) . $(PYBIN)/activate \ && quartodoc interlinks \ - && quartodoc build --config _quartodoc-core.yml --verbose \ + && echo "::group::quartodoc build core docs" \ + && SHINY_MODE="core" quartodoc build --config _quartodoc-core.yml --verbose \ && mv objects.json _objects_core.json \ - && quartodoc build --config _quartodoc-express.yml --verbose \ - && mv objects.json _objects_express.json + && echo "::endgroup::" \ + && echo "::group::quartodoc build express docs" \ + && SHINY_MODE="express" quartodoc build --config _quartodoc-express.yml --verbose \ + && mv objects.json _objects_express.json \ + && echo "::endgroup::" quartodoc_post: $(PYBIN) ## Post-process qmd files for API docs . $(PYBIN)/activate \ diff --git a/docs/_renderer_core.py b/docs/_renderer_core.py index a46d51292..332a4ab93 100644 --- a/docs/_renderer_core.py +++ b/docs/_renderer_core.py @@ -2,6 +2,7 @@ import base64 import html +import os import re from importlib.resources import files from pathlib import Path @@ -270,6 +271,10 @@ def read_file(file: str | Path, root_dir: str | Path | None = None) -> FileConte def check_if_missing_expected_example(el, converted): + if os.environ.get("SHINY_MODE") == "express": + # These errors are thrown much earlier by @add_example() + return + if re.search(r"(^|\n)#{2,6} Examples\n", converted): # Manually added examples are fine return diff --git a/shiny/_docstring.py b/shiny/_docstring.py index a45a4edce..8ecc13c08 100644 --- a/shiny/_docstring.py +++ b/shiny/_docstring.py @@ -2,7 +2,8 @@ import os import sys -from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar +from functools import wraps +from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, TypeVar def find_api_examples_dir(start_dir: str) -> Optional[str]: @@ -29,6 +30,25 @@ def no_example(func: F) -> F: return func +def no_example_express(decorator: Callable[..., F]) -> F | Callable[..., F]: + """ + Prevent ``@add_example()`` from throwing an error about missing Express examples. + """ + + @wraps(decorator) + def wrapper_decorator(*args: Any, **kwargs: Any) -> F: + try: + # Apply the potentially problematic decorator + return decorator(*args, **kwargs) + except ExpressExampleNotFoundException: + # If an error occurs, return the original function + if args and callable(args[0]): + return args[0] + raise + + return wrapper_decorator + + # This class is used to mark docstrings when @add_example() is used, so that an error # will be thrown if @doc_format() is used afterward. This is to avoid an error when # the example contains curly braces -- the @doc_format() decorator will try to evaluate @@ -50,7 +70,7 @@ def write_example(self, app_files: list[str]) -> str: def add_example( - app_file: str = "app.py", + app_file: Optional[str] = None, ex_dir: Optional[str] = None, ) -> Callable[[F], F]: """ @@ -60,6 +80,14 @@ def add_example( ``__name__`` matches the name of directory under a ``api-examples/`` directory in the current or any parent directory. + * Examples for the ``shiny`` package are in ``shiny/api-examples/``. + * Examples for the ``shiny.express`` subpackage are in ``shiny/express/api-examples/``. + + Functions that can be used in Express or Core and whose canonical implementation is + in the ``shiny`` package should have examples in ``shiny/api-examples``. In this + case, the express variant should include an ``-express`` suffix and the core + variation can be named with a ``-core`` suffix or ``app.py``. + Parameters ---------- app_file: @@ -88,25 +116,40 @@ def _(func: F) -> F: ex_dir_found = find_api_examples_dir(func_dir) if ex_dir_found is None: - raise ValueError( + raise FileNotFoundError( f"No example directory found for {fn_name} in {func_dir} or its parent directories." ) example_dir = os.path.join(ex_dir_found, fn_name) else: - example_dir = os.path.join(func_dir, ex_dir) + example_dir = os.path.abspath(os.path.join(func_dir, ex_dir)) - example_file = os.path.join(example_dir, app_file) - if not os.path.exists(example_file): - raise ValueError( - f"No example for {fn_name} found in '{os.path.abspath(example_dir)}'." + if not os.path.exists(example_dir): + raise FileNotFoundError( + f"Example directory '{example_dir}' does not exist for {fn_name}." + ) + + app_file_name = app_file or "app.py" + try: + example_file = app_choose_core_or_express( + os.path.join(example_dir, app_file_name) ) + except ExampleNotFoundException as e: + func_dir = get_decorated_source_directory(func).split("py-shiny/")[1] + if "__code__" in dir(func): + print( + f"::warning file={func_dir},line={func.__code__.co_firstlineno}::{e}" + ) + else: + print(f"::warning file={func_dir}::{e}") + + return func other_files: list[str] = [] for f in os.listdir(example_dir): abs_f = os.path.join(example_dir, f) is_support_file = ( os.path.isfile(abs_f) - and f != app_file + and f != app_file_name and f != "app.py" and not f.startswith("app-") and not f.startswith("__") @@ -150,6 +193,86 @@ def _(func: F) -> F: return _ +def is_express_app(app_path: str) -> bool: + # We can't use .shiny.express._is_express.is_express_app() here because that would + # create a circular import. + if not os.path.exists(app_path): + return False + + with open(app_path) as f: + for line in f: + if "from shiny.express" in line: + return True + elif "import shiny.express" in line: + return True + return False + + +class ExampleNotFoundException(FileNotFoundError): + def __init__( + self, + file_names: list[str] | str, + dir: str, + type: Optional[Literal["core", "express"]] = None, + ) -> None: + self.type = type or os.environ.get("SHINY_MODE") or "core" + self.file_names = [file_names] if isinstance(file_names, str) else file_names + self.dir = dir + + def __str__(self): + if self.type in ("core", "express"): + # Capitalize first letter + type = "a Shiny Express" if self.type == "express" else "a Shiny Core" + else: + type = "an" + + return ( + f"Could not find {type} example file named " + + f"{' or '.join(self.file_names)} in {self.dir}." + ) + + +class ExpressExampleNotFoundException(ExampleNotFoundException): + def __init__( + self, + file_names: list[str] | str, + dir: str, + ) -> None: + super().__init__(file_names, dir, "express") + + +def app_choose_core_or_express(app_path: Optional[str] = None) -> str: + app_path = app_path or "app.py" + + if os.environ.get("SHINY_MODE") == "express": + if is_express_app(app_path): + return app_path + + app_path = app_path.replace("-core.py", ".py") + + path, ext = os.path.splitext(app_path) + app_path_express = f"{path}-express{ext}" + + if not is_express_app(app_path_express): + raise ExpressExampleNotFoundException( + [os.path.basename(app_path), os.path.basename(app_path_express)], + os.path.dirname(app_path), + ) + + return app_path_express + + if os.path.basename(app_path) == "app.py" and not os.path.exists(app_path): + app_path = app_path.replace("app.py", "app-core.py") + + if not os.path.exists(app_path): + raise ExampleNotFoundException( + os.path.basename(app_path), + os.path.dirname(app_path), + ) + + return app_path + + def get_decorated_source_directory(func: FuncType) -> str: if hasattr(func, "__module__"): path = os.path.abspath(str(sys.modules[func.__module__].__file__)) diff --git a/shiny/api-examples/Module/app.py b/shiny/api-examples/Module/app-core.py similarity index 100% rename from shiny/api-examples/Module/app.py rename to shiny/api-examples/Module/app-core.py diff --git a/shiny/api-examples/Progress/app.py b/shiny/api-examples/Progress/app-core.py similarity index 100% rename from shiny/api-examples/Progress/app.py rename to shiny/api-examples/Progress/app-core.py diff --git a/shiny/api-examples/Progress/app-express.py b/shiny/api-examples/Progress/app-express.py new file mode 100644 index 000000000..b4bde03f6 --- /dev/null +++ b/shiny/api-examples/Progress/app-express.py @@ -0,0 +1,21 @@ +import asyncio + +from shiny import reactive +from shiny.express import input, render, ui + +ui.input_action_button("button", "Compute") + + +@render.text +@reactive.event(input.button) +async def compute(): + with ui.Progress(min=1, max=15) as p: + p.set(message="Calculation in progress", detail="This may take a while...") + + for i in range(1, 15): + p.set(i, message="Computing") + await asyncio.sleep(0.1) + # Normally use time.sleep() instead, but it doesn't yet work in Pyodide. + # https://github.com/pyodide/pyodide/issues/2354 + + return "Done computing!" diff --git a/shiny/api-examples/Renderer/app.py b/shiny/api-examples/Renderer/app-core.py similarity index 100% rename from shiny/api-examples/Renderer/app.py rename to shiny/api-examples/Renderer/app-core.py diff --git a/shiny/api-examples/SafeException/app.py b/shiny/api-examples/SafeException/app-core.py similarity index 100% rename from shiny/api-examples/SafeException/app.py rename to shiny/api-examples/SafeException/app-core.py diff --git a/shiny/api-examples/SilentCancelOutputException/app.py b/shiny/api-examples/SilentCancelOutputException/app-core.py similarity index 100% rename from shiny/api-examples/SilentCancelOutputException/app.py rename to shiny/api-examples/SilentCancelOutputException/app-core.py diff --git a/shiny/api-examples/SilentException/app.py b/shiny/api-examples/SilentException/app-core.py similarity index 100% rename from shiny/api-examples/SilentException/app.py rename to shiny/api-examples/SilentException/app-core.py diff --git a/shiny/api-examples/Value/app.py b/shiny/api-examples/Value/app-core.py similarity index 72% rename from shiny/api-examples/Value/app.py rename to shiny/api-examples/Value/app-core.py index fe0112954..04479492a 100644 --- a/shiny/api-examples/Value/app.py +++ b/shiny/api-examples/Value/app-core.py @@ -1,10 +1,10 @@ from shiny import App, Inputs, Outputs, Session, reactive, render, ui -app_ui = ui.page_fluid( - ui.input_action_button("minus", "-1"), - " ", - ui.input_action_button("plus", "+1"), - ui.br(), +app_ui = ui.page_sidebar( + ui.sidebar( + ui.input_action_button("minus", "-1"), + ui.input_action_button("plus", "+1"), + ), ui.output_text("value"), ) @@ -12,13 +12,13 @@ def server(input: Inputs, output: Outputs, session: Session): val = reactive.Value(0) - @reactive.Effect + @reactive.effect @reactive.event(input.minus) def _(): newVal = val.get() - 1 val.set(newVal) - @reactive.Effect + @reactive.effect @reactive.event(input.plus) def _(): newVal = val.get() + 1 diff --git a/shiny/api-examples/Value/app-express.py b/shiny/api-examples/Value/app-express.py new file mode 100644 index 000000000..80a55e165 --- /dev/null +++ b/shiny/api-examples/Value/app-express.py @@ -0,0 +1,28 @@ +from shiny import reactive +from shiny.express import input, render, ui + +val = reactive.Value(0) + + +@reactive.effect +@reactive.event(input.minus) +def _(): + newVal = val.get() - 1 + val.set(newVal) + + +@reactive.effect +@reactive.event(input.plus) +def _(): + newVal = val.get() + 1 + val.set(newVal) + + +with ui.sidebar(): + ui.input_action_button("minus", "-1") + ui.input_action_button("plus", "+1") + + +@render.text +def value(): + return str(val.get()) diff --git a/shiny/api-examples/accordion/app.py b/shiny/api-examples/accordion/app-core.py similarity index 100% rename from shiny/api-examples/accordion/app.py rename to shiny/api-examples/accordion/app-core.py diff --git a/shiny/api-examples/accordion_panel/app.py b/shiny/api-examples/accordion_panel/app-core.py similarity index 100% rename from shiny/api-examples/accordion_panel/app.py rename to shiny/api-examples/accordion_panel/app-core.py diff --git a/shiny/api-examples/as_fill_item/app.py b/shiny/api-examples/as_fill_item/app-core.py similarity index 100% rename from shiny/api-examples/as_fill_item/app.py rename to shiny/api-examples/as_fill_item/app-core.py diff --git a/shiny/api-examples/as_fillable_container/app.py b/shiny/api-examples/as_fillable_container/app-core.py similarity index 100% rename from shiny/api-examples/as_fillable_container/app.py rename to shiny/api-examples/as_fillable_container/app-core.py diff --git a/shiny/api-examples/calc/app.py b/shiny/api-examples/calc/app-core.py similarity index 64% rename from shiny/api-examples/calc/app.py rename to shiny/api-examples/calc/app-core.py index 603313593..fe2620253 100644 --- a/shiny/api-examples/calc/app.py +++ b/shiny/api-examples/calc/app-core.py @@ -4,16 +4,18 @@ from shiny import App, Inputs, Outputs, Session, reactive, render, ui app_ui = ui.page_fluid( - ui.input_action_button("first", "Invalidate first (slow) computation"), - " ", - ui.input_action_button("second", "Invalidate second (fast) computation"), - ui.br(), - ui.output_ui("result"), + ui.card( + ui.layout_columns( + ui.input_action_button("first", "Invalidate first (slow) computation"), + ui.input_action_button("second", "Invalidate second (fast) computation"), + ), + ui.output_text_verbatim("result"), + ) ) def server(input: Inputs, output: Outputs, session: Session): - @reactive.Calc + @reactive.calc def first(): input.first() p = ui.Progress() @@ -23,12 +25,12 @@ def first(): p.close() return random.randint(1, 1000) - @reactive.Calc + @reactive.calc def second(): input.second() return random.randint(1, 1000) - @render.ui + @render.text def result(): return first() + second() diff --git a/shiny/api-examples/calc/app-express.py b/shiny/api-examples/calc/app-express.py new file mode 100644 index 000000000..2361cea80 --- /dev/null +++ b/shiny/api-examples/calc/app-express.py @@ -0,0 +1,32 @@ +import random +import time + +from shiny import reactive +from shiny.express import input, render, ui + + +@reactive.calc +def first(): + input.first() + p = ui.Progress() + for i in range(30): + p.set(i / 30, message="Computing, please wait...") + time.sleep(0.1) + p.close() + return random.randint(1, 1000) + + +@reactive.calc +def second(): + input.second() + return random.randint(1, 1000) + + +with ui.card(): + with ui.layout_columns(): + ui.input_action_button("first", "Invalidate first (slow) computation") + ui.input_action_button("second", "Invalidate second (fast) computation") + + @render.text + def result(): + return first() + second() diff --git a/shiny/api-examples/card/app.py b/shiny/api-examples/card/app-core.py similarity index 100% rename from shiny/api-examples/card/app.py rename to shiny/api-examples/card/app-core.py diff --git a/shiny/api-examples/card_body/app.py b/shiny/api-examples/card_body/app-core.py similarity index 100% rename from shiny/api-examples/card_body/app.py rename to shiny/api-examples/card_body/app-core.py diff --git a/shiny/api-examples/card_footer/app.py b/shiny/api-examples/card_footer/app-core.py similarity index 100% rename from shiny/api-examples/card_footer/app.py rename to shiny/api-examples/card_footer/app-core.py diff --git a/shiny/api-examples/card_header/app.py b/shiny/api-examples/card_header/app-core.py similarity index 100% rename from shiny/api-examples/card_header/app.py rename to shiny/api-examples/card_header/app-core.py diff --git a/shiny/api-examples/close/app.py b/shiny/api-examples/close/app-core.py similarity index 100% rename from shiny/api-examples/close/app.py rename to shiny/api-examples/close/app-core.py diff --git a/shiny/api-examples/data_frame/app.py b/shiny/api-examples/data_frame/app-core.py similarity index 100% rename from shiny/api-examples/data_frame/app.py rename to shiny/api-examples/data_frame/app-core.py diff --git a/shiny/api-examples/download/app.py b/shiny/api-examples/download/app-core.py similarity index 100% rename from shiny/api-examples/download/app.py rename to shiny/api-examples/download/app-core.py diff --git a/shiny/api-examples/download_button/app.py b/shiny/api-examples/download_button/app-core.py similarity index 100% rename from shiny/api-examples/download_button/app.py rename to shiny/api-examples/download_button/app-core.py diff --git a/shiny/api-examples/download_link/app.py b/shiny/api-examples/download_link/app-core.py similarity index 100% rename from shiny/api-examples/download_link/app.py rename to shiny/api-examples/download_link/app-core.py diff --git a/shiny/api-examples/dynamic_route/app.py b/shiny/api-examples/dynamic_route/app-core.py similarity index 100% rename from shiny/api-examples/dynamic_route/app.py rename to shiny/api-examples/dynamic_route/app-core.py diff --git a/shiny/api-examples/effect/app.py b/shiny/api-examples/effect/app-core.py similarity index 100% rename from shiny/api-examples/effect/app.py rename to shiny/api-examples/effect/app-core.py diff --git a/shiny/api-examples/effect/app-express.py b/shiny/api-examples/effect/app-express.py new file mode 100644 index 000000000..363ed50cd --- /dev/null +++ b/shiny/api-examples/effect/app-express.py @@ -0,0 +1,15 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_action_button("show", "Show modal dialog") + + +@reactive.effect +@reactive.event(input.show) +def show_important_message(): + m = ui.modal( + "This is a somewhat important message.", + easy_close=True, + footer=None, + ) + ui.modal_show(m) diff --git a/shiny/api-examples/event/app.py b/shiny/api-examples/event/app-core.py similarity index 100% rename from shiny/api-examples/event/app.py rename to shiny/api-examples/event/app-core.py diff --git a/shiny/api-examples/extended_task/app.py b/shiny/api-examples/extended_task/app-core.py similarity index 100% rename from shiny/api-examples/extended_task/app.py rename to shiny/api-examples/extended_task/app-core.py diff --git a/shiny/api-examples/file_reader/app.py b/shiny/api-examples/file_reader/app-core.py similarity index 100% rename from shiny/api-examples/file_reader/app.py rename to shiny/api-examples/file_reader/app-core.py diff --git a/shiny/api-examples/include_css/app.py b/shiny/api-examples/include_css/app-core.py similarity index 100% rename from shiny/api-examples/include_css/app.py rename to shiny/api-examples/include_css/app-core.py diff --git a/shiny/api-examples/include_js/app.py b/shiny/api-examples/include_js/app-core.py similarity index 100% rename from shiny/api-examples/include_js/app.py rename to shiny/api-examples/include_js/app-core.py diff --git a/shiny/api-examples/input_action_button/app.py b/shiny/api-examples/input_action_button/app-core.py similarity index 100% rename from shiny/api-examples/input_action_button/app.py rename to shiny/api-examples/input_action_button/app-core.py diff --git a/shiny/api-examples/input_action_link/app.py b/shiny/api-examples/input_action_link/app-core.py similarity index 100% rename from shiny/api-examples/input_action_link/app.py rename to shiny/api-examples/input_action_link/app-core.py diff --git a/shiny/api-examples/input_action_link/app-express.py b/shiny/api-examples/input_action_link/app-express.py new file mode 100644 index 000000000..39939d756 --- /dev/null +++ b/shiny/api-examples/input_action_link/app-express.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt +import numpy as np + +from shiny import reactive +from shiny.express import input, render, ui + +ui.input_slider("n", "Number of observations", min=0, max=1000, value=500) +ui.input_action_link("go", "Go!") + + +@render.plot(alt="A histogram") +# reactive.event() to invalidate the plot when the button is pressed but not when +# the slider is changed +@reactive.event(input.go, ignore_none=False) +def plot(): + np.random.seed(19680801) + x = 100 + 15 * np.random.randn(input.n()) + fig, ax = plt.subplots() + ax.hist(x, bins=30, density=True) + return fig diff --git a/shiny/api-examples/input_checkbox/app.py b/shiny/api-examples/input_checkbox/app-core.py similarity index 100% rename from shiny/api-examples/input_checkbox/app.py rename to shiny/api-examples/input_checkbox/app-core.py diff --git a/shiny/api-examples/input_checkbox/app-express.py b/shiny/api-examples/input_checkbox/app-express.py new file mode 100644 index 000000000..1c7e87f7e --- /dev/null +++ b/shiny/api-examples/input_checkbox/app-express.py @@ -0,0 +1,8 @@ +from shiny.express import input, render, ui + +ui.input_checkbox("somevalue", "Some value", False) + + +@render.ui +def value(): + return input.somevalue() diff --git a/shiny/api-examples/input_checkbox_group/app.py b/shiny/api-examples/input_checkbox_group/app-core.py similarity index 100% rename from shiny/api-examples/input_checkbox_group/app.py rename to shiny/api-examples/input_checkbox_group/app-core.py diff --git a/shiny/api-examples/input_checkbox_group/app-express.py b/shiny/api-examples/input_checkbox_group/app-express.py new file mode 100644 index 000000000..9f3b9652b --- /dev/null +++ b/shiny/api-examples/input_checkbox_group/app-express.py @@ -0,0 +1,18 @@ +from shiny import req +from shiny.express import input, render, ui + +ui.input_checkbox_group( + "colors", + "Choose color(s):", + { + "red": ui.span("Red", style="color: #FF0000;"), + "green": ui.span("Green", style="color: #00AA00;"), + "blue": ui.span("Blue", style="color: #0000AA;"), + }, +) + + +@render.ui +def val(): + req(input.colors()) + return "You chose " + ", ".join(input.colors()) diff --git a/shiny/api-examples/input_date/app.py b/shiny/api-examples/input_date/app-core.py similarity index 100% rename from shiny/api-examples/input_date/app.py rename to shiny/api-examples/input_date/app-core.py diff --git a/shiny/api-examples/input_date/app-express.py b/shiny/api-examples/input_date/app-express.py new file mode 100644 index 000000000..bd696beb3 --- /dev/null +++ b/shiny/api-examples/input_date/app-express.py @@ -0,0 +1,24 @@ +from datetime import date + +from shiny.express import ui + +ui.input_date("date1", "Date:", value="2016-02-29") +# Default value is the date in client's time zone +ui.input_date("date2", "Date:") +# value is always yyyy-mm-dd, even if the display format is different +ui.input_date("date3", "Date:", value="2016-02-29", format="mm/dd/yy") +# Pass in a Date object +ui.input_date("date4", "Date:", value=date(2016, 2, 29)) +# Use different language and different first day of week +ui.input_date("date5", "Date:", language="ru", weekstart=1) +# Start with decade view instead of default month view +ui.input_date("date6", "Date:", startview="decade") +# Disable Mondays and Tuesdays. +ui.input_date("date7", "Date:", daysofweekdisabled=[1, 2]) +# Disable specific dates. +ui.input_date( + "date8", + "Date:", + value="2016-02-29", + datesdisabled=["2016-03-01", "2016-03-02"], +) diff --git a/shiny/api-examples/input_date_range/app.py b/shiny/api-examples/input_date_range/app-core.py similarity index 100% rename from shiny/api-examples/input_date_range/app.py rename to shiny/api-examples/input_date_range/app-core.py diff --git a/shiny/api-examples/input_date_range/app-express.py b/shiny/api-examples/input_date_range/app-express.py new file mode 100644 index 000000000..66f79c5c4 --- /dev/null +++ b/shiny/api-examples/input_date_range/app-express.py @@ -0,0 +1,27 @@ +from datetime import date + +from shiny.express import ui + +ui.input_date_range("daterange1", "Date range:", start="2001-01-01", end="2010-12-31") +# Default start and end is the current date in the client's time zone +ui.input_date_range("daterange2", "Date range:") +# start and end are always specified in yyyy-mm-dd, even if the display +# format is different +ui.input_date_range( + "daterange3", + "Date range:", + start="2001-01-01", + end="2010-12-31", + min="2001-01-01", + max="2012-12-21", + format="mm/dd/yy", + separator=" - ", +) +# Pass in Date objects +ui.input_date_range( + "daterange4", "Date range:", start=date(2001, 1, 1), end=date(2010, 12, 31) +) +# Use different language and different first day of week +ui.input_date_range("daterange5", "Date range:", language="de", weekstart=1) +# Start with decade view instead of default month view +ui.input_date_range("daterange6", "Date range:", startview="decade") diff --git a/shiny/api-examples/input_file/app.py b/shiny/api-examples/input_file/app-core.py similarity index 100% rename from shiny/api-examples/input_file/app.py rename to shiny/api-examples/input_file/app-core.py diff --git a/shiny/api-examples/input_file/app-express.py b/shiny/api-examples/input_file/app-express.py new file mode 100644 index 000000000..81222e4f7 --- /dev/null +++ b/shiny/api-examples/input_file/app-express.py @@ -0,0 +1,48 @@ +import pandas as pd + +from shiny import reactive +from shiny.express import input, render, ui +from shiny.types import FileInfo + +ui.input_file("file1", "Choose CSV File", accept=[".csv"], multiple=False) +ui.input_checkbox_group( + "stats", + "Summary Stats", + choices=["Row Count", "Column Count", "Column Names"], + selected=["Row Count", "Column Count", "Column Names"], +) + + +@reactive.Calc +def parsed_file(): + file: list[FileInfo] | None = input.file1() + if file is None: + return pd.DataFrame() + return pd.read_csv(file[0]["datapath"]) # pyright: ignore[reportUnknownMemberType] + + +@render.table +def summary(): + df = parsed_file() + + if df.empty: + return pd.DataFrame() + + # Get the row count, column count, and column names of the DataFrame + row_count = df.shape[0] + column_count = df.shape[1] + names = df.columns.tolist() + column_names = ", ".join(str(name) for name in names) + + # Create a new DataFrame to display the information + info_df = pd.DataFrame( + { + "Row Count": [row_count], + "Column Count": [column_count], + "Column Names": [column_names], + } + ) + + # input.stats() is a list of strings; subset the columns based on the selected + # checkboxes + return info_df.loc[:, input.stats()] diff --git a/shiny/api-examples/input_numeric/app.py b/shiny/api-examples/input_numeric/app-core.py similarity index 100% rename from shiny/api-examples/input_numeric/app.py rename to shiny/api-examples/input_numeric/app-core.py diff --git a/shiny/api-examples/input_numeric/app-express.py b/shiny/api-examples/input_numeric/app-express.py new file mode 100644 index 000000000..7e06f1c0a --- /dev/null +++ b/shiny/api-examples/input_numeric/app-express.py @@ -0,0 +1,8 @@ +from shiny.express import input, render, ui + +ui.input_numeric("obs", "Observations:", 10, min=1, max=100) + + +@render.code +def value(): + return input.obs() diff --git a/shiny/api-examples/input_password/app.py b/shiny/api-examples/input_password/app-core.py similarity index 100% rename from shiny/api-examples/input_password/app.py rename to shiny/api-examples/input_password/app-core.py diff --git a/shiny/api-examples/input_password/app-express.py b/shiny/api-examples/input_password/app-express.py new file mode 100644 index 000000000..4366c5303 --- /dev/null +++ b/shiny/api-examples/input_password/app-express.py @@ -0,0 +1,11 @@ +from shiny import reactive +from shiny.express import input, render, ui + +ui.input_password("password", "Password:") +ui.input_action_button("go", "Go") + + +@render.code +@reactive.event(input.go) +def value(): + return input.password() diff --git a/shiny/api-examples/input_radio_buttons/app.py b/shiny/api-examples/input_radio_buttons/app-core.py similarity index 100% rename from shiny/api-examples/input_radio_buttons/app.py rename to shiny/api-examples/input_radio_buttons/app-core.py diff --git a/shiny/api-examples/input_radio_buttons/app-express.py b/shiny/api-examples/input_radio_buttons/app-express.py new file mode 100644 index 000000000..5354f4134 --- /dev/null +++ b/shiny/api-examples/input_radio_buttons/app-express.py @@ -0,0 +1,15 @@ +from shiny.express import input, render, ui + +ui.input_radio_buttons( + "rb", + "Choose one:", + { + "html": ui.HTML("Red Text"), + "text": "Normal text", + }, +) + + +@render.express +def val(): + "You chose " + input.rb() diff --git a/shiny/api-examples/input_select/app.py b/shiny/api-examples/input_select/app-core.py similarity index 100% rename from shiny/api-examples/input_select/app.py rename to shiny/api-examples/input_select/app-core.py diff --git a/shiny/api-examples/input_select/app-express.py b/shiny/api-examples/input_select/app-express.py new file mode 100644 index 000000000..c45482f0c --- /dev/null +++ b/shiny/api-examples/input_select/app-express.py @@ -0,0 +1,16 @@ +from shiny.express import input, render, ui + +ui.input_select( + "state", + "Choose a state:", + { + "East Coast": {"NY": "New York", "NJ": "New Jersey", "CT": "Connecticut"}, + "West Coast": {"WA": "Washington", "OR": "Oregon", "CA": "California"}, + "Midwest": {"MN": "Minnesota", "WI": "Wisconsin", "IA": "Iowa"}, + }, +) + + +@render.text +def value(): + return "You choose: " + str(input.state()) diff --git a/shiny/api-examples/input_selectize/app.py b/shiny/api-examples/input_selectize/app-core.py similarity index 97% rename from shiny/api-examples/input_selectize/app.py rename to shiny/api-examples/input_selectize/app-core.py index bc2dd9bd9..23a63d376 100644 --- a/shiny/api-examples/input_selectize/app.py +++ b/shiny/api-examples/input_selectize/app-core.py @@ -17,7 +17,7 @@ ), ui.output_text("value"), ui.input_selectize( - "state", + "state2", "Selectize Options", states, multiple=True, @@ -32,7 +32,7 @@ ), ), ui.input_selectize( - "state", + "state3", "Selectize plugins", states, multiple=True, diff --git a/shiny/api-examples/input_selectize/app-express.py b/shiny/api-examples/input_selectize/app-express.py new file mode 100644 index 000000000..d58e04154 --- /dev/null +++ b/shiny/api-examples/input_selectize/app-express.py @@ -0,0 +1,45 @@ +from html import escape # noqa: F401 + +from shiny.express import input, render, ui + +states = { + "East Coast": {"NY": "New York", "NJ": "New Jersey", "CT": "Connecticut"}, + "West Coast": {"WA": "Washington", "OR": "Oregon", "CA": "California"}, + "Midwest": {"MN": "Minnesota", "WI": "Wisconsin", "IA": "Iowa"}, +} + +ui.input_selectize( + "state", + "Choose a state:", + states, + multiple=True, +) + + +@render.text +def value(): + return "You choose: " + str(input.state()) + + +ui.input_selectize( + "state2", + "Selectize Options", + states, + multiple=True, + options=( + { + "placeholder": "Enter text", + "render": ui.js_eval( + '{option: function(item, escape) {return "