Skip to content

Commit

Permalink
Use a renderer generator to create sync/async render methods with ful…
Browse files Browse the repository at this point in the history
…l typing (#621)

Co-authored-by: Winston Chang <[email protected]>
  • Loading branch information
schloerke and wch authored Aug 10, 2023
1 parent f469421 commit da69ea0
Show file tree
Hide file tree
Showing 22 changed files with 1,743 additions and 617 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [UNRELEASED]

### New features

* Added `shiny.render.renderer_components` decorator to help create new output renderers. (#621)

### Bug fixes

### Other changes


## [0.5.1] - 2023-08-08

### Bug fixes
Expand Down
24 changes: 21 additions & 3 deletions docs/_quartodoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ quartodoc:
- render.data_frame
- render.DataGrid
- render.DataTable
- kind: page
path: OutputRender
flatten: true
summary:
name: "Create rendering outputs"
desc: ""
contents:
- render.transformer.output_transformer
- render.transformer.OutputTransformer
- render.transformer.TransformerMetadata
- render.transformer.TransformerParams
- render.transformer.OutputRenderer
- render.transformer.OutputRendererSync
- render.transformer.OutputRendererAsync
- render.transformer.is_async_callable
- render.transformer.resolve_value_fn
- render.transformer.ValueFn
- render.transformer.TransformFn
- title: Reactive programming
desc: ""
contents:
Expand Down Expand Up @@ -178,7 +196,7 @@ quartodoc:
desc: ""
contents:
- kind: page
path: MiscTypes.html
path: MiscTypes
flatten: true
summary:
name: "Miscellaneous types"
Expand All @@ -192,7 +210,7 @@ quartodoc:
- ui._input_slider.SliderValueArg
- ui._input_slider.SliderStepArg
- kind: page
path: TagTypes.html
path: TagTypes
summary:
name: "Tag types"
desc: ""
Expand All @@ -205,7 +223,7 @@ quartodoc:
- htmltools.TagChild
- htmltools.TagList
- kind: page
path: ExceptionTypes.html
path: ExceptionTypes
summary:
name: "Exception types"
desc: ""
Expand Down
4 changes: 2 additions & 2 deletions e2e/inputs/test_input_checkbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_input_checkbox_kitchen(page: Page, app: ShinyAppProc) -> None:
somevalue.expect_checked(False)
somevalue.expect_width(None)

# TODO-karan test output value
# TODO-karan: test output value

somevalue.set(True)

Expand All @@ -28,4 +28,4 @@ def test_input_checkbox_kitchen(page: Page, app: ShinyAppProc) -> None:
somevalue.toggle()
somevalue.expect_checked(True)

# TODO-karan test output value
# TODO-karan: test output value
105 changes: 105 additions & 0 deletions e2e/server/output_transformer/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from __future__ import annotations

from typing import Optional, overload

from shiny import App, Inputs, Outputs, Session, ui
from shiny.render.transformer import (
TransformerMetadata,
ValueFn,
is_async_callable,
output_transformer,
resolve_value_fn,
)


@output_transformer
async def TestTextTransformer(
_meta: TransformerMetadata,
_fn: ValueFn[str | None],
*,
extra_txt: Optional[str] = None,
) -> str | None:
value = await resolve_value_fn(_fn)
value = str(value)
value += "; "
value += "async" if is_async_callable(_fn) else "sync"
if extra_txt:
value = value + "; " + str(extra_txt)
return value


@overload
def render_test_text(
*, extra_txt: Optional[str] = None
) -> TestTextTransformer.OutputRendererDecorator:
...


@overload
def render_test_text(
_fn: TestTextTransformer.ValueFn,
) -> TestTextTransformer.OutputRenderer:
...


def render_test_text(
_fn: TestTextTransformer.ValueFn | None = None,
*,
extra_txt: Optional[str] = None,
) -> TestTextTransformer.OutputRenderer | TestTextTransformer.OutputRendererDecorator:
return TestTextTransformer(
_fn,
TestTextTransformer.params(extra_txt=extra_txt),
)


app_ui = ui.page_fluid(
ui.code("t1:"),
ui.output_text_verbatim("t1"),
ui.code("t2:"),
ui.output_text_verbatim("t2"),
ui.code("t3:"),
ui.output_text_verbatim("t3"),
ui.code("t4:"),
ui.output_text_verbatim("t4"),
ui.code("t5:"),
ui.output_text_verbatim("t5"),
ui.code("t6:"),
ui.output_text_verbatim("t6"),
)


def server(input: Inputs, output: Outputs, session: Session):
@output
@render_test_text
def t1():
return "t1; no call"
# return "hello"

@output
@render_test_text
async def t2():
return "t2; no call"

@output
@render_test_text()
def t3():
return "t3; call"

@output
@render_test_text()
async def t4():
return "t4; call"

@output
@render_test_text(extra_txt="w/ extra_txt")
def t5():
return "t5; call"

@output
@render_test_text(extra_txt="w/ extra_txt")
async def t6():
return "t6; call"


app = App(app_ui, server)
14 changes: 14 additions & 0 deletions e2e/server/output_transformer/test_output_transformer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from conftest import ShinyAppProc
from controls import OutputTextVerbatim
from playwright.sync_api import Page


def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)

OutputTextVerbatim(page, "t1").expect_value("t1; no call; sync")
OutputTextVerbatim(page, "t2").expect_value("t2; no call; async")
OutputTextVerbatim(page, "t3").expect_value("t3; call; sync")
OutputTextVerbatim(page, "t4").expect_value("t4; call; async")
OutputTextVerbatim(page, "t5").expect_value("t5; call; sync; w/ extra_txt")
OutputTextVerbatim(page, "t6").expect_value("t6; call; async; w/ extra_txt")
1 change: 0 additions & 1 deletion examples/event/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def btn() -> int:
def _():
print("@calc() event: ", str(btn()))

@output
@render.ui
@reactive.event(input.btn)
def btn_value():
Expand Down
2 changes: 1 addition & 1 deletion shiny/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""A package for building reactive web applications."""

__version__ = "0.5.1"
__version__ = "0.5.1.9000"

from ._shinyenv import is_pyodide as _is_pyodide

Expand Down
8 changes: 4 additions & 4 deletions shiny/_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ def render_ui():
return render.ui()


def render_plot(*args: Any, **kwargs: Any):
def render_plot(*args: Any, **kwargs: Any): # type: ignore
"""Deprecated. Please use render.plot() instead of render_plot()."""
warn_deprecated("render_plot() is deprecated. Use render.plot() instead.")
return render.plot(*args, **kwargs)
return render.plot(*args, **kwargs) # type: ignore


def render_image(*args: Any, **kwargs: Any):
def render_image(*args: Any, **kwargs: Any): # type: ignore
"""Deprecated. Please use render.image() instead of render_image()."""
warn_deprecated("render_image() is deprecated. Use render.image() instead.")
return render.image(*args, **kwargs)
return render.image(*args, **kwargs) # type: ignore


def event(*args: Any, **kwargs: Any):
Expand Down
22 changes: 19 additions & 3 deletions shiny/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,13 +245,23 @@ async def fn_async(*args: P.args, **kwargs: P.kwargs) -> T:
return fn_async


# This function should generally be used in this code base instead of
# `iscoroutinefunction()`.
def is_async_callable(
obj: Callable[P, T] | Callable[P, Awaitable[T]]
) -> TypeGuard[Callable[P, Awaitable[T]]]:
"""
Returns True if `obj` is an `async def` function, or if it's an object with a
`__call__` method which is an `async def` function. This function should generally
be used in this code base instead of iscoroutinefunction().
Determine if an object is an async function.
This is a more general version of `inspect.iscoroutinefunction()`, which only works
on functions. This function works on any object that has a `__call__` method, such
as a class instance.
Returns
-------
:
Returns True if `obj` is an `async def` function, or if it's an object with a
`__call__` method which is an `async def` function.
"""
if inspect.iscoroutinefunction(obj):
return True
Expand All @@ -262,6 +272,12 @@ def is_async_callable(
return False


# def not_is_async_callable(
# obj: Callable[P, T] | Callable[P, Awaitable[T]]
# ) -> TypeGuard[Callable[P, T]]:
# return not is_async_callable(obj)


# See https://stackoverflow.com/a/59780868/412655 for an excellent explanation
# of how this stuff works.
# For a more in-depth explanation, see
Expand Down
Loading

0 comments on commit da69ea0

Please sign in to comment.