diff --git a/docs/Makefile b/docs/Makefile index 91fabe65c..583ca792c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -47,6 +47,8 @@ deps: $(PYBIN) ## Install build dependencies $(PYBIN)/pip install -e ..[doc] quartodoc: $(PYBIN) ## Build qmd files for API docs + $(eval export SHINY_ADD_EXAMPLES=true) + $(eval export IN_QUARTODOC=true) . $(PYBIN)/activate \ && quartodoc interlinks \ && quartodoc build --config _quartodoc.yml --verbose diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 99bd5290c..4b5942af1 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -5,6 +5,7 @@ quartodoc: package: shiny rewrite_all_pages: false sidebar: api/_sidebar.yml + dynamic: true renderer: style: _renderer.py show_signature_annotations: false @@ -63,8 +64,6 @@ quartodoc: - ui.showcase_bottom - ui.showcase_left_center - ui.showcase_top_right - - ui.ValueBoxTheme - - ui.ShowcaseLayout - title: Navigation (tab) panels desc: Create segments of UI content. contents: @@ -177,10 +176,14 @@ quartodoc: desc: "" contents: - render.renderer.Renderer - - render.renderer.Jsonifiable - - render.renderer.ValueFn - - render.renderer.AsyncValueFn - - render.renderer.RendererT + - name: render.renderer.Jsonifiable + dynamic: false + - name: render.renderer.ValueFn + dynamic: false + - name: render.renderer.AsyncValueFn + dynamic: false + - name: render.renderer.RendererT + dynamic: false - title: Reactive programming desc: "" contents: @@ -251,7 +254,8 @@ quartodoc: - ui.Sidebar - ui.CardItem - ui.AccordionPanel - - ui.css.CssUnit + - name: ui.css.CssUnit + dynamic: false - ui._input_slider.SliderValueArg - ui._input_slider.SliderStepArg - kind: page @@ -262,11 +266,16 @@ quartodoc: flatten: true package: null contents: - - htmltools.Tag - - htmltools.TagAttrs - - htmltools.TagAttrValue - - htmltools.TagChild - - htmltools.TagList + - name: htmltools.Tag + dynamic: false + - name: htmltools.TagAttrs + dynamic: false + - name: htmltools.TagAttrValue + dynamic: false + - name: htmltools.TagChild + dynamic: false + - name: htmltools.TagList + dynamic: false - kind: page path: ExceptionTypes summary: @@ -336,8 +345,13 @@ quartodoc: desc: "Cards are a common organizing unit for modern user interfaces (UI). At their core, they're just rectangular containers with borders and padding. However, when utilized properly to group related information, they help users better digest, engage, and navigate through content." flatten: true contents: - - experimental.ui.card_body - - experimental.ui.card_title - - experimental.ui.card_image - - experimental.ui.ImgContainer - - experimental.ui.WrapperCallable + - name: experimental.ui.card_body + dynamic: false + - name: experimental.ui.card_title + dynamic: false + - name: experimental.ui.card_image + dynamic: false + - name: experimental.ui.ImgContainer + dynamic: false + - name: experimental.ui.WrapperCallable + dynamic: false diff --git a/docs/_renderer.py b/docs/_renderer.py index cab180bdb..47c47f7e5 100644 --- a/docs/_renderer.py +++ b/docs/_renderer.py @@ -20,23 +20,6 @@ SHINY_PATH = Path(files("shiny").joinpath()) -SHINYLIVE_CODE_TEMPLATE = """ -```{{shinylive-python}} -#| standalone: true -#| components: [editor, viewer] -#| layout: vertical -#| viewerHeight: 400{0} -``` -""" - -DOCSTRING_TEMPLATE = """\ -{rendered} - -{header} Examples - -{examples} -""" - # This is the same as the FileContentJson type in TypeScript. class FileContentJson(TypedDict): @@ -68,37 +51,9 @@ def render(self, el: Union[dc.Object, dc.Alias]): converted = convert_rst_link_to_md(rendered) - if isinstance(el, dc.Alias) and "experimental" in el.target_path: - p_example_dir = SHINY_PATH / "experimental" / "api-examples" / el.name - else: - p_example_dir = SHINY_PATH / "api-examples" / el.name - - if (p_example_dir / "app.py").exists(): - example = "" + check_if_missing_expected_example(el, converted) - files = list(p_example_dir.glob("**/*")) - - # Sort, and then move app.py to first position. - files.sort() - app_py_idx = files.index(p_example_dir / "app.py") - files = [files[app_py_idx]] + files[:app_py_idx] + files[app_py_idx + 1 :] - - for f in files: - if f.is_dir(): - continue - file_info = read_file(f, p_example_dir) - if file_info["type"] == "text": - example += f"\n## file: {file_info['name']}\n{file_info['content']}" - else: - example += f"\n## file: {file_info['name']}\n## type: binary\n{file_info['content']}" - - example = SHINYLIVE_CODE_TEMPLATE.format(example) - - return DOCSTRING_TEMPLATE.format( - rendered=converted, - examples=example, - header="#" * (self.crnt_header_level + 1), - ) + assert_no_sphinx_comments(el, converted) return converted @@ -282,3 +237,59 @@ def read_file(file: str | Path, root_dir: str | Path | None = None) -> FileConte "content": file_content, "type": type, } + + +def check_if_missing_expected_example(el, converted): + if re.search(r"(^|\n)#{2,6} Examples\n", converted): + # Manually added examples are fine + return + + if not el.canonical_path.startswith("shiny"): + # Only check Shiny objects for examples + return + + if hasattr(el, "decorators") and "no_example" in [ + d.value.canonical_name for d in el.decorators + ]: + # When an example is intentionally omitted, we mark the fn with `@no_example` + return + + if not el.is_function: + # Don't throw for things that can't be decorated + return + + if not el.is_explicitely_exported: + # Don't require examples on "implicitly exported" functions + # In practice, this covers methods of exported classes (class still needs ex) + return + + # TODO: Remove shiny.express from no_req_examples when we have examples ready + no_req_examples = ["shiny.express", "shiny.experimental"] + if any([el.target_path.startswith(mod) for mod in no_req_examples]): + return + + raise RuntimeError( + f"{el.name} needs an example, use `@add_example()` or manually add `Examples` section:\n" + + (f"> file : {el.filepath}\n" if hasattr(el, "filepath") else "") + + (f"> target : {el.target_path}\n" if hasattr(el, "target_path") else "") + + (f"> canonical: {el.canonical_path}" if hasattr(el, "canonical_path") else "") + ) + + +def assert_no_sphinx_comments(el, converted: str) -> None: + """ + Sphinx allows `..`-prefixed comments in docstrings, which are not valid markdown. + We don't allow Sphinx comments or directives, sorry! + """ + pattern = r"\n[.]{2} .+(\n|$)" + if re.search(pattern, converted): + raise RuntimeError( + f"{el.name} includes Sphinx-styled comments or directives, please remove.\n" + + (f"> file : {el.filepath}\n" if hasattr(el, "filepath") else "") + + (f"> target : {el.target_path}\n" if hasattr(el, "target_path") else "") + + ( + f"> canonical: {el.canonical_path}" + if hasattr(el, "canonical_path") + else "" + ) + ) diff --git a/setup.cfg b/setup.cfg index aa33f612c..8ea08f3c8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -102,7 +102,7 @@ doc = jupyter jupyter_client < 8.0.0 tabulate - shinylive==0.1.1 + shinylive @ git+https://github.com/posit-dev/py-shinylive.git@main pydantic==1.10 quartodoc==0.7.2 griffe==0.33.0 diff --git a/shiny/_app.py b/shiny/_app.py index c338b1ff1..30d25bba0 100644 --- a/shiny/_app.py +++ b/shiny/_app.py @@ -62,8 +62,8 @@ class App: debug Whether to enable debug mode. - Example - ------- + Examples + -------- ```{python} #| eval: false diff --git a/shiny/_docstring.py b/shiny/_docstring.py index 699fcc450..c177e4ebc 100644 --- a/shiny/_docstring.py +++ b/shiny/_docstring.py @@ -1,15 +1,34 @@ from __future__ import annotations -import json import os -from typing import Any, Callable, Literal, TypeVar +import sys +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar + + +def find_api_examples_dir(start_dir: str) -> Optional[str]: + current_dir = os.path.abspath(start_dir) + while True: + api_examples_dir = os.path.join(current_dir, "api-examples") + if os.path.isdir(api_examples_dir): + return api_examples_dir + root_files = ["setup.cfg", "pyproject.toml"] + dir_files = os.listdir(current_dir) + if any(rf in dir_files for rf in root_files): + break # Reached the package root directory + if current_dir == os.path.dirname(current_dir): + break # Reached the global root directory + current_dir = os.path.dirname(current_dir) + return None -ex_dir: str = os.path.join(os.path.dirname(os.path.abspath(__file__)), "api-examples") FuncType = Callable[..., Any] F = TypeVar("F", bound=FuncType) +def no_example(func: F) -> F: + return func + + # 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 @@ -18,104 +37,128 @@ class DocStringWithExample(str): ... +class ExampleWriter: + def write_example(self, app_files: list[str]) -> str: + app_file = app_files[0] + with open(app_file) as f: + code = f.read() + + return f"```.python\n{code.strip()}\n```\n" + + +example_writer = ExampleWriter() + + def add_example( - directive: Literal[ - "shinyapp::", - "shinylive-editor::", - "code-block:: python", - "cell::", - "terminal::", - ] = "shinylive-editor::", - **options: object, + app_file: str = "app.py", + ex_dir: Optional[str] = None, ) -> Callable[[F], F]: """ Add an example to the docstring of a function, method, or class. This decorator must, at the moment, be used on a function, method, or class whose - ``__name__`` matches the name of directory under ``shiny/api-examples/``, and must - also contain a ``app.py`` file in that directory. + ``__name__`` matches the name of directory under a ``api-examples/`` directory in + the current or any parent directory. Parameters ---------- - directive - A directive for rendering the example. This can be one of: - - ``shinyapp``: A live shiny app (statically served via wasm). - - ``code``: A python code snippet. - - ``shinylive-editor``: A live shiny app with editor (statically served via wasm). - - ``cell``: A executable Python cell. - - ``terminal``: A minimal Python IDE - **options - Options for the directive. See docs/source/sphinxext/pyshinyapp.py for details. + app_file: + The primary app file to use for the example. This allows you to have multiple + example files for a single function or to use a different file name than + ``app.py``. Support files _cannot_ be named ``app.py`` or start with ``app-``, + as these files will never be included in the example. + ex_dir: + The directory containing the example. If not specified, ``add_example()`` will + find a directory named after the current function in the first ``api-examples/`` + directory it finds in the current directory or its parent directories. """ def _(func: F) -> F: # To avoid a performance hit on `import shiny`, we only add examples to the - # docstrings if this env variable is set (as it is in docs/source/conf.py). + # docstrings if this env variable is set (as it is in `make quartodoc`). if os.getenv("SHINY_ADD_EXAMPLES") != "true": if func.__doc__ is not None: func.__doc__ = DocStringWithExample(func.__doc__) return func + func_dir = get_decorated_source_directory(func) fn_name = func.__name__ - example_dir = os.path.join(ex_dir, fn_name) - example_file = os.path.join(example_dir, "app.py") + + if ex_dir is None: + ex_dir_found = find_api_examples_dir(func_dir) + + if ex_dir_found is None: + raise ValueError( + 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_file = os.path.join(example_dir, app_file) if not os.path.exists(example_file): - raise ValueError(f"No example for {fn_name}") + raise ValueError( + f"No example for {fn_name} found in '{os.path.abspath(example_dir)}'." + ) other_files: list[str] = [] for f in os.listdir(example_dir): abs_f = os.path.join(example_dir, f) - if os.path.isfile(abs_f) and f != "app.py": + is_support_file = ( + os.path.isfile(abs_f) + and f != app_file + and f != "app.py" + and not f.startswith("app-") + and not f.startswith("__") + ) + if is_support_file: other_files.append(abs_f) - if "files" not in options: - options["files"] = json.dumps(other_files) - if func.__doc__ is None: func.__doc__ = "" + example = example_writer.write_example([example_file, *other_files]) + example_lines = example.split("\n") + # How many leading spaces does the docstring start with? doc = func.__doc__.replace("\n", "") indent = " " * (len(doc) - len(doc.lstrip())) + nl_indent = "\n" + indent - with open(example_file) as f: - example = indent.join([" " * 4 + x for x in f.readlines()]) - - # When rendering a standalone app, put the code above it (maybe this should be - # handled by the directive itself?) - example_prefix: list[str] = [] - if directive == "shinyapp::": - example_prefix.extend( - [ - ".. code-block:: python", - "", - example, - "", - ] - ) + # Add example header if not already present + # WARNING: All `add_example()` calls must be coalesced. + # Note that we're using numpydoc-style headers here, quartodoc will handle + # converting them to markdown headers. + if isinstance(func.__doc__, DocStringWithExample): + ex_header = "Examples" + nl_indent + "--------" + before, after = func.__doc__.split(ex_header, 1) + func.__doc__ = before + ex_header + else: + func.__doc__ += nl_indent + "Examples" + func.__doc__ += nl_indent + "--------" + after = None + + # Insert the example under the Examples heading + func.__doc__ += nl_indent * 2 + func.__doc__ += nl_indent.join(example_lines) + if after is not None: + func.__doc__ += after - example_section = ("\n" + indent).join( - [ - "", - "", - "Example", - "-------", - "", - *example_prefix, - f".. {directive}", - *[f" :{k}: {v}" for k, v in options.items()], - "", - example, - ] - ) - - func.__doc__ += example_section func.__doc__ = DocStringWithExample(func.__doc__) return func return _ +def get_decorated_source_directory(func: FuncType) -> str: + if hasattr(func, "__module__"): + path = os.path.abspath(str(sys.modules[func.__module__].__file__)) + else: + path = os.path.abspath(func.__code__.co_filename) + + return os.path.dirname(path) + + def doc_format(**kwargs: str) -> Callable[[F], F]: def _(func: F) -> F: if isinstance(func.__doc__, DocStringWithExample): @@ -127,3 +170,36 @@ def _(func: F) -> F: return func return _ + + +if not TYPE_CHECKING and os.environ.get("IN_QUARTODOC") == "true": + # When running in quartodoc, we use shinylive to embed the examples in the docs. + # This part is hidden from the typechecker because shinylive is not a direct + # dependency of shiny and we only need this section when building the docs. + try: + import shinylive + except ModuleNotFoundError: + raise RuntimeError("Please install shinylive to build the docs.") + + SHINYLIVE_CODE_TEMPLATE = """ +```{{shinylive-python}} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 400 + +{0} +``` +""" + + class ShinyliveExampleWriter(ExampleWriter): + def write_example(self, app_files: list[str]) -> str: + app_file = app_files.pop(0) + bundle = shinylive._url.create_shinylive_bundle_file( + app_file, app_files, language="py" + ) + code = shinylive._url.create_shinylive_chunk_contents(bundle) + + return SHINYLIVE_CODE_TEMPLATE.format(code.strip()) + + example_writer = ShinyliveExampleWriter() diff --git a/shiny/_main.py b/shiny/_main.py index acdca39d7..984fbc043 100644 --- a/shiny/_main.py +++ b/shiny/_main.py @@ -19,6 +19,7 @@ import shiny from . import _autoreload, _hostenv, _static, _utils +from ._docstring import no_example from ._typing_extensions import NotRequired, TypedDict from .express import is_express_app from .express._utils import escape_to_var_name @@ -142,6 +143,7 @@ def main() -> None: help="Launch app browser after app starts, using the Python webbrowser module.", show_default=True, ) +@no_example def run( app: str | shiny.App, host: str, diff --git a/shiny/api-examples/Effect/app.py b/shiny/api-examples/Effect/app.py index 5046475bc..8b9c2c599 100644 --- a/shiny/api-examples/Effect/app.py +++ b/shiny/api-examples/Effect/app.py @@ -8,7 +8,9 @@ def server(input: Inputs, output: Outputs, session: Session): @reactive.event(input.btn) def _(): ui.insert_ui( - ui.p("Number of clicks: ", input.btn()), selector="#btn", where="afterEnd" + ui.p("Number of clicks: ", input.btn()), + selector="#btn", + where="afterEnd", ) diff --git a/shiny/api-examples/calc/app.py b/shiny/api-examples/calc/app.py new file mode 100644 index 000000000..603313593 --- /dev/null +++ b/shiny/api-examples/calc/app.py @@ -0,0 +1,36 @@ +import random +import time + +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"), +) + + +def server(input: Inputs, output: Outputs, session: Session): + @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) + + @render.ui + def result(): + return first() + second() + + +app = App(app_ui, server) diff --git a/shiny/experimental/api-examples/card_body/app.py b/shiny/api-examples/card_body/app.py similarity index 84% rename from shiny/experimental/api-examples/card_body/app.py rename to shiny/api-examples/card_body/app.py index 3d51164fe..3702c676d 100644 --- a/shiny/experimental/api-examples/card_body/app.py +++ b/shiny/api-examples/card_body/app.py @@ -2,17 +2,17 @@ from shiny import App, ui app_ui = ui.page_fluid( - x.ui.card( - x.ui.card_header("This is the header"), + ui.card( + ui.card_header("This is the header"), x.ui.card_body( x.ui.card_title("This is the title"), ui.p("This is the body."), ui.p("This is still the body."), ), - x.ui.card_footer("This is the footer"), + ui.card_footer("This is the footer"), full_screen=True, ), - x.ui.card( + ui.card( ui.p("These first two elements will be wrapped in `card_body()` together."), ui.p("These first two elements will be wrapped in `card_body()` together."), x.ui.card_body(ui.p("A card body.")), diff --git a/shiny/api-examples/effect/app.py b/shiny/api-examples/effect/app.py new file mode 100644 index 000000000..8b9c2c599 --- /dev/null +++ b/shiny/api-examples/effect/app.py @@ -0,0 +1,17 @@ +from shiny import App, Inputs, Outputs, Session, reactive, ui + +app_ui = ui.page_fluid(ui.input_action_button("btn", "Press me!")) + + +def server(input: Inputs, output: Outputs, session: Session): + @reactive.Effect + @reactive.event(input.btn) + def _(): + ui.insert_ui( + ui.p("Number of clicks: ", input.btn()), + selector="#btn", + where="afterEnd", + ) + + +app = App(app_ui, server) diff --git a/shiny/api-examples/include_javascript/app.py b/shiny/api-examples/include_js/app.py similarity index 100% rename from shiny/api-examples/include_javascript/app.py rename to shiny/api-examples/include_js/app.py diff --git a/shiny/api-examples/include_javascript/js/app.js b/shiny/api-examples/include_js/js/app.js similarity index 100% rename from shiny/api-examples/include_javascript/js/app.js rename to shiny/api-examples/include_js/js/app.js diff --git a/shiny/api-examples/nav_panel/app-basic.py b/shiny/api-examples/nav_panel/app-basic.py new file mode 100644 index 000000000..bc5b797c2 --- /dev/null +++ b/shiny/api-examples/nav_panel/app-basic.py @@ -0,0 +1,16 @@ +from shiny import App, Inputs, ui + +app_ui = ui.page_fixed( + ui.panel_title("Basic Nav Example"), + ui.navset_tab( + ui.nav_panel("One", "First tab content."), + ui.nav_panel("Two", "Second tab content."), + ), +) + + +def server(input: Inputs): + pass + + +app = App(app_ui, server) diff --git a/shiny/api-examples/nav/app.py b/shiny/api-examples/nav_panel/app.py similarity index 100% rename from shiny/api-examples/nav/app.py rename to shiny/api-examples/nav_panel/app.py diff --git a/shiny/experimental/ui/_card.py b/shiny/experimental/ui/_card.py index 34d518a72..3ecff0557 100644 --- a/shiny/experimental/ui/_card.py +++ b/shiny/experimental/ui/_card.py @@ -17,6 +17,7 @@ tags, ) +from ..._docstring import add_example from ...types import MISSING, MISSING_TYPE from ...ui._card import CardItem, WrapperCallable, _card_impl, card_body from ...ui.css import CssUnit, as_css_unit @@ -33,7 +34,6 @@ ) -# TODO-maindocs; @add_example() def card( *args: TagChild | TagAttrs | CardItem, full_screen: bool = False, @@ -111,7 +111,7 @@ def card( ############################################################################ -# TODO-maindocs; @add_example() +@add_example() def card_title( *args: TagChild | TagAttrs, container: TagFunction = tags.h5, @@ -174,7 +174,7 @@ def __call__(self, *args: Tag) -> Tagifiable: ... -# TODO-maindocs; @add_example() +@add_example() def card_image( file: str | Path | PurePath | io.BytesIO | None, *args: TagAttrs, diff --git a/shiny/module.py b/shiny/module.py index 227fb55a2..2045e3a93 100644 --- a/shiny/module.py +++ b/shiny/module.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Callable, TypeVar +from ._docstring import no_example from ._namespaces import Id, current_namespace, namespace_context, resolve_id from ._typing_extensions import Concatenate, ParamSpec @@ -14,6 +15,7 @@ R = TypeVar("R") +@no_example def ui(fn: Callable[P, R]) -> Callable[Concatenate[str, P], R]: def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R: with namespace_context(id): @@ -22,6 +24,7 @@ def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R: return wrapper +@no_example def server( fn: Callable[Concatenate[Inputs, Outputs, Session, P], R] ) -> Callable[Concatenate[str, P], R]: diff --git a/shiny/reactive/_core.py b/shiny/reactive/_core.py index 699d557d4..86fed00fc 100644 --- a/shiny/reactive/_core.py +++ b/shiny/reactive/_core.py @@ -22,7 +22,7 @@ from .. import _utils from .._datastructures import PriorityQueueFIFO -from .._docstring import add_example +from .._docstring import add_example, no_example from ..types import MISSING, MISSING_TYPE if TYPE_CHECKING: @@ -247,6 +247,7 @@ def get_current_context() -> Context: return _reactive_environment.current_context() +@no_example async def flush() -> None: """ Run any pending invalidations (i.e., flush the reactive environment). @@ -259,6 +260,7 @@ async def flush() -> None: await _reactive_environment.flush() +@no_example def on_flushed( func: Callable[[], Awaitable[None]], once: bool = False ) -> Callable[[], None]: @@ -286,6 +288,7 @@ def on_flushed( return _reactive_environment.on_flushed(func, once) +@no_example def lock() -> asyncio.Lock: """ A lock that should be held whenever manipulating the reactive graph. diff --git a/shiny/render/_dataframe.py b/shiny/render/_dataframe.py index c6a42b993..573075478 100644 --- a/shiny/render/_dataframe.py +++ b/shiny/render/_dataframe.py @@ -7,7 +7,7 @@ from htmltools import Tag from .. import ui -from .._docstring import add_example +from .._docstring import add_example, no_example from ._dataframe_unsafe import serialize_numpy_dtypes from .renderer import Jsonifiable, Renderer @@ -21,6 +21,7 @@ def to_payload(self) -> Jsonifiable: ... +@add_example(ex_dir="../api-examples/data_frame") class DataGrid(AbstractTabularData): """ Holds the data and options for a ``shiny.render.data_frame`` output, for a @@ -107,6 +108,7 @@ def to_payload(self) -> Jsonifiable: return res +@no_example class DataTable(AbstractTabularData): """ Holds the data and options for a ``shiny.render.data_frame`` output, for a diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 54356da60..d6f4c09bf 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -27,6 +27,7 @@ from .. import _utils from .. import ui as _ui +from .._docstring import add_example from .._namespaces import ResolvedId from .._typing_extensions import Self from ..session import get_current_session, require_active_session @@ -59,6 +60,7 @@ # ====================================================================================== +@add_example(ex_dir="../api-examples/output_text") class text(Renderer[str]): """ Reactively render text. @@ -185,6 +187,7 @@ async def transform(self, value: str) -> Jsonifiable: # a nontrivial amount of overhead. So for now, we're just using `object`. +@add_example(ex_dir="../api-examples/output_plot") class plot(Renderer[object]): """ Reactively render a plot object as an HTML image. @@ -383,6 +386,7 @@ def cast_result(result: ImgData | None) -> dict[str, Jsonifiable] | None: # ====================================================================================== # RenderImage # ====================================================================================== +@add_example(ex_dir="../api-examples/output_image") class image(Renderer[ImgData]): """ Reactively render a image file as an HTML image. @@ -456,6 +460,7 @@ def to_pandas(self) -> "pd.DataFrame": TableResult = Union["pd.DataFrame", PandasCompatible, None] +@add_example(ex_dir="../api-examples/output_table") class table(Renderer[TableResult]): """ Reactively render a pandas ``DataFrame`` object (or similar) as a basic HTML @@ -561,6 +566,7 @@ async def transform(self, value: TableResult) -> dict[str, Jsonifiable]: # ====================================================================================== # RenderUI # ====================================================================================== +@add_example(ex_dir="../api-examples/output_ui") class ui(Renderer[TagChild]): """ Reactively render HTML content. diff --git a/shiny/session/_session.py b/shiny/session/_session.py index b2c8be525..443a801b4 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -224,6 +224,7 @@ async def _run_session_end_tasks(self) -> None: finally: self.app._remove_session(self) + @add_example() async def close(self, code: int = 1001) -> None: """ Close the session. diff --git a/shiny/session/_utils.py b/shiny/session/_utils.py index dd1c255cd..dba7c3ce7 100644 --- a/shiny/session/_utils.py +++ b/shiny/session/_utils.py @@ -15,6 +15,7 @@ if TYPE_CHECKING: from ._session import Session +from .._docstring import no_example from .._namespaces import namespace_context from .._typing_extensions import TypedDict @@ -33,6 +34,7 @@ class RenderedDeps(TypedDict): _default_session: Optional[Session] = None +@no_example def get_current_session() -> Optional[Session]: """ Get the current user session. @@ -74,6 +76,7 @@ def session_context(session: Optional[Session]): _current_session.reset(token) +@no_example def require_active_session(session: Optional[Session]) -> Session: """ Raise an exception if no Shiny session is currently active. diff --git a/shiny/ui/_bootstrap.py b/shiny/ui/_bootstrap.py index b66c79755..dd6b38fdb 100644 --- a/shiny/ui/_bootstrap.py +++ b/shiny/ui/_bootstrap.py @@ -27,7 +27,7 @@ tags, ) -from .._docstring import add_example +from .._docstring import add_example, no_example from ..module import current_namespace from ..types import MISSING, MISSING_TYPE from ._html_deps_external import jqui_deps @@ -67,6 +67,7 @@ def row(*args: TagChild | TagAttrs, **kwargs: TagAttrValue) -> Tag: return div({"class": "row"}, *args, **kwargs) +@add_example(ex_dir="../api-examples/row") def column( width: int, *args: TagChild | TagAttrs, offset: int = 0, **kwargs: TagAttrValue ) -> Tag: @@ -108,6 +109,7 @@ def column( return div({"class": cls}, *args, **kwargs) +@no_example def panel_well(*args: TagChild | TagAttrs, **kwargs: TagAttrValue) -> Tag: """ Create a well panel. @@ -231,6 +233,7 @@ def panel_title( return TagList(get_window_title(title, window_title), title) +@no_example def panel_fixed( *args: TagChild | TagAttrs, top: Optional[str] = None, @@ -382,6 +385,7 @@ def panel_absolute( return TagList(deps, divTag, tags.script(f'$(".draggable").draggable({dragOpts});')) +@no_example def help_text(*args: TagChild | TagAttrs, **kwargs: TagAttrValue) -> Tag: """ Create a help text element diff --git a/shiny/ui/_card.py b/shiny/ui/_card.py index daa3785cd..c402a4442 100644 --- a/shiny/ui/_card.py +++ b/shiny/ui/_card.py @@ -259,7 +259,7 @@ def _wrap_children_in_card( return tag_children -# TODO-maindocs; @add_example() +@add_example() def card_body( *args: TagChild | TagAttrs, fillable: bool = True, diff --git a/shiny/ui/_input_update.py b/shiny/ui/_input_update.py index 85571e40c..7ce709306 100644 --- a/shiny/ui/_input_update.py +++ b/shiny/ui/_input_update.py @@ -27,7 +27,7 @@ from starlette.requests import Request from starlette.responses import JSONResponse, Response -from .._docstring import add_example, doc_format +from .._docstring import add_example, doc_format, no_example from .._namespaces import resolve_id from .._typing_extensions import NotRequired, TypedDict from .._utils import drop_none @@ -145,6 +145,7 @@ def update_checkbox( session.send_input_message(id, drop_none(msg)) +@no_example @doc_format(note=_note) def update_switch( id: str, @@ -951,7 +952,7 @@ def update_tooltip( # ----------------------------------------------------------------------------- -# @add_example() +@add_example() def update_popover( id: str, *args: TagChild, diff --git a/shiny/ui/_layout.py b/shiny/ui/_layout.py index c4bce6481..7a877f2ea 100644 --- a/shiny/ui/_layout.py +++ b/shiny/ui/_layout.py @@ -5,6 +5,7 @@ from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, css, div from .._deprecated import warn_deprecated +from .._docstring import add_example from ..types import MISSING, MISSING_TYPE from ._html_deps_shinyverse import components_dependency from ._tag import consolidate_attrs @@ -14,6 +15,7 @@ from .fill import as_fill_item, as_fillable_container +@add_example() def layout_column_wrap( *args: TagChild | TagAttrs, width: CssUnit | None | MISSING_TYPE = MISSING, diff --git a/shiny/ui/_layout_columns.py b/shiny/ui/_layout_columns.py index 7bd2394db..19ba3eac6 100644 --- a/shiny/ui/_layout_columns.py +++ b/shiny/ui/_layout_columns.py @@ -6,6 +6,7 @@ from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, css +from .._docstring import add_example from ._html_deps_shinyverse import web_component_dependency from ._layout import wrap_all_in_gap_spaced_container from ._tag import consolidate_attrs @@ -32,6 +33,7 @@ BreakpointsUser = Union[BreakpointsSoft[T], Iterable[T], T, None] +@add_example() def layout_columns( *args: TagChild | TagAttrs, col_widths: BreakpointsUser[int] = None, diff --git a/shiny/ui/_modal.py b/shiny/ui/_modal.py index 8f6532f33..8f2e934a3 100644 --- a/shiny/ui/_modal.py +++ b/shiny/ui/_modal.py @@ -16,6 +16,7 @@ from ..types import MISSING, MISSING_TYPE +@add_example(ex_dir="../api-examples/modal") def modal_button(label: TagChild, icon: TagChild = None, **kwargs: TagAttrValue) -> Tag: """ Creates a button that will dismiss a :func:`modal`. :func:`~shiny.ui.modal_button` is usually @@ -41,10 +42,6 @@ def modal_button(label: TagChild, icon: TagChild = None, **kwargs: TagAttrValue) ~shiny.ui.modal ~shiny.ui.modal_show ~shiny.ui.modal_remove - - Example - ------- - See :func:`modal`. """ return tags.button( icon, @@ -156,6 +153,7 @@ def modal( ) +@add_example(ex_dir="../api-examples/modal") def modal_show(modal: Tag, session: Optional[Session] = None) -> None: """ Show a modal dialog. @@ -175,16 +173,13 @@ def modal_show(modal: Tag, session: Optional[Session] = None) -> None: ------- ~shiny.ui.modal_remove ~shiny.ui.modal - - Example - ------- - See :func:`modal`. """ session = require_active_session(session) msg = session._process_ui(modal) session._send_message_sync({"modal": {"type": "show", "message": msg}}) +@add_example(ex_dir="../api-examples/modal") def modal_remove(session: Optional[Session] = None) -> None: """ Remove a modal dialog box. @@ -203,10 +198,6 @@ def modal_remove(session: Optional[Session] = None) -> None: ------- ~shiny.ui.modal_show ~shiny.ui.modal - - Example - ------- - See :func:`modal`. """ session = require_active_session(session) session._send_message_sync({"modal": {"type": "remove", "message": None}}) diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index 44e686b87..5672d8bb9 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -27,7 +27,7 @@ from htmltools import MetadataNode, Tag, TagAttrs, TagChild, TagList, css, div, tags from .._deprecated import warn_deprecated -from .._docstring import add_example +from .._docstring import add_example, no_example from .._namespaces import resolve_id_or_none from .._utils import private_random_int from ..types import NavSetArg @@ -97,7 +97,7 @@ def tagify(self) -> None: ) -@add_example() +@add_example(app_file="app-basic.py") def nav_panel( title: TagChild, *args: TagChild, @@ -157,6 +157,7 @@ def nav_panel( ) +@no_example def nav_control(*args: TagChild) -> NavPanel: """ Place a control in the navigation container. @@ -188,6 +189,7 @@ def nav_control(*args: TagChild) -> NavPanel: return NavPanel(tags.li(*args)) +@no_example def nav_spacer() -> NavPanel: """ Create space between nav items. @@ -294,6 +296,7 @@ def menu_string_as_nav(x: str | NavSetArg) -> NavSetArg: return NavPanel(nav) +@no_example def nav_menu( title: TagChild, *args: NavPanel | str, @@ -401,6 +404,7 @@ def layout(self, nav: Tag, content: Tag) -> TagList | Tag: # ----------------------------------------------------------------------------- # Navigation containers # ----------------------------------------------------------------------------- +@no_example def navset_tab( *args: NavSetArg, id: Optional[str] = None, @@ -457,6 +461,7 @@ def navset_tab( ) +@no_example def navset_pill( *args: NavSetArg, id: Optional[str] = None, @@ -512,6 +517,7 @@ def navset_pill( ) +@no_example def navset_underline( *args: NavSetArg, id: Optional[str] = None, @@ -678,6 +684,7 @@ def layout(self, nav: Tag, content: Tag) -> Tag: ) +@no_example def navset_card_tab( *args: NavSetArg, id: Optional[str] = None, @@ -740,6 +747,7 @@ def navset_card_tab( ) +@no_example def navset_card_pill( *args: NavSetArg, id: Optional[str] = None, @@ -805,6 +813,7 @@ def navset_card_pill( ) +@no_example def navset_card_underline( *args: NavSetArg, id: Optional[str] = None, @@ -903,6 +912,7 @@ def layout(self, nav: TagChild, content: TagChild) -> Tag: ) +@no_example def navset_pill_list( *args: NavSetArg | MetadataNode, id: Optional[str] = None, @@ -1138,6 +1148,7 @@ def _make_tabs_fillable( # TODO-future; Content should not be indented unless when called from `page_navbar()` +@no_example def navset_bar( *args: NavSetArg | MetadataNode | Sequence[MetadataNode], title: TagChild, @@ -1382,6 +1393,7 @@ def navset_tab_card( # Deprecated 2023-12-07 +@no_example def nav( title: TagChild, *args: TagChild, diff --git a/shiny/ui/_notification.py b/shiny/ui/_notification.py index 15f9d6f50..73cdc1efb 100644 --- a/shiny/ui/_notification.py +++ b/shiny/ui/_notification.py @@ -6,7 +6,7 @@ from htmltools import TagChild -from .._docstring import add_example +from .._docstring import add_example, no_example from .._utils import rand_hex from ..session import Session, require_active_session @@ -90,6 +90,7 @@ def notification_show( return id +@no_example def notification_remove(id: str, *, session: Optional[Session] = None) -> str: """ Remove a notification. diff --git a/shiny/ui/_output.py b/shiny/ui/_output.py index aab99a35a..683c0cd55 100644 --- a/shiny/ui/_output.py +++ b/shiny/ui/_output.py @@ -14,7 +14,7 @@ from htmltools import Tag, TagAttrValue, TagFunction, css, div, tags -from .._docstring import add_example +from .._docstring import add_example, no_example from .._namespaces import resolve_id from ..types import MISSING, MISSING_TYPE from ._plot_output_opts import ( @@ -271,6 +271,7 @@ def output_text( return container(id=resolve_id(id), class_="shiny-text-output") +@no_example def output_code(id: str, placeholder: bool = True) -> Tag: """ Create a output container for code (monospaced text). @@ -312,6 +313,7 @@ def output_code(id: str, placeholder: bool = True) -> Tag: return tags.pre(id=resolve_id(id), class_=cls) +@add_example(ex_dir="../api-examples/input_text") def output_text_verbatim(id: str, placeholder: bool = False) -> Tag: """ Create a output container for some text. diff --git a/shiny/ui/_page.py b/shiny/ui/_page.py index 1675f4b48..8993b2c92 100644 --- a/shiny/ui/_page.py +++ b/shiny/ui/_page.py @@ -26,7 +26,7 @@ tags, ) -from .._docstring import add_example +from .._docstring import add_example, no_example from .._namespaces import resolve_id_or_none from ..types import MISSING, MISSING_TYPE, NavSetArg from ._bootstrap import panel_title @@ -41,6 +41,7 @@ from .fill._fill import as_fillable_container +@add_example() def page_sidebar( sidebar: Sidebar, *args: TagChild | TagAttrs, @@ -109,6 +110,7 @@ def page_sidebar( ) +@no_example def page_navbar( *args: NavSetArg | MetadataNode | Sequence[MetadataNode], title: Optional[str | Tag | TagList] = None, @@ -253,6 +255,7 @@ def page_navbar( ) +@no_example def page_fillable( *args: TagChild | TagAttrs, padding: Optional[CssUnit | list[CssUnit]] = None, @@ -404,6 +407,7 @@ def page_fixed( # TODO: implement theme (just Bootswatch for now?) +@no_example def page_bootstrap( *args: TagChild | TagAttrs, title: Optional[str] = None, @@ -445,6 +449,7 @@ def page_bootstrap( ) +@no_example def page_auto( *args: TagChild | TagAttrs, title: str | MISSING_TYPE = MISSING, @@ -628,6 +633,7 @@ def _page_auto_fixed( ) +@no_example def page_output(id: str) -> Tag: """ Create a page container where the entire body is a UI output. diff --git a/shiny/ui/_sidebar.py b/shiny/ui/_sidebar.py index ea5c4ee00..ffa09c2f5 100644 --- a/shiny/ui/_sidebar.py +++ b/shiny/ui/_sidebar.py @@ -17,7 +17,7 @@ ) from .._deprecated import warn_deprecated -from .._docstring import add_example +from .._docstring import add_example, no_example from .._namespaces import resolve_id_or_none from ..session import Session, require_active_session from ._card import CardItem @@ -38,6 +38,7 @@ ) +@no_example class Sidebar: """ A sidebar object @@ -89,8 +90,6 @@ class directly. Instead, supply the :func:`~shiny.ui.sidebar` object to A foreground color. color_bg A background color. - - """ def __init__( @@ -577,6 +576,7 @@ def _sidebar_init_js() -> Tag: # Deprecated 2023-06-13 # Includes: DeprecatedPanelSidebar +@no_example def panel_sidebar( *args: TagChild | TagAttrs, width: int = 4, @@ -599,6 +599,7 @@ def panel_sidebar( # Deprecated 2023-06-13 # Includes: DeprecatedPanelMain +@no_example def panel_main( *args: TagChild | TagAttrs, width: int = 8, diff --git a/shiny/ui/_tooltip.py b/shiny/ui/_tooltip.py index 85c3b7a76..a8d197ee4 100644 --- a/shiny/ui/_tooltip.py +++ b/shiny/ui/_tooltip.py @@ -5,11 +5,13 @@ from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, tags +from .._docstring import add_example from .._namespaces import resolve_id_or_none from ._tag import consolidate_attrs from ._web_component import web_component +@add_example() def tooltip( trigger: TagChild, *args: TagChild | TagAttrs, diff --git a/shiny/ui/_valuebox.py b/shiny/ui/_valuebox.py index 219c77cbd..7f7bd470e 100644 --- a/shiny/ui/_valuebox.py +++ b/shiny/ui/_valuebox.py @@ -14,7 +14,7 @@ tags, ) -from .._docstring import add_example +from .._docstring import add_example, no_example from ._card import CardItem, card, card_body from ._tag import consolidate_attrs from ._utils import css_no_sub @@ -91,6 +91,7 @@ def __init__( self.max_height_full_screen = as_css_unit(max_height_full_screen) +@add_example() def showcase_left_center( *, width: CssUnit = "30%", @@ -124,6 +125,7 @@ def showcase_left_center( ) +@add_example() def showcase_top_right( *, width: CssUnit = "40%", @@ -158,6 +160,7 @@ def showcase_top_right( ) +@add_example() def showcase_bottom( *, width: CssUnit = "100%", @@ -224,6 +227,7 @@ class ValueBoxTheme: bg: str | None +@no_example def value_box_theme( name: Optional[str] = None, *, diff --git a/shiny/ui/css/_css_unit.py b/shiny/ui/css/_css_unit.py index 3ab0445bb..29d99880f 100644 --- a/shiny/ui/css/_css_unit.py +++ b/shiny/ui/css/_css_unit.py @@ -45,6 +45,21 @@ def as_css_unit(value: None | CssUnit) -> None | str: ------- : If the `value` is `None`, then `None`. If the value is `0`, then `"0"`. If the `value` is numeric, then a formatted pixel value. Otherwise, the `value` as-is. + + Examples + -------- + + ```{python} + from shiny.ui.css import as_css_unit + + as_css_unit(0) + ``` + ```{python} + as_css_unit(300) + ``` + ```{python} + as_css_unit("1em") + ``` """ # TODO-future: Actually validate. Or don't validate, but then change # the function name to to_css_unit() or something. @@ -80,6 +95,15 @@ def as_css_padding(padding: CssUnit | list[CssUnit] | None) -> str | None: ------- : A CSS padding value. + + Examples + -------- + + ```{python} + from shiny.ui.css import as_css_padding + + as_css_padding([0, "1em"]) + ``` """ if padding is None: return None diff --git a/shiny/ui/dataframe/_dataframe.py b/shiny/ui/dataframe/_dataframe.py index a4c1db11e..73d8a27af 100644 --- a/shiny/ui/dataframe/_dataframe.py +++ b/shiny/ui/dataframe/_dataframe.py @@ -4,11 +4,13 @@ from htmltools import Tag +from ..._docstring import add_example from ..._namespaces import resolve_id from .._html_deps_py_shiny import data_frame_deps from ..fill import as_fill_item, as_fillable_container +@add_example(ex_dir="../../api-examples/data_frame") def output_data_frame(id: str) -> Tag: """ Create an output container for an interactive table or grid. Features fast diff --git a/shiny/ui/fill/_fill.py b/shiny/ui/fill/_fill.py index 6dd6d1c88..4b148b85a 100644 --- a/shiny/ui/fill/_fill.py +++ b/shiny/ui/fill/_fill.py @@ -5,7 +5,7 @@ from htmltools import Tag, TagAttrs -from ..._docstring import add_example +from ..._docstring import add_example, no_example from .._html_deps_shinyverse import fill_dependency __all__ = ( @@ -77,6 +77,7 @@ def as_fill_item( return res +@no_example def remove_all_fill( tag: TagT, ) -> TagT: