From 2924cf093cb2d24e993a6ad9d3e220df2f7900d9 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 19 Jan 2024 15:04:13 -0500 Subject: [PATCH 1/4] Add Renderer docs --- docs/_quartodoc.yml | 6 +- shiny/api-examples/Renderer/app.py | 3 +- shiny/render/renderer/_renderer.py | 137 ++++++++++++++++++++++------- shiny/ui/_card.py | 4 +- 4 files changed, 111 insertions(+), 39 deletions(-) diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 99bd5290c..8a9831f9f 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -170,11 +170,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 - 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..171ce0c77 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,46 @@ # 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, it the renderer will be auto registered with + the current session's `Output` class, hookinng 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 progromatic `id` is needed, `@output(id="foo")` can still be + used!) + + There are two methods that must be implemented by the subclasses: `.render(self)` + and `.auto_output_ui(self, id: str)`. + + * The `render` method is responsible for retrieving the value from the value + function and performing any transformations to be the return value + JSON-serializable. (`None` is a valid return value.) + * 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. """ # Q: Could we do this with typing without putting `P` in the Generic? @@ -108,7 +137,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 +150,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 +163,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. + + 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 - TODO-barret-docs + 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 +224,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 +255,39 @@ def __init__( async def transform(self, value: IT) -> Jsonifiable: """ - Renderer - transform docs here - - TODO-barret-docs + Transform an non-`None` output value into a JSON-serializable object. + + If an `.render()` method is not implemented, this method **must** be + implemented. When requested, the `.render()` method's default behavior will + resolve 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. If both are + implemented, `.render()` will be used. """ 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 `value` (of type `IT`)" + " into Jsonifiable 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)\n" + "By default, the `render` retrieves the value and then calls `transform`" + " method on non-`None` values." ) 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. It + should resolve the value function `.fn` and return a (most likely) transformed + value. - TODO-barret-docs + The current implementation is to resolve the value function. If the value is + `None`, `None` will be returned. If the value is not `None`, the `.transform()` + method will be called to transform the value into a JSON-serializable object. """ + value = await self.fn() if value is None: return None @@ -254,11 +322,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 +335,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 +355,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 daa3785cd..12efa9842 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__ = ( From 7bdff26a49057aecfecad244d25931c87ee8fd2d Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 19 Jan 2024 15:28:42 -0500 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Carson Sievert --- shiny/render/renderer/_renderer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 171ce0c77..71cf8e8bf 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -109,10 +109,10 @@ class Renderer(Generic[IT]): function), transform the returned value into a JSON-serializable object, and send the result to the browser. - When the value function is received, it the renderer will be auto registered with - the current session's `Output` class, hookinng it into Shiny's reactive graph. By + 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 progromatic `id` is needed, `@output(id="foo")` can still be + 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: `.render(self)` @@ -255,9 +255,9 @@ def __init__( async def transform(self, value: IT) -> Jsonifiable: """ - Transform an non-`None` output value into a JSON-serializable object. + Transform an output value into a JSON-serializable object. - If an `.render()` method is not implemented, this method **must** be + If a `.render()` method is not implemented, this method **must** be implemented. When requested, the `.render()` method's default behavior will resolve 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, From 1edbb8167435ec34c2e9232afed5b588c30092b2 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 19 Jan 2024 15:33:39 -0500 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Carson Sievert --- shiny/render/renderer/_renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 71cf8e8bf..a02f3ded2 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -280,10 +280,10 @@ async def render(self) -> Jsonifiable: Renders the output value function. This method is called when the renderer is requested to render its output. It - should resolve the value function `.fn` and return a (most likely) transformed + should execute the value function `.fn` and return a (most likely) transformed value. - The current implementation is to resolve the value function. If the value is + The default implementation is to resolve the value function. If the value is `None`, `None` will be returned. If the value is not `None`, the `.transform()` method will be called to transform the value into a JSON-serializable object. """ From 12af22c6a048ae7ee24797935315cc9cd404716d Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 19 Jan 2024 16:01:19 -0500 Subject: [PATCH 4/4] Update docs --- shiny/render/renderer/_renderer.py | 57 ++++++++++++++++++------------ 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 71cf8e8bf..e8f5be8e0 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -115,12 +115,10 @@ class Renderer(Generic[IT]): 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: `.render(self)` - and `.auto_output_ui(self, id: str)`. + 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)`. - * The `render` method is responsible for retrieving the value from the value - function and performing any transformations to be the return value - JSON-serializable. (`None` is a valid return value.) * 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 @@ -128,6 +126,12 @@ class Renderer(Generic[IT]): 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? @@ -257,35 +261,42 @@ async def transform(self, value: IT) -> Jsonifiable: """ Transform an output value into a JSON-serializable object. - If a `.render()` method is not implemented, this method **must** be - implemented. When requested, the `.render()` method's default behavior will - resolve 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. If both are - implemented, `.render()` will be used. + When subclassing `Renderer`, this method can be implemented to transform + non-`None` values into a JSON-serializable object. + + 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`. (common)\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)\n" - "By default, the `render` retrieves the value and then calls `transform`" - " method on non-`None` values." + " retrieved (`self._fn()`) and processed. (rare)" ) async def render(self) -> Jsonifiable: """ Renders the output value function. - This method is called when the renderer is requested to render its output. It - should resolve the value function `.fn` and return a (most likely) transformed - value. + This method is called when the renderer is requested to render its output. + + The `Renderer`'s `render` method 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. - The current implementation is to resolve the value function. If the value is - `None`, `None` will be returned. If the value is not `None`, the `.transform()` - method will be called to transform the value into a JSON-serializable object. + 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() @@ -308,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