Skip to content

Commit

Permalink
bug: Restore legacy renderers while packages transition (#1023)
Browse files Browse the repository at this point in the history
  • Loading branch information
schloerke authored Jan 17, 2024
1 parent d49bed4 commit 48e4293
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 29 deletions.
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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)
* Legacy renderers that will be removed in the near future:
* `shiny.render.RenderFunction`
* `shiny.render.RenderFunctionAsync`
* `shiny.render.transformer.OutputRenderer`
* `shiny.render.transformer.OutputRendererSync`
* `shiny.render.transformer.OutputRendererAsync`

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

* `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`. `shiny.render.*` will be removed in the near future. (#964)

### Other changes

Expand Down
4 changes: 4 additions & 0 deletions shiny/render/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
ui,
download,
)
from ._deprecated import ( # noqa: F401
RenderFunction, # pyright: ignore[reportUnusedImport]
RenderFunctionAsync, # pyright: ignore[reportUnusedImport]
)

__all__ = (
# TODO-future: Document which variables are exposed via different import approaches
Expand Down
79 changes: 79 additions & 0 deletions shiny/render/_deprecated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Generic

from .transformer._transformer import (
IT,
OT,
OutputRendererAsync,
OutputRendererSync,
TransformerMetadata,
ValueFn,
ValueFnAsync,
ValueFnSync,
empty_params,
)

# ======================================================================================
# Deprecated classes
# ======================================================================================


# A RenderFunction object is given a app-supplied function which returns an `IT`. When
# the .__call__ method is invoked, it calls the app-supplied function (which returns an
# `IT`), then converts the `IT` to an `OT`. Note that in many cases but not all, `IT`
# and `OT` will be the same.
class RenderFunction(Generic[IT, OT], OutputRendererSync[OT], ABC):
"""
Deprecated. Please use :func:`~shiny.render.renderer_components` instead.
"""

@abstractmethod
def __call__(self) -> OT:
...

@abstractmethod
async def run(self) -> OT:
...

def __init__(self, fn: ValueFnSync[IT]) -> None:
async def transformer(_meta: TransformerMetadata, _fn: ValueFn[IT]) -> OT:
ret = await self.run()
return ret

super().__init__(
value_fn=fn,
transform_fn=transformer,
params=empty_params(),
)
self._fn = fn


# The reason for having a separate RenderFunctionAsync class is because the __call__
# method is marked here as async; you can't have a single class where one method could
# be either sync or async.
class RenderFunctionAsync(Generic[IT, OT], OutputRendererAsync[OT], ABC):
"""
Deprecated. Please use :func:`~shiny.render.renderer_components` instead.
"""

@abstractmethod
async def __call__(self) -> OT: # pyright: ignore[reportIncompatibleMethodOverride]
...

@abstractmethod
async def run(self) -> OT:
...

def __init__(self, fn: ValueFnAsync[IT]) -> None:
async def transformer(_meta: TransformerMetadata, _fn: ValueFn[IT]) -> OT:
ret = await self.run()
return ret

super().__init__(
value_fn=fn,
transform_fn=transformer,
params=empty_params(),
)
self._fn = fn
2 changes: 2 additions & 0 deletions shiny/render/transformer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
ValueFnAsync, # pyright: ignore[reportUnusedImport]
TransformFn, # pyright: ignore[reportUnusedImport]
OutputTransformer, # pyright: ignore[reportUnusedImport]
OutputRendererSync, # pyright: ignore[reportUnusedImport]
OutputRendererAsync, # pyright: ignore[reportUnusedImport]
resolve_value_fn, # pyright: ignore[reportUnusedImport]
)

Expand Down
163 changes: 142 additions & 21 deletions shiny/render/transformer/_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)

