From 9b22a2fecc1caada7afa926affcc851131cbac56 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 18 Jan 2024 22:02:18 -0600 Subject: [PATCH 01/11] Rename suspend_display() to hide() --- shiny/express/__init__.py | 4 +- shiny/express/_output.py | 46 ++++++++++++------- .../{suspend_display => hide}/app.py | 4 +- .../test_hide.py} | 0 tests/pytest/test_express_ui.py | 12 ++--- 5 files changed, 41 insertions(+), 25 deletions(-) rename tests/playwright/shiny/shiny-express/{suspend_display => hide}/app.py (86%) rename tests/playwright/shiny/shiny-express/{suspend_display/test_suspend_display.py => hide/test_hide.py} (100%) diff --git a/shiny/express/__init__.py b/shiny/express/__init__.py index 8add0557a..e9bcfaa1e 100644 --- a/shiny/express/__init__.py +++ b/shiny/express/__init__.py @@ -8,8 +8,9 @@ from . import ui from ._is_express import is_express_app from ._output import ( # noqa: F401 - suspend_display, + hide, output_args, # pyright: ignore[reportUnusedImport] + suspend_display, # pyright: ignore[reportUnusedImport] - Deprecated ) from ._run import wrap_express_app from .display_decorator import display_body @@ -22,6 +23,7 @@ "session", "is_express_app", "suspend_display", + "hide", "wrap_express_app", "ui", "display_body", diff --git a/shiny/express/_output.py b/shiny/express/_output.py index 7fce0d9f0..bf8c00c4d 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -6,10 +6,14 @@ from typing import Callable, Generator, TypeVar, overload from .. import ui +from .._deprecated import warn_deprecated from .._typing_extensions import ParamSpec from ..render.renderer import RendererBase, RendererBaseT -__all__ = ("suspend_display",) +__all__ = ( + "hide", + "suspend_display", +) P = ParamSpec("P") R = TypeVar("R") @@ -49,29 +53,29 @@ def wrapper(renderer: RendererBaseT) -> RendererBaseT: @overload -def suspend_display(fn: CallableT) -> CallableT: +def hide(fn: CallableT) -> CallableT: ... @overload -def suspend_display(fn: RendererBaseT) -> RendererBaseT: +def hide(fn: RendererBaseT) -> RendererBaseT: ... @overload -def suspend_display() -> AbstractContextManager[None]: +def hide() -> AbstractContextManager[None]: ... -def suspend_display( +def hide( fn: Callable[P, R] | RendererBaseT | None = None ) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: - """Suppresses the display of UI elements in various ways. + """Prevent 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 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 @@ -85,19 +89,19 @@ def suspend_display( Parameters ---------- fn - The function to decorate. If `None`, returns a context manager that suppresses - the display of UI elements within the context block. + 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 suppresses the display of UI + 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`. """ if fn is None: - return suspend_display_ctxmgr() + return hide_ctxmgr() # Special case for RendererBase; when we decorate those, we just mean "don't # display yourself" @@ -106,11 +110,21 @@ def suspend_display( fn.auto_output_ui = null_ui return fn - return suspend_display_ctxmgr()(fn) + return hide_ctxmgr()(fn) + + +def suspend_display( + fn: Callable[P, R] | RendererBaseT | None = None +) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: + warn_deprecated( + "`suspend_display` is deprecated. Please use `hide` instead. " + "It has a new name, but the exact same functionality." + ) + return hide(fn) # type: ignore @contextlib.contextmanager -def suspend_display_ctxmgr() -> Generator[None, None, None]: +def hide_ctxmgr() -> Generator[None, None, None]: oldhook = sys.displayhook sys.displayhook = null_displayhook try: diff --git a/tests/playwright/shiny/shiny-express/suspend_display/app.py b/tests/playwright/shiny/shiny-express/hide/app.py similarity index 86% rename from tests/playwright/shiny/shiny-express/suspend_display/app.py rename to tests/playwright/shiny/shiny-express/hide/app.py index 4f4b510ef..3c02e8f90 100644 --- a/tests/playwright/shiny/shiny-express/suspend_display/app.py +++ b/tests/playwright/shiny/shiny-express/hide/app.py @@ -1,10 +1,10 @@ from shiny import render, ui -from shiny.express import input, suspend_display +from shiny.express import hide, input with ui.card(id="card"): ui.input_slider("s1", "A", 1, 100, 20) - @suspend_display + @hide @render.code def hidden(): return input.s1() diff --git a/tests/playwright/shiny/shiny-express/suspend_display/test_suspend_display.py b/tests/playwright/shiny/shiny-express/hide/test_hide.py similarity index 100% rename from tests/playwright/shiny/shiny-express/suspend_display/test_suspend_display.py rename to tests/playwright/shiny/shiny-express/hide/test_hide.py diff --git a/tests/pytest/test_express_ui.py b/tests/pytest/test_express_ui.py index 0c1afcb43..c8e6b3d73 100644 --- a/tests/pytest/test_express_ui.py +++ b/tests/pytest/test_express_ui.py @@ -6,7 +6,7 @@ import pytest from shiny import render, ui -from shiny.express import output_args, suspend_display +from shiny.express import hide, output_args from shiny.express._run import run_express @@ -53,7 +53,7 @@ def text1(): == ui.output_text("text1").get_html_string() ) - @suspend_display + @hide @render.text def text2(): return "text" @@ -79,7 +79,7 @@ def code2(): code2.tagify() -def test_suspend_display(): +def test_hide(): old_displayhook = sys.displayhook try: called = False @@ -90,11 +90,11 @@ def display_hook_spy(_: object) -> Any: sys.displayhook = display_hook_spy - with suspend_display(): + with hide(): sys.displayhook("foo") - suspend_display(lambda: sys.displayhook("bar"))() + hide(lambda: sys.displayhook("bar"))() - @suspend_display + @hide def whatever(x: Any): sys.displayhook(x) From 7d7db43226d188a5e1ac5d184d06d2a4212c5744 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 00:36:24 -0600 Subject: [PATCH 02/11] Rename express.hide() to express.ui.hold() --- shiny/express/__init__.py | 2 - shiny/express/_output.py | 98 ++----------------- shiny/express/ui/__init__.py | 11 ++- .../shiny/shiny-express/hide/app.py | 4 +- tests/pytest/test_express_ui.py | 14 ++- 5 files changed, 24 insertions(+), 105 deletions(-) diff --git a/shiny/express/__init__.py b/shiny/express/__init__.py index e9bcfaa1e..2f1795c50 100644 --- a/shiny/express/__init__.py +++ b/shiny/express/__init__.py @@ -8,7 +8,6 @@ from . import ui from ._is_express import is_express_app from ._output import ( # noqa: F401 - hide, output_args, # pyright: ignore[reportUnusedImport] suspend_display, # pyright: ignore[reportUnusedImport] - Deprecated ) @@ -23,7 +22,6 @@ "session", "is_express_app", "suspend_display", - "hide", "wrap_express_app", "ui", "display_body", diff --git a/shiny/express/_output.py b/shiny/express/_output.py index bf8c00c4d..498138574 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -1,19 +1,14 @@ from __future__ import annotations -import contextlib -import sys from contextlib import AbstractContextManager -from typing import Callable, Generator, TypeVar, overload +from typing import Callable, TypeVar -from .. import ui from .._deprecated import warn_deprecated from .._typing_extensions import ParamSpec -from ..render.renderer import RendererBase, RendererBaseT +from ..render.renderer import RendererBaseT +from .ui import hold -__all__ = ( - "hide", - "suspend_display", -) +__all__ = ("suspend_display",) P = ParamSpec("P") R = TypeVar("R") @@ -52,92 +47,11 @@ def wrapper(renderer: RendererBaseT) -> RendererBaseT: return wrapper -@overload -def hide(fn: CallableT) -> CallableT: - ... - - -@overload -def hide(fn: RendererBaseT) -> RendererBaseT: - ... - - -@overload -def hide() -> AbstractContextManager[None]: - ... - - -def hide( - fn: Callable[P, R] | RendererBaseT | None = None -) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: - """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`. - """ - - if fn is None: - return hide_ctxmgr() - - # Special case for RendererBase; when we decorate those, we just mean "don't - # display yourself" - if isinstance(fn, RendererBase): - # By setting the class value, the `self` arg will be auto added. - fn.auto_output_ui = null_ui - return fn - - return hide_ctxmgr()(fn) - - def suspend_display( fn: Callable[P, R] | RendererBaseT | None = None ) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: warn_deprecated( - "`suspend_display` is deprecated. Please use `hide` instead. " + "`express.suspend_display` is deprecated. Please use `express.ui.hide` instead. " "It has a new name, but the exact same functionality." ) - return hide(fn) # type: ignore - - -@contextlib.contextmanager -def hide_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 + return hold(fn) # type: ignore diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index 533371a84..3a78031af 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -132,6 +132,10 @@ page_opts, ) +from ._hold import ( + hold, +) + __all__ = ( # Imports from htmltools "TagList", @@ -257,6 +261,8 @@ "tooltip", # Imports from ._page "page_opts", + # Imports from ._hold + "hold", ) @@ -296,5 +302,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/tests/playwright/shiny/shiny-express/hide/app.py b/tests/playwright/shiny/shiny-express/hide/app.py index 3c02e8f90..93f5b666a 100644 --- a/tests/playwright/shiny/shiny-express/hide/app.py +++ b/tests/playwright/shiny/shiny-express/hide/app.py @@ -1,10 +1,10 @@ from shiny import render, ui -from shiny.express import hide, input +from shiny.express import hold, input with ui.card(id="card"): ui.input_slider("s1", "A", 1, 100, 20) - @hide + @hold @render.code def hidden(): return input.s1() diff --git a/tests/pytest/test_express_ui.py b/tests/pytest/test_express_ui.py index c8e6b3d73..fd6844c75 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 hide, output_args +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,7 +51,7 @@ def text1(): == ui.output_text("text1").get_html_string() ) - @hide + @xui.hold @render.text def text2(): return "text" @@ -90,11 +88,11 @@ def display_hook_spy(_: object) -> Any: sys.displayhook = display_hook_spy - with hide(): + with xui.hold(): sys.displayhook("foo") - hide(lambda: sys.displayhook("bar"))() + xui.hold(lambda: sys.displayhook("bar"))() - @hide + @xui.hold def whatever(x: Any): sys.displayhook(x) From 8fd05bb80f5308f63c544c20351cc2d1e3bd77b5 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 00:40:06 -0600 Subject: [PATCH 03/11] More renaming of 'hide' to 'hold' --- tests/playwright/shiny/shiny-express/{hide => hold}/app.py | 0 .../shiny-express/{hide/test_hide.py => hold/test_hold.py} | 0 tests/pytest/test_express_ui.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename tests/playwright/shiny/shiny-express/{hide => hold}/app.py (100%) rename tests/playwright/shiny/shiny-express/{hide/test_hide.py => hold/test_hold.py} (100%) diff --git a/tests/playwright/shiny/shiny-express/hide/app.py b/tests/playwright/shiny/shiny-express/hold/app.py similarity index 100% rename from tests/playwright/shiny/shiny-express/hide/app.py rename to tests/playwright/shiny/shiny-express/hold/app.py diff --git a/tests/playwright/shiny/shiny-express/hide/test_hide.py b/tests/playwright/shiny/shiny-express/hold/test_hold.py similarity index 100% rename from tests/playwright/shiny/shiny-express/hide/test_hide.py rename to tests/playwright/shiny/shiny-express/hold/test_hold.py diff --git a/tests/pytest/test_express_ui.py b/tests/pytest/test_express_ui.py index fd6844c75..9975b032c 100644 --- a/tests/pytest/test_express_ui.py +++ b/tests/pytest/test_express_ui.py @@ -77,7 +77,7 @@ def code2(): code2.tagify() -def test_hide(): +def test_hold(): old_displayhook = sys.displayhook try: called = False From b460dc155afac317d652a8cc640785d1d111df3c Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 00:40:53 -0600 Subject: [PATCH 04/11] Fix name --- shiny/express/_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/express/_output.py b/shiny/express/_output.py index 498138574..859285c5f 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -51,7 +51,7 @@ def suspend_display( fn: Callable[P, R] | RendererBaseT | None = None ) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: warn_deprecated( - "`express.suspend_display` is deprecated. Please use `express.ui.hide` instead. " + "`express.suspend_display()` is deprecated. Please use `express.ui.hold()` instead. " "It has a new name, but the exact same functionality." ) return hold(fn) # type: ignore From 729b2a07ac5eb743c6c0b6fbde185909d811bd0c Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 02:31:34 -0600 Subject: [PATCH 05/11] Actually add _hold.py --- shiny/express/_output.py | 98 ++++++++++++++++++++++++++++++++++++--- shiny/express/ui/_hold.py | 97 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 shiny/express/ui/_hold.py diff --git a/shiny/express/_output.py b/shiny/express/_output.py index 859285c5f..bf8c00c4d 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -1,14 +1,19 @@ from __future__ import annotations +import contextlib +import sys from contextlib import AbstractContextManager -from typing import Callable, TypeVar +from typing import Callable, Generator, TypeVar, overload +from .. import ui from .._deprecated import warn_deprecated from .._typing_extensions import ParamSpec -from ..render.renderer import RendererBaseT -from .ui import hold +from ..render.renderer import RendererBase, RendererBaseT -__all__ = ("suspend_display",) +__all__ = ( + "hide", + "suspend_display", +) P = ParamSpec("P") R = TypeVar("R") @@ -47,11 +52,92 @@ def wrapper(renderer: RendererBaseT) -> RendererBaseT: return wrapper +@overload +def hide(fn: CallableT) -> CallableT: + ... + + +@overload +def hide(fn: RendererBaseT) -> RendererBaseT: + ... + + +@overload +def hide() -> AbstractContextManager[None]: + ... + + +def hide( + fn: Callable[P, R] | RendererBaseT | None = None +) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: + """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`. + """ + + if fn is None: + return hide_ctxmgr() + + # Special case for RendererBase; when we decorate those, we just mean "don't + # display yourself" + if isinstance(fn, RendererBase): + # By setting the class value, the `self` arg will be auto added. + fn.auto_output_ui = null_ui + return fn + + return hide_ctxmgr()(fn) + + def suspend_display( fn: Callable[P, R] | RendererBaseT | None = None ) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: warn_deprecated( - "`express.suspend_display()` is deprecated. Please use `express.ui.hold()` instead. " + "`suspend_display` is deprecated. Please use `hide` instead. " "It has a new name, but the exact same functionality." ) - return hold(fn) # type: ignore + return hide(fn) # type: ignore + + +@contextlib.contextmanager +def hide_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 diff --git a/shiny/express/ui/_hold.py b/shiny/express/ui/_hold.py new file mode 100644 index 000000000..7b70a4cde --- /dev/null +++ b/shiny/express/ui/_hold.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import contextlib +import sys +from contextlib import AbstractContextManager +from typing import Callable, Generator, TypeVar, overload + +from ... import ui +from ..._typing_extensions import ParamSpec +from ...render.renderer import RendererBase, RendererBaseT + +__all__ = ("hold",) + +P = ParamSpec("P") +R = TypeVar("R") +CallableT = TypeVar("CallableT", bound=Callable[..., object]) + + +@overload +def hold(fn: CallableT) -> CallableT: + ... + + +@overload +def hold(fn: RendererBaseT) -> RendererBaseT: + ... + + +@overload +def hold() -> AbstractContextManager[None]: + ... + + +def hold( + fn: Callable[P, R] | RendererBaseT | None = None +) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: + """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`. + """ + + if fn is None: + return hide_ctxmgr() + + # Special case for RendererBase; when we decorate those, we just mean "don't + # display yourself" + if isinstance(fn, RendererBase): + # By setting the class value, the `self` arg will be auto added. + fn.auto_output_ui = null_ui + return fn + + return hide_ctxmgr()(fn) + + +@contextlib.contextmanager +def hide_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 From 957bd553b7372e33fd20e8375d973ae281f76fba Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 02:37:15 -0600 Subject: [PATCH 06/11] Start conversion to HideContextManager --- shiny/express/ui/_hold.py | 45 ++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/shiny/express/ui/_hold.py b/shiny/express/ui/_hold.py index 7b70a4cde..21ba183a7 100644 --- a/shiny/express/ui/_hold.py +++ b/shiny/express/ui/_hold.py @@ -1,9 +1,9 @@ from __future__ import annotations -import contextlib import sys -from contextlib import AbstractContextManager -from typing import Callable, Generator, TypeVar, overload +from contextlib import ContextDecorator +from types import TracebackType +from typing import Callable, Optional, Type, TypeVar, overload from ... import ui from ..._typing_extensions import ParamSpec @@ -27,13 +27,13 @@ def hold(fn: RendererBaseT) -> RendererBaseT: @overload -def hold() -> AbstractContextManager[None]: +def hold() -> HideContextManager: ... def hold( fn: Callable[P, R] | RendererBaseT | None = None -) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: +) -> Callable[P, R] | RendererBaseT | HideContextManager: """Prevent the display of UI elements in various ways. If used as a context manager (`with hide():`), it prevents the display of all UI @@ -65,7 +65,7 @@ def hold( """ if fn is None: - return hide_ctxmgr() + return HideContextManager() # Special case for RendererBase; when we decorate those, we just mean "don't # display yourself" @@ -74,17 +74,32 @@ def hold( fn.auto_output_ui = null_ui return fn - return hide_ctxmgr()(fn) + return HideContextManager()(fn) -@contextlib.contextmanager -def hide_ctxmgr() -> Generator[None, None, None]: - oldhook = sys.displayhook - sys.displayhook = null_displayhook - try: - yield - finally: - sys.displayhook = oldhook +class HideContextManager(ContextDecorator): + def __init__(self): + self.content = ui.TagList() + + def __enter__(self) -> ui.TagList: + from htmltools import wrap_displayhook_handler + + 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 + if exc_type: + print(f"An exception occurred: {exc_value}") + return False def null_ui( From 0de10b1d9594702bfe88d6c033af68dd130e6472 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 14:50:04 -0600 Subject: [PATCH 07/11] Make hold() work only as a context manager, and fix class name --- shiny/express/ui/_hold.py | 48 ++++----------------------------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/shiny/express/ui/_hold.py b/shiny/express/ui/_hold.py index 21ba183a7..f7260b58c 100644 --- a/shiny/express/ui/_hold.py +++ b/shiny/express/ui/_hold.py @@ -5,9 +5,10 @@ from types import TracebackType from typing import Callable, Optional, Type, TypeVar, overload +from htmltools import wrap_displayhook_handler + from ... import ui from ..._typing_extensions import ParamSpec -from ...render.renderer import RendererBase, RendererBaseT __all__ = ("hold",) @@ -16,24 +17,7 @@ CallableT = TypeVar("CallableT", bound=Callable[..., object]) -@overload -def hold(fn: CallableT) -> CallableT: - ... - - -@overload -def hold(fn: RendererBaseT) -> RendererBaseT: - ... - - -@overload -def hold() -> HideContextManager: - ... - - -def hold( - fn: Callable[P, R] | RendererBaseT | None = None -) -> Callable[P, R] | RendererBaseT | HideContextManager: +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 @@ -64,26 +48,14 @@ def hold( `fn`. """ - if fn is None: - return HideContextManager() + return HoldContextManager() - # Special case for RendererBase; when we decorate those, we just mean "don't - # display yourself" - if isinstance(fn, RendererBase): - # By setting the class value, the `self` arg will be auto added. - fn.auto_output_ui = null_ui - return fn - return HideContextManager()(fn) - - -class HideContextManager(ContextDecorator): +class HoldContextManager: def __init__(self): self.content = ui.TagList() def __enter__(self) -> ui.TagList: - from htmltools import wrap_displayhook_handler - self.prev_displayhook = sys.displayhook sys.displayhook = wrap_displayhook_handler( self.content.append # pyright: ignore[reportGeneralTypeIssues] @@ -100,13 +72,3 @@ def __exit__( if exc_type: print(f"An exception occurred: {exc_value}") return False - - -def null_ui( - **kwargs: object, -) -> ui.TagList: - return ui.TagList() - - -def null_displayhook(x: object) -> None: - pass From 8fbb26216c3928a10262329b81fd6de7a425e25f Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 15:02:56 -0600 Subject: [PATCH 08/11] Rename render.display to render.express --- shiny/api-examples/render_display/app.py | 15 +++++++-------- shiny/render/__init__.py | 6 +++--- shiny/render/{_display.py => _express.py} | 12 ++++++------ .../deploys/apps/shiny-express-folium/app.py | 2 +- .../playwright/shiny/shiny-express/folium/app.py | 2 +- 5 files changed, 18 insertions(+), 19 deletions(-) rename shiny/render/{_display.py => _express.py} (90%) diff --git a/shiny/api-examples/render_display/app.py b/shiny/api-examples/render_display/app.py index 7bd372616..9ba6e813f 100644 --- a/shiny/api-examples/render_display/app.py +++ b/shiny/api-examples/render_display/app.py @@ -1,19 +1,18 @@ import datetime -from shiny import render, ui -from shiny.express import input, layout +from shiny.express import input, render, ui -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("val", "slider", 0, 100, 50) - "Text outside of render display call" + "Text outside of render express call" ui.tags.br() f"Rendered time: {str(datetime.datetime.now())}" - @render.display - def render_display(): - "Text inside of render display call" + @render.express + def render_express(): + "Text inside of render express call" ui.tags.br() "Dynamic slider value: " input.val() ui.tags.br() - f"Display's rendered time: {str(datetime.datetime.now())}" + f"Rendered time: {str(datetime.datetime.now())}" 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 90% rename from shiny/render/_display.py rename to shiny/render/_express.py index f71eb1109..c6cf05c5b 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,15 +41,15 @@ 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 ( + from ..express.display_decorator._display_body import ( display_body_unwrap_inplace, ) @@ -84,7 +84,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 +93,7 @@ async def render(self) -> JsonifiableDict | None: ret = sync_value_fn() if ret is not None: raise RuntimeError( - "@render.display functions should not return values. (`None` is allowed)." + "@render.express functions should not return values. (`None` is allowed)." ) 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 15dac58e2..ac06c233b 100644 --- a/tests/playwright/deploys/apps/shiny-express-folium/app.py +++ b/tests/playwright/deploys/apps/shiny-express-folium/app.py @@ -19,7 +19,7 @@ "location", "Location", ["San Francisco", "New York", "Los Angeles"] ) - @render.display + @render.express def folium_map(): "Map inside of render display call" folium.Map( diff --git a/tests/playwright/shiny/shiny-express/folium/app.py b/tests/playwright/shiny/shiny-express/folium/app.py index a6d5e8ad1..b59572a73 100644 --- a/tests/playwright/shiny/shiny-express/folium/app.py +++ b/tests/playwright/shiny/shiny-express/folium/app.py @@ -19,7 +19,7 @@ "location", "Location", ["San Francisco", "New York", "Los Angeles"] ) - @render.display + @render.express def folium_map(): "Map inside of render display call" folium.Map( # pyright: ignore[reportUnknownMemberType,reportGeneralTypeIssues] From 735703f5faa374073041b5a6dc7d691f804afac5 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 15:07:03 -0600 Subject: [PATCH 09/11] Remove unused imports --- shiny/express/ui/_hold.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shiny/express/ui/_hold.py b/shiny/express/ui/_hold.py index f7260b58c..96427cf78 100644 --- a/shiny/express/ui/_hold.py +++ b/shiny/express/ui/_hold.py @@ -1,9 +1,8 @@ from __future__ import annotations import sys -from contextlib import ContextDecorator from types import TracebackType -from typing import Callable, Optional, Type, TypeVar, overload +from typing import Callable, Optional, Type, TypeVar from htmltools import wrap_displayhook_handler From bdbc83daeffb83924a48af861c3d529aded28dee Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 15:30:17 -0600 Subject: [PATCH 10/11] Rename display_body to expressify --- shiny/express/__init__.py | 5 +- shiny/express/_run.py | 8 +-- shiny/express/display_decorator/__init__.py | 3 -- .../express/expressify_decorator/__init__.py | 3 ++ .../_expressify.py} | 51 +++++++++++++------ .../_func_displayhook.py | 2 +- .../_helpers.py | 0 .../_node_transformers.py | 6 +-- shiny/render/_express.py | 6 +-- tests/pytest/test_display_decorator.py | 32 ++++++------ 10 files changed, 67 insertions(+), 49 deletions(-) delete mode 100644 shiny/express/display_decorator/__init__.py create mode 100644 shiny/express/expressify_decorator/__init__.py rename shiny/express/{display_decorator/_display_body.py => expressify_decorator/_expressify.py} (84%) rename shiny/express/{display_decorator => expressify_decorator}/_func_displayhook.py (86%) rename shiny/express/{display_decorator => expressify_decorator}/_helpers.py (100%) rename shiny/express/{display_decorator => expressify_decorator}/_node_transformers.py (94%) diff --git a/shiny/express/__init__.py b/shiny/express/__init__.py index 2f1795c50..4896159a2 100644 --- a/shiny/express/__init__.py +++ b/shiny/express/__init__.py @@ -12,7 +12,7 @@ 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/_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/render/_express.py b/shiny/render/_express.py index c6cf05c5b..fe4810087 100644 --- a/shiny/render/_express.py +++ b/shiny/render/_express.py @@ -49,11 +49,9 @@ def __call__(self, fn: ValueFn[None]) -> Self: "@render.express does not support async functions. Use @render.ui instead." ) - from ..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) 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 From e90cfdf30c164266871a261ee5db271b62cbf828 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Jan 2024 15:49:33 -0600 Subject: [PATCH 11/11] Add example apps for expressify, render.express, and hold --- examples/express/expressify_app.py | 28 ++++++++++++++++++++++++++ examples/express/hold_app.py | 28 ++++++++++++++++++++++++++ examples/express/render_express_app.py | 17 ++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 examples/express/expressify_app.py create mode 100644 examples/express/hold_app.py create mode 100644 examples/express/render_express_app.py diff --git a/examples/express/expressify_app.py b/examples/express/expressify_app.py new file mode 100644 index 000000000..76865cce0 --- /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}" + ui.br() + + +expressified1("Hello") + +expressified1("world") + + +ui.br() + + +# @expressify() also works with parens +@expressify() +def expressified2(s: str): + f"Expressified function 2: {s}" + 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()