diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 4b5942af1..e34ede0d8 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -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 diff --git a/shiny/api-examples/Renderer/app.py b/shiny/api-examples/Renderer/app.py index e4b0fe900..1877778c5 100644 --- a/shiny/api-examples/Renderer/app.py +++ b/shiny/api-examples/Renderer/app.py @@ -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, @@ -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: diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index b311aa225..2e2972e66 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -16,15 +16,14 @@ 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. @@ -32,19 +31,25 @@ "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 @@ -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? @@ -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. """ @@ -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] @@ -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): @@ -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__( @@ -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 @@ -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 @@ -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: @@ -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 @@ -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`. diff --git a/shiny/ui/_card.py b/shiny/ui/_card.py index c402a4442..c8357ead8 100644 --- a/shiny/ui/_card.py +++ b/shiny/ui/_card.py @@ -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__ = (