Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions docs/_quartodoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,11 @@ quartodoc:
- render.DataGrid
- render.DataTable
- kind: page
path: OutputRender
path: Renderer
flatten: true
summary:
name: "Create rendering outputs"
desc: ""
name: "Create output renderers"
desc: "Package author methods for creating new output renderers."
contents:
- render.renderer.Renderer
- name: render.renderer.Jsonifiable
Expand Down
3 changes: 1 addition & 2 deletions shiny/api-examples/Renderer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def auto_output_ui(self):

def __init__(
self,
_fn: Optional[ValueFn[str]] | None = None,
_fn: Optional[ValueFn[str]] = None,
*,
to_case: Literal["upper", "lower", "ignore"] = "upper",
placeholder: bool = True,
Expand Down Expand Up @@ -64,7 +64,6 @@ def __init__(
"""
# Do not pass params
super().__init__(_fn)
self.widget = None
self.to_case = to_case

async def render(self) -> str | None:
Expand Down
148 changes: 115 additions & 33 deletions shiny/render/renderer/_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,40 @@

from htmltools import MetadataNode, Tag, TagList

from ..._docstring import add_example
from ..._typing_extensions import Self
from ..._utils import is_async_callable, wrap_async

# TODO-barret; POST-merge; Update shinywidgets


# TODO-future: docs; missing first paragraph from some classes: Example: TransformerMetadata.
# TODO-barret-future: Double check docs are rendererd
# Missing first paragraph from some classes: Example: TransformerMetadata.
# No init method for TransformerParams. This is because the `DocClass` object does not
# display methods that start with `_`. THerefore no `__init__` or `__call__` methods are
# display methods that start with `_`. Therefore no `__init__` or `__call__` methods are
# displayed. Even if they have docs.


__all__ = (
"Renderer",
"Jsonifiable",
"ValueFn",
"Jsonifiable",
"AsyncValueFn",
"RendererT",
)


RendererT = TypeVar("RendererT", bound="Renderer[Any]")
"""
Generic class to pass the Renderer class through a decorator.
Generic output renderer class to pass the original Renderer subclass through a decorator
function.

When accepting and returning a `Renderer` class, utilize this TypeVar as to not reduce the variable type to `Renderer[Any]`
When accepting and returning a `Renderer` class, utilize this TypeVar as to not reduce
the variable type to `Renderer[Any]`
"""

# Input type for the user-spplied function that is passed to a render.xx
IT = TypeVar("IT")
"""
Return type from the user-supplied value function passed into the renderer.
"""


# https://github.com/python/cpython/blob/df1eec3dae3b1eddff819fd70f58b03b3fbd0eda/Lib/json/encoder.py#L77-L95
Expand Down Expand Up @@ -83,22 +88,50 @@

# Requiring `None` type throughout the value functions as `return` returns `None` type.
# This is typically paired with `req(False)` to exit quickly.
# If package authors want to NOT allow `None` type, they can capture it in a custom render method with a runtime error. (Or make a new RendererThatCantBeNone class)
# If package authors want to NOT allow `None` type, they can capture it in a custom
# render method with a runtime error. (Or make a new RendererThatCantBeNone class)
ValueFn = Union[
Callable[[], Union[IT, None]],
Callable[[], Awaitable[Union[IT, None]]],
]
"""
App-supplied output value function which returns type `IT`. This function can be
synchronous or asynchronous.
App-supplied output value function which returns type `IT` or `None`. This function can
be synchronous or asynchronous.
"""


@add_example()
class Renderer(Generic[IT]):
"""
Output renderer class

TODO-barret-docs
An output renderer is a class that will take in a callable function (value
function), transform the returned value into a JSON-serializable object, and send
the result to the browser.

When the value function is received, the renderer will be auto registered with
the current session's `Output` class, hooking it into Shiny's reactive graph. By
auto registering as an `Output`, it allows for App authors to skip adding `@output`
above the renderer. (If programmatic `id` is needed, `@output(id="foo")` can still be
used!)

There are two methods that must be implemented by the subclasses:
`.auto_output_ui(self, id: str)` and either `.transform(self, value: IT)` or
`.render(self)`.

* In Express mode, the output renderer will automatically render its UI via
`.auto_output_ui(self, id: str)`. This helper method allows App authors to skip
adding a `ui.output_*` function to their UI, making Express mode even more
concise. If more control is needed over the UI, `@suspend_display` can be used to
suppress the auto rendering of the UI. When using `@suspend_display` on a
renderer, the renderer's UI will need to be added to the app to connect the
rendered output to Shiny's reactive graph.
* The `render` method is responsible for executing the value function and performing
any transformations for the output value to be JSON-serializable (`None` is a
valid value!). To avoid the boilerplate of resolving the value function and
returning early if `None` is received, package authors may implement the
`.transform(self, value: IT)` method. The `transform` method's sole job is to
_transform_ non-`None` values into an object that is JSON-serializable.
"""

# Q: Could we do this with typing without putting `P` in the Generic?
Expand All @@ -108,7 +141,7 @@ class Renderer(Generic[IT]):

__name__: str
"""
Name of output function supplied. (The value will not contain any module prefix.)
Name of output function supplied. (The value **will not** contain a module prefix.)

Set within `.__call__()` method.
"""
Expand All @@ -121,8 +154,8 @@ class Renderer(Generic[IT]):
the fully resolved ID, call
`shiny.session.require_active_session(None).ns(self.output_id)`.

An initial value of `.__name__` (set within `Renderer.__call__(_fn)`) will be used until the
output renderer is registered within the session.
An initial value of `.__name__` (set within `Renderer.__call__(_fn)`) will be used
until the output renderer is registered within the session.
"""

fn: AsyncValueFn[IT]
Expand All @@ -134,9 +167,23 @@ class Renderer(Generic[IT]):

def __call__(self, _fn: ValueFn[IT]) -> Self:
"""
Renderer __call__ docs here; Sets app's value function
Add the value function to the renderer.

TODO-barret-docs
Addition actions performed:
* Store the value function name.
* Set the Renderer's `output_id` to the function name.
* Auto register (given a `Session` exists) the Renderer

Parameters
----------
_fn
Value function supplied by the App author. This function can be synchronous
or asynchronous.

Returns
-------
:
Original renderer instance.
"""

if not callable(_fn):
Expand Down Expand Up @@ -181,6 +228,15 @@ def auto_output_ui(
# *
# **kwargs: object,
) -> DefaultUIFnResultOrNone:
"""
Express mode method that automatically generates the output's UI.

Parameters
----------
id
Output function name or ID (provided to `@output(id=)`). This value will
contain any module prefix.
"""
return None

def __init__(
Expand All @@ -203,23 +259,46 @@ def __init__(

async def transform(self, value: IT) -> Jsonifiable:
"""
Renderer - transform docs here
Transform an output value into a JSON-serializable object.

When subclassing `Renderer`, this method can be implemented to transform
non-`None` values into a JSON-serializable object.

TODO-barret-docs
If a `.render()` method is not implemented, this method **must** be implemented.
When the output is requested, the `Renderer`'s `.render()` method will execute
the output value function, return `None` if the value is `None`, and call this
method to transform the value into a JSON-serializable object.

Note, only one of `.transform()` or `.render()` should be implemented.
"""
raise NotImplementedError(
"Please implement either the `transform(self, value: IT)` or `render(self)` method.\n"
"* `transform(self, value: IT)` should transform the `value` (of type `IT`) into Jsonifiable object. Ex: `dict`, `None`, `str`. (standard)\n"
"* `render(self)` method has full control of how an App author's value is retrieved (`self._fn()`) and utilized. (rare)\n"
"By default, the `render` retrieves the value and then calls `transform` method on non-`None` values."
"Please implement either the `transform(self, value: IT)`"
" or `render(self)` method.\n"
"* `transform(self, value: IT)` should transform the non-`None` `value`"
" (of type `IT`) into a JSON-serializable object."
" Ex: `dict`, `None`, `str`. (common)\n"
"* `render(self)` method has full control of how an App author's value is"
" retrieved (`self._fn()`) and processed. (rare)"
)

async def render(self) -> Jsonifiable:
"""
Renderer - render docs here
Renders the output value function.

This method is called when the renderer is requested to render its output.

The `Renderer`'s `render()` implementation goes as follows:

* Execute the value function supplied to the renderer.
* If the output value is `None`, `None` will be returned.
* If the output value is not `None`, the `.transform()` method will be called to
transform the value into a JSON-serializable object.

TODO-barret-docs
When overwriting this method in a subclass, the implementation should execute
the value function `.fn` and return the transformed value (which is
JSON-serializable).
"""

value = await self.fn()
if value is None:
return None
Expand All @@ -240,7 +319,7 @@ def tagify(self) -> DefaultUIFnResult:
rendered_ui = self._render_auto_output_ui()
if rendered_ui is None:
raise TypeError(
"No default UI exists for this type of render function: ",
"No output UI exists for this type of render function: ",
self.__class__.__name__,
)
return rendered_ui
Expand All @@ -254,11 +333,6 @@ def _render_auto_output_ui(self) -> DefaultUIFnResultOrNone:
# ######
# Auto registering output
# ######
"""
Auto registers the rendering method then the renderer is called.

When `@output` is called on the renderer, the renderer is automatically un-registered via `._on_register()`.
"""

def _on_register(self) -> None:
if self._auto_registered:
Expand All @@ -272,6 +346,13 @@ def _on_register(self) -> None:
self._auto_registered = False

def _auto_register(self) -> None:
"""
Auto registers the rendering method to the session output then the renderer is
called.

When `@output` is called on the renderer, the renderer is automatically
un-registered via `._on_register()`.
"""
# If in Express mode, register the output
if not self._auto_registered:
from ...session import get_current_session
Expand All @@ -285,8 +366,9 @@ def _auto_register(self) -> None:
self._auto_registered = True


# Not inheriting from `WrapAsync[[], IT]` as python 3.8 needs typing extensions that doesn't support `[]` for a ParamSpec definition. :-(
# Would be minimal/clean if we could do `class AsyncValueFn(WrapAsync[[], IT]):`
# Not inheriting from `WrapAsync[[], IT]` as python 3.8 needs typing extensions that
# doesn't support `[]` for a ParamSpec definition. :-( Would be minimal/clean if we
# could do `class AsyncValueFn(WrapAsync[[], IT]):`
class AsyncValueFn(Generic[IT]):
"""
App-supplied output value function which returns type `IT`.
Expand Down
4 changes: 3 additions & 1 deletion shiny/ui/_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
from .css._css_unit import CssUnit, as_css_padding, as_css_unit
from .fill import as_fill_item, as_fillable_container

# TODO-barret-future; Update header to return CardHeader class. Same for footer. Then we can check `*args` for a CardHeader class and move it to the top. And footer to the bottom. Can throw error if multiple headers/footers are provided or could concatenate.
# TODO-barret-future; Update header to return CardHeader class. Same for footer. Then we
# can check `*args` for a CardHeader class and move it to the top. And footer to the
# bottom. Can throw error if multiple headers/footers are provided or could concatenate.


__all__ = (
Expand Down