Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
f41c020
api!: Drop `shiny.render.RenderFunction` and `shiny.render.RenderFunc…
schloerke Dec 15, 2023
98483b8
api!: Deprecate `render.transformer.resolve_value_fn`; Drop `render. …
schloerke Dec 15, 2023
fbf4b26
Remove usage of `render_value_fn`
schloerke Dec 15, 2023
f54234f
Update tests
schloerke Dec 15, 2023
1dddec0
Only use async execution for output renderer
schloerke Dec 15, 2023
7119894
Update _display.py
schloerke Dec 18, 2023
14c7b4b
Add `value_fn_is_async` to `TransformerMetadata`
schloerke Dec 18, 2023
365dc51
Use new `value_fn_is_async` meta key for `render.plot`
schloerke Dec 18, 2023
302c10b
Add more errors to ignore. Ignore blank lines
schloerke Dec 18, 2023
4670c17
Update _quartodoc.yml
schloerke Dec 18, 2023
182cd0b
Update test_examples.py
schloerke Dec 18, 2023
8367c7a
Commit all code just in case my computer breaks
schloerke Dec 22, 2023
3979ed8
Commit code before computer possibly blows up
schloerke Jan 2, 2024
98a1daa
Commit again before machine blows up
schloerke Jan 3, 2024
edb73e3
Add `WrapAsync` utility class to expose common info about a now async…
schloerke Jan 4, 2024
7f7a783
Update _quartodoc.yml
schloerke Jan 4, 2024
a2d9b20
Add `ui_kwargs()` (superceeding `output_args`)
schloerke Jan 4, 2024
3e8a719
Make new renderer class
schloerke Jan 4, 2024
f4cd0c7
First pass of having `OutputRenderer` inherit from `Renderer`
schloerke Jan 4, 2024
273afb8
Expose `ui_kwargs` over `output_args`
schloerke Jan 4, 2024
f59faab
Utilize `Renderer` class instead of `Callable[[], OT]`
schloerke Jan 4, 2024
2ec83d6
Add `suspend_display` test
schloerke Jan 4, 2024
6b6bc22
Create test_renderer.py
schloerke Jan 4, 2024
bf7b4da
Static type checking!
schloerke Jan 4, 2024
3e74acf
More output_args -> ui_kwargs
schloerke Jan 4, 2024
01933ce
Remove large comments. Tweak types / imports
schloerke Jan 4, 2024
8eeafdc
Update render methods to use new Renderer class
schloerke Jan 4, 2024
5d98e0b
Remove unused code; Add docs TODO
schloerke Jan 4, 2024
8ba5f81
Update CHANGELOG.md
schloerke Jan 4, 2024
a6b516e
Add demo app for render.display
schloerke Jan 4, 2024
1f74aba
Update _quartodoc.yml
schloerke Jan 4, 2024
3a21034
Fix typing.Self issue
schloerke Jan 4, 2024
5a10652
Add TODO on type issue
schloerke Jan 4, 2024
88ba832
Remove ugly shorthand
schloerke Jan 4, 2024
2151404
Fix legacy typing check
schloerke Jan 5, 2024
64e144a
Add playright test for reactive.event on a output render function and…
schloerke Jan 5, 2024
7144bf6
If already a WrapAsync function, return itself
schloerke Jan 5, 2024
6d11f97
Separate the setting of value function and auto registering
schloerke Jan 5, 2024
8892869
Auto register the OutputRenderer class on init.
schloerke Jan 5, 2024
81bc28b
Add deprecation warning to OutputRenderer
schloerke Jan 5, 2024
064a8c6
Copy/update current output_transformer ex app for testing
schloerke Jan 5, 2024
8a3c5fd
Diff syntax for ignoring P type in Generic
schloerke Jan 5, 2024
827737b
lints
schloerke Jan 5, 2024
5554456
init Renderer api-example app
schloerke Jan 5, 2024
3c3eb1d
Moving WrapAsync code into AsyncValueFn as older versions of python d…
schloerke Jan 5, 2024
f401bfc
Update app.py
schloerke Jan 5, 2024
c6db4f0
Remove test warning
schloerke Jan 5, 2024
03ac5f9
Update _utils.py
schloerke Jan 5, 2024
4aae5cd
Update errors being caught and diagnostics
schloerke Jan 8, 2024
3a8e55e
Convert `@render.data_frame()`
schloerke Jan 8, 2024
43edd0d
Pass through `Renderer` type using `RendererBaseT` `TypeVar`
schloerke Jan 8, 2024
6539a9e
Create _utils.py
schloerke Jan 8, 2024
b185cd6
Remove helper method. Just use `__call__(value_fn)`
schloerke Jan 8, 2024
12da490
Add `.async_fn()` and `.sync_fn()` methods to `AsyncValueFn` class
schloerke Jan 8, 2024
5e2056e
Implement `@render.display` class
schloerke Jan 8, 2024
31e047b
Typing and test updates
schloerke Jan 8, 2024
e70b9df
lint
schloerke Jan 8, 2024
d8610ce
Move `._on_register()` to occur after output metadata has been set
schloerke Jan 8, 2024
15ec2b1
Use `require_active_session(None)` to retrieve current Session; Add s…
schloerke Jan 8, 2024
8f345ec
Use `AutoRegisterMixin` class for `RendererBase`
schloerke Jan 8, 2024
644ce5b
Test lint
schloerke Jan 8, 2024
0dd6117
Update _quartodoc.yml
schloerke Jan 8, 2024
fc688cb
Fix test
schloerke Jan 8, 2024
8a5bbf5
Update _quartodoc.yml
schloerke Jan 8, 2024
547bc7e
Merge branch 'main' into output_transformer_class
schloerke Jan 8, 2024
4326967
Docs and remove lingering `power` in renderer.text
schloerke Jan 8, 2024
dc206d3
Merge followup
schloerke Jan 8, 2024
41f44fe
Remove comments
schloerke Jan 8, 2024
7798972
Merge branch 'main' into output_transformer_class
schloerke Jan 8, 2024
212cf4f
Update `Renderer` api-example
schloerke Jan 8, 2024
8106952
Merge branch 'main' into output_transformer_class
schloerke Jan 9, 2024
533f0f9
Update comments
schloerke Jan 9, 2024
19c2ea4
Update to latest shiny bindings pkgs
schloerke Jan 9, 2024
bc65c4d
Use `selector` for workaround
schloerke Jan 9, 2024
ff9d8e7
Move mixin back in main class. Move internal variable up in definition
schloerke Jan 9, 2024
f7720c6
Update CHANGELOG.md
schloerke Jan 9, 2024
075c0d6
`Renderer.output_name` -> `Renderer.output_id`
schloerke Jan 9, 2024
3e49d50
Remove `@property` from `AsyncValueFn` class
schloerke Jan 9, 2024
1755fdf
`JSONifiable` -> `Jsonifiable`
schloerke Jan 9, 2024
2fb222e
Update _transformer.py
schloerke Jan 9, 2024
ee0429d
Merge branch 'main' into output_transformer_class
schloerke Jan 9, 2024
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* prompt users to install `requirements.txt`
* Fixed `js-react` template build error. (#965)

### Developer features

* 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)

