Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions examples/express/expressify_app.py
Original file line number Diff line number Diff line change
@@ -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")
28 changes: 28 additions & 0 deletions examples/express/hold_app.py
Original file line number Diff line number Diff line change
@@ -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)
17 changes: 17 additions & 0 deletions examples/express/render_express_app.py
Original file line number Diff line number Diff line change
@@ -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()
19 changes: 0 additions & 19 deletions shiny/api-examples/render_display/app.py

This file was deleted.

18 changes: 18 additions & 0 deletions shiny/api-examples/render_express/app.py
Original file line number Diff line number Diff line change
@@ -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())}"
7 changes: 3 additions & 4 deletions shiny/express/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = (
Expand 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
Expand Down
97 changes: 14 additions & 83 deletions shiny/express/_output.py
Original file line number Diff line number Diff line change
@@ -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])


Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions shiny/express/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",)
Expand Down Expand Up @@ -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(),
}

Expand Down
3 changes: 0 additions & 3 deletions shiny/express/display_decorator/__init__.py

This file was deleted.

3 changes: 3 additions & 0 deletions shiny/express/expressify_decorator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._expressify import expressify

__all__ = ("expressify",)
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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.
"""
Expand All @@ -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:
Expand All @@ -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__]
Expand All @@ -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__,
Expand All @@ -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


Expand Down Expand Up @@ -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
Loading