import inspect
from abc import ABC, abstractmethod
from typing import (
TYPE_CHECKING,
Awaitable,
Expand All @@ -40,7 +41,7 @@
from ..._deprecated import warn_deprecated
from ..._docstring import add_example
from ..._typing_extensions import Concatenate, ParamSpec
from ..._utils import is_async_callable
from ..._utils import is_async_callable, run_coro_sync
from ...types import MISSING

# Input type for the user-spplied function that is passed to a render.xx
Expand Down Expand Up @@ -165,11 +166,14 @@ def inner(*args: P.args, **kwargs: P.kwargs) -> TransformerParams[P]:
TransformFn = Callable[Concatenate[TransformerMetadata, ValueFn[IT], P], Awaitable[OT]]
"""
Package author function that transforms an object of type `IT` into type `OT`. It should
be defined as an asynchronous function.
be defined as an asynchronous function but should only asynchronously yield when the
second parameter (of type `ValueFn[IT]`) is awaitable. If the second function argument
is not awaitable (a _synchronous_ function), then the execution of the transform
function should also be synchronous.
"""


class OutputRenderer(RendererBase, Generic[OT]):
class OutputRenderer(RendererBase, ABC, Generic[OT]):
"""
Output Renderer
Expand All @@ -178,7 +182,11 @@ class OutputRenderer(RendererBase, Generic[OT]):
:class:`~shiny.Outputs` output value.
When the `.__call__` method is invoked, the transform function (`transform_fn`)
(typically defined by package authors) is invoked.
(typically defined by package authors) is invoked. The wrapping classes
(:class:`~shiny.render.transformer.OutputRendererSync` and
:class:`~shiny.render.transformer.OutputRendererAsync`) will enforce whether the
transform function is synchronous or asynchronous independent of the awaitable
syntax.
The transform function (`transform_fn`) is given `meta` information
(:class:`~shiny.render.transformer.TranformerMetadata`), the (app-supplied) value
Expand All @@ -201,18 +209,19 @@ class OutputRenderer(RendererBase, Generic[OT]):
* The parameter specification defined by the transform function (`transform_fn`).
It should **not** contain any `*args`. All keyword arguments should have a type
and default value.
See Also
--------
* :class:`~shiny.render.transformer.OutputRendererSync`
* :class:`~shiny.render.transformer.OutputRendererAsync`
"""

async def __call__(self) -> OT:
@abstractmethod
def __call__(self) -> OT:
"""
Asynchronously executes the output renderer (both the app's output value function and transformer).
All output renderers are asynchronous to accomodate that users can supply
asyncronous output value functions and package authors can supply asynchronous
transformer functions. To handle both possible situations cleanly, the
`.__call__` method is executed as asynchronous.
Executes the output renderer (both the app's output value function and transformer).
"""
return await self._run()
...

def __init__(
self,
Expand All @@ -230,7 +239,10 @@ def __init__(
App-provided output value function. It should return an object of type `IT`.
transform_fn
Package author function that transforms an object of type `IT` into type
`OT`. The `params` will used as variadic keyword arguments.
`OT`. The `params` will used as variadic keyword arguments. This method
should only use `await` syntax when the value function (`ValueFn[IT]`) is
awaitable. If the value function is not awaitable (a _synchronous_
function), then the function should execute synchronously.
params
App-provided parameters for the transform function (`transform_fn`).
default_ui
Expand All @@ -250,7 +262,8 @@ def __init__(

if not is_async_callable(transform_fn):
raise TypeError(
"OutputRenderer requires an async tranformer function (`transform_fn`)."
self.__class__.__name__
+ " requires an async tranformer function (`transform_fn`)."
" Please define your transform function as asynchronous."
" Ex `async def my_transformer(....`"
)
Expand Down Expand Up @@ -346,6 +359,93 @@ async def render(self) -> Jsonifiable:
return jsonifiable_ret


# Using a second class to help clarify that it is of a particular type
class OutputRendererSync(OutputRenderer[OT]):
"""
Output Renderer (Synchronous)
This class is used to define a synchronous renderer. The `.__call__` method is
implemented to call the `._run` method synchronously.
See Also
--------
* :class:`~shiny.render.transformer.OutputRenderer`
* :class:`~shiny.render.transformer.OutputRendererAsync`
"""

def __init__(
self,
value_fn: ValueFnSync[IT],
transform_fn: TransformFn[IT, P, OT],
params: TransformerParams[P],
default_ui: Optional[DefaultUIFn] = None,
default_ui_passthrough_args: Optional[tuple[str, ...]] = None,
) -> None:
if is_async_callable(value_fn):
raise TypeError(
self.__class__.__name__ + " requires a synchronous render function"
)
# super == OutputRenderer[OT]
super().__init__(
value_fn=value_fn,
transform_fn=transform_fn,
params=params,
default_ui=default_ui,
default_ui_passthrough_args=default_ui_passthrough_args,
)

def __call__(self) -> OT:
"""
Synchronously executes the output renderer as a function.
"""
return run_coro_sync(self._run())


# The reason for having a separate RendererAsync class is because the __call__
# method is marked here as async; you can't have a single class where one method could
# be either sync or async.
class OutputRendererAsync(OutputRenderer[OT]):
"""
Output Renderer (Asynchronous)
This class is used to define an asynchronous renderer. The `.__call__` method is
implemented to call the `._run` method asynchronously.
See Also
--------
* :class:`~shiny.render.transformer.OutputRenderer`
* :class:`~shiny.render.transformer.OutputRendererSync`
"""

def __init__(
self,
value_fn: ValueFnAsync[IT],
transform_fn: TransformFn[IT, P, OT],
params: TransformerParams[P],
default_ui: Optional[DefaultUIFn] = None,
default_ui_passthrough_args: Optional[tuple[str, ...]] = None,
) -> None:
if not is_async_callable(value_fn):
raise TypeError(
self.__class__.__name__ + " requires an asynchronous render function"
)

# super == OutputRenderer[OT]
super().__init__(
value_fn=value_fn,
transform_fn=transform_fn,
params=params,
default_ui=default_ui,
default_ui_passthrough_args=default_ui_passthrough_args,
)

async def __call__(self) -> OT: # pyright: ignore[reportIncompatibleMethodOverride]
"""
Asynchronously executes the output renderer as a function.
"""
return await self._run()


# ======================================================================================
# Restrict the transformer function
# ======================================================================================
Expand Down Expand Up @@ -581,6 +681,11 @@ def output_transformer(
this, you can use `**kwargs: Any` instead or add `_fn: None = None` as the first
parameter in the overload containing the `**kwargs: object`.
* The `transform_fn` should be defined as an asynchronous function but should only
asynchronously yield (i.e. use `await` syntax) when the value function (the second
parameter of type `ValueFn[IT]`) is awaitable. If the value function is not
awaitable (i.e. it is a _synchronous_ function), then the execution of the
transform function should also be synchronous.
Parameters
----------
Expand Down Expand Up @@ -616,13 +721,24 @@ def renderer_decorator(
def as_value_fn(
fn: ValueFn[IT],
) -> OutputRenderer[OT]:
return OutputRenderer(
value_fn=fn,
transform_fn=transform_fn,
params=params,
default_ui=default_ui,
default_ui_passthrough_args=default_ui_passthrough_args,
)
if is_async_callable(fn):
return OutputRendererAsync(
fn,
transform_fn,
params,
default_ui,
default_ui_passthrough_args,
)
else:
# To avoid duplicate work just for a typeguard, we cast the function
fn = cast(ValueFnSync[IT], fn)
return OutputRendererSync(
fn,
transform_fn,
params,
default_ui,
default_ui_passthrough_args,
)

if value_fn is None:
return as_value_fn
Expand Down Expand Up @@ -661,6 +777,11 @@ async def resolve_value_fn(value_fn: ValueFn[IT]) -> IT:
x = await resolve_value_fn(_fn)
```
This code substitution is safe as the implementation does not _actually_
asynchronously yield to another process if the `value_fn` is synchronous. The
`__call__` method of the :class:`~shiny.render.transformer.OutputRendererSync` is
built to execute asynchronously defined methods that execute synchronously.
Parameters
----------
value_fn
Expand Down
1 change: 1 addition & 0 deletions tests/playwright/examples/example_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def get_apps(path: str) -> typing.List[str]:
}
output_transformer_errors = [
"ShinyDeprecationWarning: `shiny.render.transformer.output_transformer()`",
" super().__init__(",
" return OutputRenderer",
# brownian example app
"ShinyDeprecationWarning:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from playwright.sync_api import Page


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

OutputTextVerbatim(page, "t1").expect_value("t1; no call; sync")
Expand Down
Loading

0 comments on commit 48e4293

Please sign in to comment.