* `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)

* 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)

* `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)


## [0.6.1.1] - 2023-12-22
Expand Down
17 changes: 6 additions & 11 deletions docs/_quartodoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,17 +175,11 @@ quartodoc:
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
- render.renderer.Renderer
- render.renderer.RendererBase
- render.renderer.Jsonifiable
- render.renderer.ValueFn
- render.renderer.AsyncValueFn
- title: Reactive programming
desc: ""
contents:
Expand Down Expand Up @@ -330,6 +324,7 @@ quartodoc:
- ui.panel_main
- ui.panel_sidebar
- ui.nav
- render.transformer.resolve_value_fn
- title: Experimental
desc: "These methods are under consideration and are considered unstable. However, if there is a method you are excited about, please let us know!"
contents:
Expand Down
6 changes: 3 additions & 3 deletions shiny/_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
# they should both come from the same typing module.
# https://peps.python.org/pep-0655/#usage-in-python-3-11
if sys.version_info >= (3, 11):
from typing import NotRequired, TypedDict, assert_type
from typing import NotRequired, Self, TypedDict, assert_type
else:
from typing_extensions import NotRequired, TypedDict, assert_type
from typing_extensions import NotRequired, Self, TypedDict, assert_type


# The only purpose of the following line is so that pyright will put all of the
# conditional imports into the .pyi file when generating type stubs. Without this line,
# pyright will not include the above imports in the generated .pyi file, and it will
# result in a lot of red squiggles in user code.
_: 'Concatenate[str, ParamSpec("P")] | ParamSpec | TypeGuard | NotRequired | TypedDict | assert_type' # type:ignore
_: 'Concatenate[str, ParamSpec("P")] | ParamSpec | TypeGuard | NotRequired | TypedDict | assert_type | Self' # type:ignore
76 changes: 65 additions & 11 deletions shiny/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,35 +221,89 @@ def private_seed() -> Generator[None, None, None]:
# Async-related functions
# ==============================================================================

