Skip to content

Commit da69ea0

Browse files
schloerkewch
andauthored
Use a renderer generator to create sync/async render methods with full typing (#621)
Co-authored-by: Winston Chang <[email protected]>
1 parent f469421 commit da69ea0

22 files changed

+1743
-617
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88

9+
## [UNRELEASED]
10+
11+
### New features
12+
13+
* Added `shiny.render.renderer_components` decorator to help create new output renderers. (#621)
14+
15+
### Bug fixes
16+
17+
### Other changes
18+
19+
920
## [0.5.1] - 2023-08-08
1021

1122
### Bug fixes

docs/_quartodoc.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,24 @@ quartodoc:
125125
- render.data_frame
126126
- render.DataGrid
127127
- render.DataTable
128+
- kind: page
129+
path: OutputRender
130+
flatten: true
131+
summary:
132+
name: "Create rendering outputs"
133+
desc: ""
134+
contents:
135+
- render.transformer.output_transformer
136+
- render.transformer.OutputTransformer
137+
- render.transformer.TransformerMetadata
138+
- render.transformer.TransformerParams
139+
- render.transformer.OutputRenderer
140+
- render.transformer.OutputRendererSync
141+
- render.transformer.OutputRendererAsync
142+
- render.transformer.is_async_callable
143+
- render.transformer.resolve_value_fn
144+
- render.transformer.ValueFn
145+
- render.transformer.TransformFn
128146
- title: Reactive programming
129147
desc: ""
130148
contents:
@@ -178,7 +196,7 @@ quartodoc:
178196
desc: ""
179197
contents:
180198
- kind: page
181-
path: MiscTypes.html
199+
path: MiscTypes
182200
flatten: true
183201
summary:
184202
name: "Miscellaneous types"
@@ -192,7 +210,7 @@ quartodoc:
192210
- ui._input_slider.SliderValueArg
193211
- ui._input_slider.SliderStepArg
194212
- kind: page
195-
path: TagTypes.html
213+
path: TagTypes
196214
summary:
197215
name: "Tag types"
198216
desc: ""
@@ -205,7 +223,7 @@ quartodoc:
205223
- htmltools.TagChild
206224
- htmltools.TagList
207225
- kind: page
208-
path: ExceptionTypes.html
226+
path: ExceptionTypes
209227
summary:
210228
name: "Exception types"
211229
desc: ""

e2e/inputs/test_input_checkbox.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def test_input_checkbox_kitchen(page: Page, app: ShinyAppProc) -> None:
1616
somevalue.expect_checked(False)
1717
somevalue.expect_width(None)
1818

19-
# TODO-karan test output value
19+
# TODO-karan: test output value
2020

2121
somevalue.set(True)
2222

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

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

e2e/server/output_transformer/app.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from __future__ import annotations
2+
3+
from typing import Optional, overload
4+
5+
from shiny import App, Inputs, Outputs, Session, ui
6+
from shiny.render.transformer import (
7+
TransformerMetadata,
8+
ValueFn,
9+
is_async_callable,
10+
output_transformer,
11+
resolve_value_fn,
12+
)
13+
14+
15+
@output_transformer
16+
async def TestTextTransformer(
17+
_meta: TransformerMetadata,
18+
_fn: ValueFn[str | None],
19+
*,
20+
extra_txt: Optional[str] = None,
21+
) -> str | None:
22+
value = await resolve_value_fn(_fn)
23+
value = str(value)
24+
value += "; "
25+
value += "async" if is_async_callable(_fn) else "sync"
26+
if extra_txt:
27+
value = value + "; " + str(extra_txt)
28+
return value
29+
30+
31+
@overload
32+
def render_test_text(
33+
*, extra_txt: Optional[str] = None
34+
) -> TestTextTransformer.OutputRendererDecorator:
35+
...
36+
37+
38+
@overload
39+
def render_test_text(
40+
_fn: TestTextTransformer.ValueFn,
41+
) -> TestTextTransformer.OutputRenderer:
42+
...
43+
44+
45+
def render_test_text(
46+
_fn: TestTextTransformer.ValueFn | None = None,
47+
*,
48+
extra_txt: Optional[str] = None,
49+
) -> TestTextTransformer.OutputRenderer | TestTextTransformer.OutputRendererDecorator:
50+
return TestTextTransformer(
51+
_fn,
52+
TestTextTransformer.params(extra_txt=extra_txt),
53+
)
54+
55+
56+
app_ui = ui.page_fluid(
57+
ui.code("t1:"),
58+
ui.output_text_verbatim("t1"),
59+
ui.code("t2:"),
60+
ui.output_text_verbatim("t2"),
61+
ui.code("t3:"),
62+
ui.output_text_verbatim("t3"),
63+
ui.code("t4:"),
64+
ui.output_text_verbatim("t4"),
65+
ui.code("t5:"),
66+
ui.output_text_verbatim("t5"),
67+
ui.code("t6:"),
68+
ui.output_text_verbatim("t6"),
69+
)
70+
71+
72+
def server(input: Inputs, output: Outputs, session: Session):
73+
@output
74+
@render_test_text
75+
def t1():
76+
return "t1; no call"
77+
# return "hello"
78+
79+
@output
80+
@render_test_text
81+
async def t2():
82+
return "t2; no call"
83+
84+
@output
85+
@render_test_text()
86+
def t3():
87+
return "t3; call"
88+
89+
@output
90+
@render_test_text()
91+
async def t4():
92+
return "t4; call"
93+
94+
@output
95+
@render_test_text(extra_txt="w/ extra_txt")
96+
def t5():
97+
return "t5; call"
98+
99+
@output
100+
@render_test_text(extra_txt="w/ extra_txt")
101+
async def t6():
102+
return "t6; call"
103+
104+
105+
app = App(app_ui, server)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from conftest import ShinyAppProc
2+
from controls import OutputTextVerbatim
3+
from playwright.sync_api import Page
4+
5+
6+
def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None:
7+
page.goto(local_app.url)
8+
9+
OutputTextVerbatim(page, "t1").expect_value("t1; no call; sync")
10+
OutputTextVerbatim(page, "t2").expect_value("t2; no call; async")
11+
OutputTextVerbatim(page, "t3").expect_value("t3; call; sync")
12+
OutputTextVerbatim(page, "t4").expect_value("t4; call; async")
13+
OutputTextVerbatim(page, "t5").expect_value("t5; call; sync; w/ extra_txt")
14+
OutputTextVerbatim(page, "t6").expect_value("t6; call; async; w/ extra_txt")

examples/event/app.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def btn() -> int:
4141
def _():
4242
print("@calc() event: ", str(btn()))
4343

44-
@output
4544
@render.ui
4645
@reactive.event(input.btn)
4746
def btn_value():

shiny/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""A package for building reactive web applications."""
22

3-
__version__ = "0.5.1"
3+
__version__ = "0.5.1.9000"
44

55
from ._shinyenv import is_pyodide as _is_pyodide
66

shiny/_deprecated.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,16 @@ def render_ui():
3939
return render.ui()
4040

4141

42-
def render_plot(*args: Any, **kwargs: Any):
42+
def render_plot(*args: Any, **kwargs: Any): # type: ignore
4343
"""Deprecated. Please use render.plot() instead of render_plot()."""
4444
warn_deprecated("render_plot() is deprecated. Use render.plot() instead.")
45-
return render.plot(*args, **kwargs)
45+
return render.plot(*args, **kwargs) # type: ignore
4646

4747

48-
def render_image(*args: Any, **kwargs: Any):
48+
def render_image(*args: Any, **kwargs: Any): # type: ignore
4949
"""Deprecated. Please use render.image() instead of render_image()."""
5050
warn_deprecated("render_image() is deprecated. Use render.image() instead.")
51-
return render.image(*args, **kwargs)
51+
return render.image(*args, **kwargs) # type: ignore
5252

5353

5454
def event(*args: Any, **kwargs: Any):

shiny/_utils.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,13 +245,23 @@ async def fn_async(*args: P.args, **kwargs: P.kwargs) -> T:
245245
return fn_async
246246

247247

248+
# This function should generally be used in this code base instead of
249+
# `iscoroutinefunction()`.
248250
def is_async_callable(
249251
obj: Callable[P, T] | Callable[P, Awaitable[T]]
250252
) -> TypeGuard[Callable[P, Awaitable[T]]]:
251253
"""
252-
Returns True if `obj` is an `async def` function, or if it's an object with a
253-
`__call__` method which is an `async def` function. This function should generally
254-
be used in this code base instead of iscoroutinefunction().
254+
Determine if an object is an async function.
255+
256+
This is a more general version of `inspect.iscoroutinefunction()`, which only works
257+
on functions. This function works on any object that has a `__call__` method, such
258+
as a class instance.
259+
260+
Returns
261+
-------
262+
:
263+
Returns True if `obj` is an `async def` function, or if it's an object with a
264+
`__call__` method which is an `async def` function.
255265
"""
256266
if inspect.iscoroutinefunction(obj):
257267
return True
@@ -262,6 +272,12 @@ def is_async_callable(
262272
return False
263273

264274

275+
# def not_is_async_callable(
276+
# obj: Callable[P, T] | Callable[P, Awaitable[T]]
277+
# ) -> TypeGuard[Callable[P, T]]:
278+
# return not is_async_callable(obj)
279+
280+
265281
# See https://stackoverflow.com/a/59780868/412655 for an excellent explanation
266282
# of how this stuff works.
267283
# For a more in-depth explanation, see

0 commit comments

Comments
 (0)