diff --git a/examples/express/expressify_app.py b/examples/express/expressify_app.py new file mode 100644 index 000000000..f814564da --- /dev/null +++ b/examples/express/expressify_app.py @@ -0,0 +1,28 @@ +from shiny.express import expressify, ui + + +# @expressify converts a function to work with Express syntax +@expressify +def expressified1(s: str): + f"Expressified function 1: {s}" # noqa: B021 + ui.br() + + +expressified1("Hello") + +expressified1("world") + + +ui.br() + + +# @expressify() also works with parens +@expressify() +def expressified2(s: str): + f"Expressified function 2: {s}" # noqa: B021 + ui.br() + + +expressified2("Hello") + +expressified2("world") diff --git a/examples/express/hold_app.py b/examples/express/hold_app.py new file mode 100644 index 000000000..8e08c1b63 --- /dev/null +++ b/examples/express/hold_app.py @@ -0,0 +1,28 @@ +import shiny.ui +from shiny.express import input, render, ui + +# `ui.hold() as x` can be used to save `x` for later output +with ui.hold() as hello_card: + with ui.card(): + with ui.span(): + "This is a" + ui.span(" card", style="color: red;") + +hello_card + +hello_card + +ui.hr() + + +# `ui.hold()` can be used to just suppress output +with ui.hold(): + + @render.text() + def txt(): + return f"Slider value: {input.n()}" + + +ui.input_slider("n", "N", 1, 100, 50) + +shiny.ui.output_text("txt", inline=True) diff --git a/examples/express/render_express_app.py b/examples/express/render_express_app.py new file mode 100644 index 000000000..4a07abca6 --- /dev/null +++ b/examples/express/render_express_app.py @@ -0,0 +1,17 @@ +from shiny.express import input, render, ui + +ui.input_slider("n", "N", 1, 100, 50) + + +# @render.express is like @render.ui, but with Express syntax +@render.express +def render_express1(): + "Slider value:" + input.n() + + +# @render.express() also works with parens +@render.express() +def render_express2(): + "Slider value:" + input.n() diff --git a/shiny/api-examples/render_display/app.py b/shiny/api-examples/render_display/app.py deleted file mode 100644 index 7bd372616..000000000 --- a/shiny/api-examples/render_display/app.py +++ /dev/null @@ -1,19 +0,0 @@ -import datetime - -from shiny import render, ui -from shiny.express import input, layout - -with layout.card(id="card"): - ui.input_slider("val", "slider", 0, 100, 50) - "Text outside of render display call" - ui.tags.br() - f"Rendered time: {str(datetime.datetime.now())}" - - @render.display - def render_display(): - "Text inside of render display call" - ui.tags.br() - "Dynamic slider value: " - input.val() - ui.tags.br() - f"Display's rendered time: {str(datetime.datetime.now())}" diff --git a/shiny/api-examples/render_express/app.py b/shiny/api-examples/render_express/app.py new file mode 100644 index 000000000..9ba6e813f --- /dev/null +++ b/shiny/api-examples/render_express/app.py @@ -0,0 +1,18 @@ +import datetime + +from shiny.express import input, render, ui + +with ui.card(id="card"): + ui.input_slider("val", "slider", 0, 100, 50) + "Text outside of render express call" + ui.tags.br() + f"Rendered time: {str(datetime.datetime.now())}" + + @render.express + def render_express(): + "Text inside of render express call" + ui.tags.br() + "Dynamic slider value: " + input.val() + ui.tags.br() + f"Rendered time: {str(datetime.datetime.now())}" diff --git a/shiny/express/__init__.py b/shiny/express/__init__.py index 8add0557a..4896159a2 100644 --- a/shiny/express/__init__.py +++ b/shiny/express/__init__.py @@ -8,11 +8,11 @@ from . import ui from ._is_express import is_express_app from ._output import ( # noqa: F401 - suspend_display, output_args, # pyright: ignore[reportUnusedImport] + suspend_display, # pyright: ignore[reportUnusedImport] - Deprecated ) from ._run import wrap_express_app -from .display_decorator import display_body +from .expressify_decorator import expressify __all__ = ( @@ -21,10 +21,9 @@ "output", "session", "is_express_app", - "suspend_display", "wrap_express_app", "ui", - "display_body", + "expressify", ) # Add types to help type checkers diff --git a/shiny/express/_output.py b/shiny/express/_output.py index 5f64f459f..da2952e24 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -1,15 +1,17 @@ from __future__ import annotations -import contextlib -import sys from contextlib import AbstractContextManager -from typing import Callable, Generator, TypeVar, cast, overload +from typing import Callable, TypeVar -from .. import ui -from ..render.renderer import Renderer, RendererT +from .._deprecated import warn_deprecated +from .._typing_extensions import ParamSpec +from ..render.renderer import RendererT +from .ui import hold __all__ = ("suspend_display",) +P = ParamSpec("P") +R = TypeVar("R") CallableT = TypeVar("CallableT", bound=Callable[..., object]) @@ -45,82 +47,11 @@ def wrapper(renderer: RendererT) -> RendererT: return wrapper -@overload -def suspend_display(fn: RendererT) -> RendererT: - ... - - -@overload -def suspend_display(fn: CallableT) -> CallableT: - ... - - -@overload -def suspend_display() -> AbstractContextManager[None]: - ... - - def suspend_display( - fn: RendererT | CallableT | None = None, -) -> RendererT | CallableT | AbstractContextManager[None]: - """Suppresses the display of UI elements in various ways. - - If used as a context manager (`with suspend_display():`), it suppresses the display - of all UI elements within the context block. (This is useful when you want to - temporarily suppress the display of a large number of UI elements, or when you want - to suppress the display of UI elements that are not directly under your control.) - - If used as a decorator (without parentheses) on a Shiny rendering function, it - prevents that function from automatically outputting itself at the point of its - declaration. (This is useful when you want to define the rendering logic for an - output, but want to explicitly call a UI output function to indicate where and how - it should be displayed.) - - If used as a decorator (without parentheses) on any other function, it turns - Python's `sys.displayhook` into a no-op for the duration of the function call. - - Parameters - ---------- - fn - The function to decorate. If `None`, returns a context manager that suppresses - the display of UI elements within the context block. - - Returns - ------- - : - If `fn` is `None`, returns a context manager that suppresses the display of UI - elements within the context block. Otherwise, returns a decorated version of - `fn`. - """ - - if fn is None: - return suspend_display_ctxmgr() - - # Special case for Renderer; when we decorate those, we just mean "don't - # display yourself" - if isinstance(fn, Renderer): - # By setting the class value, the `self` arg will be auto added. - fn.auto_output_ui = null_ui - return cast(RendererT, fn) - - return suspend_display_ctxmgr()(fn) - - -@contextlib.contextmanager -def suspend_display_ctxmgr() -> Generator[None, None, None]: - oldhook = sys.displayhook - sys.displayhook = null_displayhook - try: - yield - finally: - sys.displayhook = oldhook - - -def null_ui( - **kwargs: object, -) -> ui.TagList: - return ui.TagList() - - -def null_displayhook(x: object) -> None: - pass + fn: Callable[P, R] | RendererT | None = None +) -> Callable[P, R] | RendererT | AbstractContextManager[None]: + warn_deprecated( + "`suspend_display` is deprecated. Please use `ui.hold` instead. " + "It has a new name, but the exact same functionality." + ) + return hold(fn) # type: ignore diff --git a/shiny/express/_run.py b/shiny/express/_run.py index d65190b1e..3261cc1e3 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -10,10 +10,10 @@ from .._app import App from ..session import Inputs, Outputs, Session from ._recall_context import RecallContextManager -from .display_decorator._func_displayhook import _display_decorator_function_def -from .display_decorator._node_transformers import ( +from .expressify_decorator._func_displayhook import _expressify_decorator_function_def +from .expressify_decorator._node_transformers import ( DisplayFuncsTransformer, - display_decorator_func_name, + expressify_decorator_func_name, ) __all__ = ("wrap_express_app",) @@ -83,7 +83,7 @@ def set_result(x: object): var_context: dict[str, object] = { "__file__": file_path, - display_decorator_func_name: _display_decorator_function_def, + expressify_decorator_func_name: _expressify_decorator_function_def, "input": InputNotImportedShim(), } diff --git a/shiny/express/display_decorator/__init__.py b/shiny/express/display_decorator/__init__.py deleted file mode 100644 index 066db7f9b..000000000 --- a/shiny/express/display_decorator/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._display_body import display_body - -__all__ = ("display_body",) diff --git a/shiny/express/expressify_decorator/__init__.py b/shiny/express/expressify_decorator/__init__.py new file mode 100644 index 000000000..a3e132f0a --- /dev/null +++ b/shiny/express/expressify_decorator/__init__.py @@ -0,0 +1,3 @@ +from ._expressify import expressify + +__all__ = ("expressify",) diff --git a/shiny/express/display_decorator/_display_body.py b/shiny/express/expressify_decorator/_expressify.py similarity index 84% rename from shiny/express/display_decorator/_display_body.py rename to shiny/express/expressify_decorator/_expressify.py index 526d01ee3..3bc99ed20 100644 --- a/shiny/express/display_decorator/_display_body.py +++ b/shiny/express/expressify_decorator/_expressify.py @@ -6,22 +6,30 @@ import linecache import sys import types -from typing import Any, Callable, Dict, Protocol, TypeVar, cast, runtime_checkable +from typing import ( + Any, + Callable, + Dict, + Protocol, + TypeVar, + cast, + overload, + runtime_checkable, +) -from ._func_displayhook import _display_decorator_function_def +from ._func_displayhook import _expressify_decorator_function_def from ._helpers import find_code_for_func from ._node_transformers import ( DisplayFuncsTransformer, FuncBodyDisplayHookTransformer, TargetFunctionTransformer, - display_decorator_func_name, + expressify_decorator_func_name, sys_alias, ) -# It's quite expensive to decorate with display_body, and it could be done to -# inner functions where the outer function is called a lot. Use a cache to save -# us from having to do the expensive stuff (parsing, transforming, compiling) -# more than once. +# It's quite expensive to decorate with expressify, and it could be done to inner +# functions where the outer function is called a lot. Use a cache to save us from having +# to do the expensive stuff (parsing, transforming, compiling) more than once. code_cache: Dict[types.CodeType, types.CodeType] = {} T = TypeVar("T") @@ -45,12 +53,12 @@ def unwrap(fn: TFunc) -> TFunc: return fn -display_body_attr = "__display_body__" +expressify_attr = "__expressify__" -def display_body_unwrap_inplace() -> Callable[[TFunc], TFunc]: +def expressify_unwrap_inplace() -> Callable[[TFunc], TFunc]: """ - Like `display_body`, but far more violent. This will attempt to traverse any + Like `expressify`, but far more violent. This will attempt to traverse any decorators between this one and the function, and then modify the function _in place_. It will then return the function that was passed in. """ @@ -59,7 +67,7 @@ def decorator(fn: TFunc) -> TFunc: unwrapped_fn = unwrap(fn) # Check if we've already done this - if hasattr(unwrapped_fn, display_body_attr): + if hasattr(unwrapped_fn, expressify_attr): return fn if unwrapped_fn.__code__ in code_cache: @@ -70,13 +78,23 @@ def decorator(fn: TFunc) -> TFunc: code_cache[unwrapped_fn.__code__] = fcode unwrapped_fn.__code__ = fcode - setattr(unwrapped_fn, display_body_attr, True) + setattr(unwrapped_fn, expressify_attr, True) return fn return decorator -def display_body() -> Callable[[TFunc], TFunc]: +@overload +def expressify(fn: TFunc) -> TFunc: + ... + + +@overload +def expressify() -> Callable[[TFunc], TFunc]: + ... + + +def expressify(fn: TFunc | None = None) -> TFunc | Callable[[TFunc], TFunc]: def decorator(fn: TFunc) -> TFunc: if fn.__code__ in code_cache: fcode = code_cache[fn.__code__] @@ -93,7 +111,7 @@ def decorator(fn: TFunc) -> TFunc: # have a different `sys` alias in scope. globals={ sys_alias: auto_displayhook, - display_decorator_func_name: _display_decorator_function_def, + expressify_decorator_func_name: _expressify_decorator_function_def, **fn.__globals__, }, name=fn.__name__, @@ -106,6 +124,9 @@ def decorator(fn: TFunc) -> TFunc: new_func.__dict__.update(fn.__dict__) return cast(TFunc, functools.wraps(fn)(new_func)) + if fn is not None: + return decorator(fn) + return decorator @@ -219,4 +240,4 @@ def comparator(candidate: types.CodeType, target: types.CodeType) -> bool: __builtins__[sys_alias] = auto_displayhook -__builtins__[display_decorator_func_name] = _display_decorator_function_def +__builtins__[expressify_decorator_func_name] = _expressify_decorator_function_def diff --git a/shiny/express/display_decorator/_func_displayhook.py b/shiny/express/expressify_decorator/_func_displayhook.py similarity index 86% rename from shiny/express/display_decorator/_func_displayhook.py rename to shiny/express/expressify_decorator/_func_displayhook.py index 26b977c6c..ab6b4f60e 100644 --- a/shiny/express/display_decorator/_func_displayhook.py +++ b/shiny/express/expressify_decorator/_func_displayhook.py @@ -6,7 +6,7 @@ # A decorator used for `def` statements. It makes sure that any `def` statement which # returns a tag-like object, or one with a `_repr_html` method, will be passed on to # the current sys.displayhook. -def _display_decorator_function_def(fn: object) -> object: +def _expressify_decorator_function_def(fn: object) -> object: if isinstance(fn, (Tag, TagList, Tagifiable)) or hasattr(fn, "_repr_html_"): sys.displayhook(fn) diff --git a/shiny/express/display_decorator/_helpers.py b/shiny/express/expressify_decorator/_helpers.py similarity index 100% rename from shiny/express/display_decorator/_helpers.py rename to shiny/express/expressify_decorator/_helpers.py diff --git a/shiny/express/display_decorator/_node_transformers.py b/shiny/express/expressify_decorator/_node_transformers.py similarity index 94% rename from shiny/express/display_decorator/_node_transformers.py rename to shiny/express/expressify_decorator/_node_transformers.py index 14bbcfca9..f7e362e0e 100644 --- a/shiny/express/display_decorator/_node_transformers.py +++ b/shiny/express/expressify_decorator/_node_transformers.py @@ -7,7 +7,7 @@ from ._helpers import ast_matches_func sys_alias = "__auto_displayhook_sys__" -display_decorator_func_name = "_display_decorator_function_def" +expressify_decorator_func_name = "_expressify_decorator_function_def" class TopLevelTransformer(ast.NodeTransformer): @@ -116,12 +116,12 @@ class DisplayFuncsTransformer(TopLevelTransformer): def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: node.decorator_list.insert( - 0, ast.Name(id=display_decorator_func_name, ctx=ast.Load()) + 0, ast.Name(id=expressify_decorator_func_name, ctx=ast.Load()) ) return node def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> object: node.decorator_list.insert( - 0, ast.Name(id=display_decorator_func_name, ctx=ast.Load()) + 0, ast.Name(id=expressify_decorator_func_name, ctx=ast.Load()) ) return node diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index eb0c63ba7..bb062ad8a 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -135,6 +135,10 @@ page_opts, ) +from ._hold import ( + hold, +) + __all__ = ( # Imports from htmltools "TagList", @@ -263,6 +267,8 @@ "tooltip", # Imports from ._page "page_opts", + # Imports from ._hold + "hold", ) @@ -302,5 +308,8 @@ "output_data_frame", ), # Items from shiny.express.ui that don't have a counterpart in shiny.ui - "shiny.express.ui": ("page_opts",), + "shiny.express.ui": ( + "page_opts", + "hold", + ), } diff --git a/shiny/express/ui/_hold.py b/shiny/express/ui/_hold.py new file mode 100644 index 000000000..86678cfc3 --- /dev/null +++ b/shiny/express/ui/_hold.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import sys +from types import TracebackType +from typing import Callable, Optional, Type, TypeVar + +from htmltools import wrap_displayhook_handler + +from ... import ui +from ..._typing_extensions import ParamSpec + +__all__ = ("hold",) + +P = ParamSpec("P") +R = TypeVar("R") +CallableT = TypeVar("CallableT", bound=Callable[..., object]) + + +def hold() -> HoldContextManager: + """Prevent the display of UI elements in various ways. + + If used as a context manager (`with hide():`), it prevents the display of all UI + elements within the context block. (This is useful when you want to temporarily + prevent the display of a large number of UI elements, or when you want to prevent + the display of UI elements that are not directly under your control.) + + If used as a decorator (without parentheses) on a Shiny rendering function, it + prevents that function from automatically outputting itself at the point of its + declaration. (This is useful when you want to define the rendering logic for an + output, but want to explicitly call a UI output function to indicate where and how + it should be displayed.) + + If used as a decorator (without parentheses) on any other function, it turns + Python's `sys.displayhook` into a no-op for the duration of the function call. + + Parameters + ---------- + fn + The function to decorate. If `None`, returns a context manager that prevents the + display of UI elements within the context block. + + Returns + ------- + : + If `fn` is `None`, returns a context manager that prevents the display of UI + elements within the context block. Otherwise, returns a decorated version of + `fn`. + """ + + return HoldContextManager() + + +class HoldContextManager: + def __init__(self): + self.content = ui.TagList() + + def __enter__(self) -> ui.TagList: + self.prev_displayhook = sys.displayhook + sys.displayhook = wrap_displayhook_handler( + self.content.append # pyright: ignore[reportGeneralTypeIssues] + ) + return self.content + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> bool: + sys.displayhook = self.prev_displayhook + return False diff --git a/shiny/render/__init__.py b/shiny/render/__init__.py index 84b0ce12c..62efc7ced 100644 --- a/shiny/render/__init__.py +++ b/shiny/render/__init__.py @@ -11,8 +11,8 @@ DataTable, data_frame, ) -from ._display import ( - display, +from ._express import ( + express, ) from ._render import ( code, @@ -31,7 +31,7 @@ __all__ = ( # TODO-future: Document which variables are exposed via different import approaches "data_frame", - "display", + "express", "text", "code", "plot", diff --git a/shiny/render/_display.py b/shiny/render/_express.py similarity index 83% rename from shiny/render/_display.py rename to shiny/render/_express.py index c54fb5df4..149c8cbc1 100644 --- a/shiny/render/_display.py +++ b/shiny/render/_express.py @@ -17,7 +17,7 @@ ) -class display(Renderer[None]): +class express(Renderer[None]): def auto_output_ui( self, *, @@ -41,19 +41,17 @@ def auto_output_ui( def __call__(self, fn: ValueFn[None]) -> Self: if fn is None: # pyright: ignore[reportUnnecessaryComparison] - raise TypeError("@render.display requires a function when called") + raise TypeError("@render.express requires a function when called") async_fn = AsyncValueFn(fn) if async_fn.is_async(): raise TypeError( - "@render.display does not support async functions. Use @render.ui instead." + "@render.express does not support async functions. Use @render.ui instead." ) - from shiny.express.display_decorator._display_body import ( - display_body_unwrap_inplace, - ) + from ..express.expressify_decorator._expressify import expressify_unwrap_inplace - fn = display_body_unwrap_inplace()(fn) + fn = expressify_unwrap_inplace()(fn) # Call the superclass method with upgraded `fn` value super().__call__(fn) @@ -84,7 +82,7 @@ async def render(self) -> JsonifiableDict | None: if self.fn.is_async(): raise TypeError( - "@render.display does not support async functions. Use @render.ui instead." + "@render.express does not support async functions. Use @render.ui instead." ) try: @@ -93,7 +91,9 @@ async def render(self) -> JsonifiableDict | None: ret = sync_value_fn() if ret is not None: raise RuntimeError( - "@render.display functions should not return values. Instead, @render.display dynamically renders every printable line within the function body. (`None` is a valid return value.)" + "@render.express functions should not return values. " + "Instead, @render.express dynamically renders every printable line " + "within the function body. (`None` is a valid return value.)" ) finally: sys.displayhook = orig_displayhook diff --git a/tests/playwright/deploys/apps/shiny-express-folium/app.py b/tests/playwright/deploys/apps/shiny-express-folium/app.py index a6d5e8ad1..3790535ba 100644 --- a/tests/playwright/deploys/apps/shiny-express-folium/app.py +++ b/tests/playwright/deploys/apps/shiny-express-folium/app.py @@ -19,9 +19,9 @@ "location", "Location", ["San Francisco", "New York", "Los Angeles"] ) - @render.display + @render.express def folium_map(): - "Map inside of render display call" + "Map inside of render express call" folium.Map( # pyright: ignore[reportUnknownMemberType,reportGeneralTypeIssues] location=locations_coords[input.location()], tiles="cartodb positron", diff --git a/tests/playwright/examples/example_apps.py b/tests/playwright/examples/example_apps.py index 26ca18b1c..d8ee55ec1 100644 --- a/tests/playwright/examples/example_apps.py +++ b/tests/playwright/examples/example_apps.py @@ -66,7 +66,7 @@ def get_apps(path: str) -> typing.List[str]: "model-score": [*output_transformer_errors], "data_frame": [*output_transformer_errors], "output_transformer": [*output_transformer_errors], - "render_display": [*express_warnings], + "render_express": [*express_warnings], } app_allow_external_errors: typing.List[str] = [ # if shiny express app detected diff --git a/tests/playwright/shiny/shiny-express/hold/app.py b/tests/playwright/shiny/shiny-express/hold/app.py new file mode 100644 index 000000000..6f73e657a --- /dev/null +++ b/tests/playwright/shiny/shiny-express/hold/app.py @@ -0,0 +1,27 @@ +from shiny.express import input, render, ui + +with ui.card(id="card"): + ui.input_slider("s1", "A", 1, 100, 20) + + with ui.hold(): + + @render.code + def hidden(): + return input.s1() + + ui.input_slider("s2", "B", 1, 100, 40) + + # from shiny.express import ui_kwargs + # @ui_kwargs(placeholder=False) + # @ui_kwargs(placeholder=True) + with ui.hold() as held: + + @render.code() + def visible(): + # from shiny import req + + # req(False) + return input.s2() + + "Some text here" + held # pyright: ignore[reportUnusedExpression] diff --git a/tests/playwright/shiny/shiny-express/suspend_display/test_suspend_display.py b/tests/playwright/shiny/shiny-express/hold/test_hold.py similarity index 60% rename from tests/playwright/shiny/shiny-express/suspend_display/test_suspend_display.py rename to tests/playwright/shiny/shiny-express/hold/test_hold.py index a609183fa..77df73f52 100644 --- a/tests/playwright/shiny/shiny-express/suspend_display/test_suspend_display.py +++ b/tests/playwright/shiny/shiny-express/hold/test_hold.py @@ -1,7 +1,7 @@ from conftest import ShinyAppProc from controls import OutputTextVerbatim from playwright.sync_api import Page -from playwright.sync_api import expect as playright_expect +from playwright.sync_api import expect as playwright_expect def test_express_page_fluid(page: Page, local_app: ShinyAppProc) -> None: @@ -10,5 +10,5 @@ def test_express_page_fluid(page: Page, local_app: ShinyAppProc) -> None: txt = OutputTextVerbatim(page, "visible") txt.expect_value("40") - playright_expect(page.locator("#visible")).to_have_count(1) - playright_expect(page.locator("#hidden")).to_have_count(0) + playwright_expect(page.locator("#visible")).to_have_count(1) + playwright_expect(page.locator("#hidden")).to_have_count(0) diff --git a/tests/playwright/shiny/shiny-express/render_display/test_render_display.py b/tests/playwright/shiny/shiny-express/render_express/test_render_express.py similarity index 61% rename from tests/playwright/shiny/shiny-express/render_display/test_render_display.py rename to tests/playwright/shiny/shiny-express/render_express/test_render_express.py index d097556f4..f60f31478 100644 --- a/tests/playwright/shiny/shiny-express/render_display/test_render_display.py +++ b/tests/playwright/shiny/shiny-express/render_express/test_render_express.py @@ -1,17 +1,17 @@ from conftest import ShinyAppProc, create_doc_example_fixture from playwright.sync_api import Page, expect -app = create_doc_example_fixture("render_display") +app = create_doc_example_fixture("render_express") EXPECT_TIMEOUT = 30 * 1000 -def test_render_display(page: Page, app: ShinyAppProc) -> None: +def test_render_express(page: Page, app: ShinyAppProc) -> None: page.goto(app.url) - expect(page.get_by_text("Text outside of render display call")).to_have_count( + expect(page.get_by_text("Text outside of render express call")).to_have_count( 1, timeout=EXPECT_TIMEOUT ) - expect(page.get_by_text("Text inside of render display call")).to_have_count( + expect(page.get_by_text("Text inside of render express call")).to_have_count( 1, timeout=EXPECT_TIMEOUT ) expect(page.get_by_text("Dynamic slider value: 50")).to_have_count( diff --git a/tests/playwright/shiny/shiny-express/suspend_display/app.py b/tests/playwright/shiny/shiny-express/suspend_display/app.py deleted file mode 100644 index 4f4b510ef..000000000 --- a/tests/playwright/shiny/shiny-express/suspend_display/app.py +++ /dev/null @@ -1,22 +0,0 @@ -from shiny import render, ui -from shiny.express import input, suspend_display - -with ui.card(id="card"): - ui.input_slider("s1", "A", 1, 100, 20) - - @suspend_display - @render.code - def hidden(): - return input.s1() - - ui.input_slider("s2", "B", 1, 100, 40) - - # from shiny.express import ui_kwargs - # @ui_kwargs(placeholder=False) - # @ui_kwargs(placeholder=True) - @render.code() - def visible(): - # from shiny import req - - # req(False) - return input.s2() diff --git a/tests/playwright/utils/express_utils.py b/tests/playwright/utils/express_utils.py index a40a9abbf..1dfa2373e 100644 --- a/tests/playwright/utils/express_utils.py +++ b/tests/playwright/utils/express_utils.py @@ -99,12 +99,12 @@ def verify_express_dataframe(page: Page) -> None: def verify_express_folium_render(page: Page) -> None: expect(page.get_by_text("Static Map")).to_have_count(1) - expect(page.get_by_text("Map inside of render display call")).to_have_count(1) - # map inside the @render.display + expect(page.get_by_text("Map inside of render express call")).to_have_count(1) + # map inside the @render.express expect( page.frame_locator("iframe").nth(1).get_by_role("link", name="OpenStreetMap") ).to_have_count(1) - # map outside of the @render.display at the top level + # map outside of the @render.express at the top level expect( page.frame_locator("iframe") .nth(0) diff --git a/tests/pytest/test_display_decorator.py b/tests/pytest/test_display_decorator.py index aa8f330dc..9aed9918d 100644 --- a/tests/pytest/test_display_decorator.py +++ b/tests/pytest/test_display_decorator.py @@ -11,7 +11,7 @@ from htmltools import Tagifiable from shiny import render, ui -from shiny.express import display_body +from shiny.express import expressify @contextlib.contextmanager @@ -25,7 +25,7 @@ def capture_display() -> Generator[list[object], None, None]: sys1.displayhook = old_displayhook -@display_body() +@expressify() def display_repeated(value: str, /, times: int, *, sep: str = " ") -> None: sep.join([value] * times) @@ -47,7 +47,7 @@ def test_simple(): display_repeated("hello") # type: ignore -@display_body() +@expressify() def display_variadic(*args: object, **kwargs: object): "# args" for arg in args: @@ -58,7 +58,7 @@ def display_variadic(*args: object, **kwargs: object): def test_null_filtered(): - @display_body() + @expressify() def has_none(): 1 None @@ -78,7 +78,7 @@ def test_variadic(): def nested(z: int = 1): x = 2 - @display_body() + @expressify() def inner(): x * 10 * z @@ -88,12 +88,12 @@ def inner(): def test_caching(): - import shiny.express.display_decorator._display_body as _display_body + import shiny.express.expressify_decorator._expressify as _expressify nested() - cache_len_before = len(_display_body.code_cache) + cache_len_before = len(_expressify.code_cache) nested(z=3) - cache_len_after = len(_display_body.code_cache) + cache_len_after = len(_expressify.code_cache) assert cache_len_before == cache_len_after @@ -105,13 +105,13 @@ def test_duplicate_func_names_ok(): x = "hello" - @display_body() + @expressify() def inner(): # pyright: ignore[reportGeneralTypeIssues] x + " world" inner_old = inner - @display_body() + @expressify() def inner(): # pyright: ignore[reportGeneralTypeIssues] x + " universe" @@ -124,7 +124,7 @@ def inner(): # pyright: ignore[reportGeneralTypeIssues] assert d == ["hello universe"] # Here's yet another one, just to be mean - @display_body() + @expressify() def inner(): x + " nobody" @@ -135,7 +135,7 @@ def not_decorated(): 2 3 - decorated = display_body()(not_decorated) + decorated = expressify()(not_decorated) with capture_display() as d: decorated() @@ -149,7 +149,7 @@ def not_decorated(): def test_annotations(): - @display_body() + @expressify() def annotated(x: int, y: int) -> int: """Here's a docstring""" x + y @@ -163,7 +163,7 @@ def annotated(x: int, y: int) -> int: def test_implicit_output(): - @display_body() + @expressify() def has_implicit_outputs(): @render.code def foo(): @@ -177,7 +177,7 @@ def foo(): def test_no_nested_transform_unless_explicit(): - @display_body() + @expressify() def inner1(): 1 2 @@ -188,7 +188,7 @@ def inner2(): 3 4 - @display_body() + @expressify() def inner3(): # Does transform, it has the decorator again 5 diff --git a/tests/pytest/test_express_ui.py b/tests/pytest/test_express_ui.py index 0c1afcb43..90b36c360 100644 --- a/tests/pytest/test_express_ui.py +++ b/tests/pytest/test_express_ui.py @@ -6,7 +6,8 @@ import pytest from shiny import render, ui -from shiny.express import output_args, suspend_display +from shiny.express import output_args +from shiny.express import ui as xui from shiny.express._run import run_express @@ -17,9 +18,6 @@ def test_express_ui_is_complete(): These entries are in `_known_missing` in shiny/express/ui/__init__.py """ - from shiny import ui - from shiny.express import ui as xui - ui_all = set(ui.__all__) xui_all = set(xui.__all__) ui_known_missing = set(xui._known_missing["shiny.ui"]) @@ -53,13 +51,6 @@ def text1(): == ui.output_text("text1").get_html_string() ) - @suspend_display - @render.text - def text2(): - return "text" - - assert ui.TagList(text2.tagify()).get_html_string() == "" - @output_args(placeholder=False) @render.code def code1(): @@ -79,7 +70,7 @@ def code2(): code2.tagify() -def test_suspend_display(): +def test_hold(): old_displayhook = sys.displayhook try: called = False @@ -90,21 +81,21 @@ def display_hook_spy(_: object) -> Any: sys.displayhook = display_hook_spy - with suspend_display(): + with xui.hold(): sys.displayhook("foo") - suspend_display(lambda: sys.displayhook("bar"))() - - @suspend_display - def whatever(x: Any): - sys.displayhook(x) - - whatever(100) assert not called sys.displayhook("baz") assert called + called = False + with xui.hold() as held: + sys.displayhook("foo") + assert not called + sys.displayhook(held) + assert called + finally: sys.displayhook = old_displayhook