T = TypeVar("T")
R = TypeVar("R") # Return type
P = ParamSpec("P")


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

if is_async_callable(fn):
return fn

fn = cast(Callable[P, T], fn)
fn = cast(Callable[P, R], fn)

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

return fn_async


# # TODO-barret-future; Q: Keep code?
# class WrapAsync(Generic[P, R]):
# """
# Make a function asynchronous.

# Parameters
# ----------
# fn
# Function to make asynchronous.

# Returns
# -------
# :
# Asynchronous function (within the `WrapAsync` instance)
# """

# def __init__(self, fn: Callable[P, R] | Callable[P, Awaitable[R]]):
# if isinstance(fn, WrapAsync):
# fn = cast(WrapAsync[P, R], fn)
# return fn
# self._is_async = is_async_callable(fn)
# self._fn = wrap_async(fn)

# async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
# """
# Call the asynchronous function.
# """
# return await self._fn(*args, **kwargs)

# @property
# def is_async(self) -> bool:
# """
# Was the original function asynchronous?

# Returns
# -------
# :
# Whether the original function is asynchronous.
# """
# return self._is_async

# @property
# def fn(self) -> Callable[P, R] | Callable[P, Awaitable[R]]:
# """
# Retrieve the original function

# Returns
# -------
# :
# Original function supplied to the `WrapAsync` constructor.
# """
# return self._fn


# 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]]]:
obj: Callable[P, R] | Callable[P, Awaitable[R]]
) -> TypeGuard[Callable[P, Awaitable[R]]]:
"""
Determine if an object is an async function.

Expand Down Expand Up @@ -282,7 +336,7 @@ def is_async_callable(
# of how this stuff works.
# For a more in-depth explanation, see
# https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/.
def run_coro_sync(coro: Awaitable[T]) -> T:
def run_coro_sync(coro: Awaitable[R]) -> R:
"""
Run a coroutine that is in fact synchronous. Given a coroutine (which is
returned by calling an `async def` function), this function will run the
Expand Down Expand Up @@ -310,7 +364,7 @@ def run_coro_sync(coro: Awaitable[T]) -> T:
)


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

if not inspect.iscoroutine(coro):
raise TypeError("run_coro_hybrid requires a Coroutine object.")
Expand Down
182 changes: 182 additions & 0 deletions shiny/api-examples/Renderer/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
from __future__ import annotations

from typing import Literal

from shiny import App, Inputs, Outputs, Session, ui
from shiny.render.renderer import Renderer, ValueFn

#######
# Start of package author code
#######


class render_capitalize(Renderer[str]):
# The documentation for the class will be displayed when the user hovers over the
# decorator when **no** parenthesis are used. Ex: `@render_capitalize`
# If no documentation is supplied to the `__init__()` method, then this
# documentation will be displayed when parenthesis are used on the decorator.
"""
Render capitalize class documentation goes here.
"""

