Skip to content

Commit 2436e0b

Browse files
authored
api!: Add Renderer class for creating output renderers (#964)
1 parent 5e84e3b commit 2436e0b

File tree

45 files changed

+1790
-1025
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1790
-1025
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
* prompt users to install `requirements.txt`
1717
* Fixed `js-react` template build error. (#965)
1818

19+
### Developer features
20+
21+
* Output renderers should now be created with the `shiny.render.renderer.Renderer` class. This class should contain either a `.transform(self, value)` method (common) or a `.render(self)` (rare). These two methods should return something can be converted to JSON. In addition, `.default_ui(self, id)` should be implemented by returning `htmltools.Tag`-like content for use within Shiny Express. To make your own output renderer, please inherit from the `Renderer[IT]` class where `IT` is the type (excluding `None`) required to be returned from the App author. (#964)
22+
23+
* `shiny.render.RenderFunction` and `shiny.render.RenderFunctionAsync` have been removed. They were deprecated in v0.6.0. Instead, please use `shiny.render.renderer.Renderer`. (#964)
24+
25+
* When transforming values within `shiny.render.transformer.output_transformer` transform function, `shiny.render.transformer.resolve_value_fn` is no longer needed as the value function given to the output transformer is now **always** an asynchronous function. `resolve_value_fn(fn)` method has been deprecated. Please change your code from `value = await resolve_value_fn(_fn)` to `value = await _fn()`. (#964)
26+
27+
* `shiny.render.OutputRendererSync` and `shiny.render.OutputRendererAsync` helper classes have been removed in favor of an updated `shiny.render.OutputRenderer` class. Now, the app's output value function will be transformed into an asynchronous function for simplified, consistent execution behavior. If redesigning your code, instead please create a new renderer that inherits from `shiny.render.renderer.Renderer`. (#964)
1928

2029

2130
## [0.6.1.1] - 2023-12-22

docs/_quartodoc.yml

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -175,17 +175,11 @@ quartodoc:
175175
name: "Create rendering outputs"
176176
desc: ""
177177
contents:
178-
- render.transformer.output_transformer
179-
- render.transformer.OutputTransformer
180-
- render.transformer.TransformerMetadata
181-
- render.transformer.TransformerParams
182-
- render.transformer.OutputRenderer
183-
- render.transformer.OutputRendererSync
184-
- render.transformer.OutputRendererAsync
185-
- render.transformer.is_async_callable
186-
- render.transformer.resolve_value_fn
187-
- render.transformer.ValueFn
188-
- render.transformer.TransformFn
178+
- render.renderer.Renderer
179+
- render.renderer.RendererBase
180+
- render.renderer.Jsonifiable
181+
- render.renderer.ValueFn
182+
- render.renderer.AsyncValueFn
189183
- title: Reactive programming
190184
desc: ""
191185
contents:
@@ -330,6 +324,7 @@ quartodoc:
330324
- ui.panel_main
331325
- ui.panel_sidebar
332326
- ui.nav
327+
- render.transformer.resolve_value_fn
333328
- title: Experimental
334329
desc: "These methods are under consideration and are considered unstable. However, if there is a method you are excited about, please let us know!"
335330
contents:

shiny/_typing_extensions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
# they should both come from the same typing module.
2424
# https://peps.python.org/pep-0655/#usage-in-python-3-11
2525
if sys.version_info >= (3, 11):
26-
from typing import NotRequired, TypedDict, assert_type
26+
from typing import NotRequired, Self, TypedDict, assert_type
2727
else:
28-
from typing_extensions import NotRequired, TypedDict, assert_type
28+
from typing_extensions import NotRequired, Self, TypedDict, assert_type
2929

3030

3131
# The only purpose of the following line is so that pyright will put all of the
3232
# conditional imports into the .pyi file when generating type stubs. Without this line,
3333
# pyright will not include the above imports in the generated .pyi file, and it will
3434
# result in a lot of red squiggles in user code.
35-
_: 'Concatenate[str, ParamSpec("P")] | ParamSpec | TypeGuard | NotRequired | TypedDict | assert_type' # type:ignore
35+
_: 'Concatenate[str, ParamSpec("P")] | ParamSpec | TypeGuard | NotRequired | TypedDict | assert_type | Self' # type:ignore

shiny/_utils.py

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -221,35 +221,89 @@ def private_seed() -> Generator[None, None, None]:
221221
# Async-related functions
222222
# ==============================================================================
223223

224-
T = TypeVar("T")
224+
R = TypeVar("R") # Return type
225225
P = ParamSpec("P")
226226

227227

228228
def wrap_async(
229-
fn: Callable[P, T] | Callable[P, Awaitable[T]]
230-
) -> Callable[P, Awaitable[T]]:
229+
fn: Callable[P, R] | Callable[P, Awaitable[R]]
230+
) -> Callable[P, Awaitable[R]]:
231231
"""
232-
Given a synchronous function that returns T, return an async function that wraps the
232+
Given a synchronous function that returns R, return an async function that wraps the
233233
original function. If the input function is already async, then return it unchanged.
234234
"""
235235

236236
if is_async_callable(fn):
237237
return fn
238238

239-
fn = cast(Callable[P, T], fn)
239+
fn = cast(Callable[P, R], fn)
240240

241241
@functools.wraps(fn)
242-
async def fn_async(*args: P.args, **kwargs: P.kwargs) -> T:
242+
async def fn_async(*args: P.args, **kwargs: P.kwargs) -> R:
243243
return fn(*args, **kwargs)
244244

245245
return fn_async
246246

247247

248+
# # TODO-barret-future; Q: Keep code?
249+
# class WrapAsync(Generic[P, R]):
250+
# """
251+
# Make a function asynchronous.
252+
253+
# Parameters
254+
# ----------
255+
# fn
256+
# Function to make asynchronous.
257+
258+
# Returns
259+
# -------
260+
# :
261+
# Asynchronous function (within the `WrapAsync` instance)
262+
# """
263+
264+
# def __init__(self, fn: Callable[P, R] | Callable[P, Awaitable[R]]):
265+
# if isinstance(fn, WrapAsync):
266+
# fn = cast(WrapAsync[P, R], fn)
267+
# return fn
268+
# self._is_async = is_async_callable(fn)
269+
# self._fn = wrap_async(fn)
270+
271+
# async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
272+
# """
273+
# Call the asynchronous function.
274+
# """
275+
# return await self._fn(*args, **kwargs)
276+
277+
# @property
278+
# def is_async(self) -> bool:
279+
# """
280+
# Was the original function asynchronous?
281+
282+
# Returns
283+
# -------
284+
# :
285+
# Whether the original function is asynchronous.
286+
# """
287+
# return self._is_async
288+
289+
# @property
290+
# def fn(self) -> Callable[P, R] | Callable[P, Awaitable[R]]:
291+
# """
292+
# Retrieve the original function
293+
294+
# Returns
295+
# -------
296+
# :
297+
# Original function supplied to the `WrapAsync` constructor.
298+
# """
299+
# return self._fn
300+
301+
248302
# This function should generally be used in this code base instead of
249303
# `iscoroutinefunction()`.
250304
def is_async_callable(
251-
obj: Callable[P, T] | Callable[P, Awaitable[T]]
252-
) -> TypeGuard[Callable[P, Awaitable[T]]]:
305+
obj: Callable[P, R] | Callable[P, Awaitable[R]]
306+
) -> TypeGuard[Callable[P, Awaitable[R]]]:
253307
"""
254308
Determine if an object is an async function.
255309
@@ -282,7 +336,7 @@ def is_async_callable(
282336
# of how this stuff works.
283337
# For a more in-depth explanation, see
284338
# https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/.
285-
def run_coro_sync(coro: Awaitable[T]) -> T:
339+
def run_coro_sync(coro: Awaitable[R]) -> R:
286340
"""
287341
Run a coroutine that is in fact synchronous. Given a coroutine (which is
288342
returned by calling an `async def` function), this function will run the
@@ -310,7 +364,7 @@ def run_coro_sync(coro: Awaitable[T]) -> T:
310364
)
311365

312366

313-
def run_coro_hybrid(coro: Awaitable[T]) -> "asyncio.Future[T]":
367+
def run_coro_hybrid(coro: Awaitable[R]) -> "asyncio.Future[R]":
314368
"""
315369
Synchronously runs the given coro up to its first yield, then runs the rest of the
316370
coro by scheduling it on the current event loop, as per normal. You can think of
@@ -325,7 +379,7 @@ def run_coro_hybrid(coro: Awaitable[T]) -> "asyncio.Future[T]":
325379
asyncio Task implementation, this is a hastily assembled hack job; who knows what
326380
unknown unknowns lurk here.
327381
"""
328-
result_future: asyncio.Future[T] = asyncio.Future()
382+
result_future: asyncio.Future[R] = asyncio.Future()
329383

330384
if not inspect.iscoroutine(coro):
331385
raise TypeError("run_coro_hybrid requires a Coroutine object.")

shiny/api-examples/Renderer/app.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from __future__ import annotations
2+
3+
from typing import Literal
4+
5+
from shiny import App, Inputs, Outputs, Session, ui
6+
from shiny.render.renderer import Renderer, ValueFn
7+
8+
#######
9+
# Start of package author code
10+
#######
11+
12+
13+
class render_capitalize(Renderer[str]):
14+
# The documentation for the class will be displayed when the user hovers over the
15+
# decorator when **no** parenthesis are used. Ex: `@render_capitalize`
16+
# If no documentation is supplied to the `__init__()` method, then this
17+
# documentation will be displayed when parenthesis are used on the decorator.
18+
"""
19+
Render capitalize class documentation goes here.
20+
"""
21+
22+
to_case: Literal["upper", "lower", "ignore"]
23+
"""
24+
The case to render the value in.
25+
"""
26+
placeholder: bool
27+
"""
28+
Whether to render a placeholder value. (Defaults to `True`)
29+
"""
30+
31+
def default_ui(self, id: str):
32+
"""
33+
Express UI for the renderer
34+
"""
35+
return ui.output_text_verbatim(id, placeholder=self.placeholder)
36+
37+
def __init__(
38+
self,
39+
_fn: ValueFn[str | None] | None = None,
40+
*,
41+
to_case: Literal["upper", "lower", "ignore"] = "upper",
42+
placeholder: bool = True,
43+
) -> None:
44+
# If a different set of documentation is supplied to the `__init__` method,
45+
# then this documentation will be displayed when parenthesis are used on the decorator.
46+
# Ex: `@render_capitalize()`
47+
"""
48+
Render capitalize documentation goes here.
49+
50+
It is a good idea to talk about parameters here!
51+
52+
Parameters
53+
----------
54+
to_case
55+
The case to render the value. (`"upper"`)
56+
57+
Options:
58+
- `"upper"`: Render the value in upper case.
59+
- `"lower"`: Render the value in lower case.
60+
- `"ignore"`: Do not alter the case of the value.
61+
62+
placeholder
63+
Whether to render a placeholder value. (`True`)
64+
"""
65+
# Do not pass params
66+
super().__init__(_fn)
67+
self.widget = None
68+
self.to_case = to_case
69+
70+
async def render(self) -> str | None:
71+
value = await self.value_fn()
72+
if value is None:
73+
# If `None` is returned, then do not render anything.
74+
return None
75+
76+
ret = str(value)
77+
if self.to_case == "upper":
78+
return ret.upper()
79+
if self.to_case == "lower":
80+
return ret.lower()
81+
if self.to_case == "ignore":
82+
return ret
83+
raise ValueError(f"Invalid value for `to_case`: {self.to_case}")
84+
85+
86+
class render_upper(Renderer[str]):
87+
"""
88+
Minimal capitalize string transformation renderer.
89+
90+
No parameters are supplied to this renderer. This allows us to skip the `__init__()`
91+
method and `__init__()` documentation. If you hover over this decorator with and
92+
without parenthesis, you will see this documentation in both situations.
93+
94+
Note: This renderer is equivalent to `render_capitalize(to="upper")`.
95+
"""
96+
97+
def default_ui(self, id: str):
98+
"""
99+
Express UI for the renderer
100+
"""
101+
return ui.output_text_verbatim(id, placeholder=True)
102+
103+
async def transform(self, value: str) -> str:
104+
"""
105+
Transform the value to upper case.
106+
107+
This method is shorthand for the default `render()` method. It is useful to
108+
transform non-`None` values. (Any `None` value returned by the app author will
109+
be forwarded to the browser.)
110+
111+
Parameters
112+
----------
113+
value
114+
The a non-`None` value to transform.
115+
116+
Returns
117+
-------
118+
str
119+
The transformed value. (Must be a subset of `Jsonifiable`.)
120+
"""
121+
122+
return str(value).upper()
123+
124+
125+
#######
126+
# End of package author code
127+
#######
128+
129+
130+
#######
131+
# Start of app author code
132+
#######
133+
134+
135+
def text_row(id: str, label: str):
136+
return ui.tags.tr(
137+
ui.tags.td(f"{label}:"),
138+
ui.tags.td(ui.output_text_verbatim(id, placeholder=True)),
139+
)
140+
return ui.row(
141+
ui.column(6, f"{id}:"),
142+
ui.column(6, ui.output_text_verbatim(id, placeholder=True)),
143+
)
144+
145+
146+
app_ui = ui.page_fluid(
147+
ui.h1("Capitalization renderer"),
148+
ui.input_text("caption", "Caption:", "Data summary"),
149+
ui.tags.table(
150+
text_row("upper", "@render_upper"),
151+
text_row("upper_with_paren", "@render_upper()"),
152+
#
153+
text_row("cap_upper", "@render_capitalize"),
154+
text_row("cap_lower", "@render_capitalize(to='lower')"),
155+
),
156+
)
157+
158+
159+
def server(input: Inputs, output: Outputs, session: Session):
160+
# Hovering over `@render_upper` will display the class documentation
161+
@render_upper
162+
def upper():
163+
return input.caption()
164+
165+
# Hovering over `@render_upper` will display the class documentation as there is no
166+
# `__init__()` documentation
167+
@render_upper()
168+
def upper_with_paren():
169+
return input.caption()
170+
171+
# Hovering over `@render_capitalize` will display the class documentation
172+
@render_capitalize
173+
def cap_upper():
174+
return input.caption()
175+
176+
# Hovering over `@render_capitalize` will display the `__init__()` documentation
177+
@render_capitalize(to_case="lower")
178+
def cap_lower():
179+
return input.caption()
180+
181+
182+
app = App(app_ui, server)

shiny/api-examples/output_transformer/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
)
1212

1313
#######
14+
# DEPRECATED. Please see `shiny.render.renderer.Renderer` for the latest API.
15+
# This example is kept for backwards compatibility.
16+
#
17+
#
1418
# Package authors can create their own output transformer methods by leveraging
1519
# `output_transformer` decorator.
1620
#

0 commit comments

Comments
 (0)