to_case: Literal["upper", "lower", "ignore"]
"""
The case to render the value in.
"""
placeholder: bool
"""
Whether to render a placeholder value. (Defaults to `True`)
"""

def default_ui(self, id: str):
"""
Express UI for the renderer
"""
return ui.output_text_verbatim(id, placeholder=self.placeholder)

def __init__(
self,
_fn: ValueFn[str | None] | None = None,
*,
to_case: Literal["upper", "lower", "ignore"] = "upper",
placeholder: bool = True,
) -> None:
# If a different set of documentation is supplied to the `__init__` method,
# then this documentation will be displayed when parenthesis are used on the decorator.
# Ex: `@render_capitalize()`
"""
Render capitalize documentation goes here.

It is a good idea to talk about parameters here!

Parameters
----------
to_case
The case to render the value. (`"upper"`)

Options:
- `"upper"`: Render the value in upper case.
- `"lower"`: Render the value in lower case.
- `"ignore"`: Do not alter the case of the value.

placeholder
Whether to render a placeholder value. (`True`)
"""
# Do not pass params
super().__init__(_fn)
self.widget = None
self.to_case = to_case

async def render(self) -> str | None:
value = await self.value_fn()
if value is None:
# If `None` is returned, then do not render anything.
return None

ret = str(value)
if self.to_case == "upper":
return ret.upper()
if self.to_case == "lower":
return ret.lower()
if self.to_case == "ignore":
return ret
raise ValueError(f"Invalid value for `to_case`: {self.to_case}")


class render_upper(Renderer[str]):
"""
Minimal capitalize string transformation renderer.

No parameters are supplied to this renderer. This allows us to skip the `__init__()`
method and `__init__()` documentation. If you hover over this decorator with and
without parenthesis, you will see this documentation in both situations.

Note: This renderer is equivalent to `render_capitalize(to="upper")`.
"""

def default_ui(self, id: str):
"""
Express UI for the renderer
"""
return ui.output_text_verbatim(id, placeholder=True)

async def transform(self, value: str) -> str:
"""
Transform the value to upper case.

This method is shorthand for the default `render()` method. It is useful to
transform non-`None` values. (Any `None` value returned by the app author will
be forwarded to the browser.)

Parameters
----------
value
The a non-`None` value to transform.

Returns
-------
str
The transformed value. (Must be a subset of `Jsonifiable`.)
"""

return str(value).upper()


#######
# End of package author code
#######


#######
# Start of app author code
#######


def text_row(id: str, label: str):
return ui.tags.tr(
ui.tags.td(f"{label}:"),
ui.tags.td(ui.output_text_verbatim(id, placeholder=True)),
)
return ui.row(
ui.column(6, f"{id}:"),
ui.column(6, ui.output_text_verbatim(id, placeholder=True)),
)


app_ui = ui.page_fluid(
ui.h1("Capitalization renderer"),
ui.input_text("caption", "Caption:", "Data summary"),
ui.tags.table(
text_row("upper", "@render_upper"),
text_row("upper_with_paren", "@render_upper()"),
#
text_row("cap_upper", "@render_capitalize"),
text_row("cap_lower", "@render_capitalize(to='lower')"),
),
)


def server(input: Inputs, output: Outputs, session: Session):
# Hovering over `@render_upper` will display the class documentation
@render_upper
def upper():
return input.caption()

# Hovering over `@render_upper` will display the class documentation as there is no
# `__init__()` documentation
@render_upper()
def upper_with_paren():
return input.caption()

# Hovering over `@render_capitalize` will display the class documentation
@render_capitalize
def cap_upper():
return input.caption()

# Hovering over `@render_capitalize` will display the `__init__()` documentation
@render_capitalize(to_case="lower")
def cap_lower():
return input.caption()


app = App(app_ui, server)
4 changes: 4 additions & 0 deletions shiny/api-examples/output_transformer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
)

#######
# DEPRECATED. Please see `shiny.render.renderer.Renderer` for the latest API.
# This example is kept for backwards compatibility.
#
#
# Package authors can create their own output transformer methods by leveraging
# `output_transformer` decorator.
#
Expand Down
Loading