From f41c020798eba37910bd35f1ae9e61c86993b641 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 15 Dec 2023 16:40:51 -0500 Subject: [PATCH 01/77] api!: Drop `shiny.render.RenderFunction` and `shiny.render.RenderFunctionAsync` --- CHANGELOG.md | 2 + shiny/render/__init__.py | 4 -- shiny/render/_deprecated.py | 79 ------------------------ shiny/render/transformer/_transformer.py | 3 - 4 files changed, 2 insertions(+), 86 deletions(-) delete mode 100644 shiny/render/_deprecated.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed31cc7b..3edcf8c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `ui.layout_columns()` for creating responsive column-forward layouts based on Bootstrap's 12-column CSS Grid. (#856) * Added support for Shiny Express apps, which has a simpler, easier-to-use API than the existing API (Shiny Core). Please note that this API is still experimental and may change. (#767) +* `shiny.render.RenderFunction` and `shiny.render.RenderFunctionAsync` have been removed. They were deprecated in v0.6.0. Instead, please use `shiny.render.output_transformer`. (TODO-barret) + ### Bug fixes * Fix support for `shiny.ui.accordion(multiple=)` (#799). diff --git a/shiny/render/__init__.py b/shiny/render/__init__.py index 8e9d32551..ffd4460a5 100644 --- a/shiny/render/__init__.py +++ b/shiny/render/__init__.py @@ -11,10 +11,6 @@ DataTable, data_frame, ) -from ._deprecated import ( # noqa: F401 - RenderFunction, # pyright: ignore[reportUnusedImport] - RenderFunctionAsync, # pyright: ignore[reportUnusedImport] -) from ._display import ( display, ) diff --git a/shiny/render/_deprecated.py b/shiny/render/_deprecated.py deleted file mode 100644 index 01b957e49..000000000 --- a/shiny/render/_deprecated.py +++ /dev/null @@ -1,79 +0,0 @@ -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 diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 0392f4d97..cd85c983a 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -800,6 +800,3 @@ async def resolve_value_fn(value_fn: ValueFn[IT]) -> IT: # To avoid duplicate work just for a typeguard, we cast the function value_fn = cast(ValueFnSync[IT], value_fn) return value_fn() - - -R = TypeVar("R") From 98483b8fa9d4ce50be0358fed64310b4139b57c7 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 15 Dec 2023 16:46:48 -0500 Subject: [PATCH 02/77] api!: Deprecate `render.transformer.resolve_value_fn`; Drop `render. OutputRendererSync`, `render. OutputRendererAsync` --- CHANGELOG.md | 4 + shiny/render/transformer/__init__.py | 6 +- shiny/render/transformer/_transformer.py | 293 +++++++++++------------ 3 files changed, 145 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3edcf8c37..8781a04ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `shiny.render.RenderFunction` and `shiny.render.RenderFunctionAsync` have been removed. They were deprecated in v0.6.0. Instead, please use `shiny.render.output_transformer`. (TODO-barret) +* `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. This method has been deprecated. Please change your code from `value = await resolve_value_fn(_fn)` to `value = await _fn()`. (TODO-barret) + +* `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. (TODO-barret) + ### Bug fixes * Fix support for `shiny.ui.accordion(multiple=)` (#799). diff --git a/shiny/render/transformer/__init__.py b/shiny/render/transformer/__init__.py index f28d91f1a..9df846c2d 100644 --- a/shiny/render/transformer/__init__.py +++ b/shiny/render/transformer/__init__.py @@ -4,14 +4,13 @@ OutputRenderer, output_transformer, is_async_callable, - resolve_value_fn, ValueFn, + ValueFnApp, # pyright: ignore[reportUnusedImport] ValueFnSync, # pyright: ignore[reportUnusedImport] 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] ) __all__ = ( @@ -21,5 +20,4 @@ "ValueFn", "output_transformer", "is_async_callable", - "resolve_value_fn", ) diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index cd85c983a..dd19cef4b 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -1,5 +1,17 @@ from __future__ import annotations +# TODO-barret; √ - Allow for transformer to be async +# TODO-barret; √ Remove OutputRendererSync/Async ? +# TODO-barret; √ Use single `await run()` method in session.py +# TODO-barret; √ remove await docs caveats for output renderer / transformer +# TODO-barret; display works with async? +# TODO-barret; plot stuff with seaborn +# TODO-barret; Convey the original function was async or not? +# TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` + +# TODO-barret; Simplified renderer with no param support + + # TODO-future: docs; 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 @@ -22,7 +34,6 @@ ) import inspect -from abc import ABC, abstractmethod from typing import ( TYPE_CHECKING, Awaitable, @@ -42,9 +53,10 @@ if TYPE_CHECKING: from ...session import Session +from ..._deprecated import warn_deprecated from ..._docstring import add_example from ..._typing_extensions import Concatenate, ParamSpec -from ..._utils import is_async_callable, run_coro_sync +from ..._utils import is_async_callable, wrap_async from ...types import MISSING # Input type for the user-spplied function that is passed to a render.xx @@ -149,7 +161,13 @@ def inner(*args: P.args, **kwargs: P.kwargs) -> TransformerParams[P]: App-supplied output value function which returns type `IT`. This function is asynchronous. """ -ValueFn = Union[ValueFnSync[IT], ValueFnAsync[IT]] +ValueFn = ValueFnAsync[IT] +""" +App-supplied output value function which returns type `IT`. This function is always +asyncronous as the original app-supplied function possibly wrapped to execute +asynchonously. +""" +ValueFnApp = Union[ValueFnSync[IT], ValueFnAsync[IT]] """ App-supplied output value function which returns type `IT`. This function can be synchronous or asynchronous. @@ -160,10 +178,7 @@ 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 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. +be defined as an asynchronous function. """ DefaultUIFn = Callable[[str], Union[TagList, Tag, MetadataNode, str]] @@ -173,7 +188,7 @@ def inner(*args: P.args, **kwargs: P.kwargs) -> TransformerParams[P]: ] -class OutputRenderer(Generic[OT], ABC): +class OutputRenderer(Generic[OT]): """ Output Renderer @@ -182,11 +197,7 @@ class OutputRenderer(Generic[OT], ABC): :class:`~shiny.Outputs` output value. When the `.__call__` method is invoked, the transform function (`transform_fn`) - (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. + (typically defined by package authors) is invoked. The transform function (`transform_fn`) is given `meta` information (:class:`~shiny.render.transformer.TranformerMetadata`), the (app-supplied) value @@ -209,25 +220,23 @@ class OutputRenderer(Generic[OT], ABC): * 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` """ - @abstractmethod - def __call__(self) -> OT: + async def __call__(self) -> OT: """ - Executes the output renderer as a function. Must be implemented by subclasses. + 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. """ - ... + return await self._run() def __init__( self, *, - value_fn: ValueFn[IT], + value_fn: ValueFnApp[IT], transform_fn: TransformFn[IT, P, OT], params: TransformerParams[P], default_ui: Optional[DefaultUIFnImpl] = None, @@ -240,10 +249,7 @@ 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. 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. + `OT`. The `params` will used as variadic keyword arguments. params App-provided parameters for the transform function (`transform_fn`). default_ui @@ -258,11 +264,18 @@ def __init__( if not is_async_callable(transform_fn): raise TypeError( - self.__class__.__name__ - + " requires an async tranformer function (`transform_fn`)" + """\ + OutputRenderer requires an async tranformer function (`transform_fn`). + Please define your transform function as asynchronous. Ex `async def my_transformer(....` + """ ) - self._value_fn = value_fn + # Upgrade value function to be async; + # Calling an async function has a ~35ns overhead (barret's machine) + # Checking if a function is async has a 180+ns overhead (barret's machine) + # -> It is faster to always call an async function than to always check if it is async + # Always being async simplifies the execution + self._value_fn: ValueFn[IT] = wrap_async(value_fn) self._transformer = transform_fn self._params = params self.default_ui = default_ui @@ -322,7 +335,7 @@ async def _run(self) -> OT: ret = await self._transformer( # TransformerMetadata self._meta(), - # Callable[[], Awaitable[IT]] | Callable[[], IT] + # Callable[[], Awaitable[IT]] self._value_fn, # P *self._params.args, @@ -365,90 +378,78 @@ def _render_default(self) -> TagList | Tag | MetadataNode | str: ) -# 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[DefaultUIFnImpl] = 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 == Renderer - 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[DefaultUIFnImpl] = 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 == Renderer - 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() +# # 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[DefaultUIFnImpl] = 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 == Renderer +# super().__init__( +# value_fn=value_fn, +# transform_fn=transform_fn, +# params=params, +# default_ui=default_ui, +# default_ui_passthrough_args=default_ui_passthrough_args, +# ) + + +# # 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[DefaultUIFnImpl] = 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 == Renderer +# super().__init__( +# value_fn=value_fn, +# transform_fn=transform_fn, +# params=params, +# default_ui=default_ui, +# default_ui_passthrough_args=default_ui_passthrough_args, +# ) # ====================================================================================== @@ -467,7 +468,7 @@ def _assert_transformer(transform_fn: TransformFn[IT, P, OT]) -> None: if len(params) < 2: raise TypeError( "`transformer=` must have 2 positional parameters which have type " - "`TransformerMetadata` and `RenderFnAsync` respectively" + "`TransformerMetadata` and `ValueFn` respectively" ) for i, param in zip(range(len(params)), params.values()): @@ -514,7 +515,7 @@ def _assert_transformer(transform_fn: TransformFn[IT, P, OT]) -> None: # Signature of a renderer decorator function -OutputRendererDecorator = Callable[[ValueFn[IT]], OutputRenderer[OT]] +OutputRendererDecorator = Callable[[ValueFnApp[IT]], OutputRenderer[OT]] """ Decorator function that takes the output value function (then calls it and transforms the value) and returns an :class:`~shiny.render.transformer.OutputRenderer`. @@ -525,7 +526,7 @@ def _assert_transformer(transform_fn: TransformFn[IT, P, OT]) -> None: # Without parens returns a `OutputRendererDeco[IT, OT]` OutputTransformerFn = Callable[ [ - Optional[ValueFn[IT]], + Optional[ValueFnApp[IT]], TransformerParams[P], ], Union[OutputRenderer[OT], OutputRendererDecorator[IT, OT]], @@ -581,7 +582,7 @@ def params( def __call__( self, - value_fn: ValueFn[IT] | None, + value_fn: ValueFnApp[IT] | None, params: TransformerParams[P] | None = None, ) -> OutputRenderer[OT] | OutputRendererDecorator[IT, OT]: if params is None: @@ -599,7 +600,7 @@ def __init__( fn: OutputTransformerFn[IT, P, OT], ) -> None: self._fn = fn - self.ValueFn = ValueFn[IT] + self.ValueFn = ValueFnApp[IT] self.OutputRenderer = OutputRenderer[OT] self.OutputRendererDecorator = OutputRendererDecorator[IT, OT] @@ -681,12 +682,6 @@ 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 ---------- @@ -716,30 +711,19 @@ def output_transformer_impl( _assert_transformer(transform_fn) def renderer_decorator( - value_fn: ValueFn[IT] | None, + value_fn: ValueFnApp[IT] | None, params: TransformerParams[P], ) -> OutputRenderer[OT] | OutputRendererDecorator[IT, OT]: def as_value_fn( - fn: ValueFn[IT], + fn: ValueFnApp[IT], ) -> OutputRenderer[OT]: - 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, - ) + return OutputRenderer( + value_fn=fn, + transform_fn=transform_fn, + params=params, + default_ui=default_ui, + default_ui_passthrough_args=default_ui_passthrough_args, + ) if value_fn is None: return as_value_fn @@ -754,9 +738,12 @@ def as_value_fn( return output_transformer_impl -async def resolve_value_fn(value_fn: ValueFn[IT]) -> IT: +async def resolve_value_fn(value_fn: ValueFnApp[IT]) -> IT: """ - Resolve the value function + Soft deprecated. Resolve the value function + + Deprecated: v0.7.0 - This function is no longer needed as all value functions are + now async for consistency and speed. This function is used to resolve the value function (`value_fn`) to an object of type `IT`. If the value function is asynchronous, it will be awaited. If the value @@ -778,11 +765,6 @@ 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 @@ -794,6 +776,9 @@ async def resolve_value_fn(value_fn: ValueFn[IT]) -> IT: : The resolved value from `value_fn`. """ + warn_deprecated( + "`resolve_value_fn()` is unnecessary when resolving the value function in a custom render method. Now, the value function is always async. `resolve_value_fn()` will be removed in a future release." + ) if is_async_callable(value_fn): return await value_fn() else: From fbf4b262928508bd7b34aa8da38c5f54707e746d Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 15 Dec 2023 16:47:29 -0500 Subject: [PATCH 03/77] Remove usage of `render_value_fn` --- shiny/api-examples/output_transformer/app.py | 14 ++------------ shiny/render/_dataframe.py | 9 ++------- shiny/render/_render.py | 11 +++++------ .../js-output/custom_component/custom_component.py | 9 ++------- .../shiny/server/output_transformer/app.py | 3 +-- 5 files changed, 12 insertions(+), 34 deletions(-) diff --git a/shiny/api-examples/output_transformer/app.py b/shiny/api-examples/output_transformer/app.py index 09bc1b51e..84988789b 100644 --- a/shiny/api-examples/output_transformer/app.py +++ b/shiny/api-examples/output_transformer/app.py @@ -3,12 +3,7 @@ from typing import Literal, overload from shiny import App, Inputs, Outputs, Session, ui -from shiny.render.transformer import ( - TransformerMetadata, - ValueFn, - output_transformer, - resolve_value_fn, -) +from shiny.render.transformer import TransformerMetadata, ValueFn, output_transformer ####### # Package authors can create their own output transformer methods by leveraging @@ -32,12 +27,7 @@ async def CapitalizeTransformer( to: Literal["upper", "lower"] = "upper", ) -> str | None: # Get the value - value = await resolve_value_fn(_fn) - # Equvalent to: - # if shiny.render.transformer.is_async_callable(_fn): - # value = await _fn() - # else: - # value = _fn() + value = await _fn() # Render nothing if `value` is `None` if value is None: diff --git a/shiny/render/_dataframe.py b/shiny/render/_dataframe.py index 2e3af17ce..473a2e2bd 100644 --- a/shiny/render/_dataframe.py +++ b/shiny/render/_dataframe.py @@ -16,12 +16,7 @@ from .. import ui from .._docstring import add_example from ._dataframe_unsafe import serialize_numpy_dtypes -from .transformer import ( - TransformerMetadata, - ValueFn, - output_transformer, - resolve_value_fn, -) +from .transformer import TransformerMetadata, ValueFn, output_transformer if TYPE_CHECKING: import pandas as pd @@ -230,7 +225,7 @@ async def DataFrameTransformer( _meta: TransformerMetadata, _fn: ValueFn[DataFrameResult | None], ) -> object | None: - x = await resolve_value_fn(_fn) + x = await _fn() if x is None: return None diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 9c0d986e0..4d4402f82 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -45,7 +45,6 @@ ValueFn, is_async_callable, output_transformer, - resolve_value_fn, ) # ====================================================================================== @@ -58,7 +57,7 @@ async def TextTransformer( _meta: TransformerMetadata, _fn: ValueFn[str | None], ) -> str | None: - value = await resolve_value_fn(_fn) + value = await _fn() if value is None: return None return str(value) @@ -156,7 +155,7 @@ def container_size(dimension: Literal["width", "height"]) -> float: ) # Call the user function to get the plot object. - x = await resolve_value_fn(_fn) + x = await _fn() # Note that x might be None; it could be a matplotlib.pyplot @@ -308,7 +307,7 @@ async def ImageTransformer( *, delete_file: bool = False, ) -> ImgData | None: - res = await resolve_value_fn(_fn) + res = await _fn() if res is None: return None @@ -396,7 +395,7 @@ async def TableTransformer( border: int = 0, **kwargs: object, ) -> RenderedDeps | None: - x = await resolve_value_fn(_fn) + x = await _fn() if x is None: return None @@ -520,7 +519,7 @@ async def UiTransformer( _meta: TransformerMetadata, _fn: ValueFn[TagChild], ) -> RenderedDeps | None: - ui = await resolve_value_fn(_fn) + ui = await _fn() if ui is None: return None diff --git a/shiny/templates/package-templates/js-output/custom_component/custom_component.py b/shiny/templates/package-templates/js-output/custom_component/custom_component.py index 789e73371..d2f9fda07 100644 --- a/shiny/templates/package-templates/js-output/custom_component/custom_component.py +++ b/shiny/templates/package-templates/js-output/custom_component/custom_component.py @@ -4,12 +4,7 @@ from htmltools import HTMLDependency, Tag from shiny.module import resolve_id -from shiny.render.transformer import ( - TransformerMetadata, - ValueFn, - output_transformer, - resolve_value_fn, -) +from shiny.render.transformer import TransformerMetadata, ValueFn, output_transformer # This object is used to let Shiny know where the dependencies needed to run # our component all live. In this case, we're just using a single javascript @@ -30,7 +25,7 @@ async def render_custom_component( _meta: TransformerMetadata, _fn: ValueFn[int | None], ): - res = await resolve_value_fn(_fn) + res = await _fn() if res is None: return None diff --git a/tests/playwright/shiny/server/output_transformer/app.py b/tests/playwright/shiny/server/output_transformer/app.py index 0c39cb48b..f30d1e9b3 100644 --- a/tests/playwright/shiny/server/output_transformer/app.py +++ b/tests/playwright/shiny/server/output_transformer/app.py @@ -8,7 +8,6 @@ ValueFn, is_async_callable, output_transformer, - resolve_value_fn, ) @@ -19,7 +18,7 @@ async def TestTextTransformer( *, extra_txt: Optional[str] = None, ) -> str | None: - value = await resolve_value_fn(_fn) + value = await _fn() value = str(value) value += "; " value += "async" if is_async_callable(_fn) else "sync" From f54234f4c6942f289db4a651f8166507772b3bb6 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 15 Dec 2023 16:47:53 -0500 Subject: [PATCH 04/77] Update tests --- tests/playwright/examples/test_examples.py | 17 ++++++++ .../test_output_transformer.py | 6 +-- tests/pytest/test_output_transformer.py | 41 +++++++++++-------- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/tests/playwright/examples/test_examples.py b/tests/playwright/examples/test_examples.py index 95b8ff02a..e3cfcc9ed 100644 --- a/tests/playwright/examples/test_examples.py +++ b/tests/playwright/examples/test_examples.py @@ -50,6 +50,23 @@ def get_apps(path: str) -> typing.List[str]: "RuntimeWarning: divide by zero encountered", "UserWarning: This figure includes Axes that are not compatible with tight_layout", ], + # Remove after shinywidgets accepts `resolve_value_fn()` PR + "airmass": [ + "ShinyDeprecationWarning: `resolve_value_fn()`", + "value = await resolve_value_fn(_fn)", + ], + "multi-page": [ + "ShinyDeprecationWarning: `resolve_value_fn()`", + "value = await resolve_value_fn(_fn)", + ], + "model-score": [ + "ShinyDeprecationWarning: `resolve_value_fn()`", + "value = await resolve_value_fn(_fn)", + ], + "data_frame": [ + "ShinyDeprecationWarning: `resolve_value_fn()`", + "value = await resolve_value_fn(_fn)", + ], } app_allow_external_errors: typing.List[str] = [ # plotnine: https://github.com/has2k1/plotnine/issues/713 diff --git a/tests/playwright/shiny/server/output_transformer/test_output_transformer.py b/tests/playwright/shiny/server/output_transformer/test_output_transformer.py index e2c1fc5f7..858ecd2af 100644 --- a/tests/playwright/shiny/server/output_transformer/test_output_transformer.py +++ b/tests/playwright/shiny/server/output_transformer/test_output_transformer.py @@ -6,9 +6,9 @@ def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) - OutputTextVerbatim(page, "t1").expect_value("t1; no call; sync") + OutputTextVerbatim(page, "t1").expect_value("t1; no call; async") OutputTextVerbatim(page, "t2").expect_value("t2; no call; async") - OutputTextVerbatim(page, "t3").expect_value("t3; call; sync") + OutputTextVerbatim(page, "t3").expect_value("t3; call; async") OutputTextVerbatim(page, "t4").expect_value("t4; call; async") - OutputTextVerbatim(page, "t5").expect_value("t5; call; sync; w/ extra_txt") + OutputTextVerbatim(page, "t5").expect_value("t5; call; async; w/ extra_txt") OutputTextVerbatim(page, "t6").expect_value("t6; call; async; w/ extra_txt") diff --git a/tests/pytest/test_output_transformer.py b/tests/pytest/test_output_transformer.py index 04506b8e8..d684003da 100644 --- a/tests/pytest/test_output_transformer.py +++ b/tests/pytest/test_output_transformer.py @@ -5,6 +5,7 @@ import pytest +from shiny._deprecated import ShinyDeprecationWarning from shiny._utils import is_async_callable from shiny.render.transformer import ( TransformerMetadata, @@ -196,15 +197,16 @@ def render_fn_sync(*args: str): # "Currently, `ValueFn` can not be truly async and "support sync render methods" @pytest.mark.asyncio -async def test_renderer_handler_fn_can_be_async(): +async def test_renderer_handler_or_transform_fn_can_be_async(): @output_transformer async def AsyncTransformer( _meta: TransformerMetadata, _fn: ValueFn[str], ) -> str: + assert is_async_callable(_fn) # Actually sleep to test that the handler is truly async await asyncio.sleep(0) - ret = await resolve_value_fn(_fn) + ret = await _fn() return ret # ## Setup overloads ============================================= @@ -236,16 +238,11 @@ def app_render_fn() -> str: None, # pyright: ignore[reportGeneralTypeIssues] "renderer_sync", ) - if is_async_callable(renderer_sync): - raise RuntimeError("Expected `renderer_sync` to be a sync function") + # All renderers are async in execution. + assert is_async_callable(renderer_sync) - # !! This line is currently not possible !! - try: - ret = renderer_sync() - raise Exception("Expected an exception to occur while calling `renderer_sync`") - assert ret == test_val - except RuntimeError as e: - assert "async function yielded control" in str(e) + val = await renderer_sync() + assert val == test_val # ## Test Async: √ ============================================= @@ -279,7 +276,7 @@ async def YieldTransformer( if is_async_callable(_fn): # Actually sleep to test that the handler is truly async await asyncio.sleep(0) - ret = await resolve_value_fn(_fn) + ret = await _fn() return ret # ## Setup overloads ============================================= @@ -311,10 +308,9 @@ def app_render_fn() -> str: None, # pyright: ignore[reportGeneralTypeIssues] "renderer_sync", ) - if is_async_callable(renderer_sync): - raise RuntimeError("Expected `renderer_sync` to be a sync function") + assert is_async_callable(renderer_sync) - ret = renderer_sync() + ret = await renderer_sync() assert ret == test_val # ## Test Async: √ ============================================= @@ -330,8 +326,19 @@ async def async_app_render_fn() -> str: None, # pyright: ignore[reportGeneralTypeIssues] "renderer_async", ) - if not is_async_callable(renderer_async): - raise RuntimeError("Expected `renderer_async` to be a coro function") + assert is_async_callable(renderer_async) ret = await renderer_async() assert ret == async_test_val + + +@pytest.mark.asyncio +async def test_resolve_value_fn_is_deprecated(): + with pytest.warns(ShinyDeprecationWarning): + test_val = 42 + + async def value_fn(): + return test_val + + ret = await resolve_value_fn(value_fn) + assert test_val == ret From 1dddec016e7a3ce2a222bae3b1f67cb936c8e416 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 15 Dec 2023 16:48:13 -0500 Subject: [PATCH 05/77] Only use async execution for output renderer --- shiny/session/_session.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/shiny/session/_session.py b/shiny/session/_session.py index b46eec44c..986e227cc 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -1019,10 +1019,7 @@ async def output_obs(): message: dict[str, Optional[OT]] = {} try: - if _utils.is_async_callable(renderer_fn): - message[output_name] = await renderer_fn() - else: - message[output_name] = renderer_fn() + message[output_name] = await renderer_fn() except SilentCancelOutputException: return except SilentException: From 71198946f737fc0c9a7b7b88e93a963069482452 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 18 Dec 2023 13:45:05 -0500 Subject: [PATCH 06/77] Update _display.py --- shiny/render/_display.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/shiny/render/_display.py b/shiny/render/_display.py index b9eb002ee..229152c74 100644 --- a/shiny/render/_display.py +++ b/shiny/render/_display.py @@ -7,9 +7,10 @@ from htmltools import TagAttrValue, TagFunction, TagList, wrap_displayhook_handler from .. import ui as _ui +from .._utils import run_coro_sync from ..session._utils import RenderedDeps from .transformer import ( - OutputRendererSync, + OutputRenderer, TransformerMetadata, TransformerParams, ValueFn, @@ -17,7 +18,7 @@ ) -async def DisplayTransformer( +async def display_transformer( _meta: TransformerMetadata, _fn: ValueFn[None], *, @@ -31,10 +32,11 @@ async def DisplayTransformer( orig_displayhook = sys.displayhook sys.displayhook = wrap_displayhook_handler(results.append) try: - x = _fn() - if inspect.iscoroutine(x): - raise TypeError( - "@render.display does not support async functions. Use @render.ui instead." + # We check for sync function below. Therefore, we can run `run_coro_sync` here. + ret = run_coro_sync(_fn()) + if ret is not None: + raise RuntimeError( + "@render.display functions should not return values. (`None` is allowed)." ) finally: sys.displayhook = orig_displayhook @@ -45,7 +47,7 @@ async def DisplayTransformer( ) -DisplayRenderer = OutputRendererSync[Union[RenderedDeps, None]] +DisplayRenderer = OutputRenderer[Union[RenderedDeps, None]] @overload @@ -105,16 +107,20 @@ def display( A decorator for a function whose top-level expressions will be displayed as UI. """ - def impl(fn: ValueFnSync[None]) -> OutputRendererSync[RenderedDeps | None]: + def impl(fn: ValueFnSync[None]) -> OutputRenderer[RenderedDeps | None]: + if inspect.iscoroutine(fn): + raise TypeError( + "@render.display does not support async functions. Use @render.ui instead." + ) from shiny.express.display_decorator._display_body import ( display_body_unwrap_inplace, ) fn = display_body_unwrap_inplace()(fn) - return OutputRendererSync( - fn, - DisplayTransformer, - TransformerParams( + return OutputRenderer( + value_fn=fn, + transform_fn=display_transformer, + params=TransformerParams( inline=inline, container=container, fill=fill, From 14c7b4b84f923438888b0c2e069fdc7f57b82db5 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 18 Dec 2023 13:46:03 -0500 Subject: [PATCH 07/77] Add `value_fn_is_async` to `TransformerMetadata` --- shiny/render/transformer/_transformer.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index dd19cef4b..f724b2d88 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -1,12 +1,5 @@ from __future__ import annotations -# TODO-barret; √ - Allow for transformer to be async -# TODO-barret; √ Remove OutputRendererSync/Async ? -# TODO-barret; √ Use single `await run()` method in session.py -# TODO-barret; √ remove await docs caveats for output renderer / transformer -# TODO-barret; display works with async? -# TODO-barret; plot stuff with seaborn -# TODO-barret; Convey the original function was async or not? # TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` # TODO-barret; Simplified renderer with no param support @@ -89,6 +82,7 @@ class TransformerMetadata(NamedTuple): session: Session name: str + value_fn_is_async: bool # Motivation for using this class: @@ -275,6 +269,7 @@ def __init__( # Checking if a function is async has a 180+ns overhead (barret's machine) # -> It is faster to always call an async function than to always check if it is async # Always being async simplifies the execution + self._value_fn_is_async = is_async_callable(value_fn) self._value_fn: ValueFn[IT] = wrap_async(value_fn) self._transformer = transform_fn self._params = params @@ -316,6 +311,7 @@ def _meta(self) -> TransformerMetadata: return TransformerMetadata( session=self._session, name=self._name, + value_fn_is_async=self._value_fn_is_async, ) async def _run(self) -> OT: From 365dc51014b7eb03683a8aa7ae9f7172255bc12e Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 18 Dec 2023 13:46:35 -0500 Subject: [PATCH 08/77] Use new `value_fn_is_async` meta key for `render.plot` --- shiny/render/_render.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 4d4402f82..fd809bb1d 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -40,12 +40,7 @@ try_render_pil, try_render_plotnine, ) -from .transformer import ( - TransformerMetadata, - ValueFn, - is_async_callable, - output_transformer, -) +from .transformer import TransformerMetadata, ValueFn, output_transformer # ====================================================================================== # RenderText @@ -118,7 +113,7 @@ async def PlotTransformer( height: float | None | MISSING_TYPE = MISSING, **kwargs: object, ) -> ImgData | None: - is_userfn_async = is_async_callable(_fn) + is_userfn_async = _meta.value_fn_is_async name = _meta.name session = _meta.session @@ -293,7 +288,13 @@ def plot( ~shiny.ui.output_plot ~shiny.render.image """ return PlotTransformer( - _fn, PlotTransformer.params(alt=alt, width=width, height=height, **kwargs) + _fn, + PlotTransformer.params( + alt=alt, + width=width, + height=height, + **kwargs, + ), ) From 302c10b4cae8dfb376d6728f1cf4b3db4f0dc6bd Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 18 Dec 2023 13:46:56 -0500 Subject: [PATCH 09/77] Add more errors to ignore. Ignore blank lines --- tests/playwright/examples/test_examples.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/playwright/examples/test_examples.py b/tests/playwright/examples/test_examples.py index e3cfcc9ed..24ac844fc 100644 --- a/tests/playwright/examples/test_examples.py +++ b/tests/playwright/examples/test_examples.py @@ -55,6 +55,10 @@ def get_apps(path: str) -> typing.List[str]: "ShinyDeprecationWarning: `resolve_value_fn()`", "value = await resolve_value_fn(_fn)", ], + "brownian": [ + "ShinyDeprecationWarning: `resolve_value_fn()`", + "value = await resolve_value_fn(_fn)", + ], "multi-page": [ "ShinyDeprecationWarning: `resolve_value_fn()`", "value = await resolve_value_fn(_fn)", @@ -62,11 +66,14 @@ def get_apps(path: str) -> typing.List[str]: "model-score": [ "ShinyDeprecationWarning: `resolve_value_fn()`", "value = await resolve_value_fn(_fn)", + "ShinyDeprecationWarning:", + "`resolve_value_fn()`", ], "data_frame": [ "ShinyDeprecationWarning: `resolve_value_fn()`", "value = await resolve_value_fn(_fn)", ], + "render_display": ["Detected Shiny Express app. "], } app_allow_external_errors: typing.List[str] = [ # plotnine: https://github.com/has2k1/plotnine/issues/713 @@ -193,7 +200,9 @@ def on_console_msg(msg: ConsoleMessage) -> None: app_allowable_errors = [] # If all errors are not allowed, check for unexpected errors - if isinstance(app_allowable_errors, list): + if app_allowable_errors is not True: + if isinstance(app_allowable_errors, str): + app_allowable_errors = [app_allowable_errors] app_allowable_errors = ( # Remove ^INFO lines ["INFO:"] @@ -207,7 +216,8 @@ def on_console_msg(msg: ConsoleMessage) -> None: error_lines = [ line for line in error_lines - if not any([error_txt in line for error_txt in app_allowable_errors]) + if len(line.strip()) > 0 + and not any([error_txt in line for error_txt in app_allowable_errors]) ] if len(error_lines) > 0: print("\n".join(error_lines)) From 4670c178a42edd018da307255d05cf53a8b29d9e Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 18 Dec 2023 13:51:01 -0500 Subject: [PATCH 10/77] Update _quartodoc.yml --- docs/_quartodoc.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 9be9d456d..6503fb567 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -201,10 +201,7 @@ quartodoc: - 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 - title: Reactive programming @@ -309,6 +306,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: From 182cd0bd0aa23b6d5d6ce55dd8d889a46d1bd19b Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 18 Dec 2023 14:41:00 -0500 Subject: [PATCH 11/77] Update test_examples.py --- tests/playwright/examples/test_examples.py | 36 ++++++++-------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/tests/playwright/examples/test_examples.py b/tests/playwright/examples/test_examples.py index 24ac844fc..356ba65d4 100644 --- a/tests/playwright/examples/test_examples.py +++ b/tests/playwright/examples/test_examples.py @@ -39,6 +39,13 @@ def get_apps(path: str) -> typing.List[str]: "brownian": 250, "ui-func": 250, } +resolve_value_fn_errors = [ + "ShinyDeprecationWarning: `resolve_value_fn()`", + "value = await resolve_value_fn(_fn)", + "ShinyDeprecationWarning:", + "`resolve_value_fn()`", +] +express_warnings = ["Detected Shiny Express app. "] app_allow_shiny_errors: typing.Dict[ str, typing.Union[Literal[True], typing.List[str]] ] = { @@ -51,29 +58,12 @@ def get_apps(path: str) -> typing.List[str]: "UserWarning: This figure includes Axes that are not compatible with tight_layout", ], # Remove after shinywidgets accepts `resolve_value_fn()` PR - "airmass": [ - "ShinyDeprecationWarning: `resolve_value_fn()`", - "value = await resolve_value_fn(_fn)", - ], - "brownian": [ - "ShinyDeprecationWarning: `resolve_value_fn()`", - "value = await resolve_value_fn(_fn)", - ], - "multi-page": [ - "ShinyDeprecationWarning: `resolve_value_fn()`", - "value = await resolve_value_fn(_fn)", - ], - "model-score": [ - "ShinyDeprecationWarning: `resolve_value_fn()`", - "value = await resolve_value_fn(_fn)", - "ShinyDeprecationWarning:", - "`resolve_value_fn()`", - ], - "data_frame": [ - "ShinyDeprecationWarning: `resolve_value_fn()`", - "value = await resolve_value_fn(_fn)", - ], - "render_display": ["Detected Shiny Express app. "], + "airmass": [*resolve_value_fn_errors], + "brownian": [*resolve_value_fn_errors], + "multi-page": [*resolve_value_fn_errors], + "model-score": [*resolve_value_fn_errors], + "data_frame": [*resolve_value_fn_errors], + "render_display": [*express_warnings], } app_allow_external_errors: typing.List[str] = [ # plotnine: https://github.com/has2k1/plotnine/issues/713 From 8367c7a996fdb2956b6fcaf37bc4dab91924b954 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 22 Dec 2023 09:53:31 -0500 Subject: [PATCH 12/77] Commit all code just in case my computer breaks --- shiny/api-examples/output_transformer/app.py | 104 ++++++++ shiny/render/_render.py | 59 ++++- shiny/render/transformer/_transformer.py | 256 ++++++++++++++++++- 3 files changed, 414 insertions(+), 5 deletions(-) diff --git a/shiny/api-examples/output_transformer/app.py b/shiny/api-examples/output_transformer/app.py index 84988789b..88b28d810 100644 --- a/shiny/api-examples/output_transformer/app.py +++ b/shiny/api-examples/output_transformer/app.py @@ -4,6 +4,11 @@ from shiny import App, Inputs, Outputs, Session, ui from shiny.render.transformer import TransformerMetadata, ValueFn, output_transformer +from shiny.render.transformer._transformer import ( + output_transformer_no_params, + output_transformer_params, + output_transformer_simple, +) ####### # Package authors can create their own output transformer methods by leveraging @@ -14,6 +19,74 @@ ####### +@output_transformer_simple() +def render_caps_simple( + value: str, +) -> str: + """ + Barret - Render Caps docs (simple) + """ + # return [value.upper(), value.lower()] + return value.upper() + + +@output_transformer_simple() +def render_caps_simple2( + value: str, +) -> str: + """ + Barret - Render Caps docs (simple2) + """ + # return [value.upper(), value.lower()] + return value.upper() + + +@output_transformer_params() +async def render_caps( + # Contains information about the render call: `name` and `session` + _meta: TransformerMetadata, + # The app-supplied output value function + _fn: ValueFn[str | None], +) -> str | None: + """ + Barret - Render Caps docs no params + """ + # Get the value + value = await _fn() + + # Render nothing if `value` is `None` + if value is None: + return None + + return value.upper() + + +@output_transformer_params() +async def render_caps_params( + # Contains information about the render call: `name` and `session` + _meta: TransformerMetadata, + # The app-supplied output value function + _fn: ValueFn[str | None], + *, + to: Literal["upper", "lower"] = "upper", +) -> str | None: + """ + Barret - Render Caps docs params + """ + # Get the value + value = await _fn() + + # Render nothing if `value` is `None` + if value is None: + return None + + if to == "upper": + return value.upper() + if to == "lower": + return value.lower() + raise ValueError(f"Invalid value for `to`: {to}") + + # Create renderer components from the async handler function: `capitalize_components()` @output_transformer() async def CapitalizeTransformer( @@ -85,6 +158,9 @@ def render_capitalize( ) -> ( CapitalizeTransformer.OutputRenderer | CapitalizeTransformer.OutputRendererDecorator ): + """ + OldSchool - CapitalizeTransformer + """ return CapitalizeTransformer( _fn, CapitalizeTransformer.params(to=to), @@ -104,6 +180,14 @@ def render_capitalize( ui.output_text_verbatim("to_upper"), "To lower:", ui.output_text_verbatim("to_lower"), + "barret_caps:", + ui.output_text_verbatim("barret_caps"), + "barret_caps_simple:", + ui.output_text_verbatim("barret_caps_simple"), + "barret_caps_simple2:", + ui.output_text_verbatim("barret_caps_simple2"), + "barret_caps_params:", + ui.output_text_verbatim("barret_caps_params"), ) @@ -126,5 +210,25 @@ def to_upper(): async def to_lower(): return input.caption() + @render_caps() + def barret_caps(): + return input.caption() + + @render_caps_simple + def barret_caps_simple(): + return input.caption() + + @render_caps_simple2 + def barret_caps_simple2(): + return input.caption() + + @render_caps_params + def barret_caps_params(): + return input.caption() + + @render_caps_params(to="upper") + def barret_caps_params2(): + return input.caption() + app = App(app_ui, server) diff --git a/shiny/render/_render.py b/shiny/render/_render.py index fd809bb1d..cee3059ee 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -41,12 +41,65 @@ try_render_plotnine, ) from .transformer import TransformerMetadata, ValueFn, output_transformer +from .transformer._transformer import ( + output_transformer_params, + output_transformer_simple, +) # ====================================================================================== # RenderText # ====================================================================================== +@output_transformer_simple(default_ui=_ui.output_text_verbatim) +def text_simple( + value: str, +) -> str: + """ + Barret - Reactively render text. (simple) + """ + return str(value) + + +@text_simple +def foo() -> str: + return "foo" + + +@output_transformer_params(default_ui=_ui.output_text_verbatim) +async def text( + _meta: TransformerMetadata, + _fn: ValueFn[str | None], +) -> str | None: + """ + Barret - Reactively render text. + + Returns + ------- + : + A decorator for a function that returns a string. + + Tip + ---- + The name of the decorated function (or ``@output(id=...)``) should match the ``id`` + of a :func:`~shiny.ui.output_text` container (see :func:`~shiny.ui.output_text` for + example usage). + + See Also + -------- + ~shiny.ui.output_text + """ + value = await _fn() + if value is None: + return None + return str(value) + + +@text() +def foo2() -> str: + return "foo" + + @output_transformer(default_ui=_ui.output_text_verbatim) async def TextTransformer( _meta: TransformerMetadata, @@ -59,16 +112,16 @@ async def TextTransformer( @overload -def text() -> TextTransformer.OutputRendererDecorator: +def textOld() -> TextTransformer.OutputRendererDecorator: ... @overload -def text(_fn: TextTransformer.ValueFn) -> TextTransformer.OutputRenderer: +def textOld(_fn: TextTransformer.ValueFn) -> TextTransformer.OutputRenderer: ... -def text( +def textOld( _fn: TextTransformer.ValueFn | None = None, ) -> TextTransformer.OutputRenderer | TextTransformer.OutputRendererDecorator: """ diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index f724b2d88..52312e56e 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -2,8 +2,6 @@ # TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` -# TODO-barret; Simplified renderer with no param support - # TODO-future: docs; missing first paragraph from some classes: Example: TransformerMetadata. # No init method for TransformerParams. This is because the `DocClass` object does not @@ -20,6 +18,8 @@ # "ValueFnAsync", # "TransformFn", "output_transformer", + "output_transformer_no_params", + "output_transformer_simple", "is_async_callable", # "IT", # "OT", @@ -33,8 +33,10 @@ Callable, Dict, Generic, + List, NamedTuple, Optional, + Tuple, TypeVar, Union, cast, @@ -119,6 +121,7 @@ def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None: # Make sure there no `args` at run time! # This check is related to `_assert_transform_fn` not accepting any `args` if len(args) > 0: + print(args) raise RuntimeError("`args` should not be supplied") # `*args` must be defined with `**kwargs` (as per PEP612) @@ -734,6 +737,255 @@ def as_value_fn( return output_transformer_impl +# ====================================================================================== +# Simple transformer +# ====================================================================================== + +# TODO-barret; Requirements: +# * At app rendering, both parens and no parens must both work as expected +# * Add extra class info on the outputted function (ex .OutputRendererDecorator) + +# None + +# TODO-barret; Document internally: +# Things that break passing through docs: +# * Returning a overloads with no type in function +# * Return type contains a union of functions (that represent overloads) +# * Returning a callable class instance +# Returning type aliases works, even if the function signature is big! + +# # Simple transformer, no params +# * docs to be transferred +# * No parameters, -> no need for overloads! + +# # Simple dict transformer +# * Receives value and returns a dict + +R = TypeVar("R") +# # Does not work with function docs! +CallableDecoBad = Callable[P, R] | Callable[[], Callable[P, R]] +CallableDeco = Callable[[IT | None], OT | Callable[[IT], OT]] +TransformFnSimple = Callable[[TransformerMetadata, ValueFn[IT]], Awaitable[OT]] + + +class CallableDecoCls(Generic[IT, OT]): + def __init__(self, fn: Callable[[IT], OT]) -> None: + self._fn = fn + + async def __call__(self, fn: IT | None) -> OT | Callable[[IT], OT]: + if fn is None: + return self._fn + else: + return self._fn(fn) + # return await self._fn() + + +class OutputRendererSimple(OutputRenderer[OT]): + def __init__( + self, + *, + value_fn: ValueFnApp[IT], + transform_fn: TransformFnSimple[IT, OT], + default_ui: Optional[DefaultUIFnImpl] = None, + default_ui_passthrough_args: Optional[tuple[str, ...]] = None, + ) -> None: + super().__init__( + value_fn=value_fn, + transform_fn=transform_fn, + params=empty_params(), + default_ui=default_ui, + default_ui_passthrough_args=default_ui_passthrough_args, + ) + + +def output_transformer_no_params( + # transform_fn: TransformFnSimple[IT, OT], + # Require that all params are keyword arguments so that if a user calls `@output_transformer_no_params` (with no parens), an error is thrown + *, + default_ui: Optional[DefaultUIFn] = None, + default_ui_passthrough_args: Optional[tuple[str, ...]] = None, + # # No docs! + # ) -> CallableDecoBad[[ValueFnApp[IT]], OutputRendererSimple[OT]]: + # # Ugly signature, but it works + # ) -> Callable[ + # [ValueFnApp[IT] | None], + # OutputRendererSimple[OT] | Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]], + # ]: + # + # No Docs + # ) -> CallableDecoCls[ValueFnApp[IT], OutputRendererSimple[OT]]: + # Works! + # ) -> CallableDeco[ValueFnApp[IT], OutputRendererSimple[OT]]: + # Works! +) -> Callable[ + [TransformFnSimple[IT, OT]], Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]] +]: + def with_transformer( + transform_fn: TransformFnSimple[IT, OT], + ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]]: + def with_value_fn( + value_fn: ValueFnApp[IT], + ) -> OutputRendererSimple[OT]: + return OutputRendererSimple( + value_fn=value_fn, + transform_fn=transform_fn, + default_ui=default_ui, + default_ui_passthrough_args=default_ui_passthrough_args, + ) + + return with_value_fn + + return with_transformer + + # def renderer( + # fn: ValueFnApp[IT], + # ) -> OutputRendererSimple[OT]: + # return OutputRendererSimple[OT]( + # value_fn=fn, + # transform_fn=transform_fn, + # default_ui=default_ui, + # default_ui_passthrough_args=default_ui_passthrough_args, + # ) + + # # @overload + # # def renderer_impl() -> Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]]: + # # ... + + # # @overload + # # def renderer_impl( + # # fn: ValueFnApp[IT], + # # ) -> OutputRendererSimple[OT]: + # # ... + + # def renderer_impl( + # fn: ValueFnApp[IT] | None = None, + # ) -> ( + # OutputRendererSimple[OT] | Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]] + # ): + # if fn is None: + # return renderer + # else: + # return renderer(fn) + + # return renderer_impl + + +# https://github.com/python/cpython/blob/df1eec3dae3b1eddff819fd70f58b03b3fbd0eda/Lib/json/encoder.py#L77-L95 +# +-------------------+---------------+ +# | Python | JSON | +# +===================+===============+ +# | dict | object | +# +-------------------+---------------+ +# | list, tuple | array | +# +-------------------+---------------+ +# | str | string | +# +-------------------+---------------+ +# | int, float | number | +# +-------------------+---------------+ +# | True | true | +# +-------------------+---------------+ +# | False | false | +# +-------------------+---------------+ +# | None | null | +# +-------------------+---------------+ +JSONifiable = Union[ + str, + int, + float, + bool, + None, + List["JSONifiable"], + Tuple["JSONifiable"], + Dict[str, "JSONifiable"], +] + + +def output_transformer_simple( + *, + default_ui: Optional[DefaultUIFn] = None, + default_ui_passthrough_args: Optional[tuple[str, ...]] = None, +) -> Callable[ + [Callable[[IT], JSONifiable] | Callable[[IT], Awaitable[JSONifiable]]], + Callable[[ValueFnApp[IT]], OutputRendererSimple[JSONifiable]], +]: + def simple_transformer( + upgrade_fn: Callable[[IT], JSONifiable] | Callable[[IT], Awaitable[JSONifiable]] + ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JSONifiable]]: + upgrade_fn = wrap_async(upgrade_fn) + + async def transform_fn( + _meta: TransformerMetadata, + _fn: ValueFn[IT | None], + ) -> JSONifiable: + res = await _fn() + if res is None: + return None + + ret = await upgrade_fn(res) + return ret + + deco = output_transformer_no_params( + default_ui=default_ui, + default_ui_passthrough_args=default_ui_passthrough_args, + ) + return deco(transform_fn) + + return simple_transformer + + +# TODO-barret; Allow for no parens when calling the renderer in the app. +# TODO-barret; Allow for no parens when creating the renderer. But discourage the pkg author. +# TODO-barret; Add extra fields so that existing renderers can be used? +# TODO-barret; Replace the original `output_transformer` with this one? +# TODO-barret; Document `output_transformer_simple` +# TODO-barret; Can the return type of the output_transformer_simple be OT and not JSONifiable? (Just make sure it is a subset of JSONifiable) + + +def output_transformer_params( + # Require that all params are keyword arguments so that if a user calls `@output_transformer_no_params` (with no parens), an error is thrown + *, + default_ui: Optional[DefaultUIFn] = None, + default_ui_passthrough_args: Optional[tuple[str, ...]] = None, +) -> Callable[ + [TransformFn[IT, P, OT]], + Callable[P, Callable[[ValueFnApp[IT]], OutputRenderer[OT]]], +]: + def with_transformer( + transform_fn: TransformFn[IT, P, OT], + ) -> Callable[P, Callable[[ValueFnApp[IT]], OutputRenderer[OT]]]: + def with_args( + *args: P.args, + **kwargs: P.kwargs, + # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: + ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: + if len(args) > 0: + raise RuntimeError( + "`*args` should not be supplied." + "\nDid you forget to add `()` to your render decorator?" + ) + params = TransformerParams[P](*args, **kwargs) + + def with_value_fn( + value_fn: ValueFnApp[IT], + ) -> OutputRenderer[OT]: + return OutputRenderer( + value_fn=value_fn, + transform_fn=transform_fn, + params=params, + default_ui=default_ui, + default_ui_passthrough_args=default_ui_passthrough_args, + ) + + return with_value_fn + + return with_args + + # TODO-barret; Add more here + # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] + # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] + return with_transformer + + async def resolve_value_fn(value_fn: ValueFnApp[IT]) -> IT: """ Soft deprecated. Resolve the value function From 3979ed8a00dba9e1ad89a3df20083c5c52d7428f Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 2 Jan 2024 15:49:13 -0500 Subject: [PATCH 13/77] Commit code before computer possibly blows up --- shiny/api-examples/output_transformer/app.py | 350 +++++++++++--- shiny/render/transformer/_transformer.py | 461 ++++++++++++++++++- shiny/session/_session.py | 1 + 3 files changed, 742 insertions(+), 70 deletions(-) diff --git a/shiny/api-examples/output_transformer/app.py b/shiny/api-examples/output_transformer/app.py index 88b28d810..916d317db 100644 --- a/shiny/api-examples/output_transformer/app.py +++ b/shiny/api-examples/output_transformer/app.py @@ -1,15 +1,212 @@ +# pyright : basic from __future__ import annotations from typing import Literal, overload from shiny import App, Inputs, Outputs, Session, ui -from shiny.render.transformer import TransformerMetadata, ValueFn, output_transformer +from shiny.render.transformer import ( + TransformerMetadata, + ValueFn, + ValueFnApp, + output_transformer, +) from shiny.render.transformer._transformer import ( + BarretRenderer, + BarretSimple, + JSONifiable, + output_transformer_json, + output_transformer_json2, output_transformer_no_params, output_transformer_params, output_transformer_simple, ) +# # # Goals +# # Simple-ish interface for component author +# # Component author only needs to implement one async function +# # For user, support parens and no parens +# # For user, support async and sync usage +# # Support docstrings with pyright for parens and no parens +# # Support docstrings for quartodoc + +# # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) +# # 1. OutputRenderer becomes a protocol +# # PErform a runtime isinstance check on the class +# # Or runtime check for attribute callable field of `_set_metadata()` +# # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method +# # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. + +# class json(BarretSimple[object, jsonifiable]): +# def __init__(self, _value_fn: Callable[[], object]): +# super().__init__(_value_fn) + +# async def transform(self, value: object) -> jsonifiable: +# return json.parse(json.dumps(value)) + +# class json(BarretRenderer[jsonifiable, str]): +# default_ui = output_json +# """ +# Docs! - no params +# """ + +# def __init__(self, _value_fn: Callable[[], jsonifiable], *, indent: int = 0): +# """ +# Docs! - params +# """ +# super().__init__(_value_fn) +# self.indent = indent + +# async def render(self) -> str: +# value = await self._value_fn() +# if value is None: +# return None +# return await self.transform(value) + + +# @overload +# def __init__(self, *, a: int = 1) -> None: +# ... + + +# @overload +# def __init__(self, _fn: ValueFn[str | None]) -> None: +# ... +class sub_barret_renderer(BarretRenderer[str, str, ({"a": int})]): + """ + SubBarretSimple - class docs - Render Caps docs + """ + + # a: int + default_ui = ui.output_text_verbatim + # default_ui_passthrough_args = None + + def __init__( + self, + _fn: ValueFnApp[str | None] | None = None, + *, + a: int = 1, + placeholder: bool = True, + ) -> None: + """ + SubBarretSimple - init docs - Render Caps docs + """ + # Do not pass params + super().__init__(_fn, a=a) + # if callable(_fn): + # self(_fn) + # return + self.a: int = a + # self.default_ui = lambda(id): ui.output_text_verbatim(id, placeholder=placeholder) + self.default_ui = ui.output_text_verbatim + + async def render(self) -> str: + value = await self._value_fn() + # self.a + return ( + f"{value.upper()}, {self._params.args}, {self._params.kwargs}; a={self.a}" + ) + + +from typing import Any, Awaitable, Callable, Generic + +from shiny.render.transformer._transformer import IT, OT + +# class BarretWrap(BarretSimple[IT, OT]): +# """ +# BarretWrap - Render Caps docs +# """ + +# a: int + +# # @overload +# # def __init__(self, *, a: int = 1) -> None: +# # ... + +# # @overload +# # def __init__(self, _fn: ValueFn[str | None]) -> None: +# # ... + +# # Add default_ui? +# def __init__(self, transform_fn: Callable[[IT], Awaitable[OT]]) -> None: +# super().__init__() +# self._transform_fn = transform_fn + +# async def render(self) -> OT | None: +# """ +# BarretWrap - render docs here +# """ +# print("BarretSimple - render") +# value = await self._value_fn() +# if value is None: +# return None + +# rendered = await self.transform(value) +# return rendered + + +def simple_fn( + transform_fn: Callable[[IT], Awaitable[OT]], + *, + ignore: IT | None = None, + ignore2: OT | None = None, +): + bs = BarretSimple[IT, OT]() + + async def transform_(value: IT) -> OT: + return await transform_fn(value) + + bs.transform = transform_ + # bs is set up + + @overload + def _(_fn: None = None) -> Callable[[ValueFnApp[IT]], BarretSimple[IT, OT]]: + ... + + @overload + def _(_fn: ValueFnApp[IT]) -> BarretSimple[IT, OT]: + ... + + def _( + _fn: ValueFnApp[IT] | None = None, + ) -> BarretSimple[IT, OT] | Callable[[], BarretSimple[IT, OT]]: + if callable(_fn): + bs(_fn) + return bs + + return _ + + +# return ret + + +@simple_fn +async def barret_simple_fn(value: str) -> str: + """ + Barret - Simple function docs + """ + return value.upper() + + +class sub_barret_simple(BarretSimple[str, str]): + """ + SubBarretSimple - class - Render Caps docs + """ + + default_ui = ui.output_text_verbatim + + def __init__( + self, + _value_fn: ValueFnApp[IT] | None = None, + ): + """ + SubBarretSimple - init - docs here + """ + super().__init__() # TODO-barret; pass through _value_fn + + async def transform(self, value: str) -> str: + return value.upper() + + ####### # Package authors can create their own output transformer methods by leveraging # `output_transformer` decorator. @@ -19,7 +216,7 @@ ####### -@output_transformer_simple() +@output_transformer_json2() def render_caps_simple( value: str, ) -> str: @@ -30,7 +227,7 @@ def render_caps_simple( return value.upper() -@output_transformer_simple() +@output_transformer_json2() def render_caps_simple2( value: str, ) -> str: @@ -41,26 +238,6 @@ def render_caps_simple2( return value.upper() -@output_transformer_params() -async def render_caps( - # Contains information about the render call: `name` and `session` - _meta: TransformerMetadata, - # The app-supplied output value function - _fn: ValueFn[str | None], -) -> str | None: - """ - Barret - Render Caps docs no params - """ - # Get the value - value = await _fn() - - # Render nothing if `value` is `None` - if value is None: - return None - - return value.upper() - - @output_transformer_params() async def render_caps_params( # Contains information about the render call: `name` and `session` @@ -171,64 +348,129 @@ def render_capitalize( # End of package author code ####### + +def text_row(id: str): + return ui.tags.tr( + ui.tags.td(f"{id}:"), + 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"), - "Renderer called with out parentheses:", - ui.output_text_verbatim("no_parens"), - "To upper:", - ui.output_text_verbatim("to_upper"), - "To lower:", - ui.output_text_verbatim("to_lower"), - "barret_caps:", - ui.output_text_verbatim("barret_caps"), - "barret_caps_simple:", - ui.output_text_verbatim("barret_caps_simple"), - "barret_caps_simple2:", - ui.output_text_verbatim("barret_caps_simple2"), - "barret_caps_params:", - ui.output_text_verbatim("barret_caps_params"), + # + ui.tags.table( + text_row("old_no_paren"), + text_row("old_paren"), + # + text_row("barret_caps_simple_no_paren"), + text_row("barret_caps_simple_paren"), + # + text_row("barret_caps_params_no_paren"), + text_row("barret_caps_params_paren"), + # + text_row("barret_sub_simple_no_paren"), + text_row("barret_sub_simple_paren"), + # + text_row("barret_sub_renderer_no_paren"), + text_row("barret_sub_renderer_paren"), + ), ) +# import dominate.tags as dom_tags + +# dom_tags.h1("content") +# with dom_tags.h1(): +# "content" + def server(input: Inputs, output: Outputs, session: Session): @output # Called without parentheses @render_capitalize - def no_parens(): + def old_no_paren(): return input.caption() @output # Called with parentheses. Equivalent to `@render_capitalize()` - @render_capitalize(to="upper") - def to_upper(): + @render_capitalize(to="lower") + def old_paren(): return input.caption() - @output - @render_capitalize(to="lower") - # Works with async output value functions - async def to_lower(): + @render_caps_simple + def barret_caps_simple_no_paren(): return input.caption() - @render_caps() - def barret_caps(): + @render_caps_simple() + def barret_caps_simple_paren(): return input.caption() - @render_caps_simple - def barret_caps_simple(): + # TODO-barret; Double check this one!!!! + # Only downside is bad function name in pylance window. + @render_caps_params + def barret_caps_params_no_paren(): return input.caption() - @render_caps_simple2 - def barret_caps_simple2(): + @render_caps_params(to="lower") + def barret_caps_params_paren(): return input.caption() - @render_caps_params - def barret_caps_params(): + print("\nsub_barret_simple") + + # new (.barret_sub at 0x104bd56c0>,) {} + # creating decorator! + @sub_barret_simple + def barret_sub_simple_no_paren(): return input.caption() - @render_caps_params(to="upper") - def barret_caps_params2(): + print("\nbarret_sub_simple_paren") + + # new () {} + # init () {} + # call (.barret_sub2 at 0x106146520>,) {} + @sub_barret_simple() + def barret_sub_simple_paren() -> str: return input.caption() + print("\nbarret_sub_renderer_no_paren") + + @barret_simple_fn + def barret_simple_fn_no_paren(): + return input.caption() + + print("\nbarret_sub_simple_paren") + + # new () {} + # init () {} + # call (.barret_sub2 at 0x106146520>,) {} + @barret_simple_fn() + def barret_simple_fn_paren() -> str: + return input.caption() + + print("\nbarret_sub_renderer_no_paren") + + # new () {'a': 1} + # init () {'a': 1} + # call (.barret_sub2 at 0x106146520>,) {} + @sub_barret_renderer + def barret_sub_renderer_no_paren(): + return input.caption() + + print("\nbarret_sub_renderer_paren") + + # new () {'a': 1} + # init () {'a': 1} + # call (.barret_sub2 at 0x106146520>,) {} + @sub_barret_renderer(a=2) + def barret_sub_renderer_paren(): + return input.caption() + + print("\n") + app = App(app_ui, server) diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 52312e56e..b7dc227bd 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -1,5 +1,8 @@ from __future__ import annotations +import typing +from functools import wraps + # TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` @@ -737,6 +740,227 @@ def as_value_fn( return output_transformer_impl +# # Barret + +# Class needs to create an outputrenderer and call it later. Not convinced it'll work. +# Proposing that parens are required if typing is used. :-( + + +class BarretRenderer(Generic[IT, OT, P]): + """ + BarretRenderer cls docs here + """ + + # Meta + _session: Session + _name: str + # __name__: str ?? + + # UI + default_ui: DefaultUIFnImpl | None = None + default_ui_passthrough_args: tuple[str, ...] | None = None + # App value function + _value_fn_original: ValueFnApp[IT] + _value_fn: ValueFn[IT] + + # Transform function; transform value's IT -> OT + # _transform_fn: TransformFn[IT, P, OT] | None = None + + # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) + # 1. OutputRenderer becomes a protocol + # PErform a runtime isinstance check on the class + # Or runtime check for attribute callable field of `_set_metadata()` + # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method + # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. + + def __call__(self, value_fn: ValueFnApp[IT]) -> OutputRenderer[OT]: + """ + BarretRenderer __call__ docs here; Sets app's value function + """ + print("BarretRenderer - call", value_fn) + if not callable(value_fn): + raise TypeError("Value function must be callable") + self._value_fn_original = value_fn + self._value_fn = wrap_async(value_fn) + + async def render_wrapper( + meta: TransformerMetadata, + value_fn: ValueFn[IT], + *args: P.args, + **kwargs: P.kwargs, + ) -> OT: + print("BarretRenderer - call - render_wrapper", meta, value_fn) + rendered = await self.render() + return rendered + + return OutputRenderer( + value_fn=self._value_fn_original, + transform_fn=render_wrapper, + # params=self._params, + params=empty_params(), + default_ui=self.default_ui, + default_ui_passthrough_args=self.default_ui_passthrough_args, + ) + + def __init__( + self, + *init_args: P.args, + **init_kwargs: P.kwargs, + # value_fn: ValueFnApp[IT], + # transform_fn: TransformFn[IT, P, OT], + # params: TransformerParams[P], + # default_ui: Optional[DefaultUIFnImpl] = None, + # default_ui_passthrough_args: Optional[tuple[str, ...]] = None, + ): + """ + BarretRenderer - init docs here + """ + print("BarretRenderer - init", init_args, init_kwargs) + self._params: TransformerParams[P] = TransformerParams( + *init_args, **init_kwargs + ) + + def _set_metadata(self, session: Session, name: str) -> None: + """ + When `Renderer`s are assigned to Output object slots, this method is used to + pass along Session and name information. + """ + self._session: Session = session + self._name: str = name + + async def render(self) -> OT: + """ + BarretRenderer - render docs here + """ + print("BarretRenderer - render") + print("BarretRenderer - needs abc class?") + value = await self._value_fn() + return cast(OT, value) + + def __new__( + _cls, + _value_fn: ValueFnApp[IT] | None = None, + *new_args: object, + **new_kwargs: object, + ) -> typing.Self: + # """ + # Barret __new__ docs here; Intercepts the class creation, + # possibly returning a decorator instead of a class + + # Check if bare class is being used as a decorator (called with a single callable + # arg). If so, decorate the function and return. + # """ + + print("BarretRenderer - new", new_args, new_kwargs, _cls) + # If only one arg is passed and it is a callable, return a decorator + if callable(_value_fn): + # if len(new_args) == 1 and callable(new_args[0]) and not new_kwargs: + print("BarretRenderer - creating decorator!", _cls) + # value_fn = new_args[0] + + out_ren = _cls()(_value_fn) + return out_ren + + resolved_cls = _cls() + resolved_cls._value_fn_original = value_fn + resolved_cls._value_fn = wrap_async(value_fn) + + return resolved_cls + + new_class = super().__new__(_cls) + return new_class(value_fn) + + # @wraps(wrapped) + # def f(*f_args: object, **f_kwargs: object): + # print("BarretRenderer - new - f", f_args, f_kwargs) + + # # with _cls() as _tag: + # # return wrapped(*args, **kwargs) or _tag + + # return f + + # Return like normal. Let the other methods do the work. + return super().__new__(_cls) + + +class BarretSimple(BarretRenderer[IT, OT | None, ...]): + _params: TransformerParams[...] + + def __new__( + _cls, + _fn: ValueFnApp[IT] | None = None, + ) -> typing.Self: + # """ + # Barret __new__ docs here; Intercepts the class creation, + # possibly returning a decorator instead of a class + + # Check if bare class is being used as a decorator (called with a single callable + # arg). If so, decorate the function and return. + # """ + + print("BarretSimple - new", _cls) + # If only one arg is passed and it is a callable, return a decorator + if callable(_fn): + print("BarretSimple - creating decorator!", _cls) + + out_ren = _cls()(_fn) + return out_ren + + resolved_cls = _cls() + resolved_cls._value_fn_original = _fn + resolved_cls._value_fn = wrap_async(_fn) + + print("BarretSimple - exiting creating decorator!", _cls) + + return resolved_cls + + new_class = super().__new__(_cls) + return new_class(_fn) + + # @wraps(wrapped) + # def f(*f_args: object, **f_kwargs: object): + # print("BarretSimple - new - f", f_args, f_kwargs) + + # # with _cls() as _tag: + # # return wrapped(*args, **kwargs) or _tag + + # return f + + # Return like normal. Let the other methods do the work. + return super().__new__(_cls) + + def __init__( + self, + _value_fn: ValueFnApp[IT] | None = None, + ): + """ + BarretSimple - init docs here + """ + super().__init__() + print("BarretSimple - init - no args, no kwargs") + self._params = empty_params() + + async def transform(self, value: IT) -> OT: + """ + BarretSimple - transform docs here + """ + print("BarretSimple - transform") + print("BarretSimple - needs abc class?") + return cast(OT, value) + + async def render(self) -> OT | None: + """ + BarretSimple - render docs here + """ + print("BarretSimple - render") + value = await self._value_fn() + if value is None: + return None + + rendered = await self.transform(value) + return rendered + + # ====================================================================================== # Simple transformer # ====================================================================================== @@ -933,31 +1157,214 @@ async def transform_fn( return simple_transformer -# TODO-barret; Allow for no parens when calling the renderer in the app. +JOT = TypeVar("JOT", bound=JSONifiable) + + +def output_transformer_json( + *, + default_ui: Optional[DefaultUIFn] = None, + default_ui_passthrough_args: Optional[tuple[str, ...]] = None, + # ) -> Callable[ + # [Callable[[IT], JOT] | Callable[[IT], Awaitable[JOT]]], + # Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]], + # ]: +): + def simple_transformer( + upgrade_fn: Callable[[IT], JOT] + | Callable[[IT], Awaitable[JOT]] + # ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: + ): + upgrade_fn = wrap_async(upgrade_fn) + + async def transform_fn( + _meta: TransformerMetadata, + _fn: ValueFn[IT | None], + ) -> JOT | None: + res = await _fn() + if res is None: + return None + + ret = await upgrade_fn(res) + return ret + + with_transformer = output_transformer_params( + default_ui=default_ui, + default_ui_passthrough_args=default_ui_passthrough_args, + ) + with_args = with_transformer(transform_fn) + # def with_args2( + # (() -> (IT@simple_transformer | None)) | (() -> Awaitable[IT@simple_transformer | None]) | None + # ) -> ( + # (((() -> (IT@simple_transformer | None)) | (() -> Awaitable[IT@simple_transformer | None])) -> OutputRenderer[JOT@simple_transformer | None]) | OutputRenderer[JOT@simple_transformer | None] + # ) + return with_args + # with_value_fn = with_args() + # return with_value_fn + with_value_fn: BValueFn[IT, JOT] = with_args() + return with_value_fn + + return simple_transformer + + +def output_transformer_json2( + *, + default_ui: Optional[DefaultUIFn] = None, + default_ui_passthrough_args: Optional[tuple[str, ...]] = None, + # ) -> Callable[[TransformFn[IT, P, OT]], BArgsFn2[IT, P, OT]]: +): + # _output_transform_fn = _ # Give clearer name + + def simple_transformer( + upgrade_fn: Callable[[IT], JOT] + | Callable[[IT], Awaitable[JOT]] + # ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: + # ) -> ( + # Callable[[], Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]] + # | Callable[[Optional[ValueFnApp[IT]], OutputRendererSimple[JOT | None]] + ): + upgrade_fn = wrap_async(upgrade_fn) + + async def transform_fn( + _meta: TransformerMetadata, + _fn: ValueFn[IT | None], + ) -> JOT | None: + res = await _fn() + if res is None: + return None + + ret = await upgrade_fn(res) + return ret + + @typing.overload + def output_renderer( + _: None = None, + ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: + pass + + @typing.overload + def output_renderer( + value_fn: ValueFnApp[IT], + ) -> OutputRendererSimple[JOT | None]: + pass + + def output_renderer( # pyright: ignore[reportGeneralTypeIssues] + # def output_renderer( + _: Optional[ValueFnApp[IT]] = None, + ): + _args_value_fn = _ # Give clearer name + + def with_value_fn( + value_fn: ValueFnApp[IT], + ) -> OutputRendererSimple[JOT | None]: + return OutputRendererSimple( + value_fn=value_fn, + transform_fn=transform_fn, + default_ui=default_ui, + default_ui_passthrough_args=default_ui_passthrough_args, + ) + + if callable(_args_value_fn): + # No args were given and the function was called without parens, + # receiving an app value function. Ex: + # @output_transformer_json2 + # def my_output(): + # ... + return with_value_fn(_args_value_fn) + else: + return with_value_fn + + return output_renderer + + return simple_transformer + + # TODO-barret; Allow for no parens when creating the renderer. But discourage the pkg author. +# TODO-barret; Allow for no parens when calling the renderer in the app. # TODO-barret; Add extra fields so that existing renderers can be used? # TODO-barret; Replace the original `output_transformer` with this one? # TODO-barret; Document `output_transformer_simple` # TODO-barret; Can the return type of the output_transformer_simple be OT and not JSONifiable? (Just make sure it is a subset of JSONifiable) +# X = TypeVar("X") +# Deco = Callable[[]] + +# Callable[ +# Callable[[ValueFnApp[IT]], OutputRenderer[OT]] + +# BValueFnOut = OutputRenderer[OT] +BValueFnIn = ValueFnApp[IT] +BValueFn = Callable[[BValueFnIn[IT]], OutputRenderer[OT]] +BArgsFn = Callable[ + Concatenate[Optional[BValueFnIn[IT]], P], + BValueFn[IT, OT] | OutputRenderer[OT], +] +BArgsFn2 = BValueFnIn[IT] | Callable[P, BValueFn[IT, OT]] + + +WithValueFn = Callable[[ValueFnApp[IT]], OutputRenderer[OT]] +WithArgsFn = WithValueFn[IT, OT] | OutputRenderer[OT] + +WithTransformerFn = Callable[ + Concatenate[Optional[ValueFnApp[IT]], P], + WithArgsFn[IT, OT], +] + +# ## Barret notes: +# If we want to allow for no parens, then the return type is either +# * OutputRenderer[OT] == Callable[[], OT] +# * Callable[[ValueFnApp[IT]], OutputRenderer[OT]] + +# By type definition rules, these are incompatible as one accepts a positional arg and the other does not. +# So we need to use an overload. +# However, using an overload gives the wrong function name for the no-paren call. +# * I believe this is a pylance error and could be fixed. +# +# Implementing with overloads, somehow the docs for render_caps_params are passed through! +# Current downside is that the fn name is `output_renderer` instead of the user's function name at decorator time. (This is a pylance error?) + +# Using overloads does not allow for us to define the type of the function. +# Using overloads requires us to use pyright ignore statements as the overloads are not compatible with each other. + def output_transformer_params( # Require that all params are keyword arguments so that if a user calls `@output_transformer_no_params` (with no parens), an error is thrown + # _: Optional[Callable[[TransformFn[IT, P, OT]], BArgsFn[IT, P, OT]]] = None, *, default_ui: Optional[DefaultUIFn] = None, default_ui_passthrough_args: Optional[tuple[str, ...]] = None, -) -> Callable[ - [TransformFn[IT, P, OT]], - Callable[P, Callable[[ValueFnApp[IT]], OutputRenderer[OT]]], -]: + # ) -> Callable[[TransformFn[IT, P, OT]], BArgsFn2[IT, P, OT]]: +): + # _output_transform_fn = _ # Give clearer name + def with_transformer( transform_fn: TransformFn[IT, P, OT], - ) -> Callable[P, Callable[[ValueFnApp[IT]], OutputRenderer[OT]]]: - def with_args( + # ) -> BArgsFn2[IT, P, OT]: + ): + @typing.overload + def output_renderer( + *args: P.args, + **kwargs: P.kwargs, + # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: + # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: + ) -> BValueFn[IT, OT]: + pass + + @typing.overload + def output_renderer( # pyright: ignore[reportOverlappingOverload] + value_fn: BValueFnIn[IT], + # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: + # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: + ) -> OutputRenderer[OT]: + pass + + def output_renderer( # pyright: ignore[reportGeneralTypeIssues] + _: Optional[BValueFnIn[IT]] = None, *args: P.args, **kwargs: P.kwargs, # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: - ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: + # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: + ): + _args_value_fn = _ # Give clearer name if len(args) > 0: raise RuntimeError( "`*args` should not be supplied." @@ -966,7 +1373,7 @@ def with_args( params = TransformerParams[P](*args, **kwargs) def with_value_fn( - value_fn: ValueFnApp[IT], + value_fn: BValueFnIn[IT], ) -> OutputRenderer[OT]: return OutputRenderer( value_fn=value_fn, @@ -976,13 +1383,35 @@ def with_value_fn( default_ui_passthrough_args=default_ui_passthrough_args, ) - return with_value_fn - - return with_args - - # TODO-barret; Add more here - # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] - # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] + if callable(_args_value_fn): + # No args were given and the function was called without parens, + # receiving an app value function. Ex: + # @output_transformer_params + # def my_output(): + # ... + return with_value_fn(_args_value_fn) + else: + return with_value_fn + + # if callable(_fn): + # # No args were given and the function was called without parens, + # # receiving an app value function. Ex: + # # @output_transformer_params + # # def my_output(): + # # ... + # return with_value_fn(_fn) + # else: + # return with_value_fn + + return output_renderer + + # # TODO-barret; Add more here + # # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] + # # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] + # if callable(_output_transform_fn): + # return with_transformer(_output_transform_fn) + # else: + # return with_transformer return with_transformer diff --git a/shiny/session/_session.py b/shiny/session/_session.py index 986e227cc..d38a08019 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -49,6 +49,7 @@ from ..reactive import Effect_, Value, effect, flush, isolate from ..reactive._core import lock, on_flushed from ..render.transformer import OutputRenderer +from ..render.transformer._transformer import BarretRenderer from ..types import SafeException, SilentCancelOutputException, SilentException from ._utils import RenderedDeps, read_thunk_opt, session_context From 98a1daa09aa918fcc4c239637078b55c6b840b8a Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 3 Jan 2024 09:38:14 -0500 Subject: [PATCH 14/77] Commit again before machine blows up --- shiny/api-examples/output_transformer/app.py | 383 +++++++++++-------- shiny/render/transformer/_transformer.py | 183 ++++++++- 2 files changed, 383 insertions(+), 183 deletions(-) diff --git a/shiny/api-examples/output_transformer/app.py b/shiny/api-examples/output_transformer/app.py index 916d317db..bcb83a4ac 100644 --- a/shiny/api-examples/output_transformer/app.py +++ b/shiny/api-examples/output_transformer/app.py @@ -71,7 +71,7 @@ # @overload # def __init__(self, _fn: ValueFn[str | None]) -> None: # ... -class sub_barret_renderer(BarretRenderer[str, str, ({"a": int})]): +class sub_barret_renderer(BarretRenderer[str | None, JSONifiable]): """ SubBarretSimple - class docs - Render Caps docs """ @@ -82,6 +82,7 @@ class sub_barret_renderer(BarretRenderer[str, str, ({"a": int})]): def __init__( self, + # Required for no paren usage _fn: ValueFnApp[str | None] | None = None, *, a: int = 1, @@ -91,20 +92,21 @@ def __init__( SubBarretSimple - init docs - Render Caps docs """ # Do not pass params - super().__init__(_fn, a=a) - # if callable(_fn): - # self(_fn) - # return + super().__init__(_fn) + self.widget = None self.a: int = a # self.default_ui = lambda(id): ui.output_text_verbatim(id, placeholder=placeholder) self.default_ui = ui.output_text_verbatim - async def render(self) -> str: + async def render(self) -> str | None: value = await self._value_fn() + values = [value, value, value] + [x for x in values if isinstance(x, Sidebar)] + if value is None: + return None + self.widget = value # self.a - return ( - f"{value.upper()}, {self._params.args}, {self._params.kwargs}; a={self.a}" - ) + return f"{value.upper()}; a={self.a}" from typing import Any, Awaitable, Callable, Generic @@ -143,51 +145,55 @@ async def render(self) -> str: # rendered = await self.transform(value) # return rendered +# from typing import Sequence +# def length(value: Sequence[IT]) -> int: +# return len(value) -def simple_fn( - transform_fn: Callable[[IT], Awaitable[OT]], - *, - ignore: IT | None = None, - ignore2: OT | None = None, -): - bs = BarretSimple[IT, OT]() - async def transform_(value: IT) -> OT: - return await transform_fn(value) +# def simple_fn( +# transform_fn: Callable[[IT], Awaitable[OT]], +# # *, +# # ignore: IT | None = None, +# # ignore2: OT | None = None, +# ): +# bs = BarretSimple[IT, OT]() + +# async def transform_(value: IT) -> OT: +# return await transform_fn(value) - bs.transform = transform_ - # bs is set up +# bs.transform = transform_ +# # bs is set up - @overload - def _(_fn: None = None) -> Callable[[ValueFnApp[IT]], BarretSimple[IT, OT]]: - ... +# @overload +# def _(_fn: None = None) -> Callable[[ValueFnApp[IT]], BarretSimple[IT, OT]]: +# ... - @overload - def _(_fn: ValueFnApp[IT]) -> BarretSimple[IT, OT]: - ... +# @overload +# def _(_fn: ValueFnApp[IT]) -> BarretSimple[IT, OT]: +# ... - def _( - _fn: ValueFnApp[IT] | None = None, - ) -> BarretSimple[IT, OT] | Callable[[], BarretSimple[IT, OT]]: - if callable(_fn): - bs(_fn) - return bs +# def _( +# _fn: ValueFnApp[IT] | None = None, +# ) -> BarretSimple[IT, OT] | Callable[[ValueFnApp[IT]], BarretSimple[IT, OT]]: +# if callable(_fn): +# bs(_fn) +# return bs - return _ +# return _ # return ret -@simple_fn -async def barret_simple_fn(value: str) -> str: - """ - Barret - Simple function docs - """ - return value.upper() +# @simple_fn +# async def barret_simple_fn(value: str) -> str: +# """ +# Barret - Simple function docs +# """ +# return value.upper() -class sub_barret_simple(BarretSimple[str, str]): +class sub_barret_simple(BarretRenderer[str, str]): """ SubBarretSimple - class - Render Caps docs """ @@ -196,7 +202,7 @@ class sub_barret_simple(BarretSimple[str, str]): def __init__( self, - _value_fn: ValueFnApp[IT] | None = None, + _value_fn: ValueFnApp[str] | None = None, ): """ SubBarretSimple - init - docs here @@ -204,7 +210,16 @@ def __init__( super().__init__() # TODO-barret; pass through _value_fn async def transform(self, value: str) -> str: - return value.upper() + return str(value).upper() + + # async def render(self) -> str: + # # OPen graphics + # value = await self._value_fn() + # # close graphics + # # self.a + # return ( + # f"{value.upper()}, {self._params.args}, {self._params.kwargs}; a={self.a}" + # ) ####### @@ -216,29 +231,18 @@ async def transform(self, value: str) -> str: ####### -@output_transformer_json2() -def render_caps_simple( - value: str, -) -> str: - """ - Barret - Render Caps docs (simple) - """ - # return [value.upper(), value.lower()] - return value.upper() - - -@output_transformer_json2() -def render_caps_simple2( - value: str, -) -> str: - """ - Barret - Render Caps docs (simple2) - """ - # return [value.upper(), value.lower()] - return value.upper() +# @output_transformer_json2() +# def render_caps_simple( +# value: str, +# ) -> str: +# """ +# Barret - Render Caps docs (simple) +# """ +# # return [value.upper(), value.lower()] +# return value.upper() -@output_transformer_params() +@output_transformer_params(default_ui=ui.output_text_verbatim) async def render_caps_params( # Contains information about the render call: `name` and `session` _meta: TransformerMetadata, @@ -253,6 +257,8 @@ async def render_caps_params( # Get the value value = await _fn() + # _meta.self.widget = value # pyright: ignore + # Render nothing if `value` is `None` if value is None: return None @@ -264,84 +270,104 @@ async def render_caps_params( raise ValueError(f"Invalid value for `to`: {to}") -# Create renderer components from the async handler function: `capitalize_components()` -@output_transformer() -async def CapitalizeTransformer( - # Contains information about the render call: `name` and `session` - _meta: TransformerMetadata, - # The app-supplied output value function - _fn: ValueFn[str | None], - *, - # Extra parameters that app authors can supply to the render decorator - # (e.g. `@render_capitalize(to="upper")`) - to: Literal["upper", "lower"] = "upper", -) -> str | None: - # Get the value - value = await _fn() +# @output_transformer_params() +# async def render_caps_no_params( +# # Contains information about the render call: `name` and `session` +# _meta: TransformerMetadata, +# # The app-supplied output value function +# _fn: ValueFn[str | None], +# ) -> str | None: +# """ +# Barret - Render Caps docs no parameters +# """ +# # Get the value +# value = await _fn() + +# # Render nothing if `value` is `None` +# if value is None: +# return None + +# return value.upper() + + +# # Create renderer components from the async handler function: `capitalize_components()` +# @output_transformer() +# async def CapitalizeTransformer( +# # Contains information about the render call: `name` and `session` +# _meta: TransformerMetadata, +# # The app-supplied output value function +# _fn: ValueFn[str | None], +# *, +# # Extra parameters that app authors can supply to the render decorator +# # (e.g. `@render_capitalize(to="upper")`) +# to: Literal["upper", "lower"] = "upper", +# ) -> str | None: +# # Get the value +# value = await _fn() + +# # Render nothing if `value` is `None` +# if value is None: +# return None + +# if to == "upper": +# return value.upper() +# if to == "lower": +# return value.lower() +# raise ValueError(f"Invalid value for `to`: {to}") + + +# # First, create an overload where users can supply the extra parameters. +# # Example of usage: +# # ``` +# # @output +# # @render_capitalize(to="upper") +# # def value(): +# # return input.caption() +# # ``` +# # Note: Return type is `OutputRendererDecorator` +# @overload +# def render_capitalize( +# *, +# to: Literal["upper", "lower"] = "upper", +# ) -> CapitalizeTransformer.OutputRendererDecorator: +# ... - # Render nothing if `value` is `None` - if value is None: - return None - if to == "upper": - return value.upper() - if to == "lower": - return value.lower() - raise ValueError(f"Invalid value for `to`: {to}") +# # Second, create an overload where users are not using parentheses to the method. +# # While it doesn't look necessary, it is needed for the type checker. +# # Example of usage: +# # ``` +# # @output +# # @render_capitalize +# # def value(): +# # return input.caption() +# # ``` +# # Note: `_fn` type is the transformer's `ValueFn` +# # Note: Return type is the transformer's `OutputRenderer` +# @overload +# def render_capitalize( +# _fn: CapitalizeTransformer.ValueFn, +# ) -> CapitalizeTransformer.OutputRenderer: +# ... -# First, create an overload where users can supply the extra parameters. -# Example of usage: -# ``` -# @output -# @render_capitalize(to="upper") -# def value(): -# return input.caption() -# ``` -# Note: Return type is `OutputRendererDecorator` -@overload -def render_capitalize( - *, - to: Literal["upper", "lower"] = "upper", -) -> CapitalizeTransformer.OutputRendererDecorator: - ... - - -# Second, create an overload where users are not using parentheses to the method. -# While it doesn't look necessary, it is needed for the type checker. -# Example of usage: -# ``` -# @output -# @render_capitalize -# def value(): -# return input.caption() -# ``` -# Note: `_fn` type is the transformer's `ValueFn` -# Note: Return type is the transformer's `OutputRenderer` -@overload -def render_capitalize( - _fn: CapitalizeTransformer.ValueFn, -) -> CapitalizeTransformer.OutputRenderer: - ... - - -# Lastly, implement the renderer. -# Note: `_fn` type is the transformer's `ValueFn` or `None` -# Note: Return type is the transformer's `OutputRenderer` or `OutputRendererDecorator` -def render_capitalize( - _fn: CapitalizeTransformer.ValueFn | None = None, - *, - to: Literal["upper", "lower"] = "upper", -) -> ( - CapitalizeTransformer.OutputRenderer | CapitalizeTransformer.OutputRendererDecorator -): - """ - OldSchool - CapitalizeTransformer - """ - return CapitalizeTransformer( - _fn, - CapitalizeTransformer.params(to=to), - ) +# # Lastly, implement the renderer. +# # Note: `_fn` type is the transformer's `ValueFn` or `None` +# # Note: Return type is the transformer's `OutputRenderer` or `OutputRendererDecorator` +# def render_capitalize( +# _fn: CapitalizeTransformer.ValueFn | None = None, +# *, +# to: Literal["upper", "lower"] = "upper", +# ) -> ( +# CapitalizeTransformer.OutputRenderer | CapitalizeTransformer.OutputRendererDecorator +# ): +# """ +# OldSchool - CapitalizeTransformer +# """ +# return CapitalizeTransformer( +# _fn, +# CapitalizeTransformer.params(to=to), +# ) ####### @@ -374,6 +400,9 @@ def text_row(id: str): text_row("barret_caps_params_no_paren"), text_row("barret_caps_params_paren"), # + text_row("barret_caps_no_params_no_paren"), + text_row("barret_caps_no_params_paren"), + # text_row("barret_sub_simple_no_paren"), text_row("barret_sub_simple_paren"), # @@ -385,42 +414,60 @@ def text_row(id: str): # import dominate.tags as dom_tags # dom_tags.h1("content") -# with dom_tags.h1(): -# "content" -def server(input: Inputs, output: Outputs, session: Session): - @output - # Called without parentheses - @render_capitalize - def old_no_paren(): - return input.caption() - - @output - # Called with parentheses. Equivalent to `@render_capitalize()` - @render_capitalize(to="lower") - def old_paren(): - return input.caption() +# @dom_tags.h1 +# def _(): +# return "content" - @render_caps_simple - def barret_caps_simple_no_paren(): - return input.caption() - @render_caps_simple() - def barret_caps_simple_paren(): - return input.caption() +def server(input: Inputs, output: Outputs, session: Session): + # @output + # # Called without parentheses + # @render_capitalize + # def old_no_paren(): + # return input.caption() + + # @output + # # Called with parentheses. Equivalent to `@render_capitalize()` + # # legacy - Barret - Too much boilerplate + # @render_capitalize(to="lower") + # def old_paren(): + # return input.caption() + + # # No docstring due to overload + # @render_caps_simple + # def barret_caps_simple_no_paren(): + # return input.caption() + + # # No docstring due to overload + # @render_caps_simple() + # def barret_caps_simple_paren(): + # return input.caption() # TODO-barret; Double check this one!!!! - # Only downside is bad function name in pylance window. + # Barret - Only downside is bad function name in pylance window. Could be pylance bug? @render_caps_params def barret_caps_params_no_paren(): return input.caption() + # Barret - Correct function name @render_caps_params(to="lower") def barret_caps_params_paren(): return input.caption() - print("\nsub_barret_simple") + # @render_caps_no_params + # # TODO-barret; Double check this one!!!! + # # Barret - Only downside is bad function name in pylance window. Could be pylance bug? + # def barret_caps_no_params_no_paren(): + # return input.caption() + + # # Barret - Correct function name! + # @render_caps_no_params() + # def barret_caps_no_params_paren(): + # return input.caption() + + # print("\nsub_barret_simple") # new (.barret_sub at 0x104bd56c0>,) {} # creating decorator! @@ -439,18 +486,18 @@ def barret_sub_simple_paren() -> str: print("\nbarret_sub_renderer_no_paren") - @barret_simple_fn - def barret_simple_fn_no_paren(): - return input.caption() + # @barret_simple_fn + # def barret_simple_fn_no_paren(): + # return input.caption() print("\nbarret_sub_simple_paren") - # new () {} - # init () {} - # call (.barret_sub2 at 0x106146520>,) {} - @barret_simple_fn() - def barret_simple_fn_paren() -> str: - return input.caption() + # # new () {} + # # init () {} + # # call (.barret_sub2 at 0x106146520>,) {} + # @barret_simple_fn() + # def barret_simple_fn_paren() -> str: + # return input.caption() print("\nbarret_sub_renderer_no_paren") diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index b7dc227bd..04b6e7da3 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -746,7 +746,7 @@ def as_value_fn( # Proposing that parens are required if typing is used. :-( -class BarretRenderer(Generic[IT, OT, P]): +class BarretRenderer(Generic[IT, OT]): """ BarretRenderer cls docs here """ @@ -804,8 +804,9 @@ async def render_wrapper( def __init__( self, - *init_args: P.args, - **init_kwargs: P.kwargs, + _value_fn: ValueFnApp[IT] | None = None, + # *init_args: P.args, + # **init_kwargs: P.kwargs, # value_fn: ValueFnApp[IT], # transform_fn: TransformFn[IT, P, OT], # params: TransformerParams[P], @@ -815,10 +816,14 @@ def __init__( """ BarretRenderer - init docs here """ - print("BarretRenderer - init", init_args, init_kwargs) - self._params: TransformerParams[P] = TransformerParams( - *init_args, **init_kwargs - ) + print("BarretRenderer - init (no args/kwargs)") + if callable(_value_fn): + raise TypeError( + "This should not be called with a callable value_fn! Only the `__call__` method should be called with a callable value_fn" + ) + # self._params: TransformerParams[P] = TransformerParams( + # *init_args, **init_kwargs + # ) def _set_metadata(self, session: Session, name: str) -> None: """ @@ -840,8 +845,8 @@ async def render(self) -> OT: def __new__( _cls, _value_fn: ValueFnApp[IT] | None = None, - *new_args: object, - **new_kwargs: object, + *new_args: typing.Any, + **new_kwargs: typing.Any, ) -> typing.Self: # """ # Barret __new__ docs here; Intercepts the class creation, @@ -883,8 +888,8 @@ def __new__( return super().__new__(_cls) -class BarretSimple(BarretRenderer[IT, OT | None, ...]): - _params: TransformerParams[...] +class BarretSimple(BarretRenderer[IT, OT | None]): + # _params: TransformerParams[...] def __new__( _cls, @@ -938,7 +943,7 @@ def __init__( """ super().__init__() print("BarretSimple - init - no args, no kwargs") - self._params = empty_params() + # self._params = empty_params() async def transform(self, value: IT) -> OT: """ @@ -1334,6 +1339,11 @@ def output_transformer_params( default_ui_passthrough_args: Optional[tuple[str, ...]] = None, # ) -> Callable[[TransformFn[IT, P, OT]], BArgsFn2[IT, P, OT]]: ): + """ + Output Transformer Params docs + + Explain about default ui! + """ # _output_transform_fn = _ # Give clearer name def with_transformer( @@ -1364,21 +1374,28 @@ def output_renderer( # pyright: ignore[reportGeneralTypeIssues] # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: ): + async def transform_fn_with_params( + _meta: TransformerMetadata, _fn: ValueFn[IT | None] + ) -> OT: + return await transform_fn(_meta, _fn, *args, **kwargs) + _args_value_fn = _ # Give clearer name if len(args) > 0: raise RuntimeError( "`*args` should not be supplied." "\nDid you forget to add `()` to your render decorator?" ) - params = TransformerParams[P](*args, **kwargs) + # params = TransformerParams[P](*args, **kwargs) def with_value_fn( value_fn: BValueFnIn[IT], ) -> OutputRenderer[OT]: return OutputRenderer( value_fn=value_fn, - transform_fn=transform_fn, - params=params, + # params=params, + # transform_fn=transform_fn, + transform_fn=transform_fn_with_params, + params=empty_params(), default_ui=default_ui, default_ui_passthrough_args=default_ui_passthrough_args, ) @@ -1462,3 +1479,139 @@ async def resolve_value_fn(value_fn: ValueFnApp[IT]) -> IT: # To avoid duplicate work just for a typeguard, we cast the function value_fn = cast(ValueFnSync[IT], value_fn) return value_fn() + + +# ###################################################################################### + + +if (False): + + # # Goals + # Simple-ish interface for component author + # Component author only needs to implement one async function + # For user, support parens and no parens + # For user, support async and sync usage + # Support docstrings with pyright for parens and no parens + # Support docstrings for quartodoc + + # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) + # 1. OutputRenderer becomes a protocol + # PErform a runtime isinstance check on the class + # Or runtime check for attribute callable field of `_set_metadata()` + # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method + # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. + + class json(BarretSimple[object, jsonifiable]): + def __init__(self, _value_fn: Callable[[], object]): + super().__init__(_value_fn) + + async def transform(self, value: object) -> jsonifiable: + return json.parse(json.dumps(value)) + + class json(BarretRenderer[jsonifiable, str]): + default_ui = output_json + """ + Docs! - no params + """ + + def __init__(self, _value_fn: Callable[[], jsonifiable], *, indent: int = 0): + """ + Docs! - params + """ + super().__init__(_value_fn) + self.indent = indent + + async def render(self) -> str: + value = await self._value_fn() + if value is None: + return None + return await self.transform(value) + + + + + + + + + + + + + + # -------------------------------------------------- + + class OutputRenderer2(Generic[IT], ABC): + + # Try to warn that the parameter is not being set; Later + # @abcfield + # default_ui + + # Called inside `_session` class when trying to retrive the value? + async def _get_value(self) -> OT: + return await self.render() + + def __init__(self, _value_fn: ValueFNApp[IT] | None = None): + self._value_fn = _value_fn + # self.default_ui = Not missing + + def __call__(self, _value_fn: ValueFNApp[IT]) -> typing.Self + self._value_fn = _value_fn + # TODO-barret; REturn self! Rewrite the whole base class as (almost) nothing is necessary anymore + return LegacyOutputRenderer() + + + class text(OutputRenderer2[str]): + """Render decorator for text output""" + default_ui = output_text + default_ui = None + + def __init__(self, _value_fn: ValueFNApp[str] | None = None, *, to_case: str = "upper"): + """ + Create a text renderer + + Parameters + ---------- + _value_fn + A function that returns the text to render + to_case + The case to convert the text to, by default "upper" + """ + super().__init__(_value_fn) + self.to_case = to_case + + def transform(self, value: str) -> JSONifiable: + if self.to_case == "upper": + return value.upper() + elif self.to_case == "lower": + return value.lower() + else: + return value + + class text(OutputRenderer2[str]): + """Render decorator for text output""" + default_ui = output_text + default_ui = None + + def __init__(self, _value_fn: ValueFNApp[JSONIfiable] | None = None): + """ + Create a text renderer + + Parameters + ---------- + _value_fn + A function that returns the text to render + to_case + The case to convert the text to, by default "upper" + """ + super().__init__(_value_fn) + # self.to_case = to_case + + + @text + def foo1(): + ... + + @text(to_case="lower") + def foo2(): + ... From edb73e34e3e2cbc308d14cbbc902eda66abb5aaa Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 15:45:53 -0500 Subject: [PATCH 15/77] Add `WrapAsync` utility class to expose common info about a now async function --- shiny/_utils.py | 76 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/shiny/_utils.py b/shiny/_utils.py index 2ec1ebc16..d85123aa9 100644 --- a/shiny/_utils.py +++ b/shiny/_utils.py @@ -11,7 +11,7 @@ import secrets import socketserver import tempfile -from typing import Any, Awaitable, Callable, Optional, TypeVar, cast +from typing import Any, Awaitable, Callable, Generic, Optional, TypeVar, cast from ._typing_extensions import ParamSpec, TypeGuard @@ -221,35 +221,86 @@ def private_seed(): # 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; Q: Expose in quartodoc file? +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]]): + 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. @@ -263,6 +314,7 @@ def is_async_callable( Returns True if `obj` is an `async def` function, or if it's an object with a `__call__` method which is an `async def` function. """ + if inspect.iscoroutinefunction(obj): return True if hasattr(obj, "__call__"): # noqa: B004 @@ -282,7 +334,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 @@ -310,7 +362,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 @@ -325,7 +377,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.") From 7f7a783a098f467ebe8508c8364720d5292d6a27 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 15:46:16 -0500 Subject: [PATCH 16/77] Update _quartodoc.yml --- docs/_quartodoc.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 6503fb567..a1f95651b 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -196,14 +196,16 @@ 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.is_async_callable + # TODO-barret; UPdate with renderer classes / info! + - render.transformer.Renderer + # - render.transformer.output_transformer + # - render.transformer.OutputTransformer + # - render.transformer.TransformerMetadata + # - render.transformer.TransformerParams + # - render.transformer.OutputRenderer + # - render.transformer.is_async_callable - render.transformer.ValueFn - - render.transformer.TransformFn + # - render.transformer.TransformFn - title: Reactive programming desc: "" contents: From a2d9b207028a62ffd5499cc2eee2eb863e8d2b0b Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 15:58:49 -0500 Subject: [PATCH 17/77] Add `ui_kwargs()` (superceeding `output_args`) --- shiny/express/_output.py | 79 +++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/shiny/express/_output.py b/shiny/express/_output.py index 8733b07ec..5753d0490 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -3,27 +3,62 @@ import contextlib import sys from contextlib import AbstractContextManager -from typing import Callable, TypeVar, cast, overload +from typing import Callable, TypeVar, overload from .. import ui from .._typing_extensions import ParamSpec from ..render.transformer import OutputRenderer +from ..render.transformer._renderer import RendererBase +from ..render.transformer._transformer import OT __all__ = ( - "output_args", + "ui_kwargs", "suspend_display", ) -OT = TypeVar("OT") P = ParamSpec("P") R = TypeVar("R") CallableT = TypeVar("CallableT", bound=Callable[..., object]) +def ui_kwargs( + **kwargs: object, +) -> Callable[[RendererBase], RendererBase]: + """ + Sets default UI arguments for a Shiny rendering function. + + Each Shiny render function (like :func:`~shiny.render.plot`) can display itself when + declared within a Shiny inline-style application. In the case of + :func:`~shiny.render.plot`, the :func:`~shiny.ui.output_plot` function is called + implicitly to display the plot. Use the `@ui_kwargs` decorator to specify + arguments to be passed to `output_plot` (or whatever the corresponding UI function + is) when the render function displays itself. + + Parameters + ---------- + **kwargs + Keyword arguments to be passed to the UI function. + + Returns + ------- + : + A decorator that sets the default UI arguments for a Shiny rendering function. + """ + + def wrapper(renderer: RendererBase) -> RendererBase: + # renderer._default_ui_args = args + renderer._default_ui_kwargs = kwargs + return renderer + + return wrapper + + def output_args( - *args: object, **kwargs: object + *args: object, + **kwargs: object, ) -> Callable[[OutputRenderer[OT]], OutputRenderer[OT]]: - """Sets default UI arguments for a Shiny rendering function. + """ + Sets default UI arguments for a Shiny rendering function. Each Shiny render function (like :func:`~shiny.render.plot`) can display itself when declared within a Shiny inline-style application. In the case of @@ -32,6 +67,7 @@ def output_args( arguments to be passed to `output_plot` (or whatever the corresponding UI function is) when the render function displays itself. + Parameters ---------- *args @@ -46,8 +82,15 @@ def output_args( """ def wrapper(renderer: OutputRenderer[OT]) -> OutputRenderer[OT]: - renderer.default_ui_args = args - renderer.default_ui_kwargs = kwargs + if not isinstance(renderer, OutputRenderer): + raise TypeError( + f"Expected an OutputRenderer, but got {type(renderer).__name__}." + "\nIf you are trying to set default UI arguments for a `Renderer`, use" + " `@ui_kwargs` instead." + ) + renderer._default_ui_args = args + renderer._default_ui_kwargs = kwargs + return renderer return wrapper @@ -58,14 +101,19 @@ def suspend_display(fn: CallableT) -> CallableT: ... +@overload +def suspend_display(fn: RendererBase) -> RendererBase: + ... + + @overload def suspend_display() -> AbstractContextManager[None]: ... def suspend_display( - fn: Callable[P, R] | OutputRenderer[OT] | None = None -) -> Callable[P, R] | OutputRenderer[OT] | AbstractContextManager[None]: + fn: Callable[P, R] | RendererBase | None = None +) -> Callable[P, R] | RendererBase | AbstractContextManager[None]: """Suppresses the display of UI elements in various ways. If used as a context manager (`with suspend_display():`), it suppresses the display @@ -99,11 +147,12 @@ def suspend_display( if fn is None: return suspend_display_ctxmgr() - # Special case for OutputRenderer; when we decorate those, we just mean "don't + # Special case for RendererBase; when we decorate those, we just mean "don't # display yourself" - if isinstance(fn, OutputRenderer): + if isinstance(fn, RendererBase): + # By setting the class value, the `self` arg will be auto added. fn.default_ui = null_ui - return cast(Callable[P, R], fn) + return fn return suspend_display_ctxmgr()(fn) @@ -118,7 +167,11 @@ def suspend_display_ctxmgr(): sys.displayhook = oldhook -def null_ui(id: str, *args: object, **kwargs: object) -> ui.TagList: +def null_ui( + id: str, + *args: object, + **kwargs: object, +) -> ui.TagList: return ui.TagList() From 3e8a71978ff502808a08ef8bc928a094acdd2a8d Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:06:05 -0500 Subject: [PATCH 18/77] Make new renderer class --- shiny/render/renderer/__init__.py | 19 ++ shiny/render/renderer/_renderer.py | 432 +++++++++++++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 shiny/render/renderer/__init__.py create mode 100644 shiny/render/renderer/_renderer.py diff --git a/shiny/render/renderer/__init__.py b/shiny/render/renderer/__init__.py new file mode 100644 index 000000000..b743726f9 --- /dev/null +++ b/shiny/render/renderer/__init__.py @@ -0,0 +1,19 @@ +from ._renderer import ( # noqa: F401 + RendererBase, + Renderer, + ValueFn, + JSONifiable, + ValueFnApp, # pyright: ignore[reportUnusedImport] + ValueFnSync, # pyright: ignore[reportUnusedImport] + ValueFnAsync, # pyright: ignore[reportUnusedImport] + WrapAsync, # pyright: ignore[reportUnusedImport] + AsyncValueFn, # pyright: ignore[reportUnusedImport] + # IT, # pyright: ignore[reportUnusedImport] +) + +__all__ = ( + "RendererBase", + "Renderer", + "ValueFn", + "JSONifiable", +) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py new file mode 100644 index 000000000..1795c36d7 --- /dev/null +++ b/shiny/render/renderer/_renderer.py @@ -0,0 +1,432 @@ +from __future__ import annotations + +import typing + +# import inspect +from abc import ABC, abstractmethod +from typing import ( # NamedTuple,; Protocol,; cast,; overload, + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + TypeVar, + Union, +) + +from htmltools import MetadataNode, Tag, TagList + +# TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` + +# TODO-barret; Q: Should `Renderer.default_ui` be renamed? `ui()`? `express_ui()`? +# TODO-barret; Q: Should `Renderer.default_ui` accept args? ... Should `output_args()` be renamed to `ui_kwargs()`? (If anything rename to `ui_args()`) +# TODO-barret; Q: Should `Renderer.default_ui` accept kwargs? ... Should `output_kwargs()` be renamed to `ui_kwargs()`? (If anything rename to `ui_kwargs()`) Add `.ui_kwargs()` method? + + +# TODO-future: docs; 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 +# displayed. Even if they have docs. + + +if TYPE_CHECKING: + from ...session import Session + +# from ... import ui as _ui +# from ..._deprecated import warn_deprecated +# from ..._docstring import add_example +# from ..._typing_extensions import Concatenate +from ..._utils import WrapAsync + +__all__ = ( + "Renderer", + "AsyncValueFn", +) + +# from ...types import MISSING + +# Input type for the user-spplied function that is passed to a render.xx +IT = TypeVar("IT") +# # Output type after the Renderer.__call__ method is called on the IT object. +# OT = TypeVar("OT") +# # Param specification for value_fn function +# P = ParamSpec("P") +# # Generic return type for a function +# R = TypeVar("R") + + +# https://github.com/python/cpython/blob/df1eec3dae3b1eddff819fd70f58b03b3fbd0eda/Lib/json/encoder.py#L77-L95 +# +-------------------+---------------+ +# | Python | JSON | +# +===================+===============+ +# | dict | object | +# +-------------------+---------------+ +# | list, tuple | array | +# +-------------------+---------------+ +# | str | string | +# +-------------------+---------------+ +# | int, float | number | +# +-------------------+---------------+ +# | True | true | +# +-------------------+---------------+ +# | False | false | +# +-------------------+---------------+ +# | None | null | +# +-------------------+---------------+ +JSONifiable = Union[ + str, + int, + float, + bool, + None, + List["JSONifiable"], + Tuple["JSONifiable"], + Dict[str, "JSONifiable"], +] + + +# class DefaultUIFn(Protocol): +# def __call__( +# self, id: str, *args: Any, **kwargs: Any +# ) -> TagList | Tag | MetadataNode | str: +# ... + + +DefaultUIFnResult = Union[TagList, Tag, MetadataNode, str] +DefaultUIFnResultOrNone = Union[DefaultUIFnResult, None] +DefaultUIFn = Callable[[str], DefaultUIFnResultOrNone] +DefaultUIFnImpl = Union[ + DefaultUIFn, + Callable[[Dict[str, object], str], DefaultUIFnResultOrNone], +] + +# A `ValueFn` function is an app-supplied function which returns an IT. +# It can be either synchronous or asynchronous +ValueFnSync = Callable[[], IT] +""" +App-supplied output value function which returns type `IT`. This function is +synchronous. +""" +ValueFnAsync = Callable[[], Awaitable[IT]] +""" +App-supplied output value function which returns type `IT`. This function is +asynchronous. +""" +ValueFnApp = Union[Callable[[], IT], Callable[[], Awaitable[IT]]] +""" +App-supplied output value function which returns type `IT`. This function can be +synchronous or asynchronous. +""" +ValueFn = Optional[ValueFnApp[IT | None]] + + +class RendererBase(ABC): + __name__: str + """Name of output function supplied. (The value will not contain any module prefix.)""" + + _auto_registered: bool = False + + # Meta + session: Session + """ + :class:`~shiny.Session` object + """ + name: str + """ + Output function name or ID (provided to `@output(id=)`). This value will contain any module prefix. + """ + + def default_ui( + self, + id: str, + # *args: object, + # **kwargs: object, + ) -> DefaultUIFnResultOrNone: + return None + + @abstractmethod + async def render(self) -> JSONifiable: + ... + + def __init__(self) -> None: + super().__init__() + + # TODO-barret; Could we do this with typing without putting `P` in the Generic? + # TODO-barret; Maybe in the `Renderer` class? idk... + _default_ui_kwargs: dict[str, Any] = dict() + # _default_ui_args: tuple[Any, ...] = tuple() + + def _on_register(self) -> None: + if self._auto_registered: + # We're being explicitly registered now. Undo the auto-registration. + # (w/ module support) + ns_name = self.session.output._ns(self.__name__) + self.session.output.remove(ns_name) + self._auto_registered = False + + def _repr_html_(self) -> str | None: + rendered_ui = self._render_default_ui() + if rendered_ui is None: + return None + return TagList(rendered_ui)._repr_html_() + + def tagify(self) -> DefaultUIFnResult: + rendered_ui = self._render_default_ui() + if rendered_ui is None: + raise TypeError( + "No default UI exists for this type of render function: ", + self.__class__.__name__, + ) + return rendered_ui + + def _render_default_ui(self) -> DefaultUIFnResultOrNone: + return self.default_ui( + self.__name__, + # Pass the `@ui_kwargs(foo="bar")` kwargs through to the default_ui function. + **self._default_ui_kwargs, + ) + + +class AsyncValueFn(WrapAsync[[], IT]): + """ + App-supplied output value function which returns type `IT`. + asynchronous. + + Type definition: `Callable[[], Awaitable[IT]]` + """ + + # VALUE_FN_TYPE = Callable[[], Awaitable[IT]] + pass + + +# class RendererShim(RendererBase, Generic[IT, P]): +# def default_ui( +# self, id: str, *args: P.args, **kwargs: P.kwargs +# ) -> DefaultUIFnResultOrNone: +# return super().default_ui(id) + + +# class Renderer(RendererShim[IT, ...], Generic[IT]): +class Renderer(RendererBase, Generic[IT]): + """ + Renderer cls docs here + TODO-barret - docs + """ + + # __name__: str ?? + + # UI + # TODO-barret; Utilize these! + # default_ui_passthrough_args: tuple[str, ...] | None = None + + # App value function + # _value_fn_original: ValueFnApp[IT] # TODO-barret; Remove this? + _value_fn: AsyncValueFn[IT | None] + + @property + def value_fn(self) -> AsyncValueFn[IT | None]: + return self._value_fn + + """ + App-supplied output value function which returns type `IT`. This function is always + asyncronous as the original app-supplied function possibly wrapped to execute + asynchonously. + """ + + # Transform function; transform value's IT -> OT + # _transform_fn: TransformFn[IT, P, OT] | None = None + + # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) + # 1. OutputRenderer becomes a protocol + # Perform a runtime isinstance check on the class + # Or runtime check for attribute callable field of `_set_metadata()` + # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method + # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. + + def __call__(self, value_fn: ValueFnApp[IT | None]) -> typing.Self: + """ + Renderer __call__ docs here; Sets app's value function + TODO-barret - docs + """ + # print("Renderer - call", value_fn, value_fn.__name__) # TODO-barret; Delete! + if not callable(value_fn): + raise TypeError("Value function must be callable") + + # Copy over function name as it is consistent with how Session and Output + # retrieve function names + self.__name__ = value_fn.__name__ + + # Set value function with extra meta information + self._value_fn = AsyncValueFn(value_fn) + + # If in Express mode, register the output + if not self._auto_registered: + from ...session import get_current_session + + s = get_current_session() + if s is not None: + s.output(self) + # We mark the fact that we're auto-registered so that, if an explicit + # registration now occurs, we can undo this auto-registration. + self._auto_registered = True + + return self + + def __init__( + self, + value_fn: ValueFn[IT | None] = None, + ): + # Do not display docs here. If docs are present, it could highjack the docs of + # the subclass's `__init__` method. + # """ + # Renderer - init docs here + # """ + super().__init__() + if callable(value_fn): + # Register the value function + self(value_fn) + + async def transform(self, value: IT) -> JSONifiable: + """ + Renderer - transform docs here + TODO-barret - docs + """ + # print("Renderer - transform") + 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 a value is retrieved and utilized. For full control, use this method. (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 + TODO-barret - docs + """ + # print("Renderer - render") + value = await self.value_fn() + if value is None: + return None + + rendered = await self.transform(value) + return rendered + + +# from collections.abc import Callable +# from typing import Concatenate, ParamSpec, TypeVar + +# # P = ParamSpec("P") +# T = TypeVar("T") + + +# class text(Renderer[str]): +# """ +# Reactively render text. + +# Returns +# ------- +# : +# A decorator for a function that returns a string. + +# Tip +# ---- +# The name of the decorated function (or ``@output(id=...)``) should match the ``id`` +# of a :func:`~shiny.ui.output_text` container (see :func:`~shiny.ui.output_text` for +# example usage). + +# See Also +# -------- +# ~shiny.ui.output_text +# """ + +# def default_ui(self, id: str, placeholder: bool = True) -> str: +# return "42 - UI" + +# async def transform(self, value: str) -> JSONifiable: +# return str(value) + + +# # # import dominate + + +# # class Barret: +# class Barret(Generic[P, IT]): +# # Same args as init +# # def __new__(cls, *args: object, **kwargs: object) -> Barret[P, IT]: +# # print("Barret - new", args, kwargs) +# # return super().__new__(cls) + +# def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None: +# print("Barret - init", args, kwargs) +# # super().__init__(*args, **kwargs) + +# def __call__(self, renderer: Renderer[IT]) -> typing.Self: +# print("Barret - call", renderer.default_ui) +# # default_ui: Callable[P, R] + +# return self + + +# def _decorate_renderer( +# _renderer: RendererShim[IT, P], +# *args: P.args, +# **kwargs: P.kwargs, +# ) -> Callable[[RendererShim[IT, P]], RendererShim[IT, P]]: +# # renderer: RendererShim[IT, P] + +# def _wrapper(renderer: RendererShim[IT, P]) -> RendererShim[IT, P]: +# """Also does thing XY, but first does something else.""" +# # print(a**2) +# print("wrapper - ", args, kwargs) +# return renderer +# # return f(*args, **kwargs) + +# return _wrapper + +# def get_param_spec(fn: Callable[P, object]) -> Callable[P, object]: +# return P + + +# @Barret(placeholder=True) +# @text +# def _(): +# return "42" + + +# @_decorate_renderer(text, placeholder=True) +# @text +# def _(): +# return "42" + + +# Pinner = get_param_spec(renderer.default_ui) + +# def _decorate( +# f: Callable[Concatenate[str, P], T] +# ) -> Callable[Concatenate[float, P], T]: +# if f is not known_function: # type: ignore[comparison-overlap] +# raise RuntimeError("This is an exclusive decorator.") + +# def _wrapper(a: float, /, *args: P.args, **kwargs: P.kwargs) -> T: +# """Also does thing XY, but first does something else.""" +# print(a**2) +# return f(*args, **kwargs) + +# return _wrapper + +# renderer.default_ui = _decorate(renderer.default_ui) + +# return + + +# wrapper = _decorate(known_function) + + +# if __name__ == "__main__": +# print(known_function(1, "2")) +# print(wrapper(3.14, 10, "10")) From f4cd0c762e86562e8967b9b680c1bb135ffcab7e Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:13:57 -0500 Subject: [PATCH 19/77] First pass of having `OutputRenderer` inherit from `Renderer` --- shiny/render/transformer/_transformer.py | 1720 +++++++++++----------- 1 file changed, 864 insertions(+), 856 deletions(-) diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 04b6e7da3..78769f596 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -1,15 +1,8 @@ from __future__ import annotations -import typing -from functools import wraps +# TODO-future; When `OutputRenderer` is removed, remove `output_args()` +# TODO-barret; Why was `DefaultUIFnImpl` being used? The type does NOT make sense. Using `DefaultUIFn` -# TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` - - -# TODO-future: docs; 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 -# displayed. Even if they have docs. __all__ = ( "TransformerMetadata", @@ -21,8 +14,8 @@ # "ValueFnAsync", # "TransformFn", "output_transformer", - "output_transformer_no_params", - "output_transformer_simple", + # "output_transformer_no_params", + # "output_transformer_simple", "is_async_callable", # "IT", # "OT", @@ -34,19 +27,21 @@ TYPE_CHECKING, Awaitable, Callable, - Dict, Generic, - List, NamedTuple, Optional, - Tuple, TypeVar, Union, cast, overload, ) -from htmltools import MetadataNode, Tag, TagList +from ..renderer._renderer import ( # DefaultUIFnResult,; DefaultUIFnImpl, + DefaultUIFn, + DefaultUIFnResultOrNone, + JSONifiable, + RendererBase, +) if TYPE_CHECKING: from ...session import Session @@ -181,14 +176,8 @@ def inner(*args: P.args, **kwargs: P.kwargs) -> TransformerParams[P]: be defined as an asynchronous function. """ -DefaultUIFn = Callable[[str], Union[TagList, Tag, MetadataNode, str]] -DefaultUIFnImpl = Union[ - DefaultUIFn, - Callable[[Dict[str, object], str], Union[TagList, Tag, MetadataNode, str]], -] - -class OutputRenderer(Generic[OT]): +class OutputRenderer(RendererBase, Generic[OT]): """ Output Renderer @@ -239,7 +228,7 @@ def __init__( value_fn: ValueFnApp[IT], transform_fn: TransformFn[IT, P, OT], params: TransformerParams[P], - default_ui: Optional[DefaultUIFnImpl] = None, + default_ui: Optional[DefaultUIFn] = None, default_ui_passthrough_args: Optional[tuple[str, ...]] = None, ) -> None: """ @@ -257,6 +246,7 @@ def __init__( object that can be used to display the output. This allows render functions to respond to `_repr_html_` method calls in environments like Jupyter. """ + super().__init__() # Copy over function name as it is consistent with how Session and Output # retrieve function names @@ -279,35 +269,36 @@ def __init__( self._value_fn: ValueFn[IT] = wrap_async(value_fn) self._transformer = transform_fn self._params = params - self.default_ui = default_ui - self.default_ui_passthrough_args = default_ui_passthrough_args - self.default_ui_args: tuple[object, ...] = tuple() - self.default_ui_kwargs: dict[str, object] = dict() + self._default_ui = default_ui + self._default_ui_passthrough_args = default_ui_passthrough_args + + self._default_ui_args: tuple[object, ...] = tuple() + self._default_ui_kwargs: dict[str, object] = dict() - self._auto_registered = False + # self._auto_registered = False - from ...session import get_current_session + # from ...session import get_current_session - s = get_current_session() - if s is not None: - s.output(self) - # We mark the fact that we're auto-registered so that, if an explicit - # registration now occurs, we can undo this auto-registration. - self._auto_registered = True + # s = get_current_session() + # if s is not None: + # s.output(self) + # # We mark the fact that we're auto-registered so that, if an explicit + # # registration now occurs, we can undo this auto-registration. + # self._auto_registered = True - def on_register(self) -> None: - if self._auto_registered: - # We're being explicitly registered now. Undo the auto-registration. - self._session.output.remove(self.__name__) - self._auto_registered = False + # def on_register(self) -> None: + # if self._auto_registered: + # # We're being explicitly registered now. Undo the auto-registration. + # self._session.output.remove(self.__name__) + # self._auto_registered = False def _set_metadata(self, session: Session, name: str) -> None: """ When `Renderer`s are assigned to Output object slots, this method is used to pass along Session and name information. """ - self._session: Session = session - self._name: str = name + self.session = session + self.name = name def _meta(self) -> TransformerMetadata: """ @@ -315,8 +306,8 @@ def _meta(self) -> TransformerMetadata: and `name` (the name of the output being rendered) """ return TransformerMetadata( - session=self._session, - name=self._name, + session=self.session, + name=self.name, value_fn_is_async=self._value_fn_is_async, ) @@ -345,39 +336,68 @@ async def _run(self) -> OT: ) return ret - def _repr_html_(self) -> str | None: - import htmltools - - if self.default_ui is None: - return None - return htmltools.TagList(self._render_default())._repr_html_() + # def _repr_html_(self) -> str | None: + # import htmltools + + # if self._default_ui is None: + # return None + # return htmltools.TagList(self._render_default())._repr_html_() + + # def tagify(self) -> TagList | Tag | MetadataNode | str: + # if self._default_ui is None: + # raise TypeError("No default UI exists for this type of render function") + # return self._render_default() + + # def _render_default(self) -> TagList | Tag | MetadataNode | str: + # if self._default_ui is None: + # raise TypeError("No default UI exists for this type of render function") + + # # Merge the kwargs from the render function passthrough, with the kwargs from + # # explicit @ui_kwargs call. The latter take priority. + # kwargs: dict[str, object] = dict() + # if self._default_ui_passthrough_args is not None: + # kwargs.update( + # { + # k: v + # for k, v in self._params.kwargs.items() + # if k in self._default_ui_passthrough_args and v is not MISSING + # } + # ) + # kwargs.update( + # {k: v for k, v in self._default_ui_kwargs.items() if v is not MISSING} + # ) + # return cast(DefaultUIFn, self._default_ui)( + # self.__name__, *self._default_ui_args, **kwargs + # ) - def tagify(self) -> TagList | Tag | MetadataNode | str: - if self.default_ui is None: - raise TypeError("No default UI exists for this type of render function") - return self._render_default() + # # Shims for Renderer class ############################# - def _render_default(self) -> TagList | Tag | MetadataNode | str: - if self.default_ui is None: - raise TypeError("No default UI exists for this type of render function") + def default_ui( + self, + id: str, + **kwargs: object, + ) -> DefaultUIFnResultOrNone: + if self._default_ui is None: + return None - # Merge the kwargs from the render function passthrough, with the kwargs from - # explicit @output_args call. The latter take priority. - kwargs: dict[str, object] = dict() - if self.default_ui_passthrough_args is not None: + if self._default_ui_passthrough_args is not None: kwargs.update( { k: v for k, v in self._params.kwargs.items() - if k in self.default_ui_passthrough_args and v is not MISSING + if k in self._default_ui_passthrough_args and v is not MISSING } ) - kwargs.update( - {k: v for k, v in self.default_ui_kwargs.items() if v is not MISSING} - ) - return cast(DefaultUIFn, self.default_ui)( - self.__name__, *self.default_ui_args, **kwargs - ) + + return self._default_ui(id, *self._default_ui_args, **kwargs) + + async def render(self) -> JSONifiable: + ret = await self._run() + # Really, OT should be bound by JSONifiable. + # But we can't do that now as types like TypedDict break on JSONifiable + # (We also don't really care as we're moving to `Renderer` class) + jsonifiable_ret = cast(JSONifiable, ret) + return jsonifiable_ret # # Using a second class to help clarify that it is of a particular type @@ -740,696 +760,696 @@ def as_value_fn( return output_transformer_impl -# # Barret - -# Class needs to create an outputrenderer and call it later. Not convinced it'll work. -# Proposing that parens are required if typing is used. :-( - - -class BarretRenderer(Generic[IT, OT]): - """ - BarretRenderer cls docs here - """ - - # Meta - _session: Session - _name: str - # __name__: str ?? - - # UI - default_ui: DefaultUIFnImpl | None = None - default_ui_passthrough_args: tuple[str, ...] | None = None - # App value function - _value_fn_original: ValueFnApp[IT] - _value_fn: ValueFn[IT] - - # Transform function; transform value's IT -> OT - # _transform_fn: TransformFn[IT, P, OT] | None = None - - # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) - # 1. OutputRenderer becomes a protocol - # PErform a runtime isinstance check on the class - # Or runtime check for attribute callable field of `_set_metadata()` - # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method - # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. - - def __call__(self, value_fn: ValueFnApp[IT]) -> OutputRenderer[OT]: - """ - BarretRenderer __call__ docs here; Sets app's value function - """ - print("BarretRenderer - call", value_fn) - if not callable(value_fn): - raise TypeError("Value function must be callable") - self._value_fn_original = value_fn - self._value_fn = wrap_async(value_fn) - - async def render_wrapper( - meta: TransformerMetadata, - value_fn: ValueFn[IT], - *args: P.args, - **kwargs: P.kwargs, - ) -> OT: - print("BarretRenderer - call - render_wrapper", meta, value_fn) - rendered = await self.render() - return rendered - - return OutputRenderer( - value_fn=self._value_fn_original, - transform_fn=render_wrapper, - # params=self._params, - params=empty_params(), - default_ui=self.default_ui, - default_ui_passthrough_args=self.default_ui_passthrough_args, - ) - - def __init__( - self, - _value_fn: ValueFnApp[IT] | None = None, - # *init_args: P.args, - # **init_kwargs: P.kwargs, - # value_fn: ValueFnApp[IT], - # transform_fn: TransformFn[IT, P, OT], - # params: TransformerParams[P], - # default_ui: Optional[DefaultUIFnImpl] = None, - # default_ui_passthrough_args: Optional[tuple[str, ...]] = None, - ): - """ - BarretRenderer - init docs here - """ - print("BarretRenderer - init (no args/kwargs)") - if callable(_value_fn): - raise TypeError( - "This should not be called with a callable value_fn! Only the `__call__` method should be called with a callable value_fn" - ) - # self._params: TransformerParams[P] = TransformerParams( - # *init_args, **init_kwargs - # ) - - def _set_metadata(self, session: Session, name: str) -> None: - """ - When `Renderer`s are assigned to Output object slots, this method is used to - pass along Session and name information. - """ - self._session: Session = session - self._name: str = name - - async def render(self) -> OT: - """ - BarretRenderer - render docs here - """ - print("BarretRenderer - render") - print("BarretRenderer - needs abc class?") - value = await self._value_fn() - return cast(OT, value) - - def __new__( - _cls, - _value_fn: ValueFnApp[IT] | None = None, - *new_args: typing.Any, - **new_kwargs: typing.Any, - ) -> typing.Self: - # """ - # Barret __new__ docs here; Intercepts the class creation, - # possibly returning a decorator instead of a class - - # Check if bare class is being used as a decorator (called with a single callable - # arg). If so, decorate the function and return. - # """ - - print("BarretRenderer - new", new_args, new_kwargs, _cls) - # If only one arg is passed and it is a callable, return a decorator - if callable(_value_fn): - # if len(new_args) == 1 and callable(new_args[0]) and not new_kwargs: - print("BarretRenderer - creating decorator!", _cls) - # value_fn = new_args[0] - - out_ren = _cls()(_value_fn) - return out_ren - - resolved_cls = _cls() - resolved_cls._value_fn_original = value_fn - resolved_cls._value_fn = wrap_async(value_fn) - - return resolved_cls - - new_class = super().__new__(_cls) - return new_class(value_fn) - - # @wraps(wrapped) - # def f(*f_args: object, **f_kwargs: object): - # print("BarretRenderer - new - f", f_args, f_kwargs) - - # # with _cls() as _tag: - # # return wrapped(*args, **kwargs) or _tag - - # return f +# # # Barret - # Return like normal. Let the other methods do the work. - return super().__new__(_cls) +# # Class needs to create an outputrenderer and call it later. Not convinced it'll work. +# # Proposing that parens are required if typing is used. :-( -class BarretSimple(BarretRenderer[IT, OT | None]): - # _params: TransformerParams[...] - - def __new__( - _cls, - _fn: ValueFnApp[IT] | None = None, - ) -> typing.Self: - # """ - # Barret __new__ docs here; Intercepts the class creation, - # possibly returning a decorator instead of a class - - # Check if bare class is being used as a decorator (called with a single callable - # arg). If so, decorate the function and return. - # """ - - print("BarretSimple - new", _cls) - # If only one arg is passed and it is a callable, return a decorator - if callable(_fn): - print("BarretSimple - creating decorator!", _cls) - - out_ren = _cls()(_fn) - return out_ren - - resolved_cls = _cls() - resolved_cls._value_fn_original = _fn - resolved_cls._value_fn = wrap_async(_fn) - - print("BarretSimple - exiting creating decorator!", _cls) - - return resolved_cls - - new_class = super().__new__(_cls) - return new_class(_fn) - - # @wraps(wrapped) - # def f(*f_args: object, **f_kwargs: object): - # print("BarretSimple - new - f", f_args, f_kwargs) - - # # with _cls() as _tag: - # # return wrapped(*args, **kwargs) or _tag - - # return f - - # Return like normal. Let the other methods do the work. - return super().__new__(_cls) - - def __init__( - self, - _value_fn: ValueFnApp[IT] | None = None, - ): - """ - BarretSimple - init docs here - """ - super().__init__() - print("BarretSimple - init - no args, no kwargs") - # self._params = empty_params() - - async def transform(self, value: IT) -> OT: - """ - BarretSimple - transform docs here - """ - print("BarretSimple - transform") - print("BarretSimple - needs abc class?") - return cast(OT, value) - - async def render(self) -> OT | None: - """ - BarretSimple - render docs here - """ - print("BarretSimple - render") - value = await self._value_fn() - if value is None: - return None - - rendered = await self.transform(value) - return rendered - - -# ====================================================================================== -# Simple transformer -# ====================================================================================== - -# TODO-barret; Requirements: -# * At app rendering, both parens and no parens must both work as expected -# * Add extra class info on the outputted function (ex .OutputRendererDecorator) - -# None - -# TODO-barret; Document internally: -# Things that break passing through docs: -# * Returning a overloads with no type in function -# * Return type contains a union of functions (that represent overloads) -# * Returning a callable class instance -# Returning type aliases works, even if the function signature is big! - -# # Simple transformer, no params -# * docs to be transferred -# * No parameters, -> no need for overloads! - -# # Simple dict transformer -# * Receives value and returns a dict - -R = TypeVar("R") -# # Does not work with function docs! -CallableDecoBad = Callable[P, R] | Callable[[], Callable[P, R]] -CallableDeco = Callable[[IT | None], OT | Callable[[IT], OT]] -TransformFnSimple = Callable[[TransformerMetadata, ValueFn[IT]], Awaitable[OT]] - - -class CallableDecoCls(Generic[IT, OT]): - def __init__(self, fn: Callable[[IT], OT]) -> None: - self._fn = fn - - async def __call__(self, fn: IT | None) -> OT | Callable[[IT], OT]: - if fn is None: - return self._fn - else: - return self._fn(fn) - # return await self._fn() - - -class OutputRendererSimple(OutputRenderer[OT]): - def __init__( - self, - *, - value_fn: ValueFnApp[IT], - transform_fn: TransformFnSimple[IT, OT], - default_ui: Optional[DefaultUIFnImpl] = None, - default_ui_passthrough_args: Optional[tuple[str, ...]] = None, - ) -> None: - super().__init__( - value_fn=value_fn, - transform_fn=transform_fn, - params=empty_params(), - default_ui=default_ui, - default_ui_passthrough_args=default_ui_passthrough_args, - ) - +# class BarretRenderer(Generic[IT, OT]): +# """ +# BarretRenderer cls docs here +# """ -def output_transformer_no_params( - # transform_fn: TransformFnSimple[IT, OT], - # Require that all params are keyword arguments so that if a user calls `@output_transformer_no_params` (with no parens), an error is thrown - *, - default_ui: Optional[DefaultUIFn] = None, - default_ui_passthrough_args: Optional[tuple[str, ...]] = None, - # # No docs! - # ) -> CallableDecoBad[[ValueFnApp[IT]], OutputRendererSimple[OT]]: - # # Ugly signature, but it works - # ) -> Callable[ - # [ValueFnApp[IT] | None], - # OutputRendererSimple[OT] | Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]], - # ]: - # - # No Docs - # ) -> CallableDecoCls[ValueFnApp[IT], OutputRendererSimple[OT]]: - # Works! - # ) -> CallableDeco[ValueFnApp[IT], OutputRendererSimple[OT]]: - # Works! -) -> Callable[ - [TransformFnSimple[IT, OT]], Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]] -]: - def with_transformer( - transform_fn: TransformFnSimple[IT, OT], - ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]]: - def with_value_fn( - value_fn: ValueFnApp[IT], - ) -> OutputRendererSimple[OT]: - return OutputRendererSimple( - value_fn=value_fn, - transform_fn=transform_fn, - default_ui=default_ui, - default_ui_passthrough_args=default_ui_passthrough_args, - ) +# # Meta +# _session: Session +# _name: str +# # __name__: str ?? + +# # UI +# default_ui: DefaultUIFnImpl | None = None +# default_ui_passthrough_args: tuple[str, ...] | None = None +# # App value function +# _value_fn_original: ValueFnApp[IT] +# _value_fn: ValueFn[IT] + +# # Transform function; transform value's IT -> OT +# # _transform_fn: TransformFn[IT, P, OT] | None = None + +# # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) +# # 1. OutputRenderer becomes a protocol +# # PErform a runtime isinstance check on the class +# # Or runtime check for attribute callable field of `_set_metadata()` +# # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method +# # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. + +# def __call__(self, value_fn: ValueFnApp[IT]) -> OutputRenderer[OT]: +# """ +# BarretRenderer __call__ docs here; Sets app's value function +# """ +# print("BarretRenderer - call", value_fn) +# if not callable(value_fn): +# raise TypeError("Value function must be callable") +# self._value_fn_original = value_fn +# self._value_fn = wrap_async(value_fn) + +# async def render_wrapper( +# meta: TransformerMetadata, +# value_fn: ValueFn[IT], +# *args: P.args, +# **kwargs: P.kwargs, +# ) -> OT: +# print("BarretRenderer - call - render_wrapper", meta, value_fn) +# rendered = await self.render() +# return rendered + +# return OutputRenderer( +# value_fn=self._value_fn_original, +# transform_fn=render_wrapper, +# # params=self._params, +# params=empty_params(), +# default_ui=self.default_ui, +# default_ui_passthrough_args=self.default_ui_passthrough_args, +# ) - return with_value_fn +# def __init__( +# self, +# _value_fn: ValueFnApp[IT] | None = None, +# # *init_args: P.args, +# # **init_kwargs: P.kwargs, +# # value_fn: ValueFnApp[IT], +# # transform_fn: TransformFn[IT, P, OT], +# # params: TransformerParams[P], +# # default_ui: Optional[DefaultUIFnImpl] = None, +# # default_ui_passthrough_args: Optional[tuple[str, ...]] = None, +# ): +# """ +# BarretRenderer - init docs here +# """ +# print("BarretRenderer - init (no args/kwargs)") +# if callable(_value_fn): +# raise TypeError( +# "This should not be called with a callable value_fn! Only the `__call__` method should be called with a callable value_fn" +# ) +# # self._params: TransformerParams[P] = TransformerParams( +# # *init_args, **init_kwargs +# # ) + +# def _set_metadata(self, session: Session, name: str) -> None: +# """ +# When `Renderer`s are assigned to Output object slots, this method is used to +# pass along Session and name information. +# """ +# self._session: Session = session +# self._name: str = name + +# async def render(self) -> OT: +# """ +# BarretRenderer - render docs here +# """ +# print("BarretRenderer - render") +# print("BarretRenderer - needs abc class?") +# value = await self._value_fn() +# return cast(OT, value) - return with_transformer +# def __new__( +# _cls, +# _value_fn: ValueFnApp[IT] | None = None, +# *new_args: typing.Any, +# **new_kwargs: typing.Any, +# ) -> typing.Self: +# # """ +# # Barret __new__ docs here; Intercepts the class creation, +# # possibly returning a decorator instead of a class - # def renderer( - # fn: ValueFnApp[IT], - # ) -> OutputRendererSimple[OT]: - # return OutputRendererSimple[OT]( - # value_fn=fn, - # transform_fn=transform_fn, - # default_ui=default_ui, - # default_ui_passthrough_args=default_ui_passthrough_args, - # ) +# # Check if bare class is being used as a decorator (called with a single callable +# # arg). If so, decorate the function and return. +# # """ - # # @overload - # # def renderer_impl() -> Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]]: - # # ... - - # # @overload - # # def renderer_impl( - # # fn: ValueFnApp[IT], - # # ) -> OutputRendererSimple[OT]: - # # ... - - # def renderer_impl( - # fn: ValueFnApp[IT] | None = None, - # ) -> ( - # OutputRendererSimple[OT] | Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]] - # ): - # if fn is None: - # return renderer - # else: - # return renderer(fn) - - # return renderer_impl - - -# https://github.com/python/cpython/blob/df1eec3dae3b1eddff819fd70f58b03b3fbd0eda/Lib/json/encoder.py#L77-L95 -# +-------------------+---------------+ -# | Python | JSON | -# +===================+===============+ -# | dict | object | -# +-------------------+---------------+ -# | list, tuple | array | -# +-------------------+---------------+ -# | str | string | -# +-------------------+---------------+ -# | int, float | number | -# +-------------------+---------------+ -# | True | true | -# +-------------------+---------------+ -# | False | false | -# +-------------------+---------------+ -# | None | null | -# +-------------------+---------------+ -JSONifiable = Union[ - str, - int, - float, - bool, - None, - List["JSONifiable"], - Tuple["JSONifiable"], - Dict[str, "JSONifiable"], -] +# print("BarretRenderer - new", new_args, new_kwargs, _cls) +# # If only one arg is passed and it is a callable, return a decorator +# if callable(_value_fn): +# # if len(new_args) == 1 and callable(new_args[0]) and not new_kwargs: +# print("BarretRenderer - creating decorator!", _cls) +# # value_fn = new_args[0] +# out_ren = _cls()(_value_fn) +# return out_ren -def output_transformer_simple( - *, - default_ui: Optional[DefaultUIFn] = None, - default_ui_passthrough_args: Optional[tuple[str, ...]] = None, -) -> Callable[ - [Callable[[IT], JSONifiable] | Callable[[IT], Awaitable[JSONifiable]]], - Callable[[ValueFnApp[IT]], OutputRendererSimple[JSONifiable]], -]: - def simple_transformer( - upgrade_fn: Callable[[IT], JSONifiable] | Callable[[IT], Awaitable[JSONifiable]] - ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JSONifiable]]: - upgrade_fn = wrap_async(upgrade_fn) - - async def transform_fn( - _meta: TransformerMetadata, - _fn: ValueFn[IT | None], - ) -> JSONifiable: - res = await _fn() - if res is None: - return None - - ret = await upgrade_fn(res) - return ret - - deco = output_transformer_no_params( - default_ui=default_ui, - default_ui_passthrough_args=default_ui_passthrough_args, - ) - return deco(transform_fn) +# resolved_cls = _cls() +# resolved_cls._value_fn_original = value_fn +# resolved_cls._value_fn = wrap_async(value_fn) - return simple_transformer +# return resolved_cls +# new_class = super().__new__(_cls) +# return new_class(value_fn) -JOT = TypeVar("JOT", bound=JSONifiable) +# # @wraps(wrapped) +# # def f(*f_args: object, **f_kwargs: object): +# # print("BarretRenderer - new - f", f_args, f_kwargs) +# # # with _cls() as _tag: +# # # return wrapped(*args, **kwargs) or _tag -def output_transformer_json( - *, - default_ui: Optional[DefaultUIFn] = None, - default_ui_passthrough_args: Optional[tuple[str, ...]] = None, - # ) -> Callable[ - # [Callable[[IT], JOT] | Callable[[IT], Awaitable[JOT]]], - # Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]], - # ]: -): - def simple_transformer( - upgrade_fn: Callable[[IT], JOT] - | Callable[[IT], Awaitable[JOT]] - # ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: - ): - upgrade_fn = wrap_async(upgrade_fn) - - async def transform_fn( - _meta: TransformerMetadata, - _fn: ValueFn[IT | None], - ) -> JOT | None: - res = await _fn() - if res is None: - return None - - ret = await upgrade_fn(res) - return ret - - with_transformer = output_transformer_params( - default_ui=default_ui, - default_ui_passthrough_args=default_ui_passthrough_args, - ) - with_args = with_transformer(transform_fn) - # def with_args2( - # (() -> (IT@simple_transformer | None)) | (() -> Awaitable[IT@simple_transformer | None]) | None - # ) -> ( - # (((() -> (IT@simple_transformer | None)) | (() -> Awaitable[IT@simple_transformer | None])) -> OutputRenderer[JOT@simple_transformer | None]) | OutputRenderer[JOT@simple_transformer | None] - # ) - return with_args - # with_value_fn = with_args() - # return with_value_fn - with_value_fn: BValueFn[IT, JOT] = with_args() - return with_value_fn - - return simple_transformer - - -def output_transformer_json2( - *, - default_ui: Optional[DefaultUIFn] = None, - default_ui_passthrough_args: Optional[tuple[str, ...]] = None, - # ) -> Callable[[TransformFn[IT, P, OT]], BArgsFn2[IT, P, OT]]: -): - # _output_transform_fn = _ # Give clearer name - - def simple_transformer( - upgrade_fn: Callable[[IT], JOT] - | Callable[[IT], Awaitable[JOT]] - # ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: - # ) -> ( - # Callable[[], Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]] - # | Callable[[Optional[ValueFnApp[IT]], OutputRendererSimple[JOT | None]] - ): - upgrade_fn = wrap_async(upgrade_fn) - - async def transform_fn( - _meta: TransformerMetadata, - _fn: ValueFn[IT | None], - ) -> JOT | None: - res = await _fn() - if res is None: - return None - - ret = await upgrade_fn(res) - return ret - - @typing.overload - def output_renderer( - _: None = None, - ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: - pass - - @typing.overload - def output_renderer( - value_fn: ValueFnApp[IT], - ) -> OutputRendererSimple[JOT | None]: - pass - - def output_renderer( # pyright: ignore[reportGeneralTypeIssues] - # def output_renderer( - _: Optional[ValueFnApp[IT]] = None, - ): - _args_value_fn = _ # Give clearer name +# # return f - def with_value_fn( - value_fn: ValueFnApp[IT], - ) -> OutputRendererSimple[JOT | None]: - return OutputRendererSimple( - value_fn=value_fn, - transform_fn=transform_fn, - default_ui=default_ui, - default_ui_passthrough_args=default_ui_passthrough_args, - ) +# # Return like normal. Let the other methods do the work. +# return super().__new__(_cls) - if callable(_args_value_fn): - # No args were given and the function was called without parens, - # receiving an app value function. Ex: - # @output_transformer_json2 - # def my_output(): - # ... - return with_value_fn(_args_value_fn) - else: - return with_value_fn - return output_renderer +# class BarretSimple(BarretRenderer[IT, OT | None]): +# # _params: TransformerParams[...] - return simple_transformer +# def __new__( +# _cls, +# _fn: ValueFnApp[IT] | None = None, +# ) -> typing.Self: +# # """ +# # Barret __new__ docs here; Intercepts the class creation, +# # possibly returning a decorator instead of a class +# # Check if bare class is being used as a decorator (called with a single callable +# # arg). If so, decorate the function and return. +# # """ -# TODO-barret; Allow for no parens when creating the renderer. But discourage the pkg author. -# TODO-barret; Allow for no parens when calling the renderer in the app. -# TODO-barret; Add extra fields so that existing renderers can be used? -# TODO-barret; Replace the original `output_transformer` with this one? -# TODO-barret; Document `output_transformer_simple` -# TODO-barret; Can the return type of the output_transformer_simple be OT and not JSONifiable? (Just make sure it is a subset of JSONifiable) +# print("BarretSimple - new", _cls) +# # If only one arg is passed and it is a callable, return a decorator +# if callable(_fn): +# print("BarretSimple - creating decorator!", _cls) -# X = TypeVar("X") -# Deco = Callable[[]] +# out_ren = _cls()(_fn) +# return out_ren -# Callable[ -# Callable[[ValueFnApp[IT]], OutputRenderer[OT]] +# resolved_cls = _cls() +# resolved_cls._value_fn_original = _fn +# resolved_cls._value_fn = wrap_async(_fn) -# BValueFnOut = OutputRenderer[OT] -BValueFnIn = ValueFnApp[IT] -BValueFn = Callable[[BValueFnIn[IT]], OutputRenderer[OT]] -BArgsFn = Callable[ - Concatenate[Optional[BValueFnIn[IT]], P], - BValueFn[IT, OT] | OutputRenderer[OT], -] -BArgsFn2 = BValueFnIn[IT] | Callable[P, BValueFn[IT, OT]] +# print("BarretSimple - exiting creating decorator!", _cls) +# return resolved_cls -WithValueFn = Callable[[ValueFnApp[IT]], OutputRenderer[OT]] -WithArgsFn = WithValueFn[IT, OT] | OutputRenderer[OT] +# new_class = super().__new__(_cls) +# return new_class(_fn) -WithTransformerFn = Callable[ - Concatenate[Optional[ValueFnApp[IT]], P], - WithArgsFn[IT, OT], -] +# # @wraps(wrapped) +# # def f(*f_args: object, **f_kwargs: object): +# # print("BarretSimple - new - f", f_args, f_kwargs) -# ## Barret notes: -# If we want to allow for no parens, then the return type is either -# * OutputRenderer[OT] == Callable[[], OT] -# * Callable[[ValueFnApp[IT]], OutputRenderer[OT]] +# # # with _cls() as _tag: +# # # return wrapped(*args, **kwargs) or _tag -# By type definition rules, these are incompatible as one accepts a positional arg and the other does not. -# So we need to use an overload. -# However, using an overload gives the wrong function name for the no-paren call. -# * I believe this is a pylance error and could be fixed. -# -# Implementing with overloads, somehow the docs for render_caps_params are passed through! -# Current downside is that the fn name is `output_renderer` instead of the user's function name at decorator time. (This is a pylance error?) +# # return f -# Using overloads does not allow for us to define the type of the function. -# Using overloads requires us to use pyright ignore statements as the overloads are not compatible with each other. +# # Return like normal. Let the other methods do the work. +# return super().__new__(_cls) +# def __init__( +# self, +# _value_fn: ValueFnApp[IT] | None = None, +# ): +# """ +# BarretSimple - init docs here +# """ +# super().__init__() +# print("BarretSimple - init - no args, no kwargs") +# # self._params = empty_params() + +# async def transform(self, value: IT) -> OT: +# """ +# BarretSimple - transform docs here +# """ +# print("BarretSimple - transform") +# print("BarretSimple - needs abc class?") +# return cast(OT, value) + +# async def render(self) -> OT | None: +# """ +# BarretSimple - render docs here +# """ +# print("BarretSimple - render") +# value = await self._value_fn() +# if value is None: +# return None + +# rendered = await self.transform(value) +# return rendered + + +# # ====================================================================================== +# # Simple transformer +# # ====================================================================================== + +# # TODO-barret; Requirements: +# # * At app rendering, both parens and no parens must both work as expected +# # * Add extra class info on the outputted function (ex .OutputRendererDecorator) + +# # None + +# # TODO-barret; Document internally: +# # Things that break passing through docs: +# # * Returning a overloads with no type in function +# # * Return type contains a union of functions (that represent overloads) +# # * Returning a callable class instance +# # Returning type aliases works, even if the function signature is big! + +# # # Simple transformer, no params +# # * docs to be transferred +# # * No parameters, -> no need for overloads! + +# # # Simple dict transformer +# # * Receives value and returns a dict + +# R = TypeVar("R") +# # # Does not work with function docs! +# CallableDecoBad = Callable[P, R] | Callable[[], Callable[P, R]] +# CallableDeco = Callable[[IT | None], OT | Callable[[IT], OT]] +# TransformFnSimple = Callable[[TransformerMetadata, ValueFn[IT]], Awaitable[OT]] + + +# class CallableDecoCls(Generic[IT, OT]): +# def __init__(self, fn: Callable[[IT], OT]) -> None: +# self._fn = fn + +# async def __call__(self, fn: IT | None) -> OT | Callable[[IT], OT]: +# if fn is None: +# return self._fn +# else: +# return self._fn(fn) +# # return await self._fn() + + +# class OutputRendererSimple(OutputRenderer[OT]): +# def __init__( +# self, +# *, +# value_fn: ValueFnApp[IT], +# transform_fn: TransformFnSimple[IT, OT], +# default_ui: Optional[DefaultUIFnImpl] = None, +# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, +# ) -> None: +# super().__init__( +# value_fn=value_fn, +# transform_fn=transform_fn, +# params=empty_params(), +# default_ui=default_ui, +# default_ui_passthrough_args=default_ui_passthrough_args, +# ) -def output_transformer_params( - # Require that all params are keyword arguments so that if a user calls `@output_transformer_no_params` (with no parens), an error is thrown - # _: Optional[Callable[[TransformFn[IT, P, OT]], BArgsFn[IT, P, OT]]] = None, - *, - default_ui: Optional[DefaultUIFn] = None, - default_ui_passthrough_args: Optional[tuple[str, ...]] = None, - # ) -> Callable[[TransformFn[IT, P, OT]], BArgsFn2[IT, P, OT]]: -): - """ - Output Transformer Params docs - Explain about default ui! - """ - # _output_transform_fn = _ # Give clearer name +# def output_transformer_no_params( +# # transform_fn: TransformFnSimple[IT, OT], +# # Require that all params are keyword arguments so that if a user calls `@output_transformer_no_params` (with no parens), an error is thrown +# *, +# default_ui: Optional[DefaultUIFn] = None, +# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, +# # # No docs! +# # ) -> CallableDecoBad[[ValueFnApp[IT]], OutputRendererSimple[OT]]: +# # # Ugly signature, but it works +# # ) -> Callable[ +# # [ValueFnApp[IT] | None], +# # OutputRendererSimple[OT] | Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]], +# # ]: +# # +# # No Docs +# # ) -> CallableDecoCls[ValueFnApp[IT], OutputRendererSimple[OT]]: +# # Works! +# # ) -> CallableDeco[ValueFnApp[IT], OutputRendererSimple[OT]]: +# # Works! +# ) -> Callable[ +# [TransformFnSimple[IT, OT]], Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]] +# ]: +# def with_transformer( +# transform_fn: TransformFnSimple[IT, OT], +# ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]]: +# def with_value_fn( +# value_fn: ValueFnApp[IT], +# ) -> OutputRendererSimple[OT]: +# return OutputRendererSimple( +# value_fn=value_fn, +# transform_fn=transform_fn, +# default_ui=default_ui, +# default_ui_passthrough_args=default_ui_passthrough_args, +# ) - def with_transformer( - transform_fn: TransformFn[IT, P, OT], - # ) -> BArgsFn2[IT, P, OT]: - ): - @typing.overload - def output_renderer( - *args: P.args, - **kwargs: P.kwargs, - # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: - # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: - ) -> BValueFn[IT, OT]: - pass - - @typing.overload - def output_renderer( # pyright: ignore[reportOverlappingOverload] - value_fn: BValueFnIn[IT], - # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: - # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: - ) -> OutputRenderer[OT]: - pass - - def output_renderer( # pyright: ignore[reportGeneralTypeIssues] - _: Optional[BValueFnIn[IT]] = None, - *args: P.args, - **kwargs: P.kwargs, - # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: - # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: - ): - async def transform_fn_with_params( - _meta: TransformerMetadata, _fn: ValueFn[IT | None] - ) -> OT: - return await transform_fn(_meta, _fn, *args, **kwargs) - - _args_value_fn = _ # Give clearer name - if len(args) > 0: - raise RuntimeError( - "`*args` should not be supplied." - "\nDid you forget to add `()` to your render decorator?" - ) - # params = TransformerParams[P](*args, **kwargs) +# return with_value_fn + +# return with_transformer + +# # def renderer( +# # fn: ValueFnApp[IT], +# # ) -> OutputRendererSimple[OT]: +# # return OutputRendererSimple[OT]( +# # value_fn=fn, +# # transform_fn=transform_fn, +# # default_ui=default_ui, +# # default_ui_passthrough_args=default_ui_passthrough_args, +# # ) + +# # # @overload +# # # def renderer_impl() -> Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]]: +# # # ... + +# # # @overload +# # # def renderer_impl( +# # # fn: ValueFnApp[IT], +# # # ) -> OutputRendererSimple[OT]: +# # # ... + +# # def renderer_impl( +# # fn: ValueFnApp[IT] | None = None, +# # ) -> ( +# # OutputRendererSimple[OT] | Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]] +# # ): +# # if fn is None: +# # return renderer +# # else: +# # return renderer(fn) + +# # return renderer_impl + + +# # https://github.com/python/cpython/blob/df1eec3dae3b1eddff819fd70f58b03b3fbd0eda/Lib/json/encoder.py#L77-L95 +# # +-------------------+---------------+ +# # | Python | JSON | +# # +===================+===============+ +# # | dict | object | +# # +-------------------+---------------+ +# # | list, tuple | array | +# # +-------------------+---------------+ +# # | str | string | +# # +-------------------+---------------+ +# # | int, float | number | +# # +-------------------+---------------+ +# # | True | true | +# # +-------------------+---------------+ +# # | False | false | +# # +-------------------+---------------+ +# # | None | null | +# # +-------------------+---------------+ +# JSONifiable = Union[ +# str, +# int, +# float, +# bool, +# None, +# List["JSONifiable"], +# Tuple["JSONifiable"], +# Dict[str, "JSONifiable"], +# ] + + +# def output_transformer_simple( +# *, +# default_ui: Optional[DefaultUIFn] = None, +# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, +# ) -> Callable[ +# [Callable[[IT], JSONifiable] | Callable[[IT], Awaitable[JSONifiable]]], +# Callable[[ValueFnApp[IT]], OutputRendererSimple[JSONifiable]], +# ]: +# def simple_transformer( +# upgrade_fn: Callable[[IT], JSONifiable] | Callable[[IT], Awaitable[JSONifiable]] +# ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JSONifiable]]: +# upgrade_fn = wrap_async(upgrade_fn) + +# async def transform_fn( +# _meta: TransformerMetadata, +# _fn: ValueFn[IT | None], +# ) -> JSONifiable: +# res = await _fn() +# if res is None: +# return None + +# ret = await upgrade_fn(res) +# return ret + +# deco = output_transformer_no_params( +# default_ui=default_ui, +# default_ui_passthrough_args=default_ui_passthrough_args, +# ) +# return deco(transform_fn) + +# return simple_transformer + + +# JOT = TypeVar("JOT", bound=JSONifiable) + + +# def output_transformer_json( +# *, +# default_ui: Optional[DefaultUIFn] = None, +# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, +# # ) -> Callable[ +# # [Callable[[IT], JOT] | Callable[[IT], Awaitable[JOT]]], +# # Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]], +# # ]: +# ): +# def simple_transformer( +# upgrade_fn: Callable[[IT], JOT] +# | Callable[[IT], Awaitable[JOT]] +# # ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: +# ): +# upgrade_fn = wrap_async(upgrade_fn) + +# async def transform_fn( +# _meta: TransformerMetadata, +# _fn: ValueFn[IT | None], +# ) -> JOT | None: +# res = await _fn() +# if res is None: +# return None + +# ret = await upgrade_fn(res) +# return ret + +# with_transformer = output_transformer_params( +# default_ui=default_ui, +# default_ui_passthrough_args=default_ui_passthrough_args, +# ) +# with_args = with_transformer(transform_fn) +# # def with_args2( +# # (() -> (IT@simple_transformer | None)) | (() -> Awaitable[IT@simple_transformer | None]) | None +# # ) -> ( +# # (((() -> (IT@simple_transformer | None)) | (() -> Awaitable[IT@simple_transformer | None])) -> OutputRenderer[JOT@simple_transformer | None]) | OutputRenderer[JOT@simple_transformer | None] +# # ) +# return with_args +# # with_value_fn = with_args() +# # return with_value_fn +# with_value_fn: BValueFn[IT, JOT] = with_args() +# return with_value_fn + +# return simple_transformer + + +# def output_transformer_json2( +# *, +# default_ui: Optional[DefaultUIFn] = None, +# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, +# # ) -> Callable[[TransformFn[IT, P, OT]], BArgsFn2[IT, P, OT]]: +# ): +# # _output_transform_fn = _ # Give clearer name + +# def simple_transformer( +# upgrade_fn: Callable[[IT], JOT] +# | Callable[[IT], Awaitable[JOT]] +# # ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: +# # ) -> ( +# # Callable[[], Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]] +# # | Callable[[Optional[ValueFnApp[IT]], OutputRendererSimple[JOT | None]] +# ): +# upgrade_fn = wrap_async(upgrade_fn) + +# async def transform_fn( +# _meta: TransformerMetadata, +# _fn: ValueFn[IT | None], +# ) -> JOT | None: +# res = await _fn() +# if res is None: +# return None + +# ret = await upgrade_fn(res) +# return ret + +# @typing.overload +# def output_renderer( +# _: None = None, +# ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: +# pass + +# @typing.overload +# def output_renderer( +# value_fn: ValueFnApp[IT], +# ) -> OutputRendererSimple[JOT | None]: +# pass + +# def output_renderer( # pyright: ignore[reportGeneralTypeIssues] +# # def output_renderer( +# _: Optional[ValueFnApp[IT]] = None, +# ): +# _args_value_fn = _ # Give clearer name + +# def with_value_fn( +# value_fn: ValueFnApp[IT], +# ) -> OutputRendererSimple[JOT | None]: +# return OutputRendererSimple( +# value_fn=value_fn, +# transform_fn=transform_fn, +# default_ui=default_ui, +# default_ui_passthrough_args=default_ui_passthrough_args, +# ) + +# if callable(_args_value_fn): +# # No args were given and the function was called without parens, +# # receiving an app value function. Ex: +# # @output_transformer_json2 +# # def my_output(): +# # ... +# return with_value_fn(_args_value_fn) +# else: +# return with_value_fn + +# return output_renderer + +# return simple_transformer + + +# # TODO-barret; Allow for no parens when creating the renderer. But discourage the pkg author. +# # TODO-barret; Allow for no parens when calling the renderer in the app. +# # TODO-barret; Add extra fields so that existing renderers can be used? +# # TODO-barret; Replace the original `output_transformer` with this one? +# # TODO-barret; Document `output_transformer_simple` +# # TODO-barret; Can the return type of the output_transformer_simple be OT and not JSONifiable? (Just make sure it is a subset of JSONifiable) + +# # X = TypeVar("X") +# # Deco = Callable[[]] + +# # Callable[ +# # Callable[[ValueFnApp[IT]], OutputRenderer[OT]] + +# # BValueFnOut = OutputRenderer[OT] +# BValueFnIn = ValueFnApp[IT] +# BValueFn = Callable[[BValueFnIn[IT]], OutputRenderer[OT]] +# BArgsFn = Callable[ +# Concatenate[Optional[BValueFnIn[IT]], P], +# BValueFn[IT, OT] | OutputRenderer[OT], +# ] +# BArgsFn2 = BValueFnIn[IT] | Callable[P, BValueFn[IT, OT]] + + +# WithValueFn = Callable[[ValueFnApp[IT]], OutputRenderer[OT]] +# WithArgsFn = WithValueFn[IT, OT] | OutputRenderer[OT] + +# WithTransformerFn = Callable[ +# Concatenate[Optional[ValueFnApp[IT]], P], +# WithArgsFn[IT, OT], +# ] + +# # ## Barret notes: +# # If we want to allow for no parens, then the return type is either +# # * OutputRenderer[OT] == Callable[[], OT] +# # * Callable[[ValueFnApp[IT]], OutputRenderer[OT]] + +# # By type definition rules, these are incompatible as one accepts a positional arg and the other does not. +# # So we need to use an overload. +# # However, using an overload gives the wrong function name for the no-paren call. +# # * I believe this is a pylance error and could be fixed. +# # +# # Implementing with overloads, somehow the docs for render_caps_params are passed through! +# # Current downside is that the fn name is `output_renderer` instead of the user's function name at decorator time. (This is a pylance error?) + +# # Using overloads does not allow for us to define the type of the function. +# # Using overloads requires us to use pyright ignore statements as the overloads are not compatible with each other. + + +# def output_transformer_params( +# # Require that all params are keyword arguments so that if a user calls `@output_transformer_no_params` (with no parens), an error is thrown +# # _: Optional[Callable[[TransformFn[IT, P, OT]], BArgsFn[IT, P, OT]]] = None, +# *, +# default_ui: Optional[DefaultUIFn] = None, +# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, +# # ) -> Callable[[TransformFn[IT, P, OT]], BArgsFn2[IT, P, OT]]: +# ): +# """ +# Output Transformer Params docs - def with_value_fn( - value_fn: BValueFnIn[IT], - ) -> OutputRenderer[OT]: - return OutputRenderer( - value_fn=value_fn, - # params=params, - # transform_fn=transform_fn, - transform_fn=transform_fn_with_params, - params=empty_params(), - default_ui=default_ui, - default_ui_passthrough_args=default_ui_passthrough_args, - ) +# Explain about default ui! +# """ +# # _output_transform_fn = _ # Give clearer name - if callable(_args_value_fn): - # No args were given and the function was called without parens, - # receiving an app value function. Ex: - # @output_transformer_params - # def my_output(): - # ... - return with_value_fn(_args_value_fn) - else: - return with_value_fn - - # if callable(_fn): - # # No args were given and the function was called without parens, - # # receiving an app value function. Ex: - # # @output_transformer_params - # # def my_output(): - # # ... - # return with_value_fn(_fn) - # else: - # return with_value_fn - - return output_renderer - - # # TODO-barret; Add more here - # # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] - # # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] - # if callable(_output_transform_fn): - # return with_transformer(_output_transform_fn) - # else: - # return with_transformer - return with_transformer +# def with_transformer( +# transform_fn: TransformFn[IT, P, OT], +# # ) -> BArgsFn2[IT, P, OT]: +# ): +# @typing.overload +# def output_renderer( +# *args: P.args, +# **kwargs: P.kwargs, +# # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: +# # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: +# ) -> BValueFn[IT, OT]: +# pass + +# @typing.overload +# def output_renderer( # pyright: ignore[reportOverlappingOverload] +# value_fn: BValueFnIn[IT], +# # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: +# # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: +# ) -> OutputRenderer[OT]: +# pass + +# def output_renderer( # pyright: ignore[reportGeneralTypeIssues] +# _: Optional[BValueFnIn[IT]] = None, +# *args: P.args, +# **kwargs: P.kwargs, +# # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: +# # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: +# ): +# async def transform_fn_with_params( +# _meta: TransformerMetadata, _fn: ValueFn[IT | None] +# ) -> OT: +# return await transform_fn(_meta, _fn, *args, **kwargs) + +# _args_value_fn = _ # Give clearer name +# if len(args) > 0: +# raise RuntimeError( +# "`*args` should not be supplied." +# "\nDid you forget to add `()` to your render decorator?" +# ) +# # params = TransformerParams[P](*args, **kwargs) + +# def with_value_fn( +# value_fn: BValueFnIn[IT], +# ) -> OutputRenderer[OT]: +# return OutputRenderer( +# value_fn=value_fn, +# # params=params, +# # transform_fn=transform_fn, +# transform_fn=transform_fn_with_params, +# params=empty_params(), +# default_ui=default_ui, +# default_ui_passthrough_args=default_ui_passthrough_args, +# ) + +# if callable(_args_value_fn): +# # No args were given and the function was called without parens, +# # receiving an app value function. Ex: +# # @output_transformer_params +# # def my_output(): +# # ... +# return with_value_fn(_args_value_fn) +# else: +# return with_value_fn + +# # if callable(_fn): +# # # No args were given and the function was called without parens, +# # # receiving an app value function. Ex: +# # # @output_transformer_params +# # # def my_output(): +# # # ... +# # return with_value_fn(_fn) +# # else: +# # return with_value_fn + +# return output_renderer + +# # # TODO-barret; Add more here +# # # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] +# # # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] +# # if callable(_output_transform_fn): +# # return with_transformer(_output_transform_fn) +# # else: +# # return with_transformer +# return with_transformer async def resolve_value_fn(value_fn: ValueFnApp[IT]) -> IT: @@ -1481,137 +1501,125 @@ async def resolve_value_fn(value_fn: ValueFnApp[IT]) -> IT: return value_fn() -# ###################################################################################### - - -if (False): - - # # Goals - # Simple-ish interface for component author - # Component author only needs to implement one async function - # For user, support parens and no parens - # For user, support async and sync usage - # Support docstrings with pyright for parens and no parens - # Support docstrings for quartodoc - - # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) - # 1. OutputRenderer becomes a protocol - # PErform a runtime isinstance check on the class - # Or runtime check for attribute callable field of `_set_metadata()` - # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method - # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. - - class json(BarretSimple[object, jsonifiable]): - def __init__(self, _value_fn: Callable[[], object]): - super().__init__(_value_fn) - - async def transform(self, value: object) -> jsonifiable: - return json.parse(json.dumps(value)) - - class json(BarretRenderer[jsonifiable, str]): - default_ui = output_json - """ - Docs! - no params - """ - - def __init__(self, _value_fn: Callable[[], jsonifiable], *, indent: int = 0): - """ - Docs! - params - """ - super().__init__(_value_fn) - self.indent = indent - - async def render(self) -> str: - value = await self._value_fn() - if value is None: - return None - return await self.transform(value) - - - - - - - - - - - - - - # -------------------------------------------------- - - class OutputRenderer2(Generic[IT], ABC): - - # Try to warn that the parameter is not being set; Later - # @abcfield - # default_ui - - # Called inside `_session` class when trying to retrive the value? - async def _get_value(self) -> OT: - return await self.render() - - def __init__(self, _value_fn: ValueFNApp[IT] | None = None): - self._value_fn = _value_fn - # self.default_ui = Not missing - - def __call__(self, _value_fn: ValueFNApp[IT]) -> typing.Self - self._value_fn = _value_fn - # TODO-barret; REturn self! Rewrite the whole base class as (almost) nothing is necessary anymore - return LegacyOutputRenderer() - - - class text(OutputRenderer2[str]): - """Render decorator for text output""" - default_ui = output_text - default_ui = None - - def __init__(self, _value_fn: ValueFNApp[str] | None = None, *, to_case: str = "upper"): - """ - Create a text renderer - - Parameters - ---------- - _value_fn - A function that returns the text to render - to_case - The case to convert the text to, by default "upper" - """ - super().__init__(_value_fn) - self.to_case = to_case - - def transform(self, value: str) -> JSONifiable: - if self.to_case == "upper": - return value.upper() - elif self.to_case == "lower": - return value.lower() - else: - return value - - class text(OutputRenderer2[str]): - """Render decorator for text output""" - default_ui = output_text - default_ui = None - - def __init__(self, _value_fn: ValueFNApp[JSONIfiable] | None = None): - """ - Create a text renderer - - Parameters - ---------- - _value_fn - A function that returns the text to render - to_case - The case to convert the text to, by default "upper" - """ - super().__init__(_value_fn) - # self.to_case = to_case - - - @text - def foo1(): - ... - - @text(to_case="lower") - def foo2(): - ... +# # ###################################################################################### + + +# if False: +# # # Goals +# # Simple-ish interface for component author +# # Component author only needs to implement one async function +# # For user, support parens and no parens +# # For user, support async and sync usage +# # Support docstrings with pyright for parens and no parens +# # Support docstrings for quartodoc + +# # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) +# # 1. OutputRenderer becomes a protocol +# # PErform a runtime isinstance check on the class +# # Or runtime check for attribute callable field of `_set_metadata()` +# # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method +# # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. + +# class json(BarretSimple[object, jsonifiable]): +# def __init__(self, _value_fn: Callable[[], object]): +# super().__init__(_value_fn) + +# async def transform(self, value: object) -> jsonifiable: +# return json.parse(json.dumps(value)) + +# class json(BarretRenderer[jsonifiable, str]): +# default_ui = output_json +# """ +# Docs! - no params +# """ + +# def __init__(self, _value_fn: Callable[[], jsonifiable], *, indent: int = 0): +# """ +# Docs! - params +# """ +# super().__init__(_value_fn) +# self.indent = indent + +# async def render(self) -> str: +# value = await self._value_fn() +# if value is None: +# return None +# return await self.transform(value) + +# # -------------------------------------------------- + +# class OutputRenderer2(Generic[IT], ABC): +# # Try to warn that the parameter is not being set; Later +# # @abcfield +# # default_ui + +# # Called inside `_session` class when trying to retrive the value? +# async def _get_value(self) -> OT: +# return await self.render() + +# def __init__(self, _value_fn: ValueFNApp[IT] | None = None): +# self._value_fn = _value_fn +# # self.default_ui = Not missing + +# def __call__(self, _value_fn: ValueFNApp[IT]) -> typing.Self: +# self._value_fn = _value_fn +# # TODO-barret; REturn self! Rewrite the whole base class as (almost) nothing is necessary anymore +# return LegacyOutputRenderer() + +# class text(OutputRenderer2[str]): +# """Render decorator for text output""" + +# default_ui = output_text +# default_ui = None + +# def __init__( +# self, _value_fn: ValueFNApp[str] | None = None, *, to_case: str = "upper" +# ): +# """ +# Create a text renderer + +# Parameters +# ---------- +# _value_fn +# A function that returns the text to render +# to_case +# The case to convert the text to, by default "upper" +# """ +# super().__init__(_value_fn) +# self.to_case = to_case + +# def transform(self, value: str) -> JSONifiable: +# if self.to_case == "upper": +# return value.upper() +# elif self.to_case == "lower": +# return value.lower() +# else: +# return value + +# class text(OutputRenderer2[str]): +# """Render decorator for text output""" + +# default_ui = output_text +# default_ui = None + +# def __init__(self, _value_fn: ValueFNApp[JSONIfiable] | None = None): +# """ +# Create a text renderer + +# Parameters +# ---------- +# _value_fn +# A function that returns the text to render +# to_case +# The case to convert the text to, by default "upper" +# """ +# super().__init__(_value_fn) +# # self.to_case = to_case + +# @text +# def foo1(): +# ... + +# @text(to_case="lower") +# def foo2(): +# ... From 273afb85ec77482150c0295798c5a318f8b7b6aa Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:15:21 -0500 Subject: [PATCH 20/77] Expose `ui_kwargs` over `output_args` --- shiny/express/__init__.py | 8 ++++++-- shiny/express/_output.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/shiny/express/__init__.py b/shiny/express/__init__.py index 4cc54de50..c0ab39cc8 100644 --- a/shiny/express/__init__.py +++ b/shiny/express/__init__.py @@ -4,7 +4,11 @@ from ..session import _utils as _session_utils from . import app, layout from ._is_express import is_express_app -from ._output import output_args, suspend_display +from ._output import ( # noqa: F401 + ui_kwargs, + suspend_display, + output_args, # pyright: ignore[reportUnusedImport] +) from ._run import wrap_express_app from .display_decorator import display_body @@ -13,7 +17,7 @@ "output", "session", "is_express_app", - "output_args", + "ui_kwargs", "suspend_display", "wrap_express_app", "app", diff --git a/shiny/express/_output.py b/shiny/express/_output.py index 5753d0490..837c460d5 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -7,8 +7,8 @@ from .. import ui from .._typing_extensions import ParamSpec +from ..render.renderer import RendererBase from ..render.transformer import OutputRenderer -from ..render.transformer._renderer import RendererBase from ..render.transformer._transformer import OT __all__ = ( @@ -21,6 +21,7 @@ CallableT = TypeVar("CallableT", bound=Callable[..., object]) +# TODO-barret; quartodoc entry? def ui_kwargs( **kwargs: object, ) -> Callable[[RendererBase], RendererBase]: From f59faab2804fe594252ee18a943e5f2665e55687 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:16:38 -0500 Subject: [PATCH 21/77] Utilize `Renderer` class instead of `Callable[[], OT]` --- shiny/session/_session.py | 45 ++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/shiny/session/_session.py b/shiny/session/_session.py index d38a08019..e6b60c3a6 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -24,7 +24,6 @@ Callable, Iterable, Optional, - TypeVar, cast, overload, ) @@ -48,13 +47,10 @@ from ..input_handler import input_handlers from ..reactive import Effect_, Value, effect, flush, isolate from ..reactive._core import lock, on_flushed -from ..render.transformer import OutputRenderer -from ..render.transformer._transformer import BarretRenderer +from ..render.renderer._renderer import JSONifiable, RendererBase from ..types import SafeException, SilentCancelOutputException, SilentException from ._utils import RenderedDeps, read_thunk_opt, session_context -OT = TypeVar("OT") - class ConnectionState(enum.Enum): Start = 0 @@ -968,7 +964,7 @@ def __init__( self._suspend_when_hidden = suspend_when_hidden @overload - def __call__(self, renderer_fn: OutputRenderer[OT]) -> OutputRenderer[OT]: + def __call__(self, renderer: RendererBase) -> RendererBase: ... @overload @@ -978,32 +974,33 @@ def __call__( id: Optional[str] = None, suspend_when_hidden: bool = True, priority: int = 0, - ) -> Callable[[OutputRenderer[OT]], OutputRenderer[OT]]: + ) -> Callable[[RendererBase], RendererBase]: ... def __call__( self, - renderer_fn: Optional[OutputRenderer[OT]] = None, + renderer: Optional[RendererBase] = None, *, id: Optional[str] = None, suspend_when_hidden: bool = True, priority: int = 0, - ) -> OutputRenderer[OT] | Callable[[OutputRenderer[OT]], OutputRenderer[OT]]: - def set_renderer(renderer_fn: OutputRenderer[OT]) -> OutputRenderer[OT]: - if hasattr(renderer_fn, "on_register"): - renderer_fn.on_register() - - # Get the (possibly namespaced) output id - output_name = self._ns(id or renderer_fn.__name__) - - if not isinstance(renderer_fn, OutputRenderer): + ) -> RendererBase | Callable[[RendererBase], RendererBase]: + def set_renderer(renderer: RendererBase) -> RendererBase: + if not isinstance(renderer, RendererBase): raise TypeError( "`@output` must be applied to a `@render.xx` function.\n" + "In other words, `@output` must be above `@render.xx`." ) - # renderer_fn is a Renderer object. Give it a bit of metadata. - renderer_fn._set_metadata(self._session, output_name) + # TODO-barret; How does this work? Feels like it should be called after the `renderer.session` is set + renderer._on_register() + + # Get the (possibly namespaced) output id + output_name = self._ns(id or renderer.__name__) + + # renderer is a Renderer object. Give it a bit of metadata. + renderer.session = self._session + renderer.name = output_name self.remove(output_name) @@ -1018,9 +1015,9 @@ async def output_obs(): {"recalculating": {"name": output_name, "status": "recalculating"}} ) - message: dict[str, Optional[OT]] = {} + message: dict[str, JSONifiable] = {} try: - message[output_name] = await renderer_fn() + message[output_name] = await renderer.render() except SilentCancelOutputException: return except SilentException: @@ -1059,12 +1056,12 @@ async def output_obs(): self._effects[output_name] = output_obs - return renderer_fn + return renderer - if renderer_fn is None: + if renderer is None: return set_renderer else: - return set_renderer(renderer_fn) + return set_renderer(renderer) def remove(self, id: Id): output_name = self._ns(id) From 2ec83d6bd1aace25f0c20d16820946fd5d46f530 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:17:00 -0500 Subject: [PATCH 22/77] Add `suspend_display` test --- .../shiny-express/suspend_display/app.py | 24 +++++++++++++++++++ .../suspend_display/test_suspend_display.py | 14 +++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/playwright/shiny/shiny-express/suspend_display/app.py create mode 100644 tests/playwright/shiny/shiny-express/suspend_display/test_suspend_display.py diff --git a/tests/playwright/shiny/shiny-express/suspend_display/app.py b/tests/playwright/shiny/shiny-express/suspend_display/app.py new file mode 100644 index 000000000..3daa72125 --- /dev/null +++ b/tests/playwright/shiny/shiny-express/suspend_display/app.py @@ -0,0 +1,24 @@ +from shiny import render, ui +from shiny.express import input, layout, suspend_display + +layout.set_page(layout.page_fluid()) + +with layout.card(id="card"): + ui.input_slider("s1", "A", 1, 100, 20) + + @suspend_display + @render.text + def hidden(): + return input.s1() + + ui.input_slider("s2", "B", 1, 100, 40) + + # from shiny.express import ui_kwargs + # @ui_kwargs(placeholder=False) + # @ui_kwargs(placeholder=True) + @render.text() + def visible(): + # from shiny import req + + # req(False) + return input.s2() diff --git a/tests/playwright/shiny/shiny-express/suspend_display/test_suspend_display.py b/tests/playwright/shiny/shiny-express/suspend_display/test_suspend_display.py new file mode 100644 index 000000000..a609183fa --- /dev/null +++ b/tests/playwright/shiny/shiny-express/suspend_display/test_suspend_display.py @@ -0,0 +1,14 @@ +from conftest import ShinyAppProc +from controls import OutputTextVerbatim +from playwright.sync_api import Page +from playwright.sync_api import expect as playright_expect + + +def test_express_page_fluid(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + txt = OutputTextVerbatim(page, "visible") + txt.expect_value("40") + + playright_expect(page.locator("#visible")).to_have_count(1) + playright_expect(page.locator("#hidden")).to_have_count(0) From 6b6bc22365acdae95b8efa0f399050a4cb746933 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:17:14 -0500 Subject: [PATCH 23/77] Create test_renderer.py --- tests/pytest/test_renderer.py | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/pytest/test_renderer.py diff --git a/tests/pytest/test_renderer.py b/tests/pytest/test_renderer.py new file mode 100644 index 000000000..a50166552 --- /dev/null +++ b/tests/pytest/test_renderer.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import pytest + +from shiny.render.renderer import Renderer, ValueFn + + +@pytest.mark.asyncio +async def test_renderer_works(): + # No args works + class test_renderer(Renderer[str]): + async def transform(self, value: str) -> str: + return value + " " + value + + @test_renderer() + def txt_paren() -> str: + return "Hello World!" + + val = await txt_paren.render() + assert val == "Hello World! Hello World!" + + @test_renderer + def txt_no_paren() -> str: + return "Hello World!" + + val = await txt_no_paren.render() + assert val == "Hello World! Hello World!" + + +@pytest.mark.asyncio +async def test_renderer_works_with_args(): + # No args works + class test_renderer_with_args(Renderer[str]): + def __init__(self, _fn: ValueFn[str] = None, *, times: int = 2): + super().__init__(_fn) + self.times: int = times + + async def transform(self, value: str) -> str: + values = [value for _ in range(self.times)] + return " ".join(values) + + @test_renderer_with_args + def txt2() -> str: + return "42" + + @test_renderer_with_args(times=4) + def txt4() -> str: + return "42" + + val = await txt2.render() + assert val == "42 42" + val = await txt4.render() + assert val == "42 42 42 42" From bf7b4da74aa8dda153ae7387aff32410a8c64dc6 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:17:27 -0500 Subject: [PATCH 24/77] Static type checking! --- tests/pytest/test_reactives.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/pytest/test_reactives.py b/tests/pytest/test_reactives.py index 45eb1eed3..3d6216579 100644 --- a/tests/pytest/test_reactives.py +++ b/tests/pytest/test_reactives.py @@ -1108,7 +1108,7 @@ async def _(): with pytest.raises(TypeError): # Should complain that @event() can't take the result of @Effect (which returns # None). - @event(lambda: 1) # type: ignore + @event(lambda: 1) # pyright: ignore[reportGeneralTypeIssues] @effect() async def _(): ... @@ -1123,14 +1123,14 @@ async def _(): with pytest.raises(TypeError): # Should complain that @event must be applied before @render.text. At some point # in the future, this may be allowed. - @event(lambda: 1) # No static type error, unfortunately. + @event(lambda: 1) # pyright: ignore[reportGeneralTypeIssues] @render.text async def _(): ... with pytest.raises(TypeError): # Should complain that @event must be applied before @output. - @event(lambda: 1) # type: ignore + @event(lambda: 1) # pyright: ignore[reportGeneralTypeIssues] @output @render.text async def _(): @@ -1168,13 +1168,13 @@ async def test_output_type_check(): with pytest.raises(TypeError): # Should complain about bare function - @output # type: ignore + @output # pyright: ignore[reportGeneralTypeIssues] def _(): ... with pytest.raises(TypeError): # Should complain about @event - @output # type: ignore + @output # pyright: ignore[reportGeneralTypeIssues] @event(lambda: 1) def _(): ... @@ -1182,22 +1182,22 @@ def _(): with pytest.raises(TypeError): # Should complain about @event, even with render.text. Although maybe in the # future this will be allowed. - @output # type: ignore - @event(lambda: 1) + @output # pyright: ignore[reportGeneralTypeIssues] + @event(lambda: 1) # pyright: ignore[reportGeneralTypeIssues] @render.text def _(): ... with pytest.raises(TypeError): # Should complain about @Calc - @output # type: ignore + @output # pyright: ignore[reportGeneralTypeIssues] @calc def _(): ... with pytest.raises(TypeError): # Should complain about @Effet - @output # type: ignore + @output # pyright: ignore[reportGeneralTypeIssues] @effect def _(): ... From 3e74acf15fb3c16d5e96cbc968a3893dfc2d5b9a Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:17:45 -0500 Subject: [PATCH 25/77] More output_args -> ui_kwargs --- tests/pytest/test_express_ui.py | 6 +++--- tests/pytest/test_plot_sizing.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/pytest/test_express_ui.py b/tests/pytest/test_express_ui.py index b06fb3972..358a5bd85 100644 --- a/tests/pytest/test_express_ui.py +++ b/tests/pytest/test_express_ui.py @@ -4,7 +4,7 @@ import pytest from shiny import render, ui -from shiny.express import output_args, suspend_display +from shiny.express import suspend_display, ui_kwargs def test_render_output_controls(): @@ -24,7 +24,7 @@ def text2(): assert ui.TagList(text2.tagify()).get_html_string() == "" - @output_args(placeholder=True) + @ui_kwargs(placeholder=True) @render.text def text3(): return "text" @@ -34,7 +34,7 @@ def text3(): == ui.output_text_verbatim("text3", placeholder=True).get_html_string() ) - @output_args(width=100) + @ui_kwargs(width=100) @render.text def text4(): return "text" diff --git a/tests/pytest/test_plot_sizing.py b/tests/pytest/test_plot_sizing.py index 4390f85f2..da6a354fc 100644 --- a/tests/pytest/test_plot_sizing.py +++ b/tests/pytest/test_plot_sizing.py @@ -1,5 +1,5 @@ from shiny import render, ui -from shiny.express import output_args +from shiny.express import ui_kwargs from shiny.types import MISSING @@ -27,10 +27,10 @@ def foo(): assert rendered == str(ui.output_plot("foo")) -def test_decorator_output_args(): - """@output_args is respected""" +def test_decorator_ui_kwargs(): + """@ui_kwargs is respected""" - @output_args(width="640px", height="480px") + @ui_kwargs(width="640px", height="480px") @render.plot() def foo(): ... @@ -39,10 +39,10 @@ def foo(): assert rendered == str(ui.output_plot("foo", width="640px", height="480px")) -def test_decorator_output_args_priority(): - """@output_args should override render.plot width/height""" +def test_decorator_ui_kwargs_priority(): + """@ui_kwargs should override render.plot width/height""" - @output_args(width="640px", height=480) + @ui_kwargs(width="640px", height=480) @render.plot(width=1280, height=960) def foo(): ... @@ -52,10 +52,10 @@ def foo(): assert rendered == str(ui.output_plot("foo", width=640, height="480px")) -def test_decorator_output_args_MISSING(): +def test_decorator_ui_kwargs_MISSING(): """Not saying we support this, but test how MISSING interacts""" - @output_args(width=MISSING) + @ui_kwargs(width=MISSING) @render.plot(width=1280, height=MISSING) def foo(): ... From 01933ced436d6b14830a2951eb5f9daf2e689fe0 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:25:43 -0500 Subject: [PATCH 26/77] Remove large comments. Tweak types / imports --- shiny/reactive/_reactives.py | 4 +- shiny/render/renderer/__init__.py | 3 +- shiny/render/renderer/_renderer.py | 166 +--- shiny/render/transformer/_transformer.py | 949 +---------------------- 4 files changed, 16 insertions(+), 1106 deletions(-) diff --git a/shiny/reactive/_reactives.py b/shiny/reactive/_reactives.py index 249654b4b..9703d61e0 100644 --- a/shiny/reactive/_reactives.py +++ b/shiny/reactive/_reactives.py @@ -811,9 +811,9 @@ def decorator(user_fn: Callable[[], T]) -> Callable[[], T]: # This is here instead of at the top of the .py file in order to avoid a # circular dependency. - from ..render.transformer import OutputRenderer + from ..render.renderer import RendererBase - if isinstance(user_fn, OutputRenderer): + if isinstance(user_fn, RendererBase): # At some point in the future, we may allow this condition, if we find an # use case. For now we'll disallow it, for simplicity. raise TypeError( diff --git a/shiny/render/renderer/__init__.py b/shiny/render/renderer/__init__.py index b743726f9..c17ec13f1 100644 --- a/shiny/render/renderer/__init__.py +++ b/shiny/render/renderer/__init__.py @@ -7,7 +7,7 @@ ValueFnSync, # pyright: ignore[reportUnusedImport] ValueFnAsync, # pyright: ignore[reportUnusedImport] WrapAsync, # pyright: ignore[reportUnusedImport] - AsyncValueFn, # pyright: ignore[reportUnusedImport] + AsyncValueFn, # IT, # pyright: ignore[reportUnusedImport] ) @@ -16,4 +16,5 @@ "Renderer", "ValueFn", "JSONifiable", + "AsyncValueFn", ) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 1795c36d7..9a6f30808 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -1,10 +1,8 @@ from __future__ import annotations import typing - -# import inspect from abc import ABC, abstractmethod -from typing import ( # NamedTuple,; Protocol,; cast,; overload, +from typing import ( TYPE_CHECKING, Any, Awaitable, @@ -20,6 +18,8 @@ from htmltools import MetadataNode, Tag, TagList +from ..._utils import WrapAsync + # TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` # TODO-barret; Q: Should `Renderer.default_ui` be renamed? `ui()`? `express_ui()`? @@ -36,27 +36,18 @@ if TYPE_CHECKING: from ...session import Session -# from ... import ui as _ui -# from ..._deprecated import warn_deprecated -# from ..._docstring import add_example -# from ..._typing_extensions import Concatenate -from ..._utils import WrapAsync __all__ = ( "Renderer", + "RendererBase", + "Renderer", + "ValueFn", + "JSONifiable", "AsyncValueFn", ) -# from ...types import MISSING - # Input type for the user-spplied function that is passed to a render.xx IT = TypeVar("IT") -# # Output type after the Renderer.__call__ method is called on the IT object. -# OT = TypeVar("OT") -# # Param specification for value_fn function -# P = ParamSpec("P") -# # Generic return type for a function -# R = TypeVar("R") # https://github.com/python/cpython/blob/df1eec3dae3b1eddff819fd70f58b03b3fbd0eda/Lib/json/encoder.py#L77-L95 @@ -89,13 +80,6 @@ ] -# class DefaultUIFn(Protocol): -# def __call__( -# self, id: str, *args: Any, **kwargs: Any -# ) -> TagList | Tag | MetadataNode | str: -# ... - - DefaultUIFnResult = Union[TagList, Tag, MetadataNode, str] DefaultUIFnResultOrNone = Union[DefaultUIFnResult, None] DefaultUIFn = Callable[[str], DefaultUIFnResultOrNone] @@ -104,8 +88,6 @@ Callable[[Dict[str, object], str], DefaultUIFnResultOrNone], ] -# A `ValueFn` function is an app-supplied function which returns an IT. -# It can be either synchronous or asynchronous ValueFnSync = Callable[[], IT] """ App-supplied output value function which returns type `IT`. This function is @@ -155,8 +137,9 @@ async def render(self) -> JSONifiable: def __init__(self) -> None: super().__init__() - # TODO-barret; Could we do this with typing without putting `P` in the Generic? - # TODO-barret; Maybe in the `Renderer` class? idk... + # Q: Could we do this with typing without putting `P` in the Generic? + # A: No. Even if we had a `P` in the Generic, the calling decorator would not have access to it. + # Idea: Possibly use a chained method of `.ui_kwargs()`? https://github.com/posit-dev/py-shiny/issues/971 _default_ui_kwargs: dict[str, Any] = dict() # _default_ui_args: tuple[Any, ...] = tuple() @@ -199,7 +182,6 @@ class AsyncValueFn(WrapAsync[[], IT]): Type definition: `Callable[[], Awaitable[IT]]` """ - # VALUE_FN_TYPE = Callable[[], Awaitable[IT]] pass @@ -237,22 +219,11 @@ def value_fn(self) -> AsyncValueFn[IT | None]: asynchonously. """ - # Transform function; transform value's IT -> OT - # _transform_fn: TransformFn[IT, P, OT] | None = None - - # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) - # 1. OutputRenderer becomes a protocol - # Perform a runtime isinstance check on the class - # Or runtime check for attribute callable field of `_set_metadata()` - # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method - # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. - def __call__(self, value_fn: ValueFnApp[IT | None]) -> typing.Self: """ Renderer __call__ docs here; Sets app's value function TODO-barret - docs """ - # print("Renderer - call", value_fn, value_fn.__name__) # TODO-barret; Delete! if not callable(value_fn): raise TypeError("Value function must be callable") @@ -295,7 +266,6 @@ async def transform(self, value: IT) -> JSONifiable: Renderer - transform docs here TODO-barret - docs """ - # print("Renderer - transform") 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" @@ -308,125 +278,9 @@ async def render(self) -> JSONifiable: Renderer - render docs here TODO-barret - docs """ - # print("Renderer - render") value = await self.value_fn() if value is None: return None rendered = await self.transform(value) return rendered - - -# from collections.abc import Callable -# from typing import Concatenate, ParamSpec, TypeVar - -# # P = ParamSpec("P") -# T = TypeVar("T") - - -# class text(Renderer[str]): -# """ -# Reactively render text. - -# Returns -# ------- -# : -# A decorator for a function that returns a string. - -# Tip -# ---- -# The name of the decorated function (or ``@output(id=...)``) should match the ``id`` -# of a :func:`~shiny.ui.output_text` container (see :func:`~shiny.ui.output_text` for -# example usage). - -# See Also -# -------- -# ~shiny.ui.output_text -# """ - -# def default_ui(self, id: str, placeholder: bool = True) -> str: -# return "42 - UI" - -# async def transform(self, value: str) -> JSONifiable: -# return str(value) - - -# # # import dominate - - -# # class Barret: -# class Barret(Generic[P, IT]): -# # Same args as init -# # def __new__(cls, *args: object, **kwargs: object) -> Barret[P, IT]: -# # print("Barret - new", args, kwargs) -# # return super().__new__(cls) - -# def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None: -# print("Barret - init", args, kwargs) -# # super().__init__(*args, **kwargs) - -# def __call__(self, renderer: Renderer[IT]) -> typing.Self: -# print("Barret - call", renderer.default_ui) -# # default_ui: Callable[P, R] - -# return self - - -# def _decorate_renderer( -# _renderer: RendererShim[IT, P], -# *args: P.args, -# **kwargs: P.kwargs, -# ) -> Callable[[RendererShim[IT, P]], RendererShim[IT, P]]: -# # renderer: RendererShim[IT, P] - -# def _wrapper(renderer: RendererShim[IT, P]) -> RendererShim[IT, P]: -# """Also does thing XY, but first does something else.""" -# # print(a**2) -# print("wrapper - ", args, kwargs) -# return renderer -# # return f(*args, **kwargs) - -# return _wrapper - -# def get_param_spec(fn: Callable[P, object]) -> Callable[P, object]: -# return P - - -# @Barret(placeholder=True) -# @text -# def _(): -# return "42" - - -# @_decorate_renderer(text, placeholder=True) -# @text -# def _(): -# return "42" - - -# Pinner = get_param_spec(renderer.default_ui) - -# def _decorate( -# f: Callable[Concatenate[str, P], T] -# ) -> Callable[Concatenate[float, P], T]: -# if f is not known_function: # type: ignore[comparison-overlap] -# raise RuntimeError("This is an exclusive decorator.") - -# def _wrapper(a: float, /, *args: P.args, **kwargs: P.kwargs) -> T: -# """Also does thing XY, but first does something else.""" -# print(a**2) -# return f(*args, **kwargs) - -# return _wrapper - -# renderer.default_ui = _decorate(renderer.default_ui) - -# return - - -# wrapper = _decorate(known_function) - - -# if __name__ == "__main__": -# print(known_function(1, "2")) -# print(wrapper(3.14, 10, "10")) diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 78769f596..96ac34e74 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -1,6 +1,7 @@ from __future__ import annotations # TODO-future; When `OutputRenderer` is removed, remove `output_args()` + # TODO-barret; Why was `DefaultUIFnImpl` being used? The type does NOT make sense. Using `DefaultUIFn` @@ -14,12 +15,7 @@ # "ValueFnAsync", # "TransformFn", "output_transformer", - # "output_transformer_no_params", - # "output_transformer_simple", "is_async_callable", - # "IT", - # "OT", - # "P", ) import inspect @@ -36,7 +32,7 @@ overload, ) -from ..renderer._renderer import ( # DefaultUIFnResult,; DefaultUIFnImpl, +from ..renderer._renderer import ( DefaultUIFn, DefaultUIFnResultOrNone, JSONifiable, @@ -275,23 +271,6 @@ def __init__( self._default_ui_args: tuple[object, ...] = tuple() self._default_ui_kwargs: dict[str, object] = dict() - # self._auto_registered = False - - # from ...session import get_current_session - - # s = get_current_session() - # if s is not None: - # s.output(self) - # # We mark the fact that we're auto-registered so that, if an explicit - # # registration now occurs, we can undo this auto-registration. - # self._auto_registered = True - - # def on_register(self) -> None: - # if self._auto_registered: - # # We're being explicitly registered now. Undo the auto-registration. - # self._session.output.remove(self.__name__) - # self._auto_registered = False - def _set_metadata(self, session: Session, name: str) -> None: """ When `Renderer`s are assigned to Output object slots, this method is used to @@ -336,40 +315,6 @@ async def _run(self) -> OT: ) return ret - # def _repr_html_(self) -> str | None: - # import htmltools - - # if self._default_ui is None: - # return None - # return htmltools.TagList(self._render_default())._repr_html_() - - # def tagify(self) -> TagList | Tag | MetadataNode | str: - # if self._default_ui is None: - # raise TypeError("No default UI exists for this type of render function") - # return self._render_default() - - # def _render_default(self) -> TagList | Tag | MetadataNode | str: - # if self._default_ui is None: - # raise TypeError("No default UI exists for this type of render function") - - # # Merge the kwargs from the render function passthrough, with the kwargs from - # # explicit @ui_kwargs call. The latter take priority. - # kwargs: dict[str, object] = dict() - # if self._default_ui_passthrough_args is not None: - # kwargs.update( - # { - # k: v - # for k, v in self._params.kwargs.items() - # if k in self._default_ui_passthrough_args and v is not MISSING - # } - # ) - # kwargs.update( - # {k: v for k, v in self._default_ui_kwargs.items() if v is not MISSING} - # ) - # return cast(DefaultUIFn, self._default_ui)( - # self.__name__, *self._default_ui_args, **kwargs - # ) - # # Shims for Renderer class ############################# def default_ui( @@ -400,80 +345,6 @@ 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[DefaultUIFnImpl] = 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 == Renderer -# super().__init__( -# value_fn=value_fn, -# transform_fn=transform_fn, -# params=params, -# default_ui=default_ui, -# default_ui_passthrough_args=default_ui_passthrough_args, -# ) - - -# # 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[DefaultUIFnImpl] = 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 == Renderer -# super().__init__( -# value_fn=value_fn, -# transform_fn=transform_fn, -# params=params, -# default_ui=default_ui, -# default_ui_passthrough_args=default_ui_passthrough_args, -# ) - - # ====================================================================================== # Restrict the transformer function # ====================================================================================== @@ -760,698 +631,6 @@ def as_value_fn( return output_transformer_impl -# # # Barret - -# # Class needs to create an outputrenderer and call it later. Not convinced it'll work. -# # Proposing that parens are required if typing is used. :-( - - -# class BarretRenderer(Generic[IT, OT]): -# """ -# BarretRenderer cls docs here -# """ - -# # Meta -# _session: Session -# _name: str -# # __name__: str ?? - -# # UI -# default_ui: DefaultUIFnImpl | None = None -# default_ui_passthrough_args: tuple[str, ...] | None = None -# # App value function -# _value_fn_original: ValueFnApp[IT] -# _value_fn: ValueFn[IT] - -# # Transform function; transform value's IT -> OT -# # _transform_fn: TransformFn[IT, P, OT] | None = None - -# # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) -# # 1. OutputRenderer becomes a protocol -# # PErform a runtime isinstance check on the class -# # Or runtime check for attribute callable field of `_set_metadata()` -# # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method -# # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. - -# def __call__(self, value_fn: ValueFnApp[IT]) -> OutputRenderer[OT]: -# """ -# BarretRenderer __call__ docs here; Sets app's value function -# """ -# print("BarretRenderer - call", value_fn) -# if not callable(value_fn): -# raise TypeError("Value function must be callable") -# self._value_fn_original = value_fn -# self._value_fn = wrap_async(value_fn) - -# async def render_wrapper( -# meta: TransformerMetadata, -# value_fn: ValueFn[IT], -# *args: P.args, -# **kwargs: P.kwargs, -# ) -> OT: -# print("BarretRenderer - call - render_wrapper", meta, value_fn) -# rendered = await self.render() -# return rendered - -# return OutputRenderer( -# value_fn=self._value_fn_original, -# transform_fn=render_wrapper, -# # params=self._params, -# params=empty_params(), -# default_ui=self.default_ui, -# default_ui_passthrough_args=self.default_ui_passthrough_args, -# ) - -# def __init__( -# self, -# _value_fn: ValueFnApp[IT] | None = None, -# # *init_args: P.args, -# # **init_kwargs: P.kwargs, -# # value_fn: ValueFnApp[IT], -# # transform_fn: TransformFn[IT, P, OT], -# # params: TransformerParams[P], -# # default_ui: Optional[DefaultUIFnImpl] = None, -# # default_ui_passthrough_args: Optional[tuple[str, ...]] = None, -# ): -# """ -# BarretRenderer - init docs here -# """ -# print("BarretRenderer - init (no args/kwargs)") -# if callable(_value_fn): -# raise TypeError( -# "This should not be called with a callable value_fn! Only the `__call__` method should be called with a callable value_fn" -# ) -# # self._params: TransformerParams[P] = TransformerParams( -# # *init_args, **init_kwargs -# # ) - -# def _set_metadata(self, session: Session, name: str) -> None: -# """ -# When `Renderer`s are assigned to Output object slots, this method is used to -# pass along Session and name information. -# """ -# self._session: Session = session -# self._name: str = name - -# async def render(self) -> OT: -# """ -# BarretRenderer - render docs here -# """ -# print("BarretRenderer - render") -# print("BarretRenderer - needs abc class?") -# value = await self._value_fn() -# return cast(OT, value) - -# def __new__( -# _cls, -# _value_fn: ValueFnApp[IT] | None = None, -# *new_args: typing.Any, -# **new_kwargs: typing.Any, -# ) -> typing.Self: -# # """ -# # Barret __new__ docs here; Intercepts the class creation, -# # possibly returning a decorator instead of a class - -# # Check if bare class is being used as a decorator (called with a single callable -# # arg). If so, decorate the function and return. -# # """ - -# print("BarretRenderer - new", new_args, new_kwargs, _cls) -# # If only one arg is passed and it is a callable, return a decorator -# if callable(_value_fn): -# # if len(new_args) == 1 and callable(new_args[0]) and not new_kwargs: -# print("BarretRenderer - creating decorator!", _cls) -# # value_fn = new_args[0] - -# out_ren = _cls()(_value_fn) -# return out_ren - -# resolved_cls = _cls() -# resolved_cls._value_fn_original = value_fn -# resolved_cls._value_fn = wrap_async(value_fn) - -# return resolved_cls - -# new_class = super().__new__(_cls) -# return new_class(value_fn) - -# # @wraps(wrapped) -# # def f(*f_args: object, **f_kwargs: object): -# # print("BarretRenderer - new - f", f_args, f_kwargs) - -# # # with _cls() as _tag: -# # # return wrapped(*args, **kwargs) or _tag - -# # return f - -# # Return like normal. Let the other methods do the work. -# return super().__new__(_cls) - - -# class BarretSimple(BarretRenderer[IT, OT | None]): -# # _params: TransformerParams[...] - -# def __new__( -# _cls, -# _fn: ValueFnApp[IT] | None = None, -# ) -> typing.Self: -# # """ -# # Barret __new__ docs here; Intercepts the class creation, -# # possibly returning a decorator instead of a class - -# # Check if bare class is being used as a decorator (called with a single callable -# # arg). If so, decorate the function and return. -# # """ - -# print("BarretSimple - new", _cls) -# # If only one arg is passed and it is a callable, return a decorator -# if callable(_fn): -# print("BarretSimple - creating decorator!", _cls) - -# out_ren = _cls()(_fn) -# return out_ren - -# resolved_cls = _cls() -# resolved_cls._value_fn_original = _fn -# resolved_cls._value_fn = wrap_async(_fn) - -# print("BarretSimple - exiting creating decorator!", _cls) - -# return resolved_cls - -# new_class = super().__new__(_cls) -# return new_class(_fn) - -# # @wraps(wrapped) -# # def f(*f_args: object, **f_kwargs: object): -# # print("BarretSimple - new - f", f_args, f_kwargs) - -# # # with _cls() as _tag: -# # # return wrapped(*args, **kwargs) or _tag - -# # return f - -# # Return like normal. Let the other methods do the work. -# return super().__new__(_cls) - -# def __init__( -# self, -# _value_fn: ValueFnApp[IT] | None = None, -# ): -# """ -# BarretSimple - init docs here -# """ -# super().__init__() -# print("BarretSimple - init - no args, no kwargs") -# # self._params = empty_params() - -# async def transform(self, value: IT) -> OT: -# """ -# BarretSimple - transform docs here -# """ -# print("BarretSimple - transform") -# print("BarretSimple - needs abc class?") -# return cast(OT, value) - -# async def render(self) -> OT | None: -# """ -# BarretSimple - render docs here -# """ -# print("BarretSimple - render") -# value = await self._value_fn() -# if value is None: -# return None - -# rendered = await self.transform(value) -# return rendered - - -# # ====================================================================================== -# # Simple transformer -# # ====================================================================================== - -# # TODO-barret; Requirements: -# # * At app rendering, both parens and no parens must both work as expected -# # * Add extra class info on the outputted function (ex .OutputRendererDecorator) - -# # None - -# # TODO-barret; Document internally: -# # Things that break passing through docs: -# # * Returning a overloads with no type in function -# # * Return type contains a union of functions (that represent overloads) -# # * Returning a callable class instance -# # Returning type aliases works, even if the function signature is big! - -# # # Simple transformer, no params -# # * docs to be transferred -# # * No parameters, -> no need for overloads! - -# # # Simple dict transformer -# # * Receives value and returns a dict - -# R = TypeVar("R") -# # # Does not work with function docs! -# CallableDecoBad = Callable[P, R] | Callable[[], Callable[P, R]] -# CallableDeco = Callable[[IT | None], OT | Callable[[IT], OT]] -# TransformFnSimple = Callable[[TransformerMetadata, ValueFn[IT]], Awaitable[OT]] - - -# class CallableDecoCls(Generic[IT, OT]): -# def __init__(self, fn: Callable[[IT], OT]) -> None: -# self._fn = fn - -# async def __call__(self, fn: IT | None) -> OT | Callable[[IT], OT]: -# if fn is None: -# return self._fn -# else: -# return self._fn(fn) -# # return await self._fn() - - -# class OutputRendererSimple(OutputRenderer[OT]): -# def __init__( -# self, -# *, -# value_fn: ValueFnApp[IT], -# transform_fn: TransformFnSimple[IT, OT], -# default_ui: Optional[DefaultUIFnImpl] = None, -# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, -# ) -> None: -# super().__init__( -# value_fn=value_fn, -# transform_fn=transform_fn, -# params=empty_params(), -# default_ui=default_ui, -# default_ui_passthrough_args=default_ui_passthrough_args, -# ) - - -# def output_transformer_no_params( -# # transform_fn: TransformFnSimple[IT, OT], -# # Require that all params are keyword arguments so that if a user calls `@output_transformer_no_params` (with no parens), an error is thrown -# *, -# default_ui: Optional[DefaultUIFn] = None, -# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, -# # # No docs! -# # ) -> CallableDecoBad[[ValueFnApp[IT]], OutputRendererSimple[OT]]: -# # # Ugly signature, but it works -# # ) -> Callable[ -# # [ValueFnApp[IT] | None], -# # OutputRendererSimple[OT] | Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]], -# # ]: -# # -# # No Docs -# # ) -> CallableDecoCls[ValueFnApp[IT], OutputRendererSimple[OT]]: -# # Works! -# # ) -> CallableDeco[ValueFnApp[IT], OutputRendererSimple[OT]]: -# # Works! -# ) -> Callable[ -# [TransformFnSimple[IT, OT]], Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]] -# ]: -# def with_transformer( -# transform_fn: TransformFnSimple[IT, OT], -# ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]]: -# def with_value_fn( -# value_fn: ValueFnApp[IT], -# ) -> OutputRendererSimple[OT]: -# return OutputRendererSimple( -# value_fn=value_fn, -# transform_fn=transform_fn, -# default_ui=default_ui, -# default_ui_passthrough_args=default_ui_passthrough_args, -# ) - -# return with_value_fn - -# return with_transformer - -# # def renderer( -# # fn: ValueFnApp[IT], -# # ) -> OutputRendererSimple[OT]: -# # return OutputRendererSimple[OT]( -# # value_fn=fn, -# # transform_fn=transform_fn, -# # default_ui=default_ui, -# # default_ui_passthrough_args=default_ui_passthrough_args, -# # ) - -# # # @overload -# # # def renderer_impl() -> Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]]: -# # # ... - -# # # @overload -# # # def renderer_impl( -# # # fn: ValueFnApp[IT], -# # # ) -> OutputRendererSimple[OT]: -# # # ... - -# # def renderer_impl( -# # fn: ValueFnApp[IT] | None = None, -# # ) -> ( -# # OutputRendererSimple[OT] | Callable[[ValueFnApp[IT]], OutputRendererSimple[OT]] -# # ): -# # if fn is None: -# # return renderer -# # else: -# # return renderer(fn) - -# # return renderer_impl - - -# # https://github.com/python/cpython/blob/df1eec3dae3b1eddff819fd70f58b03b3fbd0eda/Lib/json/encoder.py#L77-L95 -# # +-------------------+---------------+ -# # | Python | JSON | -# # +===================+===============+ -# # | dict | object | -# # +-------------------+---------------+ -# # | list, tuple | array | -# # +-------------------+---------------+ -# # | str | string | -# # +-------------------+---------------+ -# # | int, float | number | -# # +-------------------+---------------+ -# # | True | true | -# # +-------------------+---------------+ -# # | False | false | -# # +-------------------+---------------+ -# # | None | null | -# # +-------------------+---------------+ -# JSONifiable = Union[ -# str, -# int, -# float, -# bool, -# None, -# List["JSONifiable"], -# Tuple["JSONifiable"], -# Dict[str, "JSONifiable"], -# ] - - -# def output_transformer_simple( -# *, -# default_ui: Optional[DefaultUIFn] = None, -# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, -# ) -> Callable[ -# [Callable[[IT], JSONifiable] | Callable[[IT], Awaitable[JSONifiable]]], -# Callable[[ValueFnApp[IT]], OutputRendererSimple[JSONifiable]], -# ]: -# def simple_transformer( -# upgrade_fn: Callable[[IT], JSONifiable] | Callable[[IT], Awaitable[JSONifiable]] -# ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JSONifiable]]: -# upgrade_fn = wrap_async(upgrade_fn) - -# async def transform_fn( -# _meta: TransformerMetadata, -# _fn: ValueFn[IT | None], -# ) -> JSONifiable: -# res = await _fn() -# if res is None: -# return None - -# ret = await upgrade_fn(res) -# return ret - -# deco = output_transformer_no_params( -# default_ui=default_ui, -# default_ui_passthrough_args=default_ui_passthrough_args, -# ) -# return deco(transform_fn) - -# return simple_transformer - - -# JOT = TypeVar("JOT", bound=JSONifiable) - - -# def output_transformer_json( -# *, -# default_ui: Optional[DefaultUIFn] = None, -# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, -# # ) -> Callable[ -# # [Callable[[IT], JOT] | Callable[[IT], Awaitable[JOT]]], -# # Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]], -# # ]: -# ): -# def simple_transformer( -# upgrade_fn: Callable[[IT], JOT] -# | Callable[[IT], Awaitable[JOT]] -# # ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: -# ): -# upgrade_fn = wrap_async(upgrade_fn) - -# async def transform_fn( -# _meta: TransformerMetadata, -# _fn: ValueFn[IT | None], -# ) -> JOT | None: -# res = await _fn() -# if res is None: -# return None - -# ret = await upgrade_fn(res) -# return ret - -# with_transformer = output_transformer_params( -# default_ui=default_ui, -# default_ui_passthrough_args=default_ui_passthrough_args, -# ) -# with_args = with_transformer(transform_fn) -# # def with_args2( -# # (() -> (IT@simple_transformer | None)) | (() -> Awaitable[IT@simple_transformer | None]) | None -# # ) -> ( -# # (((() -> (IT@simple_transformer | None)) | (() -> Awaitable[IT@simple_transformer | None])) -> OutputRenderer[JOT@simple_transformer | None]) | OutputRenderer[JOT@simple_transformer | None] -# # ) -# return with_args -# # with_value_fn = with_args() -# # return with_value_fn -# with_value_fn: BValueFn[IT, JOT] = with_args() -# return with_value_fn - -# return simple_transformer - - -# def output_transformer_json2( -# *, -# default_ui: Optional[DefaultUIFn] = None, -# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, -# # ) -> Callable[[TransformFn[IT, P, OT]], BArgsFn2[IT, P, OT]]: -# ): -# # _output_transform_fn = _ # Give clearer name - -# def simple_transformer( -# upgrade_fn: Callable[[IT], JOT] -# | Callable[[IT], Awaitable[JOT]] -# # ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: -# # ) -> ( -# # Callable[[], Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]] -# # | Callable[[Optional[ValueFnApp[IT]], OutputRendererSimple[JOT | None]] -# ): -# upgrade_fn = wrap_async(upgrade_fn) - -# async def transform_fn( -# _meta: TransformerMetadata, -# _fn: ValueFn[IT | None], -# ) -> JOT | None: -# res = await _fn() -# if res is None: -# return None - -# ret = await upgrade_fn(res) -# return ret - -# @typing.overload -# def output_renderer( -# _: None = None, -# ) -> Callable[[ValueFnApp[IT]], OutputRendererSimple[JOT | None]]: -# pass - -# @typing.overload -# def output_renderer( -# value_fn: ValueFnApp[IT], -# ) -> OutputRendererSimple[JOT | None]: -# pass - -# def output_renderer( # pyright: ignore[reportGeneralTypeIssues] -# # def output_renderer( -# _: Optional[ValueFnApp[IT]] = None, -# ): -# _args_value_fn = _ # Give clearer name - -# def with_value_fn( -# value_fn: ValueFnApp[IT], -# ) -> OutputRendererSimple[JOT | None]: -# return OutputRendererSimple( -# value_fn=value_fn, -# transform_fn=transform_fn, -# default_ui=default_ui, -# default_ui_passthrough_args=default_ui_passthrough_args, -# ) - -# if callable(_args_value_fn): -# # No args were given and the function was called without parens, -# # receiving an app value function. Ex: -# # @output_transformer_json2 -# # def my_output(): -# # ... -# return with_value_fn(_args_value_fn) -# else: -# return with_value_fn - -# return output_renderer - -# return simple_transformer - - -# # TODO-barret; Allow for no parens when creating the renderer. But discourage the pkg author. -# # TODO-barret; Allow for no parens when calling the renderer in the app. -# # TODO-barret; Add extra fields so that existing renderers can be used? -# # TODO-barret; Replace the original `output_transformer` with this one? -# # TODO-barret; Document `output_transformer_simple` -# # TODO-barret; Can the return type of the output_transformer_simple be OT and not JSONifiable? (Just make sure it is a subset of JSONifiable) - -# # X = TypeVar("X") -# # Deco = Callable[[]] - -# # Callable[ -# # Callable[[ValueFnApp[IT]], OutputRenderer[OT]] - -# # BValueFnOut = OutputRenderer[OT] -# BValueFnIn = ValueFnApp[IT] -# BValueFn = Callable[[BValueFnIn[IT]], OutputRenderer[OT]] -# BArgsFn = Callable[ -# Concatenate[Optional[BValueFnIn[IT]], P], -# BValueFn[IT, OT] | OutputRenderer[OT], -# ] -# BArgsFn2 = BValueFnIn[IT] | Callable[P, BValueFn[IT, OT]] - - -# WithValueFn = Callable[[ValueFnApp[IT]], OutputRenderer[OT]] -# WithArgsFn = WithValueFn[IT, OT] | OutputRenderer[OT] - -# WithTransformerFn = Callable[ -# Concatenate[Optional[ValueFnApp[IT]], P], -# WithArgsFn[IT, OT], -# ] - -# # ## Barret notes: -# # If we want to allow for no parens, then the return type is either -# # * OutputRenderer[OT] == Callable[[], OT] -# # * Callable[[ValueFnApp[IT]], OutputRenderer[OT]] - -# # By type definition rules, these are incompatible as one accepts a positional arg and the other does not. -# # So we need to use an overload. -# # However, using an overload gives the wrong function name for the no-paren call. -# # * I believe this is a pylance error and could be fixed. -# # -# # Implementing with overloads, somehow the docs for render_caps_params are passed through! -# # Current downside is that the fn name is `output_renderer` instead of the user's function name at decorator time. (This is a pylance error?) - -# # Using overloads does not allow for us to define the type of the function. -# # Using overloads requires us to use pyright ignore statements as the overloads are not compatible with each other. - - -# def output_transformer_params( -# # Require that all params are keyword arguments so that if a user calls `@output_transformer_no_params` (with no parens), an error is thrown -# # _: Optional[Callable[[TransformFn[IT, P, OT]], BArgsFn[IT, P, OT]]] = None, -# *, -# default_ui: Optional[DefaultUIFn] = None, -# default_ui_passthrough_args: Optional[tuple[str, ...]] = None, -# # ) -> Callable[[TransformFn[IT, P, OT]], BArgsFn2[IT, P, OT]]: -# ): -# """ -# Output Transformer Params docs - -# Explain about default ui! -# """ -# # _output_transform_fn = _ # Give clearer name - -# def with_transformer( -# transform_fn: TransformFn[IT, P, OT], -# # ) -> BArgsFn2[IT, P, OT]: -# ): -# @typing.overload -# def output_renderer( -# *args: P.args, -# **kwargs: P.kwargs, -# # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: -# # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: -# ) -> BValueFn[IT, OT]: -# pass - -# @typing.overload -# def output_renderer( # pyright: ignore[reportOverlappingOverload] -# value_fn: BValueFnIn[IT], -# # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: -# # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: -# ) -> OutputRenderer[OT]: -# pass - -# def output_renderer( # pyright: ignore[reportGeneralTypeIssues] -# _: Optional[BValueFnIn[IT]] = None, -# *args: P.args, -# **kwargs: P.kwargs, -# # ) -> Callable[[ValueFnApp[IT]], OutputRenderer[OT]]: -# # ) -> BValueFn[IT, OT] | OutputRenderer[OT]: -# ): -# async def transform_fn_with_params( -# _meta: TransformerMetadata, _fn: ValueFn[IT | None] -# ) -> OT: -# return await transform_fn(_meta, _fn, *args, **kwargs) - -# _args_value_fn = _ # Give clearer name -# if len(args) > 0: -# raise RuntimeError( -# "`*args` should not be supplied." -# "\nDid you forget to add `()` to your render decorator?" -# ) -# # params = TransformerParams[P](*args, **kwargs) - -# def with_value_fn( -# value_fn: BValueFnIn[IT], -# ) -> OutputRenderer[OT]: -# return OutputRenderer( -# value_fn=value_fn, -# # params=params, -# # transform_fn=transform_fn, -# transform_fn=transform_fn_with_params, -# params=empty_params(), -# default_ui=default_ui, -# default_ui_passthrough_args=default_ui_passthrough_args, -# ) - -# if callable(_args_value_fn): -# # No args were given and the function was called without parens, -# # receiving an app value function. Ex: -# # @output_transformer_params -# # def my_output(): -# # ... -# return with_value_fn(_args_value_fn) -# else: -# return with_value_fn - -# # if callable(_fn): -# # # No args were given and the function was called without parens, -# # # receiving an app value function. Ex: -# # # @output_transformer_params -# # # def my_output(): -# # # ... -# # return with_value_fn(_fn) -# # else: -# # return with_value_fn - -# return output_renderer - -# # # TODO-barret; Add more here -# # # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] -# # # with_transformer.OutputRendererDecorator = OutputRendererDecorator[] -# # if callable(_output_transform_fn): -# # return with_transformer(_output_transform_fn) -# # else: -# # return with_transformer -# return with_transformer - - async def resolve_value_fn(value_fn: ValueFnApp[IT]) -> IT: """ Soft deprecated. Resolve the value function @@ -1499,127 +678,3 @@ async def resolve_value_fn(value_fn: ValueFnApp[IT]) -> IT: # To avoid duplicate work just for a typeguard, we cast the function value_fn = cast(ValueFnSync[IT], value_fn) return value_fn() - - -# # ###################################################################################### - - -# if False: -# # # Goals -# # Simple-ish interface for component author -# # Component author only needs to implement one async function -# # For user, support parens and no parens -# # For user, support async and sync usage -# # Support docstrings with pyright for parens and no parens -# # Support docstrings for quartodoc - -# # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) -# # 1. OutputRenderer becomes a protocol -# # PErform a runtime isinstance check on the class -# # Or runtime check for attribute callable field of `_set_metadata()` -# # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method -# # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. - -# class json(BarretSimple[object, jsonifiable]): -# def __init__(self, _value_fn: Callable[[], object]): -# super().__init__(_value_fn) - -# async def transform(self, value: object) -> jsonifiable: -# return json.parse(json.dumps(value)) - -# class json(BarretRenderer[jsonifiable, str]): -# default_ui = output_json -# """ -# Docs! - no params -# """ - -# def __init__(self, _value_fn: Callable[[], jsonifiable], *, indent: int = 0): -# """ -# Docs! - params -# """ -# super().__init__(_value_fn) -# self.indent = indent - -# async def render(self) -> str: -# value = await self._value_fn() -# if value is None: -# return None -# return await self.transform(value) - -# # -------------------------------------------------- - -# class OutputRenderer2(Generic[IT], ABC): -# # Try to warn that the parameter is not being set; Later -# # @abcfield -# # default_ui - -# # Called inside `_session` class when trying to retrive the value? -# async def _get_value(self) -> OT: -# return await self.render() - -# def __init__(self, _value_fn: ValueFNApp[IT] | None = None): -# self._value_fn = _value_fn -# # self.default_ui = Not missing - -# def __call__(self, _value_fn: ValueFNApp[IT]) -> typing.Self: -# self._value_fn = _value_fn -# # TODO-barret; REturn self! Rewrite the whole base class as (almost) nothing is necessary anymore -# return LegacyOutputRenderer() - -# class text(OutputRenderer2[str]): -# """Render decorator for text output""" - -# default_ui = output_text -# default_ui = None - -# def __init__( -# self, _value_fn: ValueFNApp[str] | None = None, *, to_case: str = "upper" -# ): -# """ -# Create a text renderer - -# Parameters -# ---------- -# _value_fn -# A function that returns the text to render -# to_case -# The case to convert the text to, by default "upper" -# """ -# super().__init__(_value_fn) -# self.to_case = to_case - -# def transform(self, value: str) -> JSONifiable: -# if self.to_case == "upper": -# return value.upper() -# elif self.to_case == "lower": -# return value.lower() -# else: -# return value - -# class text(OutputRenderer2[str]): -# """Render decorator for text output""" - -# default_ui = output_text -# default_ui = None - -# def __init__(self, _value_fn: ValueFNApp[JSONIfiable] | None = None): -# """ -# Create a text renderer - -# Parameters -# ---------- -# _value_fn -# A function that returns the text to render -# to_case -# The case to convert the text to, by default "upper" -# """ -# super().__init__(_value_fn) -# # self.to_case = to_case - -# @text -# def foo1(): -# ... - -# @text(to_case="lower") -# def foo2(): -# ... From 8eeafdcf16bc6c6fa6e3e40cff3d6ed54a6364f5 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:30:04 -0500 Subject: [PATCH 27/77] Update render methods to use new Renderer class --- shiny/render/_dataframe.py | 3 + shiny/render/_display.py | 2 + shiny/render/_render.py | 659 ++++++++++++++++--------------------- 3 files changed, 283 insertions(+), 381 deletions(-) diff --git a/shiny/render/_dataframe.py b/shiny/render/_dataframe.py index 473a2e2bd..87e9f30f7 100644 --- a/shiny/render/_dataframe.py +++ b/shiny/render/_dataframe.py @@ -18,6 +18,9 @@ from ._dataframe_unsafe import serialize_numpy_dtypes from .transformer import TransformerMetadata, ValueFn, output_transformer +# TODO-barret; Implement dataframe + + if TYPE_CHECKING: import pandas as pd diff --git a/shiny/render/_display.py b/shiny/render/_display.py index 229152c74..3c8152b7c 100644 --- a/shiny/render/_display.py +++ b/shiny/render/_display.py @@ -17,6 +17,8 @@ ValueFnSync, ) +# TODO-barret; Implement display + async def display_transformer( _meta: TransformerMetadata, diff --git a/shiny/render/_render.py b/shiny/render/_render.py index cee3059ee..828d45517 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -1,30 +1,22 @@ from __future__ import annotations -__all__ = ( - "text", - "plot", - "image", - "table", - "ui", -) - import base64 import os import sys import typing from typing import ( TYPE_CHECKING, - Any, Literal, Optional, Protocol, Union, cast, - overload, runtime_checkable, ) -from htmltools import TagChild +from htmltools import Tag, TagAttrValue, TagChild + +from shiny.render.renderer import JSONifiable if TYPE_CHECKING: from ..session._utils import RenderedDeps @@ -40,39 +32,23 @@ try_render_pil, try_render_plotnine, ) -from .transformer import TransformerMetadata, ValueFn, output_transformer -from .transformer._transformer import ( - output_transformer_params, - output_transformer_simple, -) +from .renderer import Renderer, ValueFn +__all__ = ( + "text", + "plot", + "image", + "table", + "ui", +) # ====================================================================================== # RenderText # ====================================================================================== -@output_transformer_simple(default_ui=_ui.output_text_verbatim) -def text_simple( - value: str, -) -> str: - """ - Barret - Reactively render text. (simple) - """ - return str(value) - - -@text_simple -def foo() -> str: - return "foo" - - -@output_transformer_params(default_ui=_ui.output_text_verbatim) -async def text( - _meta: TransformerMetadata, - _fn: ValueFn[str | None], -) -> str | None: +class text(Renderer[str]): """ - Barret - Reactively render text. + Reactively render text. Returns ------- @@ -89,60 +65,19 @@ async def text( -------- ~shiny.ui.output_text """ - value = await _fn() - if value is None: - return None - return str(value) - - -@text() -def foo2() -> str: - return "foo" - - -@output_transformer(default_ui=_ui.output_text_verbatim) -async def TextTransformer( - _meta: TransformerMetadata, - _fn: ValueFn[str | None], -) -> str | None: - value = await _fn() - if value is None: - return None - return str(value) - - -@overload -def textOld() -> TextTransformer.OutputRendererDecorator: - ... - - -@overload -def textOld(_fn: TextTransformer.ValueFn) -> TextTransformer.OutputRenderer: - ... - - -def textOld( - _fn: TextTransformer.ValueFn | None = None, -) -> TextTransformer.OutputRenderer | TextTransformer.OutputRendererDecorator: - """ - Reactively render text. - Returns - ------- - : - A decorator for a function that returns a string. + def default_ui(self, id: str, placeholder: bool | MISSING_TYPE = MISSING) -> Tag: + kwargs: dict[str, bool] = {} + if not isinstance(placeholder, MISSING_TYPE): + kwargs["placeholder"] = placeholder + return _ui.output_text_verbatim(id, **kwargs) - Tip - ---- - The name of the decorated function (or ``@output(id=...)``) should match the ``id`` - of a :func:`~shiny.ui.output_text` container (see :func:`~shiny.ui.output_text` for - example usage). + def __init__(self, fn: Optional[ValueFn[str]] = None, power: int = 1) -> None: + super().__init__(fn) + self.power: int = power - See Also - -------- - ~shiny.ui.output_text - """ - return TextTransformer(_fn) + async def transform(self, value: str) -> JSONifiable: + return str(value) # ====================================================================================== @@ -150,145 +85,13 @@ def textOld( # ====================================================================================== -# It would be nice to specify the return type of RenderPlotFunc to be something like: +# It would be nice to specify the return type of ValueFn to be something like: # Union[matplotlib.figure.Figure, PIL.Image.Image] # However, if we did that, we'd have to import those modules at load time, which adds # a nontrivial amount of overhead. So for now, we're just using `object`. -@output_transformer( - default_ui=_ui.output_plot, default_ui_passthrough_args=("width", "height") -) -async def PlotTransformer( - _meta: TransformerMetadata, - _fn: ValueFn[object], - *, - alt: Optional[str] = None, - width: float | None | MISSING_TYPE = MISSING, - height: float | None | MISSING_TYPE = MISSING, - **kwargs: object, -) -> ImgData | None: - is_userfn_async = _meta.value_fn_is_async - name = _meta.name - session = _meta.session - - inputs = session.root_scope().input - - # We don't have enough information at this point to decide what size the plot should - # be. This is because the user's plotting code itself may express an opinion about - # the plot size. We'll take the information we will need and stash it in - # PlotSizeInfo, which then gets passed into the various plotting strategies. - - # Reactively read some information about the plot. - pixelratio: float = typing.cast( - float, inputs[ResolvedId(".clientdata_pixelratio")]() - ) - - # Do NOT call this unless you actually are going to respect the container dimension - # you're asking for. It takes a reactive dependency. If the client hasn't reported - # the requested dimension, you'll get a SilentException. - def container_size(dimension: Literal["width", "height"]) -> float: - result = inputs[ResolvedId(f".clientdata_output_{name}_{dimension}")]() - return typing.cast(float, result) - - non_missing_size = ( - cast(Union[float, None], width) if width is not MISSING else None, - cast(Union[float, None], height) if height is not MISSING else None, - ) - plot_size_info = PlotSizeInfo( - container_size_px_fn=( - lambda: container_size("width"), - lambda: container_size("height"), - ), - user_specified_size_px=non_missing_size, - pixelratio=pixelratio, - ) - - # Call the user function to get the plot object. - x = await _fn() - - # Note that x might be None; it could be a matplotlib.pyplot - - # Try each type of renderer in turn. The reason we do it this way is to avoid - # importing modules that aren't already loaded. That could slow things down, or - # worse, cause an error if the module isn't installed. - # - # Each try_render function should indicate whether it was able to make sense of - # the x value (or, in the case of matplotlib, possibly it decided to use the - # global pyplot figure) by returning a tuple that starts with True. The second - # tuple element may be None in this case, which means the try_render function - # explicitly wants the plot to be blanked. - # - # If a try_render function returns a tuple that starts with False, then the next - # try_render function should be tried. If none succeed, an error is raised. - ok: bool - result: ImgData | None - - if "plotnine" in sys.modules: - ok, result = try_render_plotnine( - x, - plot_size_info=plot_size_info, - alt=alt, - **kwargs, - ) - if ok: - return result - - if "matplotlib" in sys.modules: - ok, result = try_render_matplotlib( - x, - plot_size_info=plot_size_info, - allow_global=not is_userfn_async, - alt=alt, - **kwargs, - ) - if ok: - return result - - if "PIL" in sys.modules: - ok, result = try_render_pil( - x, - plot_size_info=plot_size_info, - alt=alt, - **kwargs, - ) - if ok: - return result - - # This check must happen last because - # matplotlib might be able to plot even if x is `None` - if x is None: - return None - - raise Exception( - f"@render.plot doesn't know to render objects of type '{str(type(x))}'. " - + "Consider either requesting support for this type of plot object, and/or " - + " explictly saving the object to a (png) file and using @render.image." - ) - - -@overload -def plot( - *, - alt: Optional[str] = None, - width: float | None | MISSING_TYPE = MISSING, - height: float | None | MISSING_TYPE = MISSING, - **kwargs: Any, -) -> PlotTransformer.OutputRendererDecorator: - ... - - -@overload -def plot(_fn: PlotTransformer.ValueFn) -> PlotTransformer.OutputRenderer: - ... - - -def plot( - _fn: PlotTransformer.ValueFn | None = None, - *, - alt: Optional[str] = None, - width: float | None | MISSING_TYPE = MISSING, - height: float | None | MISSING_TYPE = MISSING, - **kwargs: Any, -) -> PlotTransformer.OutputRenderer | PlotTransformer.OutputRendererDecorator: + + +class plot(Renderer[object]): """ Reactively render a plot object as an HTML image. @@ -340,62 +143,168 @@ def plot( -------- ~shiny.ui.output_plot ~shiny.render.image """ - return PlotTransformer( - _fn, - PlotTransformer.params( - alt=alt, - width=width, - height=height, - **kwargs, - ), - ) + + def default_ui( + self, + id: str, + *, + width: str | float | int | MISSING_TYPE = MISSING, + height: str | float | int | MISSING_TYPE = MISSING, + **kwargs: object, + ) -> Tag: + def set_kwarg_value( + key: str, + ui_val: str | float | int | MISSING_TYPE, + self_val: float | None | MISSING_TYPE, + ): + if not isinstance(ui_val, MISSING_TYPE): + kwargs[key] = ui_val + return + if not (isinstance(self_val, MISSING_TYPE) or self_val is None): + kwargs[key] = self_val + return + # Do nothing as we don't want to override the default value (that could change in the future) + return + + # Only set the arg if it is available. (Prevents duplicating default values) + set_kwarg_value("width", width, self.width) + set_kwarg_value("height", height, self.height) + return _ui.output_plot( + id, + # (possibly) contains `width` and `height` keys! + **kwargs, # pyright: ignore[reportGeneralTypeIssues] + ) + + def __init__( + self, + fn: Optional[ValueFn[object]] = None, + *, + alt: Optional[str] = None, + width: float | None | MISSING_TYPE = MISSING, + height: float | None | MISSING_TYPE = MISSING, + **kwargs: object, + ) -> None: + super().__init__(fn) + self.alt = alt + self.width = width + self.height = height + self.kwargs = kwargs + + async def render(self) -> dict[str, JSONifiable] | JSONifiable | None: + is_userfn_async = self.value_fn.is_async + name = self.name + session = self.session + width = self.width + height = self.height + alt = self.alt + kwargs = self.kwargs + + inputs = session.root_scope().input + + # We don't have enough information at this point to decide what size the plot should + # be. This is because the user's plotting code itself may express an opinion about + # the plot size. We'll take the information we will need and stash it in + # PlotSizeInfo, which then gets passed into the various plotting strategies. + + # Reactively read some information about the plot. + pixelratio: float = typing.cast( + float, inputs[ResolvedId(".clientdata_pixelratio")]() + ) + + # Do NOT call this unless you actually are going to respect the container dimension + # you're asking for. It takes a reactive dependency. If the client hasn't reported + # the requested dimension, you'll get a SilentException. + def container_size(dimension: Literal["width", "height"]) -> float: + result = inputs[ResolvedId(f".clientdata_output_{name}_{dimension}")]() + return typing.cast(float, result) + + non_missing_size = ( + cast(Union[float, None], width) if width is not MISSING else None, + cast(Union[float, None], height) if height is not MISSING else None, + ) + plot_size_info = PlotSizeInfo( + container_size_px_fn=( + lambda: container_size("width"), + lambda: container_size("height"), + ), + user_specified_size_px=non_missing_size, + pixelratio=pixelratio, + ) + + # Call the user function to get the plot object. + x = await self.value_fn() + + # Note that x might be None; it could be a matplotlib.pyplot + + # Try each type of renderer in turn. The reason we do it this way is to avoid + # importing modules that aren't already loaded. That could slow things down, or + # worse, cause an error if the module isn't installed. + # + # Each try_render function should indicate whether it was able to make sense of + # the x value (or, in the case of matplotlib, possibly it decided to use the + # global pyplot figure) by returning a tuple that starts with True. The second + # tuple element may be None in this case, which means the try_render function + # explicitly wants the plot to be blanked. + # + # If a try_render function returns a tuple that starts with False, then the next + # try_render function should be tried. If none succeed, an error is raised. + ok: bool + result: ImgData | None + + def cast_result(result: ImgData | None) -> dict[str, JSONifiable] | None: + if result is None: + return None + img_dict = dict(result) + img_jsonifiable = cast(dict[str, JSONifiable], img_dict) + return img_jsonifiable + + if "plotnine" in sys.modules: + ok, result = try_render_plotnine( + x, + plot_size_info=plot_size_info, + alt=alt, + **kwargs, + ) + if ok: + return cast_result(result) + + if "matplotlib" in sys.modules: + ok, result = try_render_matplotlib( + x, + plot_size_info=plot_size_info, + allow_global=not is_userfn_async, + alt=alt, + **kwargs, + ) + if ok: + return cast_result(result) + + if "PIL" in sys.modules: + ok, result = try_render_pil( + x, + plot_size_info=plot_size_info, + alt=alt, + **kwargs, + ) + if ok: + return cast_result(result) + + # This check must happen last because + # matplotlib might be able to plot even if x is `None` + if x is None: + return None + + raise Exception( + f"@render.plot doesn't know to render objects of type '{str(type(x))}'. " + + "Consider either requesting support for this type of plot object, and/or " + + " explictly saving the object to a (png) file and using @render.image." + ) # ====================================================================================== # RenderImage # ====================================================================================== -@output_transformer(default_ui=_ui.output_image) -async def ImageTransformer( - _meta: TransformerMetadata, - _fn: ValueFn[ImgData | None], - *, - delete_file: bool = False, -) -> ImgData | None: - res = await _fn() - if res is None: - return None - - src: str = res.get("src") - try: - with open(src, "rb") as f: - data = base64.b64encode(f.read()) - data_str = data.decode("utf-8") - content_type = _utils.guess_mime_type(src) - res["src"] = f"data:{content_type};base64,{data_str}" - return res - finally: - if delete_file: - os.remove(src) - - -@overload -def image( - *, - delete_file: bool = False, -) -> ImageTransformer.OutputRendererDecorator: - ... - - -@overload -def image(_fn: ImageTransformer.ValueFn) -> ImageTransformer.OutputRenderer: - ... - - -def image( - _fn: ImageTransformer.ValueFn | None = None, - *, - delete_file: bool = False, -) -> ImageTransformer.OutputRendererDecorator | ImageTransformer.OutputRenderer: +class image(Renderer[ImgData]): """ Reactively render a image file as an HTML image. @@ -421,7 +330,36 @@ def image( ~shiny.types.ImgData ~shiny.render.plot """ - return ImageTransformer(_fn, ImageTransformer.params(delete_file=delete_file)) + + def default_ui(self, id: str, **kwargs: object): + return _ui.output_image( + id, + **kwargs, # pyright: ignore[reportGeneralTypeIssues] + ) + + def __init__( + self, + fn: Optional[ValueFn[ImgData]] = None, + *, + delete_file: bool = False, + ) -> None: + super().__init__(fn) + self.delete_file: bool = delete_file + + async def transform(self, value: ImgData) -> dict[str, JSONifiable] | None: + src: str = value.get("src") + try: + with open(src, "rb") as f: + data = base64.b64encode(f.read()) + data_str = data.decode("utf-8") + content_type = _utils.guess_mime_type(src) + value["src"] = f"data:{content_type};base64,{data_str}" + value_dict = dict(value) + value_jsonifiable = cast(dict[str, JSONifiable], value_dict) + return value_jsonifiable + finally: + if self.delete_file: + os.remove(src) # ====================================================================================== @@ -439,76 +377,7 @@ def to_pandas(self) -> "pd.DataFrame": TableResult = Union["pd.DataFrame", PandasCompatible, None] -@output_transformer(default_ui=_ui.output_table) -async def TableTransformer( - _meta: TransformerMetadata, - _fn: ValueFn[TableResult | None], - *, - index: bool = False, - classes: str = "table shiny-table w-auto", - border: int = 0, - **kwargs: object, -) -> RenderedDeps | None: - x = await _fn() - - if x is None: - return None - - import pandas - import pandas.io.formats.style - - html: str - if isinstance(x, pandas.io.formats.style.Styler): - html = cast( # pyright: ignore[reportUnnecessaryCast] - str, - x.to_html(**kwargs), # pyright: ignore - ) - else: - if not isinstance(x, pandas.DataFrame): - if not isinstance(x, PandasCompatible): - raise TypeError( - "@render.table doesn't know how to render objects of type " - f"'{str(type(x))}'. Return either a pandas.DataFrame, or an object " - "that has a .to_pandas() method." - ) - x = x.to_pandas() - - html = cast( # pyright: ignore[reportUnnecessaryCast] - str, - x.to_html( # pyright: ignore - index=index, - classes=classes, - border=border, - **kwargs, # pyright: ignore[reportGeneralTypeIssues] - ), - ) - return {"deps": [], "html": html} - - -@overload -def table( - *, - index: bool = False, - classes: str = "table shiny-table w-auto", - border: int = 0, - **kwargs: Any, -) -> TableTransformer.OutputRendererDecorator: - ... - - -@overload -def table(_fn: TableTransformer.ValueFn) -> TableTransformer.OutputRenderer: - ... - - -def table( - _fn: TableTransformer.ValueFn | None = None, - *, - index: bool = False, - classes: str = "table shiny-table w-auto", - border: int = 0, - **kwargs: object, -) -> TableTransformer.OutputRenderer | TableTransformer.OutputRendererDecorator: +class table(Renderer[TableResult]): """ Reactively render a pandas ``DataFrame`` object (or similar) as a basic HTML table. @@ -554,45 +423,65 @@ def table( -------- ~shiny.ui.output_table for the corresponding UI component to this render function. """ - return TableTransformer( - _fn, - TableTransformer.params( - index=index, - classes=classes, - border=border, - **kwargs, - ), - ) + + def default_ui(self, id: str, **kwargs: TagAttrValue) -> Tag: + return _ui.output_table(id, **kwargs) + + def __init__( + self, + fn: Optional[ValueFn[TableResult]] = None, + *, + index: bool = False, + classes: str = "table shiny-table w-auto", + border: int = 0, + **kwargs: object, + ) -> None: + super().__init__(fn) + self.index: bool = index + self.classes: str = classes + self.border: int = border + self.kwargs: dict[str, object] = kwargs + + async def transform(self, value: TableResult) -> dict[str, JSONifiable]: + import pandas + import pandas.io.formats.style + + html: str + if isinstance(value, pandas.io.formats.style.Styler): + html = cast( # pyright: ignore[reportUnnecessaryCast] + str, + value.to_html(**self.kwargs), # pyright: ignore + ) + else: + if not isinstance(value, pandas.DataFrame): + if not isinstance(value, PandasCompatible): + raise TypeError( + "@render.table doesn't know how to render objects of type " + f"'{str(type(value))}'. Return either a pandas.DataFrame, or an object " + "that has a .to_pandas() method." + ) + value = value.to_pandas() + + html = cast( # pyright: ignore[reportUnnecessaryCast] + str, + value.to_html( # pyright: ignore + index=self.index, + classes=self.classes, + border=self.border, + **self.kwargs, # pyright: ignore[reportGeneralTypeIssues] + ), + ) + # Use typing to make sure the return shape matches + ret: RenderedDeps = {"deps": [], "html": html} + ret_dict = dict(ret) + ret_jsonifiable = cast(dict[str, JSONifiable], ret_dict) + return ret_jsonifiable # ====================================================================================== # RenderUI # ====================================================================================== -@output_transformer(default_ui=_ui.output_ui) -async def UiTransformer( - _meta: TransformerMetadata, - _fn: ValueFn[TagChild], -) -> RenderedDeps | None: - ui = await _fn() - if ui is None: - return None - - return _meta.session._process_ui(ui) - - -@overload -def ui() -> UiTransformer.OutputRendererDecorator: - ... - - -@overload -def ui(_fn: UiTransformer.ValueFn) -> UiTransformer.OutputRenderer: - ... - - -def ui( - _fn: UiTransformer.ValueFn | None = None, -) -> UiTransformer.OutputRenderer | UiTransformer.OutputRendererDecorator: +class ui(Renderer[TagChild]): """ Reactively render HTML content. @@ -612,4 +501,12 @@ def ui( -------- ~shiny.ui.output_ui """ - return UiTransformer(_fn) + + def default_ui(self, id: str) -> Tag: + return _ui.output_ui(id) + + async def transform(self, value: TagChild) -> JSONifiable: + res = self.session._process_ui(value) + res_dict = dict(res) + res_jsonifiable = cast(JSONifiable, res_dict) + return res_jsonifiable From 5d98e0b556c526f65b4e98652c9addf98c3e2f5b Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:32:21 -0500 Subject: [PATCH 28/77] Remove unused code; Add docs TODO --- shiny/api-examples/output_transformer/app.py | 354 +------------------ 1 file changed, 11 insertions(+), 343 deletions(-) diff --git a/shiny/api-examples/output_transformer/app.py b/shiny/api-examples/output_transformer/app.py index bcb83a4ac..0cfe64b81 100644 --- a/shiny/api-examples/output_transformer/app.py +++ b/shiny/api-examples/output_transformer/app.py @@ -1,89 +1,24 @@ # pyright : basic from __future__ import annotations -from typing import Literal, overload - from shiny import App, Inputs, Outputs, Session, ui -from shiny.render.transformer import ( - TransformerMetadata, - ValueFn, - ValueFnApp, - output_transformer, -) -from shiny.render.transformer._transformer import ( - BarretRenderer, - BarretSimple, - JSONifiable, - output_transformer_json, - output_transformer_json2, - output_transformer_no_params, - output_transformer_params, - output_transformer_simple, -) - -# # # Goals -# # Simple-ish interface for component author -# # Component author only needs to implement one async function -# # For user, support parens and no parens -# # For user, support async and sync usage -# # Support docstrings with pyright for parens and no parens -# # Support docstrings for quartodoc - -# # 0. Rename OutputRenderer to `OutputRendererLegacy` (or something) -# # 1. OutputRenderer becomes a protocol -# # PErform a runtime isinstance check on the class -# # Or runtime check for attribute callable field of `_set_metadata()` -# # 2. Don't use `P` within `OutputRenderer` base class. Instead, use `self.FOO` params within the child class / child render method -# # Only use Barret Renderer class and have users overwrite the transform or render method as they see fit. - -# class json(BarretSimple[object, jsonifiable]): -# def __init__(self, _value_fn: Callable[[], object]): -# super().__init__(_value_fn) - -# async def transform(self, value: object) -> jsonifiable: -# return json.parse(json.dumps(value)) - -# class json(BarretRenderer[jsonifiable, str]): -# default_ui = output_json -# """ -# Docs! - no params -# """ +from shiny.render.renderer import Renderer, ValueFn -# def __init__(self, _value_fn: Callable[[], jsonifiable], *, indent: int = 0): -# """ -# Docs! - params -# """ -# super().__init__(_value_fn) -# self.indent = indent +# TODO-barret Update app with docs below -# async def render(self) -> str: -# value = await self._value_fn() -# if value is None: -# return None -# return await self.transform(value) - -# @overload -# def __init__(self, *, a: int = 1) -> None: -# ... - - -# @overload -# def __init__(self, _fn: ValueFn[str | None]) -> None: -# ... -class sub_barret_renderer(BarretRenderer[str | None, JSONifiable]): +class sub_barret_renderer(Renderer[str]): """ SubBarretSimple - class docs - Render Caps docs """ - # a: int - default_ui = ui.output_text_verbatim - # default_ui_passthrough_args = None + def default_ui(self, id: str): + return ui.output_text_verbatim(id, placeholder=self.placeholder) def __init__( self, # Required for no paren usage - _fn: ValueFnApp[str | None] | None = None, + _fn: ValueFn[str | None] | None = None, *, a: int = 1, placeholder: bool = True, @@ -95,200 +30,36 @@ def __init__( super().__init__(_fn) self.widget = None self.a: int = a - # self.default_ui = lambda(id): ui.output_text_verbatim(id, placeholder=placeholder) self.default_ui = ui.output_text_verbatim async def render(self) -> str | None: value = await self._value_fn() - values = [value, value, value] - [x for x in values if isinstance(x, Sidebar)] if value is None: return None self.widget = value - # self.a return f"{value.upper()}; a={self.a}" -from typing import Any, Awaitable, Callable, Generic - -from shiny.render.transformer._transformer import IT, OT - -# class BarretWrap(BarretSimple[IT, OT]): -# """ -# BarretWrap - Render Caps docs -# """ - -# a: int - -# # @overload -# # def __init__(self, *, a: int = 1) -> None: -# # ... - -# # @overload -# # def __init__(self, _fn: ValueFn[str | None]) -> None: -# # ... - -# # Add default_ui? -# def __init__(self, transform_fn: Callable[[IT], Awaitable[OT]]) -> None: -# super().__init__() -# self._transform_fn = transform_fn - -# async def render(self) -> OT | None: -# """ -# BarretWrap - render docs here -# """ -# print("BarretSimple - render") -# value = await self._value_fn() -# if value is None: -# return None - -# rendered = await self.transform(value) -# return rendered - -# from typing import Sequence -# def length(value: Sequence[IT]) -> int: -# return len(value) - - -# def simple_fn( -# transform_fn: Callable[[IT], Awaitable[OT]], -# # *, -# # ignore: IT | None = None, -# # ignore2: OT | None = None, -# ): -# bs = BarretSimple[IT, OT]() - -# async def transform_(value: IT) -> OT: -# return await transform_fn(value) - -# bs.transform = transform_ -# # bs is set up - -# @overload -# def _(_fn: None = None) -> Callable[[ValueFnApp[IT]], BarretSimple[IT, OT]]: -# ... - -# @overload -# def _(_fn: ValueFnApp[IT]) -> BarretSimple[IT, OT]: -# ... - -# def _( -# _fn: ValueFnApp[IT] | None = None, -# ) -> BarretSimple[IT, OT] | Callable[[ValueFnApp[IT]], BarretSimple[IT, OT]]: -# if callable(_fn): -# bs(_fn) -# return bs - -# return _ - - -# return ret - - -# @simple_fn -# async def barret_simple_fn(value: str) -> str: -# """ -# Barret - Simple function docs -# """ -# return value.upper() - - -class sub_barret_simple(BarretRenderer[str, str]): +class sub_barret_simple(Renderer[str]): """ SubBarretSimple - class - Render Caps docs """ - default_ui = ui.output_text_verbatim + def default_ui(self, id: str): + return ui.output_text_verbatim(id, placeholder=True) def __init__( self, - _value_fn: ValueFnApp[str] | None = None, + _value_fn: ValueFn[str] | None = None, ): """ SubBarretSimple - init - docs here """ - super().__init__() # TODO-barret; pass through _value_fn + super().__init__(_value_fn) async def transform(self, value: str) -> str: return str(value).upper() - # async def render(self) -> str: - # # OPen graphics - # value = await self._value_fn() - # # close graphics - # # self.a - # return ( - # f"{value.upper()}, {self._params.args}, {self._params.kwargs}; a={self.a}" - # ) - - -####### -# Package authors can create their own output transformer methods by leveraging -# `output_transformer` decorator. -# -# The transformer is kept simple for demonstration purposes, but it can be much more -# complex (e.g. shiny.render.plotly) -####### - - -# @output_transformer_json2() -# def render_caps_simple( -# value: str, -# ) -> str: -# """ -# Barret - Render Caps docs (simple) -# """ -# # return [value.upper(), value.lower()] -# return value.upper() - - -@output_transformer_params(default_ui=ui.output_text_verbatim) -async def render_caps_params( - # Contains information about the render call: `name` and `session` - _meta: TransformerMetadata, - # The app-supplied output value function - _fn: ValueFn[str | None], - *, - to: Literal["upper", "lower"] = "upper", -) -> str | None: - """ - Barret - Render Caps docs params - """ - # Get the value - value = await _fn() - - # _meta.self.widget = value # pyright: ignore - - # Render nothing if `value` is `None` - if value is None: - return None - - if to == "upper": - return value.upper() - if to == "lower": - return value.lower() - raise ValueError(f"Invalid value for `to`: {to}") - - -# @output_transformer_params() -# async def render_caps_no_params( -# # Contains information about the render call: `name` and `session` -# _meta: TransformerMetadata, -# # The app-supplied output value function -# _fn: ValueFn[str | None], -# ) -> str | None: -# """ -# Barret - Render Caps docs no parameters -# """ -# # Get the value -# value = await _fn() - -# # Render nothing if `value` is `None` -# if value is None: -# return None - -# return value.upper() - # # Create renderer components from the async handler function: `capitalize_components()` # @output_transformer() @@ -389,20 +160,7 @@ def text_row(id: str): app_ui = ui.page_fluid( ui.h1("Capitalization renderer"), ui.input_text("caption", "Caption:", "Data summary"), - # ui.tags.table( - text_row("old_no_paren"), - text_row("old_paren"), - # - text_row("barret_caps_simple_no_paren"), - text_row("barret_caps_simple_paren"), - # - text_row("barret_caps_params_no_paren"), - text_row("barret_caps_params_paren"), - # - text_row("barret_caps_no_params_no_paren"), - text_row("barret_caps_no_params_paren"), - # text_row("barret_sub_simple_no_paren"), text_row("barret_sub_simple_paren"), # @@ -411,113 +169,23 @@ def text_row(id: str): ), ) -# import dominate.tags as dom_tags - -# dom_tags.h1("content") - - -# @dom_tags.h1 -# def _(): -# return "content" - def server(input: Inputs, output: Outputs, session: Session): - # @output - # # Called without parentheses - # @render_capitalize - # def old_no_paren(): - # return input.caption() - - # @output - # # Called with parentheses. Equivalent to `@render_capitalize()` - # # legacy - Barret - Too much boilerplate - # @render_capitalize(to="lower") - # def old_paren(): - # return input.caption() - - # # No docstring due to overload - # @render_caps_simple - # def barret_caps_simple_no_paren(): - # return input.caption() - - # # No docstring due to overload - # @render_caps_simple() - # def barret_caps_simple_paren(): - # return input.caption() - - # TODO-barret; Double check this one!!!! - # Barret - Only downside is bad function name in pylance window. Could be pylance bug? - @render_caps_params - def barret_caps_params_no_paren(): - return input.caption() - - # Barret - Correct function name - @render_caps_params(to="lower") - def barret_caps_params_paren(): - return input.caption() - - # @render_caps_no_params - # # TODO-barret; Double check this one!!!! - # # Barret - Only downside is bad function name in pylance window. Could be pylance bug? - # def barret_caps_no_params_no_paren(): - # return input.caption() - - # # Barret - Correct function name! - # @render_caps_no_params() - # def barret_caps_no_params_paren(): - # return input.caption() - - # print("\nsub_barret_simple") - - # new (.barret_sub at 0x104bd56c0>,) {} - # creating decorator! @sub_barret_simple def barret_sub_simple_no_paren(): return input.caption() - print("\nbarret_sub_simple_paren") - - # new () {} - # init () {} - # call (.barret_sub2 at 0x106146520>,) {} @sub_barret_simple() def barret_sub_simple_paren() -> str: return input.caption() - print("\nbarret_sub_renderer_no_paren") - - # @barret_simple_fn - # def barret_simple_fn_no_paren(): - # return input.caption() - - print("\nbarret_sub_simple_paren") - - # # new () {} - # # init () {} - # # call (.barret_sub2 at 0x106146520>,) {} - # @barret_simple_fn() - # def barret_simple_fn_paren() -> str: - # return input.caption() - - print("\nbarret_sub_renderer_no_paren") - - # new () {'a': 1} - # init () {'a': 1} - # call (.barret_sub2 at 0x106146520>,) {} @sub_barret_renderer def barret_sub_renderer_no_paren(): return input.caption() - print("\nbarret_sub_renderer_paren") - - # new () {'a': 1} - # init () {'a': 1} - # call (.barret_sub2 at 0x106146520>,) {} @sub_barret_renderer(a=2) def barret_sub_renderer_paren(): return input.caption() - print("\n") - app = App(app_ui, server) From 8ba5f81f7c94f5d68be59557d810e8881e8b29f3 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:43:31 -0500 Subject: [PATCH 29/77] Update CHANGELOG.md --- CHANGELOG.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8781a04ea..d42c822bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.2] - 2023-12-22 + +### 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`) you expect 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.output_transformer`. (#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. (#964) + + ## [0.6.1.1] - 2023-12-22 ### Bug fixes @@ -23,12 +36,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `ui.layout_columns()` for creating responsive column-forward layouts based on Bootstrap's 12-column CSS Grid. (#856) * Added support for Shiny Express apps, which has a simpler, easier-to-use API than the existing API (Shiny Core). Please note that this API is still experimental and may change. (#767) -* `shiny.render.RenderFunction` and `shiny.render.RenderFunctionAsync` have been removed. They were deprecated in v0.6.0. Instead, please use `shiny.render.output_transformer`. (TODO-barret) - -* `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. This method has been deprecated. Please change your code from `value = await resolve_value_fn(_fn)` to `value = await _fn()`. (TODO-barret) - -* `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. (TODO-barret) - ### Bug fixes * Fix support for `shiny.ui.accordion(multiple=)` (#799). From a6b516eee1561d02b522c7634e4be6bf0651fa8f Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:44:31 -0500 Subject: [PATCH 30/77] Add demo app for render.display --- shiny/api-examples/render_display/app.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 shiny/api-examples/render_display/app.py diff --git a/shiny/api-examples/render_display/app.py b/shiny/api-examples/render_display/app.py new file mode 100644 index 000000000..7bd372616 --- /dev/null +++ b/shiny/api-examples/render_display/app.py @@ -0,0 +1,19 @@ +import datetime + +from shiny import render, ui +from shiny.express import input, layout + +with layout.card(id="card"): + ui.input_slider("val", "slider", 0, 100, 50) + "Text outside of render display call" + ui.tags.br() + f"Rendered time: {str(datetime.datetime.now())}" + + @render.display + def render_display(): + "Text inside of render display call" + ui.tags.br() + "Dynamic slider value: " + input.val() + ui.tags.br() + f"Display's rendered time: {str(datetime.datetime.now())}" From 1f74aba268066eacdddc66bac67b8c52fa31f788 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 16:55:14 -0500 Subject: [PATCH 31/77] Update _quartodoc.yml --- docs/_quartodoc.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index a1f95651b..beb1faa8b 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -196,7 +196,7 @@ quartodoc: name: "Create rendering outputs" desc: "" contents: - # TODO-barret; UPdate with renderer classes / info! + # TODO-barret; Update with renderer classes / info! - render.transformer.Renderer # - render.transformer.output_transformer # - render.transformer.OutputTransformer @@ -205,6 +205,7 @@ quartodoc: # - render.transformer.OutputRenderer # - render.transformer.is_async_callable - render.transformer.ValueFn + - render.transformer.AsyncValueFn # - render.transformer.TransformFn - title: Reactive programming desc: "" From 3a2103453bd2a20ded923206fb8df0968ecebea1 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 17:09:08 -0500 Subject: [PATCH 32/77] Fix typing.Self issue --- shiny/_typing_extensions.py | 6 +++--- shiny/render/renderer/_renderer.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/shiny/_typing_extensions.py b/shiny/_typing_extensions.py index 93fe5647b..39cc1ac29 100644 --- a/shiny/_typing_extensions.py +++ b/shiny/_typing_extensions.py @@ -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 diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 9a6f30808..92c5c732a 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -1,6 +1,5 @@ from __future__ import annotations -import typing from abc import ABC, abstractmethod from typing import ( TYPE_CHECKING, @@ -18,6 +17,7 @@ from htmltools import MetadataNode, Tag, TagList +from ..._typing_extensions import Self from ..._utils import WrapAsync # TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` @@ -219,7 +219,7 @@ def value_fn(self) -> AsyncValueFn[IT | None]: asynchonously. """ - def __call__(self, value_fn: ValueFnApp[IT | None]) -> typing.Self: + def __call__(self, value_fn: ValueFnApp[IT | None]) -> Self: """ Renderer __call__ docs here; Sets app's value function TODO-barret - docs From 5a106527b2db23e031ae29346d0748e1c79588fa Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 17:09:18 -0500 Subject: [PATCH 33/77] Add TODO on type issue --- shiny/reactive/_reactives.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shiny/reactive/_reactives.py b/shiny/reactive/_reactives.py index 9703d61e0..a8af2083d 100644 --- a/shiny/reactive/_reactives.py +++ b/shiny/reactive/_reactives.py @@ -735,6 +735,7 @@ def create_effect(fn: EffectFunction | EffectFunctionAsync) -> Effect_: # ============================================================================== @add_example() def event( + # TODO-barret; Accept a RendererBase here, and then wrap the render method? *args: Callable[[], object] | Callable[[], Awaitable[object]], ignore_none: bool = True, ignore_init: bool = False, From 88ba832821fead673513cdfff2a0f43436b28b87 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 4 Jan 2024 17:09:29 -0500 Subject: [PATCH 34/77] Remove ugly shorthand --- tests/playwright/shiny/async/app.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/playwright/shiny/async/app.py b/tests/playwright/shiny/async/app.py index 0f1dcf63f..01d32ce67 100644 --- a/tests/playwright/shiny/async/app.py +++ b/tests/playwright/shiny/async/app.py @@ -2,8 +2,7 @@ import hashlib import time -import shiny as s -from shiny import reactive, ui +from shiny import App, Inputs, Outputs, Session, reactive, render, ui def calc(value: str) -> str: @@ -23,9 +22,9 @@ def calc(value: str) -> str: ) -def server(input: s.Inputs, output: s.Outputs, session: s.Session): +def server(input: Inputs, output: Outputs, session: Session): @output() - @s.render.text() + @render.text() @reactive.event(input.go) async def hash_output(): content = await hash_result() @@ -40,4 +39,4 @@ async def hash_result() -> str: return await asyncio.get_running_loop().run_in_executor(None, calc, value) -app = s.App(app_ui, server) +app = App(app_ui, server) From 21514045d422ffc596f2c5446a079318b704af76 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 09:17:38 -0500 Subject: [PATCH 35/77] Fix legacy typing check --- shiny/render/renderer/_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 92c5c732a..37a10f983 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -103,7 +103,7 @@ App-supplied output value function which returns type `IT`. This function can be synchronous or asynchronous. """ -ValueFn = Optional[ValueFnApp[IT | None]] +ValueFn = Optional[ValueFnApp[Union[IT, None]]] class RendererBase(ABC): From 64e144a1721a56ad0d1bbb294bebadd5d872559d Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 09:24:55 -0500 Subject: [PATCH 36/77] Add playright test for reactive.event on a output render function and reactive calc --- .../shiny/server/reactive_event/app.py | 40 +++++++++++++++++++ .../reactive_event/test_reactive_event.py | 40 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 tests/playwright/shiny/server/reactive_event/app.py create mode 100644 tests/playwright/shiny/server/reactive_event/test_reactive_event.py diff --git a/tests/playwright/shiny/server/reactive_event/app.py b/tests/playwright/shiny/server/reactive_event/app.py new file mode 100644 index 000000000..8fc83359f --- /dev/null +++ b/tests/playwright/shiny/server/reactive_event/app.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from shiny import App, Inputs, Outputs, Session, reactive, render, ui + +app_ui = ui.page_fluid( + ui.h2(ui.code("@reactive.event")), + ui.input_action_button("btn_count", "Immediate Count"), + ui.tags.br(), + ui.tags.label("Rendered on click:"), + ui.output_text_verbatim("txt_immediate", placeholder=True), + ui.input_action_button("btn_trigger", "Update Count"), + ui.tags.br(), + ui.tags.label("Reactive event on renderer:"), + ui.output_text_verbatim("txt_render_delayed", placeholder=True), + ui.tags.label("Reactive event on reactive calc:"), + ui.output_text_verbatim("txt_reactive_delayed", placeholder=True), +) + + +def server(input: Inputs, output: Outputs, session: Session): + @render.text + def txt_immediate(): + return input.btn_count() + + @render.text + @reactive.event(input.btn_trigger) + def txt_render_delayed(): + return input.btn_count() + + @reactive.calc() + @reactive.event(input.btn_trigger) + def delayed_btn_count() -> int: + return input.btn_count() + + @render.text + def txt_reactive_delayed(): + return str(delayed_btn_count()) + + +app = App(app_ui, server) diff --git a/tests/playwright/shiny/server/reactive_event/test_reactive_event.py b/tests/playwright/shiny/server/reactive_event/test_reactive_event.py new file mode 100644 index 000000000..4036bba5d --- /dev/null +++ b/tests/playwright/shiny/server/reactive_event/test_reactive_event.py @@ -0,0 +1,40 @@ +from conftest import ShinyAppProc +from controls import InputActionButton, OutputTextVerbatim +from playwright.sync_api import Page + + +def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + btn_count = InputActionButton(page, "btn_count") + btn_trigger = InputActionButton(page, "btn_trigger") + txt_immediate = OutputTextVerbatim(page, "txt_immediate") + txt_render_delayed = OutputTextVerbatim(page, "txt_render_delayed") + txt_reactive_delayed = OutputTextVerbatim(page, "txt_reactive_delayed") + + txt_immediate.expect_value("0") + txt_render_delayed.expect_value("") + txt_reactive_delayed.expect_value("") + + btn_count.click() + btn_count.click() + btn_count.click() + txt_immediate.expect_value("3") + txt_render_delayed.expect_value("") + txt_reactive_delayed.expect_value("") + + btn_trigger.click() + txt_immediate.expect_value("3") + txt_render_delayed.expect_value("3") + txt_reactive_delayed.expect_value("3") + + btn_count.click() + btn_count.click() + txt_immediate.expect_value("5") + txt_render_delayed.expect_value("3") + txt_reactive_delayed.expect_value("3") + + btn_trigger.click() + txt_immediate.expect_value("5") + txt_render_delayed.expect_value("5") + txt_reactive_delayed.expect_value("5") From 7144bf61c4df54d73fc12b393f45295c2bcd33e0 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 11:41:24 -0500 Subject: [PATCH 37/77] If already a WrapAsync function, return itself --- shiny/_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shiny/_utils.py b/shiny/_utils.py index d85123aa9..44abd747b 100644 --- a/shiny/_utils.py +++ b/shiny/_utils.py @@ -262,6 +262,9 @@ class WrapAsync(Generic[P, R]): """ 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) From 6d11f9708b7e0744c40570049282ac57b7fc953a Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 11:41:58 -0500 Subject: [PATCH 38/77] Separate the setting of value function and auto registering --- shiny/render/renderer/_renderer.py | 46 ++++++++++++++++++------------ 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 37a10f983..09f2aaace 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -151,6 +151,18 @@ def _on_register(self) -> None: self.session.output.remove(ns_name) self._auto_registered = False + def _auto_register(self) -> None: + # If in Express mode, register the output + if not self._auto_registered: + from ...session import get_current_session + + s = get_current_session() + if s is not None: + s.output(self) + # We mark the fact that we're auto-registered so that, if an explicit + # registration now occurs, we can undo this auto-registration. + self._auto_registered = True + def _repr_html_(self) -> str | None: rendered_ui = self._render_default_ui() if rendered_ui is None: @@ -213,6 +225,20 @@ class Renderer(RendererBase, Generic[IT]): def value_fn(self) -> AsyncValueFn[IT | None]: return self._value_fn + def _set_value_fn(self, value_fn: ValueFnApp[Any | None]) -> None: + if not callable(value_fn): + raise TypeError("Value function must be callable") + + # Copy over function name as it is consistent with how Session and Output + # retrieve function names + self.__name__ = value_fn.__name__ + + # Set value function with extra meta information + self._value_fn = AsyncValueFn(value_fn) + + # Allow for App authors to not require `@output` + self._auto_register() + """ App-supplied output value function which returns type `IT`. This function is always asyncronous as the original app-supplied function possibly wrapped to execute @@ -224,26 +250,8 @@ def __call__(self, value_fn: ValueFnApp[IT | None]) -> Self: Renderer __call__ docs here; Sets app's value function TODO-barret - docs """ - if not callable(value_fn): - raise TypeError("Value function must be callable") - - # Copy over function name as it is consistent with how Session and Output - # retrieve function names - self.__name__ = value_fn.__name__ - - # Set value function with extra meta information - self._value_fn = AsyncValueFn(value_fn) - # If in Express mode, register the output - if not self._auto_registered: - from ...session import get_current_session - - s = get_current_session() - if s is not None: - s.output(self) - # We mark the fact that we're auto-registered so that, if an explicit - # registration now occurs, we can undo this auto-registration. - self._auto_registered = True + self._set_value_fn(value_fn) return self From 889286924c299ebf840384c7df23b7d96074ee8f Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 11:43:19 -0500 Subject: [PATCH 39/77] Auto register the OutputRenderer class on init. --- shiny/render/transformer/_transformer.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 96ac34e74..1636d9312 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -32,12 +32,8 @@ overload, ) -from ..renderer._renderer import ( - DefaultUIFn, - DefaultUIFnResultOrNone, - JSONifiable, - RendererBase, -) +from ..renderer import JSONifiable, RendererBase, WrapAsync +from ..renderer._renderer import DefaultUIFn, DefaultUIFnResultOrNone if TYPE_CHECKING: from ...session import Session @@ -45,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, wrap_async +from ..._utils import is_async_callable from ...types import MISSING # Input type for the user-spplied function that is passed to a render.xx @@ -78,7 +74,6 @@ class TransformerMetadata(NamedTuple): session: Session name: str - value_fn_is_async: bool # Motivation for using this class: @@ -261,8 +256,10 @@ def __init__( # Checking if a function is async has a 180+ns overhead (barret's machine) # -> It is faster to always call an async function than to always check if it is async # Always being async simplifies the execution - self._value_fn_is_async = is_async_callable(value_fn) - self._value_fn: ValueFn[IT] = wrap_async(value_fn) + self._value_fn = WrapAsync(value_fn) + self._value_fn_is_async = self._value_fn.is_async # legacy key + self.__name__ = value_fn.__name__ + self._transformer = transform_fn self._params = params self._default_ui = default_ui @@ -271,6 +268,9 @@ def __init__( self._default_ui_args: tuple[object, ...] = tuple() self._default_ui_kwargs: dict[str, object] = dict() + # Allow for App authors to not require `@output` + self._auto_register() + def _set_metadata(self, session: Session, name: str) -> None: """ When `Renderer`s are assigned to Output object slots, this method is used to @@ -287,7 +287,6 @@ def _meta(self) -> TransformerMetadata: return TransformerMetadata( session=self.session, name=self.name, - value_fn_is_async=self._value_fn_is_async, ) async def _run(self) -> OT: From 81bc28bfba871123e3647535f45b7c12fb845055 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 11:43:38 -0500 Subject: [PATCH 40/77] Add deprecation warning to OutputRenderer --- shiny/render/transformer/_transformer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 1636d9312..b67379c81 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -239,6 +239,10 @@ def __init__( """ super().__init__() + warn_deprecated( + "`shiny.render.transformer.output_transformer()` and `shiny.render.transformer.OutputRenderer()` output render function utiltities have been superceded by `shiny.render.renderer.Renderer` and will be removed in a near future release." + ) + # Copy over function name as it is consistent with how Session and Output # retrieve function names self.__name__ = value_fn.__name__ From 064a8c63c9d78ffc5b57c6c6442687799f578960 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 11:44:23 -0500 Subject: [PATCH 41/77] Copy/update current output_transformer ex app for testing --- .../deprecated/output_transformer/app.py | 149 ++++++++++++++++++ .../test_output_transformer_example.py | 12 ++ ...er.py => test_output_transformer_async.py} | 0 3 files changed, 161 insertions(+) create mode 100644 tests/playwright/shiny/deprecated/output_transformer/app.py create mode 100644 tests/playwright/shiny/deprecated/output_transformer/test_output_transformer_example.py rename tests/playwright/shiny/server/output_transformer/{test_output_transformer.py => test_output_transformer_async.py} (100%) diff --git a/tests/playwright/shiny/deprecated/output_transformer/app.py b/tests/playwright/shiny/deprecated/output_transformer/app.py new file mode 100644 index 000000000..3dbdac72a --- /dev/null +++ b/tests/playwright/shiny/deprecated/output_transformer/app.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import warnings +from typing import Literal, overload + +from shiny import App, Inputs, Outputs, Session, ui +from shiny._deprecated import ShinyDeprecationWarning +from shiny.render.renderer import JSONifiable +from shiny.render.transformer import ( + TransformerMetadata, + ValueFn, + output_transformer, + resolve_value_fn, +) + +warnings.filterwarnings("ignore", category=ShinyDeprecationWarning) + +####### +# Package authors can create their own output transformer methods by leveraging +# `output_transformer` decorator. +# +# The transformer is kept simple for demonstration purposes, but it can be much more +# complex (e.g. shiny.render.plotly) +####### + + +@output_transformer() +async def CapitalizeTransformer( + # Contains information about the render call: `name` and `session` + _meta: TransformerMetadata, + # The app-supplied output value function + _fn: ValueFn[str | None], + *, + # Extra parameters that app authors can supply to the render decorator + # (e.g. `@render_capitalize(to="upper")`) + to: Literal["upper", "lower"] = "upper", +) -> str | None: + # Get the value + value = await resolve_value_fn(_fn) + # Equvalent to: + # if shiny.render.transformer.is_async_callable(_fn): + # value = await _fn() + # else: + # value = _fn() + + # Render nothing if `value` is `None` + if value is None: + return None + + if to == "upper": + return value.upper() + if to == "lower": + return value.lower() + raise ValueError(f"Invalid value for `to`: {to}") + + +# First, create an overload where users can supply the extra parameters. +# Example of usage: +# ``` +# @render_capitalize(to="upper") +# def value(): +# return input.caption() +# ``` +# Note: Return type is `OutputRendererDecorator` +@overload +def render_capitalize( + *, + to: Literal["upper", "lower"] = "upper", +) -> CapitalizeTransformer.OutputRendererDecorator: + ... + + +# Second, create an overload where users are not using parentheses to the method. +# While it doesn't look necessary, it is needed for the type checker. +# Example of usage: +# ``` +# @render_capitalize +# def value(): +# return input.caption() +# ``` +# Note: `_fn` type is the transformer's `ValueFn` +# Note: Return type is the transformer's `OutputRenderer` +@overload +def render_capitalize( + _fn: CapitalizeTransformer.ValueFn, +) -> CapitalizeTransformer.OutputRenderer: + ... + + +# Lastly, implement the renderer. +# Note: `_fn` type is the transformer's `ValueFn` or `None` +# Note: Return type is the transformer's `OutputRenderer` or `OutputRendererDecorator` +def render_capitalize( + _fn: CapitalizeTransformer.ValueFn | None = None, + *, + to: Literal["upper", "lower"] = "upper", +) -> ( + CapitalizeTransformer.OutputRenderer | CapitalizeTransformer.OutputRendererDecorator +): + return CapitalizeTransformer( + _fn, + CapitalizeTransformer.params(to=to), + ) + + +####### +# End of package author code +####### + +app_ui = ui.page_fluid( + ui.h1("Capitalization renderer"), + ui.input_text("caption", "Caption:", "Data summary"), + "Renderer called with out parentheses:", + ui.output_text_verbatim("no_output", placeholder=True), + ui.output_text_verbatim("no_parens", placeholder=True), + "To upper:", + ui.output_text_verbatim("to_upper", placeholder=True), + "To lower:", + ui.output_text_verbatim("to_lower", placeholder=True), +) + + +def server(input: Inputs, output: Outputs, session: Session): + # Without parentheses + @render_capitalize + def no_output(): + return input.caption() + + @output + # Without parentheses + @render_capitalize + def no_parens(): + return input.caption() + + # @output # Do not include to make sure auto registration works + # With parentheses. Equivalent to `@render_capitalize()` + @render_capitalize(to="upper") + def to_upper(): + return input.caption() + + # provide a custom name to make sure the name can be overridden + @output(id="to_lower") + @render_capitalize(to="lower") + # Works with async output value functions + async def _(): + return input.caption() + + +app = App(app_ui, server) diff --git a/tests/playwright/shiny/deprecated/output_transformer/test_output_transformer_example.py b/tests/playwright/shiny/deprecated/output_transformer/test_output_transformer_example.py new file mode 100644 index 000000000..ca16acb10 --- /dev/null +++ b/tests/playwright/shiny/deprecated/output_transformer/test_output_transformer_example.py @@ -0,0 +1,12 @@ +from conftest import ShinyAppProc +from controls import OutputTextVerbatim +from playwright.sync_api import Page + + +def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + OutputTextVerbatim(page, "no_output").expect_value("DATA SUMMARY") + OutputTextVerbatim(page, "no_parens").expect_value("DATA SUMMARY") + OutputTextVerbatim(page, "to_upper").expect_value("DATA SUMMARY") + OutputTextVerbatim(page, "to_lower").expect_value("data summary") diff --git a/tests/playwright/shiny/server/output_transformer/test_output_transformer.py b/tests/playwright/shiny/server/output_transformer/test_output_transformer_async.py similarity index 100% rename from tests/playwright/shiny/server/output_transformer/test_output_transformer.py rename to tests/playwright/shiny/server/output_transformer/test_output_transformer_async.py From 8a3c5fd378f366bf2bc6beb40ecd8bc9b3133f94 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 11:45:37 -0500 Subject: [PATCH 42/77] Diff syntax for ignoring P type in Generic --- shiny/render/renderer/_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 09f2aaace..4cc291a00 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -186,7 +186,7 @@ def _render_default_ui(self) -> DefaultUIFnResultOrNone: ) -class AsyncValueFn(WrapAsync[[], IT]): +class AsyncValueFn(WrapAsync[..., IT]): """ App-supplied output value function which returns type `IT`. asynchronous. From 827737bed7125b9b649485c94a07d58a33b1b750 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 11:47:55 -0500 Subject: [PATCH 43/77] lints --- shiny/render/_render.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 828d45517..6cda5e93e 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -6,6 +6,8 @@ import typing from typing import ( TYPE_CHECKING, + # Used for python 3.8 compatibility; Can use `dict` in python >= 3.9 + Dict, Literal, Optional, Protocol, @@ -255,7 +257,7 @@ def cast_result(result: ImgData | None) -> dict[str, JSONifiable] | None: if result is None: return None img_dict = dict(result) - img_jsonifiable = cast(dict[str, JSONifiable], img_dict) + img_jsonifiable = cast(Dict[str, JSONifiable], img_dict) return img_jsonifiable if "plotnine" in sys.modules: @@ -355,7 +357,7 @@ async def transform(self, value: ImgData) -> dict[str, JSONifiable] | None: content_type = _utils.guess_mime_type(src) value["src"] = f"data:{content_type};base64,{data_str}" value_dict = dict(value) - value_jsonifiable = cast(dict[str, JSONifiable], value_dict) + value_jsonifiable = cast(Dict[str, JSONifiable], value_dict) return value_jsonifiable finally: if self.delete_file: @@ -474,7 +476,7 @@ async def transform(self, value: TableResult) -> dict[str, JSONifiable]: # Use typing to make sure the return shape matches ret: RenderedDeps = {"deps": [], "html": html} ret_dict = dict(ret) - ret_jsonifiable = cast(dict[str, JSONifiable], ret_dict) + ret_jsonifiable = cast(Dict[str, JSONifiable], ret_dict) return ret_jsonifiable From 55544560cc857ea661b3bb221f7810bcbc0338aa Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 11:53:07 -0500 Subject: [PATCH 44/77] init Renderer api-example app --- shiny/api-examples/Renderer/app.py | 191 +++++++++++++ shiny/api-examples/output_transformer/app.py | 279 ++++++++----------- 2 files changed, 305 insertions(+), 165 deletions(-) create mode 100644 shiny/api-examples/Renderer/app.py diff --git a/shiny/api-examples/Renderer/app.py b/shiny/api-examples/Renderer/app.py new file mode 100644 index 000000000..0cfe64b81 --- /dev/null +++ b/shiny/api-examples/Renderer/app.py @@ -0,0 +1,191 @@ +# pyright : basic +from __future__ import annotations + +from shiny import App, Inputs, Outputs, Session, ui +from shiny.render.renderer import Renderer, ValueFn + +# TODO-barret Update app with docs below + + +class sub_barret_renderer(Renderer[str]): + """ + SubBarretSimple - class docs - Render Caps docs + """ + + def default_ui(self, id: str): + return ui.output_text_verbatim(id, placeholder=self.placeholder) + + def __init__( + self, + # Required for no paren usage + _fn: ValueFn[str | None] | None = None, + *, + a: int = 1, + placeholder: bool = True, + ) -> None: + """ + SubBarretSimple - init docs - Render Caps docs + """ + # Do not pass params + super().__init__(_fn) + self.widget = None + self.a: int = a + self.default_ui = ui.output_text_verbatim + + async def render(self) -> str | None: + value = await self._value_fn() + if value is None: + return None + self.widget = value + return f"{value.upper()}; a={self.a}" + + +class sub_barret_simple(Renderer[str]): + """ + SubBarretSimple - class - Render Caps docs + """ + + def default_ui(self, id: str): + return ui.output_text_verbatim(id, placeholder=True) + + def __init__( + self, + _value_fn: ValueFn[str] | None = None, + ): + """ + SubBarretSimple - init - docs here + """ + super().__init__(_value_fn) + + async def transform(self, value: str) -> str: + return str(value).upper() + + +# # Create renderer components from the async handler function: `capitalize_components()` +# @output_transformer() +# async def CapitalizeTransformer( +# # Contains information about the render call: `name` and `session` +# _meta: TransformerMetadata, +# # The app-supplied output value function +# _fn: ValueFn[str | None], +# *, +# # Extra parameters that app authors can supply to the render decorator +# # (e.g. `@render_capitalize(to="upper")`) +# to: Literal["upper", "lower"] = "upper", +# ) -> str | None: +# # Get the value +# value = await _fn() + +# # Render nothing if `value` is `None` +# if value is None: +# return None + +# if to == "upper": +# return value.upper() +# if to == "lower": +# return value.lower() +# raise ValueError(f"Invalid value for `to`: {to}") + + +# # First, create an overload where users can supply the extra parameters. +# # Example of usage: +# # ``` +# # @output +# # @render_capitalize(to="upper") +# # def value(): +# # return input.caption() +# # ``` +# # Note: Return type is `OutputRendererDecorator` +# @overload +# def render_capitalize( +# *, +# to: Literal["upper", "lower"] = "upper", +# ) -> CapitalizeTransformer.OutputRendererDecorator: +# ... + + +# # Second, create an overload where users are not using parentheses to the method. +# # While it doesn't look necessary, it is needed for the type checker. +# # Example of usage: +# # ``` +# # @output +# # @render_capitalize +# # def value(): +# # return input.caption() +# # ``` +# # Note: `_fn` type is the transformer's `ValueFn` +# # Note: Return type is the transformer's `OutputRenderer` +# @overload +# def render_capitalize( +# _fn: CapitalizeTransformer.ValueFn, +# ) -> CapitalizeTransformer.OutputRenderer: +# ... + + +# # Lastly, implement the renderer. +# # Note: `_fn` type is the transformer's `ValueFn` or `None` +# # Note: Return type is the transformer's `OutputRenderer` or `OutputRendererDecorator` +# def render_capitalize( +# _fn: CapitalizeTransformer.ValueFn | None = None, +# *, +# to: Literal["upper", "lower"] = "upper", +# ) -> ( +# CapitalizeTransformer.OutputRenderer | CapitalizeTransformer.OutputRendererDecorator +# ): +# """ +# OldSchool - CapitalizeTransformer +# """ +# return CapitalizeTransformer( +# _fn, +# CapitalizeTransformer.params(to=to), +# ) + + +####### +# End of package author code +####### + + +def text_row(id: str): + return ui.tags.tr( + ui.tags.td(f"{id}:"), + 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("barret_sub_simple_no_paren"), + text_row("barret_sub_simple_paren"), + # + text_row("barret_sub_renderer_no_paren"), + text_row("barret_sub_renderer_paren"), + ), +) + + +def server(input: Inputs, output: Outputs, session: Session): + @sub_barret_simple + def barret_sub_simple_no_paren(): + return input.caption() + + @sub_barret_simple() + def barret_sub_simple_paren() -> str: + return input.caption() + + @sub_barret_renderer + def barret_sub_renderer_no_paren(): + return input.caption() + + @sub_barret_renderer(a=2) + def barret_sub_renderer_paren(): + return input.caption() + + +app = App(app_ui, server) diff --git a/shiny/api-examples/output_transformer/app.py b/shiny/api-examples/output_transformer/app.py index 0cfe64b81..09bc1b51e 100644 --- a/shiny/api-examples/output_transformer/app.py +++ b/shiny/api-examples/output_transformer/app.py @@ -1,190 +1,139 @@ -# pyright : basic from __future__ import annotations -from shiny import App, Inputs, Outputs, Session, ui -from shiny.render.renderer import Renderer, ValueFn - -# TODO-barret Update app with docs below - - -class sub_barret_renderer(Renderer[str]): - """ - SubBarretSimple - class docs - Render Caps docs - """ - - def default_ui(self, id: str): - return ui.output_text_verbatim(id, placeholder=self.placeholder) - - def __init__( - self, - # Required for no paren usage - _fn: ValueFn[str | None] | None = None, - *, - a: int = 1, - placeholder: bool = True, - ) -> None: - """ - SubBarretSimple - init docs - Render Caps docs - """ - # Do not pass params - super().__init__(_fn) - self.widget = None - self.a: int = a - self.default_ui = ui.output_text_verbatim - - async def render(self) -> str | None: - value = await self._value_fn() - if value is None: - return None - self.widget = value - return f"{value.upper()}; a={self.a}" - - -class sub_barret_simple(Renderer[str]): - """ - SubBarretSimple - class - Render Caps docs - """ - - def default_ui(self, id: str): - return ui.output_text_verbatim(id, placeholder=True) - - def __init__( - self, - _value_fn: ValueFn[str] | None = None, - ): - """ - SubBarretSimple - init - docs here - """ - super().__init__(_value_fn) - - async def transform(self, value: str) -> str: - return str(value).upper() - - -# # Create renderer components from the async handler function: `capitalize_components()` -# @output_transformer() -# async def CapitalizeTransformer( -# # Contains information about the render call: `name` and `session` -# _meta: TransformerMetadata, -# # The app-supplied output value function -# _fn: ValueFn[str | None], -# *, -# # Extra parameters that app authors can supply to the render decorator -# # (e.g. `@render_capitalize(to="upper")`) -# to: Literal["upper", "lower"] = "upper", -# ) -> str | None: -# # Get the value -# value = await _fn() - -# # Render nothing if `value` is `None` -# if value is None: -# return None - -# if to == "upper": -# return value.upper() -# if to == "lower": -# return value.lower() -# raise ValueError(f"Invalid value for `to`: {to}") - - -# # First, create an overload where users can supply the extra parameters. -# # Example of usage: -# # ``` -# # @output -# # @render_capitalize(to="upper") -# # def value(): -# # return input.caption() -# # ``` -# # Note: Return type is `OutputRendererDecorator` -# @overload -# def render_capitalize( -# *, -# to: Literal["upper", "lower"] = "upper", -# ) -> CapitalizeTransformer.OutputRendererDecorator: -# ... - - -# # Second, create an overload where users are not using parentheses to the method. -# # While it doesn't look necessary, it is needed for the type checker. -# # Example of usage: -# # ``` -# # @output -# # @render_capitalize -# # def value(): -# # return input.caption() -# # ``` -# # Note: `_fn` type is the transformer's `ValueFn` -# # Note: Return type is the transformer's `OutputRenderer` -# @overload -# def render_capitalize( -# _fn: CapitalizeTransformer.ValueFn, -# ) -> CapitalizeTransformer.OutputRenderer: -# ... - - -# # Lastly, implement the renderer. -# # Note: `_fn` type is the transformer's `ValueFn` or `None` -# # Note: Return type is the transformer's `OutputRenderer` or `OutputRendererDecorator` -# def render_capitalize( -# _fn: CapitalizeTransformer.ValueFn | None = None, -# *, -# to: Literal["upper", "lower"] = "upper", -# ) -> ( -# CapitalizeTransformer.OutputRenderer | CapitalizeTransformer.OutputRendererDecorator -# ): -# """ -# OldSchool - CapitalizeTransformer -# """ -# return CapitalizeTransformer( -# _fn, -# CapitalizeTransformer.params(to=to), -# ) +from typing import Literal, overload +from shiny import App, Inputs, Outputs, Session, ui +from shiny.render.transformer import ( + TransformerMetadata, + ValueFn, + output_transformer, + resolve_value_fn, +) ####### -# End of package author code +# Package authors can create their own output transformer methods by leveraging +# `output_transformer` decorator. +# +# The transformer is kept simple for demonstration purposes, but it can be much more +# complex (e.g. shiny.render.plotly) ####### -def text_row(id: str): - return ui.tags.tr( - ui.tags.td(f"{id}:"), - 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)), +# Create renderer components from the async handler function: `capitalize_components()` +@output_transformer() +async def CapitalizeTransformer( + # Contains information about the render call: `name` and `session` + _meta: TransformerMetadata, + # The app-supplied output value function + _fn: ValueFn[str | None], + *, + # Extra parameters that app authors can supply to the render decorator + # (e.g. `@render_capitalize(to="upper")`) + to: Literal["upper", "lower"] = "upper", +) -> str | None: + # Get the value + value = await resolve_value_fn(_fn) + # Equvalent to: + # if shiny.render.transformer.is_async_callable(_fn): + # value = await _fn() + # else: + # value = _fn() + + # Render nothing if `value` is `None` + if value is None: + return None + + if to == "upper": + return value.upper() + if to == "lower": + return value.lower() + raise ValueError(f"Invalid value for `to`: {to}") + + +# First, create an overload where users can supply the extra parameters. +# Example of usage: +# ``` +# @output +# @render_capitalize(to="upper") +# def value(): +# return input.caption() +# ``` +# Note: Return type is `OutputRendererDecorator` +@overload +def render_capitalize( + *, + to: Literal["upper", "lower"] = "upper", +) -> CapitalizeTransformer.OutputRendererDecorator: + ... + + +# Second, create an overload where users are not using parentheses to the method. +# While it doesn't look necessary, it is needed for the type checker. +# Example of usage: +# ``` +# @output +# @render_capitalize +# def value(): +# return input.caption() +# ``` +# Note: `_fn` type is the transformer's `ValueFn` +# Note: Return type is the transformer's `OutputRenderer` +@overload +def render_capitalize( + _fn: CapitalizeTransformer.ValueFn, +) -> CapitalizeTransformer.OutputRenderer: + ... + + +# Lastly, implement the renderer. +# Note: `_fn` type is the transformer's `ValueFn` or `None` +# Note: Return type is the transformer's `OutputRenderer` or `OutputRendererDecorator` +def render_capitalize( + _fn: CapitalizeTransformer.ValueFn | None = None, + *, + to: Literal["upper", "lower"] = "upper", +) -> ( + CapitalizeTransformer.OutputRenderer | CapitalizeTransformer.OutputRendererDecorator +): + return CapitalizeTransformer( + _fn, + CapitalizeTransformer.params(to=to), ) +####### +# End of package author code +####### + app_ui = ui.page_fluid( ui.h1("Capitalization renderer"), ui.input_text("caption", "Caption:", "Data summary"), - ui.tags.table( - text_row("barret_sub_simple_no_paren"), - text_row("barret_sub_simple_paren"), - # - text_row("barret_sub_renderer_no_paren"), - text_row("barret_sub_renderer_paren"), - ), + "Renderer called with out parentheses:", + ui.output_text_verbatim("no_parens"), + "To upper:", + ui.output_text_verbatim("to_upper"), + "To lower:", + ui.output_text_verbatim("to_lower"), ) def server(input: Inputs, output: Outputs, session: Session): - @sub_barret_simple - def barret_sub_simple_no_paren(): - return input.caption() - - @sub_barret_simple() - def barret_sub_simple_paren() -> str: + @output + # Called without parentheses + @render_capitalize + def no_parens(): return input.caption() - @sub_barret_renderer - def barret_sub_renderer_no_paren(): + @output + # Called with parentheses. Equivalent to `@render_capitalize()` + @render_capitalize(to="upper") + def to_upper(): return input.caption() - @sub_barret_renderer(a=2) - def barret_sub_renderer_paren(): + @output + @render_capitalize(to="lower") + # Works with async output value functions + async def to_lower(): return input.caption() From 3c3eb1d2eb88d5ca5f9fb9faa1e0847f2f62b93e Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 12:17:43 -0500 Subject: [PATCH 45/77] Moving WrapAsync code into AsyncValueFn as older versions of python don't support [] to define ParamSpec --- shiny/_utils.py | 104 +++++++++++------------ shiny/render/_render.py | 4 +- shiny/render/renderer/__init__.py | 2 +- shiny/render/renderer/_renderer.py | 44 +++++++++- shiny/render/transformer/_transformer.py | 4 +- 5 files changed, 99 insertions(+), 59 deletions(-) diff --git a/shiny/_utils.py b/shiny/_utils.py index 44abd747b..060c1013a 100644 --- a/shiny/_utils.py +++ b/shiny/_utils.py @@ -245,58 +245,58 @@ async def fn_async(*args: P.args, **kwargs: P.kwargs) -> R: return fn_async -# TODO-barret; Q: Expose in quartodoc file? -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 +# # TODO-barret; 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 diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 6cda5e93e..b938f817c 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -4,9 +4,11 @@ import os import sys import typing + +# `typing.Dict` sed for python 3.8 compatibility +# Can use `dict` in python >= 3.9 from typing import ( TYPE_CHECKING, - # Used for python 3.8 compatibility; Can use `dict` in python >= 3.9 Dict, Literal, Optional, diff --git a/shiny/render/renderer/__init__.py b/shiny/render/renderer/__init__.py index c17ec13f1..001320c5f 100644 --- a/shiny/render/renderer/__init__.py +++ b/shiny/render/renderer/__init__.py @@ -6,7 +6,7 @@ ValueFnApp, # pyright: ignore[reportUnusedImport] ValueFnSync, # pyright: ignore[reportUnusedImport] ValueFnAsync, # pyright: ignore[reportUnusedImport] - WrapAsync, # pyright: ignore[reportUnusedImport] + # WrapAsync, # pyright: ignore[reportUnusedImport] AsyncValueFn, # IT, # pyright: ignore[reportUnusedImport] ) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 4cc291a00..dd7cf5cea 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -13,12 +13,13 @@ Tuple, TypeVar, Union, + cast, ) from htmltools import MetadataNode, Tag, TagList from ..._typing_extensions import Self -from ..._utils import WrapAsync +from ..._utils import is_async_callable, wrap_async # TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` @@ -186,7 +187,9 @@ def _render_default_ui(self) -> DefaultUIFnResultOrNone: ) -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`. asynchronous. @@ -194,7 +197,42 @@ class AsyncValueFn(WrapAsync[..., IT]): Type definition: `Callable[[], Awaitable[IT]]` """ - pass + def __init__(self, fn: Callable[[], IT] | Callable[[], Awaitable[IT]]): + if isinstance(fn, AsyncValueFn): + fn = cast(AsyncValueFn[IT], fn) + return fn + self._is_async = is_async_callable(fn) + self._fn = wrap_async(fn) + + async def __call__(self) -> IT: + """ + Call the asynchronous function. + """ + return await self._fn() + + @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[[], IT] | Callable[[], Awaitable[IT]]: + """ + Retrieve the original function + + Returns + ------- + : + Original function supplied to the `AsyncValueFn` constructor. + """ + return self._fn # class RendererShim(RendererBase, Generic[IT, P]): diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index b67379c81..4aba6f387 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -32,7 +32,7 @@ overload, ) -from ..renderer import JSONifiable, RendererBase, WrapAsync +from ..renderer import AsyncValueFn, JSONifiable, RendererBase from ..renderer._renderer import DefaultUIFn, DefaultUIFnResultOrNone if TYPE_CHECKING: @@ -260,7 +260,7 @@ def __init__( # Checking if a function is async has a 180+ns overhead (barret's machine) # -> It is faster to always call an async function than to always check if it is async # Always being async simplifies the execution - self._value_fn = WrapAsync(value_fn) + self._value_fn = AsyncValueFn(value_fn) self._value_fn_is_async = self._value_fn.is_async # legacy key self.__name__ = value_fn.__name__ From f401bfccaf069fd69060b0e6234cbeb3ebad17eb Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 12:35:30 -0500 Subject: [PATCH 46/77] Update app.py --- tests/playwright/shiny/deprecated/output_transformer/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/playwright/shiny/deprecated/output_transformer/app.py b/tests/playwright/shiny/deprecated/output_transformer/app.py index 3dbdac72a..d716f9d39 100644 --- a/tests/playwright/shiny/deprecated/output_transformer/app.py +++ b/tests/playwright/shiny/deprecated/output_transformer/app.py @@ -5,7 +5,6 @@ from shiny import App, Inputs, Outputs, Session, ui from shiny._deprecated import ShinyDeprecationWarning -from shiny.render.renderer import JSONifiable from shiny.render.transformer import ( TransformerMetadata, ValueFn, From c6db4f036d9a6e2b808aaf01f288bf2a34a442d2 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 12:35:45 -0500 Subject: [PATCH 47/77] Remove test warning --- tests/pytest/test_output_transformer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/pytest/test_output_transformer.py b/tests/pytest/test_output_transformer.py index d684003da..397406c49 100644 --- a/tests/pytest/test_output_transformer.py +++ b/tests/pytest/test_output_transformer.py @@ -14,6 +14,9 @@ resolve_value_fn, ) +# import warnings +# warnings.filterwarnings("ignore", category=ShinyDeprecationWarning) + def test_output_transformer_works(): # No args works @@ -224,7 +227,8 @@ def async_renderer( def async_renderer( _fn: AsyncTransformer.ValueFn | None = None, ) -> AsyncTransformer.OutputRenderer | AsyncTransformer.OutputRendererDecorator: - return AsyncTransformer(_fn) + with pytest.warns(ShinyDeprecationWarning): + return AsyncTransformer(_fn) test_val = "Test: Hello World!" @@ -294,7 +298,8 @@ def yield_renderer( def yield_renderer( _fn: YieldTransformer.ValueFn | None = None, ) -> YieldTransformer.OutputRenderer | YieldTransformer.OutputRendererDecorator: - return YieldTransformer(_fn) + with pytest.warns(ShinyDeprecationWarning): + return YieldTransformer(_fn) test_val = "Test: Hello World!" From 03ac5f96a87f008e5001062fa58bb3c0e0fabafe Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 5 Jan 2024 12:41:29 -0500 Subject: [PATCH 48/77] Update _utils.py --- shiny/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/_utils.py b/shiny/_utils.py index 060c1013a..c4c4dea4e 100644 --- a/shiny/_utils.py +++ b/shiny/_utils.py @@ -11,7 +11,7 @@ import secrets import socketserver import tempfile -from typing import Any, Awaitable, Callable, Generic, Optional, TypeVar, cast +from typing import Any, Awaitable, Callable, Optional, TypeVar, cast from ._typing_extensions import ParamSpec, TypeGuard From 4aae5cdc55600548e28b2a09d77920e60f00d598 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 10:08:43 -0500 Subject: [PATCH 49/77] Update errors being caught and diagnostics --- tests/playwright/examples/test_examples.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/playwright/examples/test_examples.py b/tests/playwright/examples/test_examples.py index 356ba65d4..e95d33e8e 100644 --- a/tests/playwright/examples/test_examples.py +++ b/tests/playwright/examples/test_examples.py @@ -39,11 +39,13 @@ def get_apps(path: str) -> typing.List[str]: "brownian": 250, "ui-func": 250, } -resolve_value_fn_errors = [ +output_transformer_errors = [ + "ShinyDeprecationWarning: `shiny.render.transformer.output_transformer()`", + " return OutputRenderer", "ShinyDeprecationWarning: `resolve_value_fn()`", - "value = await resolve_value_fn(_fn)", "ShinyDeprecationWarning:", "`resolve_value_fn()`", + "value = await resolve_value_fn(_fn)", ] express_warnings = ["Detected Shiny Express app. "] app_allow_shiny_errors: typing.Dict[ @@ -58,11 +60,11 @@ def get_apps(path: str) -> typing.List[str]: "UserWarning: This figure includes Axes that are not compatible with tight_layout", ], # Remove after shinywidgets accepts `resolve_value_fn()` PR - "airmass": [*resolve_value_fn_errors], - "brownian": [*resolve_value_fn_errors], - "multi-page": [*resolve_value_fn_errors], - "model-score": [*resolve_value_fn_errors], - "data_frame": [*resolve_value_fn_errors], + "airmass": [*output_transformer_errors], + "brownian": [*output_transformer_errors], + "multi-page": [*output_transformer_errors], + "model-score": [*output_transformer_errors], + "output_transformer": [*output_transformer_errors], "render_display": [*express_warnings], } app_allow_external_errors: typing.List[str] = [ @@ -210,6 +212,9 @@ def on_console_msg(msg: ConsoleMessage) -> None: and not any([error_txt in line for error_txt in app_allowable_errors]) ] if len(error_lines) > 0: + print("\napp_allowable_errors :") + print("\n".join(app_allowable_errors)) + print("\nError lines remaining:") print("\n".join(error_lines)) assert len(error_lines) == 0 From 3a8e55ed174a164835e673969b615cebd3877969 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 10:09:12 -0500 Subject: [PATCH 50/77] Convert `@render.data_frame()` --- shiny/render/_dataframe.py | 75 ++++++++++++-------------------------- 1 file changed, 24 insertions(+), 51 deletions(-) diff --git a/shiny/render/_dataframe.py b/shiny/render/_dataframe.py index 87e9f30f7..7ff56599d 100644 --- a/shiny/render/_dataframe.py +++ b/shiny/render/_dataframe.py @@ -2,21 +2,14 @@ import abc import json -from typing import ( - TYPE_CHECKING, - Any, - Literal, - Protocol, - Union, - cast, - overload, - runtime_checkable, -) +from typing import TYPE_CHECKING, Any, Literal, Protocol, Union, cast, runtime_checkable + +from htmltools import Tag from .. import ui from .._docstring import add_example from ._dataframe_unsafe import serialize_numpy_dtypes -from .transformer import TransformerMetadata, ValueFn, output_transformer +from .renderer import JSONifiable, Renderer # TODO-barret; Implement dataframe @@ -27,7 +20,7 @@ class AbstractTabularData(abc.ABC): @abc.abstractmethod - def to_payload(self) -> object: + def to_payload(self) -> JSONifiable: ... @@ -104,7 +97,7 @@ def __init__( self.filters = filters self.row_selection_mode = row_selection_mode - def to_payload(self) -> object: + def to_payload(self) -> JSONifiable: res = serialize_pandas_df(self.data) res["options"] = dict( width=self.width, @@ -192,7 +185,7 @@ def __init__( self.filters = filters self.row_selection_mode = row_selection_mode - def to_payload(self) -> object: + def to_payload(self) -> JSONifiable: res = serialize_pandas_df(self.data) res["options"] = dict( width=self.width, @@ -223,44 +216,12 @@ def serialize_pandas_df(df: "pd.DataFrame") -> dict[str, Any]: DataFrameResult = Union[None, "pd.DataFrame", DataGrid, DataTable] -@output_transformer(default_ui=ui.output_data_frame) -async def DataFrameTransformer( - _meta: TransformerMetadata, - _fn: ValueFn[DataFrameResult | None], -) -> object | None: - x = await _fn() - if x is None: - return None - - if not isinstance(x, AbstractTabularData): - x = DataGrid( - cast_to_pandas( - x, "@render.data_frame doesn't know how to render objects of type" - ) - ) - return x.to_payload() - - -@overload -def data_frame() -> DataFrameTransformer.OutputRendererDecorator: - ... - - -@overload -def data_frame( - _fn: DataFrameTransformer.ValueFn, -) -> DataFrameTransformer.OutputRenderer: - ... - - @add_example() -def data_frame( - _fn: DataFrameTransformer.ValueFn | None = None, -) -> DataFrameTransformer.OutputRenderer | DataFrameTransformer.OutputRendererDecorator: +class data_frame(Renderer[DataFrameResult]): """ - Reactively render a pandas `DataFrame` object (or similar) as an interactive table or - grid. Features fast virtualized scrolling, sorting, filtering, and row selection - (single or multiple). + Decorator for a function that returns a pandas `DataFrame` object (or similar) to + render as an interactive table or grid. Features fast virtualized scrolling, sorting, + filtering, and row selection (single or multiple). Returns ------- @@ -297,7 +258,19 @@ def data_frame( * :class:`~shiny.render.DataGrid` and :class:`~shiny.render.DataTable` are the objects you can return from the rendering function to specify options. """ - return DataFrameTransformer(_fn) + + def default_ui(self, id: str) -> Tag: + return ui.output_data_frame(id=id) + + async def transform(self, value: DataFrameResult) -> JSONifiable: + if not isinstance(value, AbstractTabularData): + value = DataGrid( + cast_to_pandas( + value, + "@render.data_frame doesn't know how to render objects of type", + ) + ) + return value.to_payload() @runtime_checkable From 43edd0d4d0fa218effb1c0847ca13e9968dcde92 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 11:28:41 -0500 Subject: [PATCH 51/77] Pass through `Renderer` type using `RendererBaseT` `TypeVar` --- shiny/express/_output.py | 12 ++++++------ shiny/reactive/_reactives.py | 1 - shiny/render/_dataframe.py | 3 --- shiny/render/renderer/__init__.py | 1 + shiny/render/renderer/_renderer.py | 2 ++ shiny/session/_session.py | 15 +++++++++------ 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/shiny/express/_output.py b/shiny/express/_output.py index 837c460d5..83d184196 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -7,7 +7,7 @@ from .. import ui from .._typing_extensions import ParamSpec -from ..render.renderer import RendererBase +from ..render.renderer import RendererBase, RendererBaseT from ..render.transformer import OutputRenderer from ..render.transformer._transformer import OT @@ -24,7 +24,7 @@ # TODO-barret; quartodoc entry? def ui_kwargs( **kwargs: object, -) -> Callable[[RendererBase], RendererBase]: +) -> Callable[[RendererBaseT], RendererBaseT]: """ Sets default UI arguments for a Shiny rendering function. @@ -46,7 +46,7 @@ def ui_kwargs( A decorator that sets the default UI arguments for a Shiny rendering function. """ - def wrapper(renderer: RendererBase) -> RendererBase: + def wrapper(renderer: RendererBaseT) -> RendererBaseT: # renderer._default_ui_args = args renderer._default_ui_kwargs = kwargs return renderer @@ -103,7 +103,7 @@ def suspend_display(fn: CallableT) -> CallableT: @overload -def suspend_display(fn: RendererBase) -> RendererBase: +def suspend_display(fn: RendererBaseT) -> RendererBaseT: ... @@ -113,8 +113,8 @@ def suspend_display() -> AbstractContextManager[None]: def suspend_display( - fn: Callable[P, R] | RendererBase | None = None -) -> Callable[P, R] | RendererBase | AbstractContextManager[None]: + fn: Callable[P, R] | RendererBaseT | None = None +) -> Callable[P, R] | RendererBaseT | AbstractContextManager[None]: """Suppresses the display of UI elements in various ways. If used as a context manager (`with suspend_display():`), it suppresses the display diff --git a/shiny/reactive/_reactives.py b/shiny/reactive/_reactives.py index a8af2083d..9703d61e0 100644 --- a/shiny/reactive/_reactives.py +++ b/shiny/reactive/_reactives.py @@ -735,7 +735,6 @@ def create_effect(fn: EffectFunction | EffectFunctionAsync) -> Effect_: # ============================================================================== @add_example() def event( - # TODO-barret; Accept a RendererBase here, and then wrap the render method? *args: Callable[[], object] | Callable[[], Awaitable[object]], ignore_none: bool = True, ignore_init: bool = False, diff --git a/shiny/render/_dataframe.py b/shiny/render/_dataframe.py index 7ff56599d..c80b8a5b7 100644 --- a/shiny/render/_dataframe.py +++ b/shiny/render/_dataframe.py @@ -11,9 +11,6 @@ from ._dataframe_unsafe import serialize_numpy_dtypes from .renderer import JSONifiable, Renderer -# TODO-barret; Implement dataframe - - if TYPE_CHECKING: import pandas as pd diff --git a/shiny/render/renderer/__init__.py b/shiny/render/renderer/__init__.py index 001320c5f..ebe018f88 100644 --- a/shiny/render/renderer/__init__.py +++ b/shiny/render/renderer/__init__.py @@ -3,6 +3,7 @@ Renderer, ValueFn, JSONifiable, + RendererBaseT, # pyright: ignore[reportUnusedImport] ValueFnApp, # pyright: ignore[reportUnusedImport] ValueFnSync, # pyright: ignore[reportUnusedImport] ValueFnAsync, # pyright: ignore[reportUnusedImport] diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index dd7cf5cea..f342e603b 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -47,6 +47,8 @@ "AsyncValueFn", ) +RendererBaseT = TypeVar("RendererBaseT", bound="RendererBase") + # Input type for the user-spplied function that is passed to a render.xx IT = TypeVar("IT") diff --git a/shiny/session/_session.py b/shiny/session/_session.py index e6b60c3a6..03c9de950 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -47,7 +47,7 @@ from ..input_handler import input_handlers from ..reactive import Effect_, Value, effect, flush, isolate from ..reactive._core import lock, on_flushed -from ..render.renderer._renderer import JSONifiable, RendererBase +from ..render.renderer import JSONifiable, RendererBase, RendererBaseT from ..types import SafeException, SilentCancelOutputException, SilentException from ._utils import RenderedDeps, read_thunk_opt, session_context @@ -946,6 +946,8 @@ def __contains__(self, key: str) -> bool: # ====================================================================================== # Outputs # ====================================================================================== + + class Outputs: """ A class representing Shiny output definitions. @@ -964,7 +966,8 @@ def __init__( self._suspend_when_hidden = suspend_when_hidden @overload - def __call__(self, renderer: RendererBase) -> RendererBase: + # TODO-barret; Use generics to pass through the type! + def __call__(self, renderer: RendererBaseT) -> RendererBaseT: ... @overload @@ -974,18 +977,18 @@ def __call__( id: Optional[str] = None, suspend_when_hidden: bool = True, priority: int = 0, - ) -> Callable[[RendererBase], RendererBase]: + ) -> Callable[[RendererBaseT], RendererBaseT]: ... def __call__( self, - renderer: Optional[RendererBase] = None, + renderer: Optional[RendererBaseT] = None, *, id: Optional[str] = None, suspend_when_hidden: bool = True, priority: int = 0, - ) -> RendererBase | Callable[[RendererBase], RendererBase]: - def set_renderer(renderer: RendererBase) -> RendererBase: + ) -> RendererBaseT | Callable[[RendererBaseT], RendererBaseT]: + def set_renderer(renderer: RendererBaseT) -> RendererBaseT: if not isinstance(renderer, RendererBase): raise TypeError( "`@output` must be applied to a `@render.xx` function.\n" From 6539a9e580b852256e38d74694141574c6dd5471 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 11:28:47 -0500 Subject: [PATCH 52/77] Create _utils.py --- shiny/render/_render.py | 21 +++++++-------------- shiny/render/renderer/_utils.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 shiny/render/renderer/_utils.py diff --git a/shiny/render/_render.py b/shiny/render/_render.py index b938f817c..47d91d6eb 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -9,7 +9,6 @@ # Can use `dict` in python >= 3.9 from typing import ( TYPE_CHECKING, - Dict, Literal, Optional, Protocol, @@ -37,6 +36,7 @@ try_render_plotnine, ) from .renderer import Renderer, ValueFn +from .renderer._utils import imgdata_to_jsonifiable, rendered_deps_to_jsonifiable __all__ = ( "text", @@ -258,9 +258,7 @@ def container_size(dimension: Literal["width", "height"]) -> float: def cast_result(result: ImgData | None) -> dict[str, JSONifiable] | None: if result is None: return None - img_dict = dict(result) - img_jsonifiable = cast(Dict[str, JSONifiable], img_dict) - return img_jsonifiable + return imgdata_to_jsonifiable(result) if "plotnine" in sys.modules: ok, result = try_render_plotnine( @@ -358,9 +356,7 @@ async def transform(self, value: ImgData) -> dict[str, JSONifiable] | None: data_str = data.decode("utf-8") content_type = _utils.guess_mime_type(src) value["src"] = f"data:{content_type};base64,{data_str}" - value_dict = dict(value) - value_jsonifiable = cast(Dict[str, JSONifiable], value_dict) - return value_jsonifiable + return imgdata_to_jsonifiable(value) finally: if self.delete_file: os.remove(src) @@ -477,9 +473,7 @@ async def transform(self, value: TableResult) -> dict[str, JSONifiable]: ) # Use typing to make sure the return shape matches ret: RenderedDeps = {"deps": [], "html": html} - ret_dict = dict(ret) - ret_jsonifiable = cast(Dict[str, JSONifiable], ret_dict) - return ret_jsonifiable + return rendered_deps_to_jsonifiable(ret) # ====================================================================================== @@ -510,7 +504,6 @@ def default_ui(self, id: str) -> Tag: return _ui.output_ui(id) async def transform(self, value: TagChild) -> JSONifiable: - res = self.session._process_ui(value) - res_dict = dict(res) - res_jsonifiable = cast(JSONifiable, res_dict) - return res_jsonifiable + return rendered_deps_to_jsonifiable( + self.session._process_ui(value), + ) diff --git a/shiny/render/renderer/_utils.py b/shiny/render/renderer/_utils.py new file mode 100644 index 000000000..f81fb6123 --- /dev/null +++ b/shiny/render/renderer/_utils.py @@ -0,0 +1,15 @@ +from typing import Dict, cast + +from ...session._utils import RenderedDeps +from ...types import ImgData +from ._renderer import JSONifiable + +JSONifiable_dict = Dict[str, JSONifiable] + + +def rendered_deps_to_jsonifiable(rendered_deps: RenderedDeps) -> JSONifiable_dict: + return cast(JSONifiable_dict, dict(rendered_deps)) + + +def imgdata_to_jsonifiable(imgdata: ImgData) -> JSONifiable_dict: + return cast(JSONifiable_dict, dict(imgdata)) From b185cd6cb6db3aaa4775b9e87192251e85c3650a Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 11:29:37 -0500 Subject: [PATCH 53/77] Remove helper method. Just use `__call__(value_fn)` --- shiny/render/renderer/_renderer.py | 35 ++++++++++++++---------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index f342e603b..0f3fa507b 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -259,25 +259,11 @@ class Renderer(RendererBase, Generic[IT]): # App value function # _value_fn_original: ValueFnApp[IT] # TODO-barret; Remove this? - _value_fn: AsyncValueFn[IT | None] + value_fn: AsyncValueFn[IT | None] - @property - def value_fn(self) -> AsyncValueFn[IT | None]: - return self._value_fn - - def _set_value_fn(self, value_fn: ValueFnApp[Any | None]) -> None: - if not callable(value_fn): - raise TypeError("Value function must be callable") - - # Copy over function name as it is consistent with how Session and Output - # retrieve function names - self.__name__ = value_fn.__name__ - - # Set value function with extra meta information - self._value_fn = AsyncValueFn(value_fn) - - # Allow for App authors to not require `@output` - self._auto_register() + # @property + # def value_fn(self) -> AsyncValueFn[IT | None]: + # return self._value_fn """ App-supplied output value function which returns type `IT`. This function is always @@ -291,7 +277,18 @@ def __call__(self, value_fn: ValueFnApp[IT | None]) -> Self: TODO-barret - docs """ - self._set_value_fn(value_fn) + if not callable(value_fn): + raise TypeError("Value function must be callable") + + # Copy over function name as it is consistent with how Session and Output + # retrieve function names + self.__name__ = value_fn.__name__ + + # Set value function with extra meta information + self.value_fn = AsyncValueFn(value_fn) + + # Allow for App authors to not require `@output` + self._auto_register() return self From 12da490f381636dc85dd9c67647455e79cb8bd69 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 12:13:22 -0500 Subject: [PATCH 54/77] Add `.async_fn()` and `.sync_fn()` methods to `AsyncValueFn` class --- shiny/render/renderer/_renderer.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 0f3fa507b..f9936c6ff 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -205,6 +205,7 @@ def __init__(self, fn: Callable[[], IT] | Callable[[], Awaitable[IT]]): return fn self._is_async = is_async_callable(fn) self._fn = wrap_async(fn) + self._orig_fn = fn async def __call__(self) -> IT: """ @@ -225,17 +226,36 @@ def is_async(self) -> bool: return self._is_async @property - def fn(self) -> Callable[[], IT] | Callable[[], Awaitable[IT]]: + def async_fn(self) -> Callable[[], Awaitable[IT]]: """ - Retrieve the original function + Return the async value function. Returns ------- : - Original function supplied to the `AsyncValueFn` constructor. + Async wrapped value function supplied to the `AsyncValueFn` constructor. """ return self._fn + @property + def sync_fn(self) -> Callable[[], IT]: + """ + Retrieve the original, synchronous value function function. + + If the original function was asynchronous, a runtime error will be thrown. + + Returns + ------- + : + Original, synchronous function supplied to the `AsyncValueFn` constructor. + """ + if self._is_async: + raise RuntimeError( + "The original function was asynchronous. Use `async_fn` instead." + ) + sync_fn = cast(Callable[[], IT], self._orig_fn) + return sync_fn + # class RendererShim(RendererBase, Generic[IT, P]): # def default_ui( From 5e2056ef249b8048488f2507646fe0649e686ce1 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 12:19:16 -0500 Subject: [PATCH 55/77] Implement `@render.display` class --- shiny/render/_display.py | 219 +++++++++++++------------------- shiny/render/_render.py | 28 ++-- shiny/render/renderer/_utils.py | 29 ++++- 3 files changed, 126 insertions(+), 150 deletions(-) diff --git a/shiny/render/_display.py b/shiny/render/_display.py index 3c8152b7c..8c033883b 100644 --- a/shiny/render/_display.py +++ b/shiny/render/_display.py @@ -1,145 +1,108 @@ from __future__ import annotations -import inspect import sys -from typing import Any, Callable, Optional, Union, overload +from typing import Optional -from htmltools import TagAttrValue, TagFunction, TagList, wrap_displayhook_handler +from htmltools import Tag, TagAttrValue, TagFunction, TagList, wrap_displayhook_handler from .. import ui as _ui -from .._utils import run_coro_sync -from ..session._utils import RenderedDeps -from .transformer import ( - OutputRenderer, - TransformerMetadata, - TransformerParams, - ValueFn, - ValueFnSync, +from .._typing_extensions import Self +from ..session._utils import require_active_session +from ..types import MISSING, MISSING_TYPE +from .renderer import AsyncValueFn, Renderer, ValueFn +from .renderer._utils import ( + JSONifiable_dict, + rendered_deps_to_jsonifiable, + set_kwargs_value, ) -# TODO-barret; Implement display - - -async def display_transformer( - _meta: TransformerMetadata, - _fn: ValueFn[None], - *, - inline: bool = False, - container: Optional[TagFunction] = None, - fill: bool = False, - fillable: bool = False, - **kwargs: TagAttrValue, -) -> RenderedDeps | None: - results: list[object] = [] - orig_displayhook = sys.displayhook - sys.displayhook = wrap_displayhook_handler(results.append) - try: - # We check for sync function below. Therefore, we can run `run_coro_sync` here. - ret = run_coro_sync(_fn()) - if ret is not None: - raise RuntimeError( - "@render.display functions should not return values. (`None` is allowed)." - ) - finally: - sys.displayhook = orig_displayhook - if len(results) == 0: - return None - return _meta.session._process_ui( - TagList(*results) # pyright: ignore[reportGeneralTypeIssues] - ) - - -DisplayRenderer = OutputRenderer[Union[RenderedDeps, None]] - - -@overload -def display( - *, - inline: bool = False, - container: Optional[TagFunction] = None, - fill: bool = False, - fillable: bool = False, - **kwargs: Any, -) -> Callable[[ValueFnSync[None]], DisplayRenderer]: - ... - - -@overload -def display( - _fn: ValueFnSync[None], -) -> DisplayRenderer: - ... - - -def display( - _fn: ValueFnSync[None] | None = None, - *, - inline: bool = False, - container: Optional[TagFunction] = None, - fill: bool = False, - fillable: bool = False, - **kwargs: Any, -) -> DisplayRenderer | Callable[[ValueFnSync[None]], DisplayRenderer]: - """ - Reactively render UI content, emitting each top-level expression of the function - body, in the same way as a Shiny Express top-level or Jupyter notebook cell. - - Parameters - ---------- - inline - If ``True``, the rendered content will be displayed inline with the surrounding - text. If ``False``, the rendered content will be displayed on its own line. If - the ``container`` argument is not ``None``, this argument is ignored. - container - A function that returns a container for the rendered content. If ``None``, a - default container will be chosen according to the ``inline`` argument. - fill - Whether or not to allow the UI output to grow/shrink to fit a fillable container - with an opinionated height (e.g., :func:`~shiny.ui.page_fillable`). - fillable - Whether or not the UI output area should be considered a fillable (i.e., - flexbox) container. - **kwargs - Attributes to be applied to the output container. - - - Returns - ------- - : - A decorator for a function whose top-level expressions will be displayed as UI. - """ - - def impl(fn: ValueFnSync[None]) -> OutputRenderer[RenderedDeps | None]: - if inspect.iscoroutine(fn): + +class display(Renderer[None]): + def default_ui( + self, + id: str, + *, + inline: bool | MISSING_TYPE = MISSING, + container: TagFunction | MISSING_TYPE = MISSING, + fill: bool | MISSING_TYPE = MISSING, + fillable: bool | MISSING_TYPE = MISSING, + **kwargs: TagAttrValue, + ) -> Tag: + # Only set the arg if it is available. (Prevents duplicating default values) + set_kwargs_value(kwargs, "inline", inline, self.inline) + set_kwargs_value(kwargs, "container", container, self.container) + set_kwargs_value(kwargs, "fill", fill, self.fill) + set_kwargs_value(kwargs, "fillable", fillable, self.fillable) + + return _ui.output_ui( + id, + # (possibly) contains `inline`, `container`, `fill`, and `fillable` keys! + **kwargs, # pyright: ignore[reportGeneralTypeIssues] + ) + + def __call__(self, fn: ValueFn[None]) -> Self: + if fn is None: + raise TypeError("@render.display requires a function when called") + + async_fn = AsyncValueFn(fn) + if async_fn.is_async: raise TypeError( "@render.display does not support async functions. Use @render.ui instead." ) + from shiny.express.display_decorator._display_body import ( display_body_unwrap_inplace, ) fn = display_body_unwrap_inplace()(fn) - return OutputRenderer( - value_fn=fn, - transform_fn=display_transformer, - params=TransformerParams( - inline=inline, - container=container, - fill=fill, - fillable=fillable, - **kwargs, - ), - default_ui=_ui.output_ui, - default_ui_passthrough_args=( - "inline", - "container", - "fill", - "fillable", - *[k for k in kwargs.keys()], - ), - ) - if _fn is not None: - return impl(_fn) - else: - return impl + # Call the superclass method with upgraded `fn` value + super().__call__(fn) + + return self + + def __init__( + self, + _fn: ValueFn[None] = None, + *, + inline: bool = False, + container: Optional[TagFunction] = None, + fill: bool = False, + fillable: bool = False, + **kwargs: TagAttrValue, + ): + super().__init__(_fn) + self.inline: bool = inline + self.container: Optional[TagFunction] = container + self.fill: bool = fill + self.fillable: bool = fillable + self.kwargs: dict[str, TagAttrValue] = kwargs + + async def render(self) -> JSONifiable_dict | None: + results: list[object] = [] + orig_displayhook = sys.displayhook + sys.displayhook = wrap_displayhook_handler(results.append) + + if self.value_fn.is_async: + raise TypeError( + "@render.display does not support async functions. Use @render.ui instead." + ) + + try: + # Run synchronously + ret = self.value_fn.sync_fn() + if ret is not None: + raise RuntimeError( + "@render.display functions should not return values. (`None` is allowed)." + ) + finally: + sys.displayhook = orig_displayhook + if len(results) == 0: + return None + + session = require_active_session(None) + return rendered_deps_to_jsonifiable( + session._process_ui( + TagList(*results) # pyright: ignore[reportGeneralTypeIssues] + ) + ) diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 47d91d6eb..989bb9904 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -19,8 +19,6 @@ from htmltools import Tag, TagAttrValue, TagChild -from shiny.render.renderer import JSONifiable - if TYPE_CHECKING: from ..session._utils import RenderedDeps import pandas as pd @@ -35,8 +33,12 @@ try_render_pil, try_render_plotnine, ) -from .renderer import Renderer, ValueFn -from .renderer._utils import imgdata_to_jsonifiable, rendered_deps_to_jsonifiable +from .renderer import JSONifiable, Renderer, ValueFn +from .renderer._utils import ( + imgdata_to_jsonifiable, + rendered_deps_to_jsonifiable, + set_kwargs_value, +) __all__ = ( "text", @@ -156,23 +158,9 @@ def default_ui( height: str | float | int | MISSING_TYPE = MISSING, **kwargs: object, ) -> Tag: - def set_kwarg_value( - key: str, - ui_val: str | float | int | MISSING_TYPE, - self_val: float | None | MISSING_TYPE, - ): - if not isinstance(ui_val, MISSING_TYPE): - kwargs[key] = ui_val - return - if not (isinstance(self_val, MISSING_TYPE) or self_val is None): - kwargs[key] = self_val - return - # Do nothing as we don't want to override the default value (that could change in the future) - return - # Only set the arg if it is available. (Prevents duplicating default values) - set_kwarg_value("width", width, self.width) - set_kwarg_value("height", height, self.height) + set_kwargs_value(kwargs, "width", width, self.width) + set_kwargs_value(kwargs, "height", height, self.height) return _ui.output_plot( id, # (possibly) contains `width` and `height` keys! diff --git a/shiny/render/renderer/_utils.py b/shiny/render/renderer/_utils.py index f81fb6123..c14857679 100644 --- a/shiny/render/renderer/_utils.py +++ b/shiny/render/renderer/_utils.py @@ -1,7 +1,9 @@ -from typing import Dict, cast +from typing import Any, Dict, cast + +from htmltools import TagFunction from ...session._utils import RenderedDeps -from ...types import ImgData +from ...types import MISSING_TYPE, ImgData from ._renderer import JSONifiable JSONifiable_dict = Dict[str, JSONifiable] @@ -13,3 +15,26 @@ def rendered_deps_to_jsonifiable(rendered_deps: RenderedDeps) -> JSONifiable_dic def imgdata_to_jsonifiable(imgdata: ImgData) -> JSONifiable_dict: return cast(JSONifiable_dict, dict(imgdata)) + + +def set_kwargs_value( + kwargs: dict[str, Any], + key: str, + ui_val: TagFunction | str | float | int | MISSING_TYPE, + self_val: TagFunction | str | float | int | None | MISSING_TYPE, +): + """ + Set kwarg value with fallback value. + + * If `ui_val` is not `MISSING`, set `kwargs[key] = ui_val`. + * If `self_val` is not `MISSING` and is not `None`, set `kwargs[key] = self_val`. + * Otherwise, do nothing. + """ + if not isinstance(ui_val, MISSING_TYPE): + kwargs[key] = ui_val + return + if not (isinstance(self_val, MISSING_TYPE) or self_val is None): + kwargs[key] = self_val + return + # Do nothing as we don't want to override the default value (that could change in the future) + return From 31e047ba1a1f81bf0ea2079456c1999464de5062 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 12:19:32 -0500 Subject: [PATCH 56/77] Typing and test updates --- shiny/api-examples/Renderer/app.py | 2 +- shiny/render/renderer/_renderer.py | 4 ---- tests/playwright/examples/test_examples.py | 1 + tests/pytest/test_reactives.py | 10 +++++----- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/shiny/api-examples/Renderer/app.py b/shiny/api-examples/Renderer/app.py index 0cfe64b81..5afc927e5 100644 --- a/shiny/api-examples/Renderer/app.py +++ b/shiny/api-examples/Renderer/app.py @@ -33,7 +33,7 @@ def __init__( self.default_ui = ui.output_text_verbatim async def render(self) -> str | None: - value = await self._value_fn() + value = await self.value_fn() if value is None: return None self.widget = value diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index f9936c6ff..c540f5c0a 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -281,10 +281,6 @@ class Renderer(RendererBase, Generic[IT]): # _value_fn_original: ValueFnApp[IT] # TODO-barret; Remove this? value_fn: AsyncValueFn[IT | None] - # @property - # def value_fn(self) -> AsyncValueFn[IT | None]: - # return self._value_fn - """ App-supplied output value function which returns type `IT`. This function is always asyncronous as the original app-supplied function possibly wrapped to execute diff --git a/tests/playwright/examples/test_examples.py b/tests/playwright/examples/test_examples.py index e95d33e8e..b97199d32 100644 --- a/tests/playwright/examples/test_examples.py +++ b/tests/playwright/examples/test_examples.py @@ -64,6 +64,7 @@ def get_apps(path: str) -> typing.List[str]: "brownian": [*output_transformer_errors], "multi-page": [*output_transformer_errors], "model-score": [*output_transformer_errors], + "data_frame": [*output_transformer_errors], "output_transformer": [*output_transformer_errors], "render_display": [*express_warnings], } diff --git a/tests/pytest/test_reactives.py b/tests/pytest/test_reactives.py index 3d6216579..ad4499f0f 100644 --- a/tests/pytest/test_reactives.py +++ b/tests/pytest/test_reactives.py @@ -1168,13 +1168,13 @@ async def test_output_type_check(): with pytest.raises(TypeError): # Should complain about bare function - @output # pyright: ignore[reportGeneralTypeIssues] + @output # pyright: ignore[reportGeneralTypeIssues,reportUntypedFunctionDecorator] def _(): ... with pytest.raises(TypeError): # Should complain about @event - @output # pyright: ignore[reportGeneralTypeIssues] + @output # pyright: ignore[reportGeneralTypeIssues,reportUntypedFunctionDecorator] @event(lambda: 1) def _(): ... @@ -1182,7 +1182,7 @@ def _(): with pytest.raises(TypeError): # Should complain about @event, even with render.text. Although maybe in the # future this will be allowed. - @output # pyright: ignore[reportGeneralTypeIssues] + @output # pyright: ignore[reportGeneralTypeIssues,reportUntypedFunctionDecorator] @event(lambda: 1) # pyright: ignore[reportGeneralTypeIssues] @render.text def _(): @@ -1190,14 +1190,14 @@ def _(): with pytest.raises(TypeError): # Should complain about @Calc - @output # pyright: ignore[reportGeneralTypeIssues] + @output # pyright: ignore[reportGeneralTypeIssues,reportUntypedFunctionDecorator] @calc def _(): ... with pytest.raises(TypeError): # Should complain about @Effet - @output # pyright: ignore[reportGeneralTypeIssues] + @output # pyright: ignore[reportGeneralTypeIssues,reportUntypedFunctionDecorator] @effect def _(): ... From e70b9dfbf405f4c31c67a402ad925f34db5cd084 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 12:31:30 -0500 Subject: [PATCH 57/77] lint --- shiny/render/_render.py | 3 +-- shiny/render/renderer/_utils.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 989bb9904..af74cb671 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -74,8 +74,7 @@ class text(Renderer[str]): def default_ui(self, id: str, placeholder: bool | MISSING_TYPE = MISSING) -> Tag: kwargs: dict[str, bool] = {} - if not isinstance(placeholder, MISSING_TYPE): - kwargs["placeholder"] = placeholder + set_kwargs_value(kwargs, "placeholder", placeholder, None) return _ui.output_text_verbatim(id, **kwargs) def __init__(self, fn: Optional[ValueFn[str]] = None, power: int = 1) -> None: diff --git a/shiny/render/renderer/_utils.py b/shiny/render/renderer/_utils.py index c14857679..2b6b846a8 100644 --- a/shiny/render/renderer/_utils.py +++ b/shiny/render/renderer/_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any, Dict, cast from htmltools import TagFunction From d8610cedbd2d5a61dfabb795f63b58fa4d3f4eab Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 12:51:34 -0500 Subject: [PATCH 58/77] Move `._on_register()` to occur after output metadata has been set --- shiny/session/_session.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shiny/session/_session.py b/shiny/session/_session.py index 03c9de950..4f4859bea 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -995,15 +995,13 @@ def set_renderer(renderer: RendererBaseT) -> RendererBaseT: + "In other words, `@output` must be above `@render.xx`." ) - # TODO-barret; How does this work? Feels like it should be called after the `renderer.session` is set - renderer._on_register() - # Get the (possibly namespaced) output id output_name = self._ns(id or renderer.__name__) # renderer is a Renderer object. Give it a bit of metadata. renderer.session = self._session renderer.name = output_name + renderer._on_register() self.remove(output_name) From 15ec2b1999b99d551308a4540d60a5025e06d662 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 12:55:15 -0500 Subject: [PATCH 59/77] Use `require_active_session(None)` to retrieve current Session; Add set metadata method --- shiny/render/_render.py | 8 +++-- shiny/render/renderer/_renderer.py | 37 ++++++++++++++---------- shiny/render/transformer/_transformer.py | 17 ++++------- shiny/session/_session.py | 5 ++-- 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/shiny/render/_render.py b/shiny/render/_render.py index af74cb671..0d56c8b9d 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -26,6 +26,7 @@ from .. import _utils from .. import ui as _ui from .._namespaces import ResolvedId +from ..session import require_active_session from ..types import MISSING, MISSING_TYPE, ImgData from ._try_render_plot import ( PlotSizeInfo, @@ -183,8 +184,8 @@ def __init__( async def render(self) -> dict[str, JSONifiable] | JSONifiable | None: is_userfn_async = self.value_fn.is_async - name = self.name - session = self.session + name = self.output_name + session = require_active_session(None) width = self.width height = self.height alt = self.alt @@ -491,6 +492,7 @@ def default_ui(self, id: str) -> Tag: return _ui.output_ui(id) async def transform(self, value: TagChild) -> JSONifiable: + session = require_active_session(None) return rendered_deps_to_jsonifiable( - self.session._process_ui(value), + session._process_ui(value), ) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index c540f5c0a..dd5da538c 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -2,7 +2,6 @@ from abc import ABC, abstractmethod from typing import ( - TYPE_CHECKING, Any, Awaitable, Callable, @@ -24,8 +23,6 @@ # TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` # TODO-barret; Q: Should `Renderer.default_ui` be renamed? `ui()`? `express_ui()`? -# TODO-barret; Q: Should `Renderer.default_ui` accept args? ... Should `output_args()` be renamed to `ui_kwargs()`? (If anything rename to `ui_args()`) -# TODO-barret; Q: Should `Renderer.default_ui` accept kwargs? ... Should `output_kwargs()` be renamed to `ui_kwargs()`? (If anything rename to `ui_kwargs()`) Add `.ui_kwargs()` method? # TODO-future: docs; missing first paragraph from some classes: Example: TransformerMetadata. @@ -34,14 +31,9 @@ # displayed. Even if they have docs. -if TYPE_CHECKING: - from ...session import Session - - __all__ = ( "Renderer", "RendererBase", - "Renderer", "ValueFn", "JSONifiable", "AsyncValueFn", @@ -111,20 +103,35 @@ class RendererBase(ABC): __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 any module prefix.) - _auto_registered: bool = False + Set within `.__call__()` method. + """ # Meta - session: Session - """ - :class:`~shiny.Session` object - """ - name: str + output_name: str """ Output function name or ID (provided to `@output(id=)`). This value will contain any module prefix. + + Set when the output is registered with the session. """ + def _set_output_metadata( + self, + *, + output_name: str, + ) -> None: + """ + Method to be called within `@output` to set the renderer's metadata. + + Parameters + ---------- + output_name : str + Output function name or ID (provided to `@output(id=)`). This value will contain any module prefix. + """ + self.output_name = output_name + def default_ui( self, id: str, diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 4aba6f387..fae407f19 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -2,7 +2,7 @@ # TODO-future; When `OutputRenderer` is removed, remove `output_args()` -# TODO-barret; Why was `DefaultUIFnImpl` being used? The type does NOT make sense. Using `DefaultUIFn` +# TODO-barret; Q: Why was `DefaultUIFnImpl` being used? The type does NOT make sense. Using `DefaultUIFn` __all__ = ( @@ -275,22 +275,17 @@ def __init__( # Allow for App authors to not require `@output` self._auto_register() - def _set_metadata(self, session: Session, name: str) -> None: - """ - When `Renderer`s are assigned to Output object slots, this method is used to - pass along Session and name information. - """ - self.session = session - self.name = name - def _meta(self) -> TransformerMetadata: """ Returns a named tuple of values: `session` (the :class:`~shiny.Session` object), and `name` (the name of the output being rendered) """ + from ...session import require_active_session + + session = require_active_session(None) return TransformerMetadata( - session=self.session, - name=self.name, + session=session, + name=self.output_name, ) async def _run(self) -> OT: diff --git a/shiny/session/_session.py b/shiny/session/_session.py index 4f4859bea..6a0604ef7 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -966,7 +966,6 @@ def __init__( self._suspend_when_hidden = suspend_when_hidden @overload - # TODO-barret; Use generics to pass through the type! def __call__(self, renderer: RendererBaseT) -> RendererBaseT: ... @@ -999,8 +998,8 @@ def set_renderer(renderer: RendererBaseT) -> RendererBaseT: output_name = self._ns(id or renderer.__name__) # renderer is a Renderer object. Give it a bit of metadata. - renderer.session = self._session - renderer.name = output_name + renderer._set_output_metadata(output_name=output_name) + renderer._on_register() self.remove(output_name) From 8f345ecff14671d9269e7805ff4cc4d3b1e32fdb Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 13:01:03 -0500 Subject: [PATCH 60/77] Use `AutoRegisterMixin` class for `RendererBase` --- shiny/render/renderer/_auto_register.py | 47 +++++++++++++++++++++++ shiny/render/renderer/_renderer.py | 50 ++++++++----------------- 2 files changed, 63 insertions(+), 34 deletions(-) create mode 100644 shiny/render/renderer/_auto_register.py diff --git a/shiny/render/renderer/_auto_register.py b/shiny/render/renderer/_auto_register.py new file mode 100644 index 000000000..dda732503 --- /dev/null +++ b/shiny/render/renderer/_auto_register.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import Protocol, cast + +# from ...session import require_active_session + + +class AutoRegisterP(Protocol): + __name__: str + _auto_registered: bool = False + + +class AutoRegisterMixin: + """ + 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()`. + """ + + _auto_registered: bool = False + + def _on_register(self: AutoRegisterP) -> None: + if self._auto_registered: + # We're being explicitly registered now. Undo the auto-registration. + # (w/ module support) + from ...session import require_active_session + + session = require_active_session(None) + ns_name = session.output._ns(self.__name__) + session.output.remove(ns_name) + self._auto_registered = False + + def _auto_register(self) -> None: + # If in Express mode, register the output + if not self._auto_registered: + from ...session import get_current_session + + s = get_current_session() + if s is not None: + from ._renderer import RendererBase + + # Cast to avoid circular import as this mixin is ONLY used within RendererBase + renderer_self = cast(RendererBase, self) + s.output(renderer_self) + # We mark the fact that we're auto-registered so that, if an explicit + # registration now occurs, we can undo this auto-registration. + self._auto_registered = True diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index dd5da538c..0040dc690 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -19,10 +19,11 @@ from ..._typing_extensions import Self from ..._utils import is_async_callable, wrap_async +from ._auto_register import AutoRegisterMixin -# TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` - +# TODO-barret; Change the base branch to `main` # TODO-barret; Q: Should `Renderer.default_ui` be renamed? `ui()`? `express_ui()`? +# TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` # TODO-future: docs; missing first paragraph from some classes: Example: TransformerMetadata. @@ -101,7 +102,13 @@ ValueFn = Optional[ValueFnApp[Union[IT, None]]] -class RendererBase(ABC): +class RendererBase(AutoRegisterMixin, ABC): + """ + Base class for all renderers. + + TODO-barret - docs + """ + __name__: str """ Name of output function supplied. (The value will not contain any module prefix.) @@ -153,26 +160,6 @@ def __init__(self) -> None: _default_ui_kwargs: dict[str, Any] = dict() # _default_ui_args: tuple[Any, ...] = tuple() - def _on_register(self) -> None: - if self._auto_registered: - # We're being explicitly registered now. Undo the auto-registration. - # (w/ module support) - ns_name = self.session.output._ns(self.__name__) - self.session.output.remove(ns_name) - self._auto_registered = False - - def _auto_register(self) -> None: - # If in Express mode, register the output - if not self._auto_registered: - from ...session import get_current_session - - s = get_current_session() - if s is not None: - s.output(self) - # We mark the fact that we're auto-registered so that, if an explicit - # registration now occurs, we can undo this auto-registration. - self._auto_registered = True - def _repr_html_(self) -> str | None: rendered_ui = self._render_default_ui() if rendered_ui is None: @@ -275,19 +262,11 @@ def sync_fn(self) -> Callable[[], IT]: class Renderer(RendererBase, Generic[IT]): """ Renderer cls docs here + TODO-barret - docs """ - # __name__: str ?? - - # UI - # TODO-barret; Utilize these! - # default_ui_passthrough_args: tuple[str, ...] | None = None - - # App value function - # _value_fn_original: ValueFnApp[IT] # TODO-barret; Remove this? value_fn: AsyncValueFn[IT | None] - """ App-supplied output value function which returns type `IT`. This function is always asyncronous as the original app-supplied function possibly wrapped to execute @@ -297,6 +276,7 @@ class Renderer(RendererBase, Generic[IT]): def __call__(self, value_fn: ValueFnApp[IT | None]) -> Self: """ Renderer __call__ docs here; Sets app's value function + TODO-barret - docs """ @@ -305,10 +285,10 @@ def __call__(self, value_fn: ValueFnApp[IT | None]) -> Self: # Copy over function name as it is consistent with how Session and Output # retrieve function names - self.__name__ = value_fn.__name__ + self.__name__: str = value_fn.__name__ # Set value function with extra meta information - self.value_fn = AsyncValueFn(value_fn) + self.value_fn: AsyncValueFn[IT | None] = AsyncValueFn(value_fn) # Allow for App authors to not require `@output` self._auto_register() @@ -332,6 +312,7 @@ def __init__( async def transform(self, value: IT) -> JSONifiable: """ Renderer - transform docs here + TODO-barret - docs """ raise NotImplementedError( @@ -344,6 +325,7 @@ async def transform(self, value: IT) -> JSONifiable: async def render(self) -> JSONifiable: """ Renderer - render docs here + TODO-barret - docs """ value = await self.value_fn() From 644ce5b4782f1d912b115a23b0f41687328e9598 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 13:05:50 -0500 Subject: [PATCH 61/77] Test lint --- tests/playwright/examples/test_examples.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/playwright/examples/test_examples.py b/tests/playwright/examples/test_examples.py index b97199d32..cae216128 100644 --- a/tests/playwright/examples/test_examples.py +++ b/tests/playwright/examples/test_examples.py @@ -46,6 +46,8 @@ def get_apps(path: str) -> typing.List[str]: "ShinyDeprecationWarning:", "`resolve_value_fn()`", "value = await resolve_value_fn(_fn)", + # brownian example app + "shiny.render.transformer.output_transformer()", ] express_warnings = ["Detected Shiny Express app. "] app_allow_shiny_errors: typing.Dict[ From 0dd611779d315908376eff0ebed7557e0e9dc1ab Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 13:20:12 -0500 Subject: [PATCH 62/77] Update _quartodoc.yml --- docs/_quartodoc.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index beb1faa8b..076f5e777 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -196,17 +196,11 @@ quartodoc: name: "Create rendering outputs" desc: "" contents: - # TODO-barret; Update with renderer classes / info! - render.transformer.Renderer - # - render.transformer.output_transformer - # - render.transformer.OutputTransformer - # - render.transformer.TransformerMetadata - # - render.transformer.TransformerParams - # - render.transformer.OutputRenderer - # - render.transformer.is_async_callable + - render.transformer.RendererBase + - render.transformer.JSONifiable - render.transformer.ValueFn - render.transformer.AsyncValueFn - # - render.transformer.TransformFn - title: Reactive programming desc: "" contents: From fc688cb0035655ba25ca67cb67edc79e1706d9b2 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 13:34:07 -0500 Subject: [PATCH 63/77] Fix test --- tests/pytest/test_output_transformer.py | 55 +++++++++++++++---------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/tests/pytest/test_output_transformer.py b/tests/pytest/test_output_transformer.py index 397406c49..3d493b53f 100644 --- a/tests/pytest/test_output_transformer.py +++ b/tests/pytest/test_output_transformer.py @@ -1,11 +1,12 @@ from __future__ import annotations import asyncio -from typing import Any, overload +from typing import Any, cast, overload import pytest from shiny._deprecated import ShinyDeprecationWarning +from shiny._namespaces import ResolvedId, Root from shiny._utils import is_async_callable from shiny.render.transformer import ( TransformerMetadata, @@ -13,11 +14,23 @@ output_transformer, resolve_value_fn, ) +from shiny.session import Session, session_context # import warnings # warnings.filterwarnings("ignore", category=ShinyDeprecationWarning) +class _MockSession: + ns: ResolvedId = Root + + # This is needed so that Outputs don't throw an error. + def _is_hidden(self, name: str) -> bool: + return False + + +test_session = cast(Session, _MockSession()) + + def test_output_transformer_works(): # No args works @output_transformer @@ -238,15 +251,15 @@ def app_render_fn() -> str: # ## Test Sync: X ============================================= renderer_sync = async_renderer(app_render_fn) - renderer_sync._set_metadata( - None, # pyright: ignore[reportGeneralTypeIssues] - "renderer_sync", + renderer_sync._set_output_metadata( + output_name="renderer_sync", ) # All renderers are async in execution. assert is_async_callable(renderer_sync) - val = await renderer_sync() - assert val == test_val + with session_context(test_session): + val = await renderer_sync() + assert val == test_val # ## Test Async: √ ============================================= @@ -257,15 +270,15 @@ async def async_app_render_fn() -> str: return async_test_val renderer_async = async_renderer(async_app_render_fn) - renderer_async._set_metadata( - None, # pyright: ignore[reportGeneralTypeIssues] - "renderer_async", + renderer_async._set_output_metadata( + output_name="renderer_async", ) if not is_async_callable(renderer_async): raise RuntimeError("Expected `renderer_async` to be a coro function") - ret = await renderer_async() - assert ret == async_test_val + with session_context(test_session): + ret = await renderer_async() + assert ret == async_test_val # "Currently, `ValueFnA` can not be truly async and "support sync render methods". @@ -309,14 +322,14 @@ def app_render_fn() -> str: # ## Test Sync: √ ============================================= renderer_sync = yield_renderer(app_render_fn) - renderer_sync._set_metadata( - None, # pyright: ignore[reportGeneralTypeIssues] - "renderer_sync", + renderer_sync._set_output_metadata( + output_name="renderer_sync", ) assert is_async_callable(renderer_sync) - ret = await renderer_sync() - assert ret == test_val + with session_context(test_session): + ret = await renderer_sync() + assert ret == test_val # ## Test Async: √ ============================================= @@ -327,14 +340,14 @@ async def async_app_render_fn() -> str: return async_test_val renderer_async = yield_renderer(async_app_render_fn) - renderer_async._set_metadata( - None, # pyright: ignore[reportGeneralTypeIssues] - "renderer_async", + renderer_async._set_output_metadata( + output_name="renderer_async", ) assert is_async_callable(renderer_async) - ret = await renderer_async() - assert ret == async_test_val + with session_context(test_session): + ret = await renderer_async() + assert ret == async_test_val @pytest.mark.asyncio From 8a5bbf5aea710a991a877b7199f583103c14b71e Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 13:37:00 -0500 Subject: [PATCH 64/77] Update _quartodoc.yml --- docs/_quartodoc.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 076f5e777..c6c5f771b 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -196,11 +196,11 @@ quartodoc: name: "Create rendering outputs" desc: "" contents: - - render.transformer.Renderer - - render.transformer.RendererBase - - render.transformer.JSONifiable - - render.transformer.ValueFn - - render.transformer.AsyncValueFn + - render.renderer.Renderer + - render.renderer.RendererBase + - render.renderer.JSONifiable + - render.renderer.ValueFn + - render.renderer.AsyncValueFn - title: Reactive programming desc: "" contents: From 43269674d2f22138c9946c42d131c2a465898300 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 13:54:53 -0500 Subject: [PATCH 65/77] Docs and remove lingering `power` in renderer.text --- shiny/api-examples/Renderer/app.py | 2 +- shiny/render/_render.py | 4 ---- shiny/render/renderer/_renderer.py | 12 +++++++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/shiny/api-examples/Renderer/app.py b/shiny/api-examples/Renderer/app.py index 5afc927e5..bb15ed269 100644 --- a/shiny/api-examples/Renderer/app.py +++ b/shiny/api-examples/Renderer/app.py @@ -4,7 +4,7 @@ from shiny import App, Inputs, Outputs, Session, ui from shiny.render.renderer import Renderer, ValueFn -# TODO-barret Update app with docs below +# TODO-barret - docs - Update app with docs below class sub_barret_renderer(Renderer[str]): diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 0d56c8b9d..60cb3fbb8 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -78,10 +78,6 @@ def default_ui(self, id: str, placeholder: bool | MISSING_TYPE = MISSING) -> Tag set_kwargs_value(kwargs, "placeholder", placeholder, None) return _ui.output_text_verbatim(id, **kwargs) - def __init__(self, fn: Optional[ValueFn[str]] = None, power: int = 1) -> None: - super().__init__(fn) - self.power: int = power - async def transform(self, value: str) -> JSONifiable: return str(value) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 0040dc690..ddd0864c1 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -21,8 +21,13 @@ from ..._utils import is_async_callable, wrap_async from ._auto_register import AutoRegisterMixin -# TODO-barret; Change the base branch to `main` # TODO-barret; Q: Should `Renderer.default_ui` be renamed? `ui()`? `express_ui()`? +# TODO-barret; Q: Should `.transform()` exist on `Renderer`? +# * If it is removed, then the `IT` type isn't required in `Renderer` and we could remove RendererBase +# * No confusion between `transform` and `render` methods +# * `render` is the only method that is required to be implemented +# * Can add helper decorator to do typical transform functionality? +# * Con: Would require the `__init__(self, fn: ValueFn[IT])` method to get typing # TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` @@ -41,6 +46,11 @@ ) RendererBaseT = TypeVar("RendererBaseT", bound="RendererBase") +""" +Generic class to pass the Renderer class through a decorator. + +When accepting and returning a `RendererBase` class, utilize this TypeVar as to not reduce the variable type to `RendererBase` +""" # Input type for the user-spplied function that is passed to a render.xx IT = TypeVar("IT") From dc206d3b57ce2aeb825beaf1ea470116e3a948de Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 14:07:02 -0500 Subject: [PATCH 66/77] Merge followup --- shiny/render/transformer/_transformer.py | 2 +- tests/playwright/shiny/shiny-express/folium/app.py | 4 ++-- tests/playwright/shiny/shiny-express/suspend_display/app.py | 6 ++---- tests/pytest/test_reactives.py | 1 - 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 062b8d309..94818a1ad 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -466,7 +466,7 @@ class OutputTransformer(Generic[IT, OT, P]): """ fn: OutputTransformerFn[IT, P, OT] - ValueFn: Type[ValueFn[IT]] + ValueFn: Type[ValueFnApp[IT]] OutputRenderer: Type[OutputRenderer[OT]] OutputRendererDecorator: Type[OutputRendererDecorator[IT, OT]] diff --git a/tests/playwright/shiny/shiny-express/folium/app.py b/tests/playwright/shiny/shiny-express/folium/app.py index 15dac58e2..a6d5e8ad1 100644 --- a/tests/playwright/shiny/shiny-express/folium/app.py +++ b/tests/playwright/shiny/shiny-express/folium/app.py @@ -12,7 +12,7 @@ with ui.card(id="card"): "Static Map" - folium.Map( + folium.Map( # pyright: ignore[reportUnknownMemberType,reportGeneralTypeIssues] location=locations_coords["San Francisco"], tiles="USGS.USTopo", zoom_start=12 ) ui.input_radio_buttons( @@ -22,7 +22,7 @@ @render.display def folium_map(): "Map inside of render display call" - folium.Map( + folium.Map( # pyright: ignore[reportUnknownMemberType,reportGeneralTypeIssues] location=locations_coords[input.location()], tiles="cartodb positron", zoom_start=12, diff --git a/tests/playwright/shiny/shiny-express/suspend_display/app.py b/tests/playwright/shiny/shiny-express/suspend_display/app.py index 3daa72125..14baf6df7 100644 --- a/tests/playwright/shiny/shiny-express/suspend_display/app.py +++ b/tests/playwright/shiny/shiny-express/suspend_display/app.py @@ -1,9 +1,7 @@ from shiny import render, ui -from shiny.express import input, layout, suspend_display +from shiny.express import input, suspend_display -layout.set_page(layout.page_fluid()) - -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("s1", "A", 1, 100, 20) @suspend_display diff --git a/tests/pytest/test_reactives.py b/tests/pytest/test_reactives.py index a1590774e..093d7aefd 100644 --- a/tests/pytest/test_reactives.py +++ b/tests/pytest/test_reactives.py @@ -1127,7 +1127,6 @@ async def _(): with pytest.raises(TypeError): # Should complain that @event must be applied before @output. @event(lambda: 1) # pyright: ignore[reportGeneralTypeIssues] - @output @render.text async def _(): ... From 41f44fee1b49ba1600273d82820b9dc9dd82af74 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 15:19:00 -0500 Subject: [PATCH 67/77] Remove comments --- shiny/_utils.py | 3 +-- shiny/render/renderer/_renderer.py | 11 ----------- shiny/render/transformer/_transformer.py | 2 -- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/shiny/_utils.py b/shiny/_utils.py index 23f4a718c..23e67af6e 100644 --- a/shiny/_utils.py +++ b/shiny/_utils.py @@ -245,7 +245,7 @@ async def fn_async(*args: P.args, **kwargs: P.kwargs) -> R: return fn_async -# # TODO-barret; Q: Keep code? +# # TODO-barret-future; Q: Keep code? # class WrapAsync(Generic[P, R]): # """ # Make a function asynchronous. @@ -317,7 +317,6 @@ def is_async_callable( Returns True if `obj` is an `async def` function, or if it's an object with a `__call__` method which is an `async def` function. """ - if inspect.iscoroutinefunction(obj): return True if hasattr(obj, "__call__"): # noqa: B004 diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index ddd0864c1..62d78d403 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -21,13 +21,6 @@ from ..._utils import is_async_callable, wrap_async from ._auto_register import AutoRegisterMixin -# TODO-barret; Q: Should `Renderer.default_ui` be renamed? `ui()`? `express_ui()`? -# TODO-barret; Q: Should `.transform()` exist on `Renderer`? -# * If it is removed, then the `IT` type isn't required in `Renderer` and we could remove RendererBase -# * No confusion between `transform` and `render` methods -# * `render` is the only method that is required to be implemented -# * Can add helper decorator to do typical transform functionality? -# * Con: Would require the `__init__(self, fn: ValueFn[IT])` method to get typing # TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` @@ -89,10 +82,6 @@ DefaultUIFnResult = Union[TagList, Tag, MetadataNode, str] DefaultUIFnResultOrNone = Union[DefaultUIFnResult, None] DefaultUIFn = Callable[[str], DefaultUIFnResultOrNone] -DefaultUIFnImpl = Union[ - DefaultUIFn, - Callable[[Dict[str, object], str], DefaultUIFnResultOrNone], -] ValueFnSync = Callable[[], IT] """ diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 94818a1ad..bd6db7a26 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -2,8 +2,6 @@ # TODO-future; When `OutputRenderer` is removed, remove `output_args()` -# TODO-barret; Q: Why was `DefaultUIFnImpl` being used? The type does NOT make sense. Using `DefaultUIFn` - __all__ = ( "TransformerMetadata", From 212cf4f1eb49bd22009b0ed2e8f6e21d195fafbd Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 8 Jan 2024 16:38:18 -0500 Subject: [PATCH 68/77] Update `Renderer` api-example --- shiny/api-examples/Renderer/app.py | 217 ++++++++++++++--------------- 1 file changed, 104 insertions(+), 113 deletions(-) diff --git a/shiny/api-examples/Renderer/app.py b/shiny/api-examples/Renderer/app.py index bb15ed269..76c347115 100644 --- a/shiny/api-examples/Renderer/app.py +++ b/shiny/api-examples/Renderer/app.py @@ -1,154 +1,140 @@ -# pyright : basic from __future__ import annotations +from typing import Literal + from shiny import App, Inputs, Outputs, Session, ui from shiny.render.renderer import Renderer, ValueFn -# TODO-barret - docs - Update app with docs below +####### +# 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. + """ -class sub_barret_renderer(Renderer[str]): + to_case: Literal["upper", "lower", "ignore"] + """ + The case to render the value in. + """ + placeholder: bool """ - SubBarretSimple - class docs - Render Caps docs + 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, - # Required for no paren usage _fn: ValueFn[str | None] | None = None, *, - a: int = 1, + 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()` """ - SubBarretSimple - init docs - Render Caps docs + 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.a: int = a - self.default_ui = ui.output_text_verbatim + 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 - self.widget = value - return f"{value.upper()}; a={self.a}" + + 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 sub_barret_simple(Renderer[str]): +class render_upper(Renderer[str]): """ - SubBarretSimple - class - Render Caps docs + 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) - def __init__( - self, - _value_fn: ValueFn[str] | None = None, - ): + async def transform(self, value: str) -> str: """ - SubBarretSimple - init - docs here + 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`.) """ - super().__init__(_value_fn) - async def transform(self, value: str) -> str: return str(value).upper() -# # Create renderer components from the async handler function: `capitalize_components()` -# @output_transformer() -# async def CapitalizeTransformer( -# # Contains information about the render call: `name` and `session` -# _meta: TransformerMetadata, -# # The app-supplied output value function -# _fn: ValueFn[str | None], -# *, -# # Extra parameters that app authors can supply to the render decorator -# # (e.g. `@render_capitalize(to="upper")`) -# to: Literal["upper", "lower"] = "upper", -# ) -> str | None: -# # Get the value -# value = await _fn() - -# # Render nothing if `value` is `None` -# if value is None: -# return None - -# if to == "upper": -# return value.upper() -# if to == "lower": -# return value.lower() -# raise ValueError(f"Invalid value for `to`: {to}") - - -# # First, create an overload where users can supply the extra parameters. -# # Example of usage: -# # ``` -# # @output -# # @render_capitalize(to="upper") -# # def value(): -# # return input.caption() -# # ``` -# # Note: Return type is `OutputRendererDecorator` -# @overload -# def render_capitalize( -# *, -# to: Literal["upper", "lower"] = "upper", -# ) -> CapitalizeTransformer.OutputRendererDecorator: -# ... - - -# # Second, create an overload where users are not using parentheses to the method. -# # While it doesn't look necessary, it is needed for the type checker. -# # Example of usage: -# # ``` -# # @output -# # @render_capitalize -# # def value(): -# # return input.caption() -# # ``` -# # Note: `_fn` type is the transformer's `ValueFn` -# # Note: Return type is the transformer's `OutputRenderer` -# @overload -# def render_capitalize( -# _fn: CapitalizeTransformer.ValueFn, -# ) -> CapitalizeTransformer.OutputRenderer: -# ... - - -# # Lastly, implement the renderer. -# # Note: `_fn` type is the transformer's `ValueFn` or `None` -# # Note: Return type is the transformer's `OutputRenderer` or `OutputRendererDecorator` -# def render_capitalize( -# _fn: CapitalizeTransformer.ValueFn | None = None, -# *, -# to: Literal["upper", "lower"] = "upper", -# ) -> ( -# CapitalizeTransformer.OutputRenderer | CapitalizeTransformer.OutputRendererDecorator -# ): -# """ -# OldSchool - CapitalizeTransformer -# """ -# return CapitalizeTransformer( -# _fn, -# CapitalizeTransformer.params(to=to), -# ) +####### +# End of package author code +####### ####### -# End of package author code +# Start of app author code ####### -def text_row(id: str): +def text_row(id: str, label: str): return ui.tags.tr( - ui.tags.td(f"{id}:"), + ui.tags.td(f"{label}:"), ui.tags.td(ui.output_text_verbatim(id, placeholder=True)), ) return ui.row( @@ -161,30 +147,35 @@ def text_row(id: str): ui.h1("Capitalization renderer"), ui.input_text("caption", "Caption:", "Data summary"), ui.tags.table( - text_row("barret_sub_simple_no_paren"), - text_row("barret_sub_simple_paren"), + text_row("upper", "@render_upper"), + text_row("upper_with_paren", "@render_upper()"), # - text_row("barret_sub_renderer_no_paren"), - text_row("barret_sub_renderer_paren"), + text_row("cap_upper", "@render_capitalize"), + text_row("cap_lower", "@render_capitalize(to='lower')"), ), ) def server(input: Inputs, output: Outputs, session: Session): - @sub_barret_simple - def barret_sub_simple_no_paren(): + # Hovering over `@render_upper` will display the class documentation + @render_upper + def upper(): return input.caption() - @sub_barret_simple() - def barret_sub_simple_paren() -> str: + # Hovering over `@render_upper` will display the class documentation as there is no + # `__init__()` documentation + @render_upper() + def upper_with_paren(): return input.caption() - @sub_barret_renderer - def barret_sub_renderer_no_paren(): + # Hovering over `@render_capitalize` will display the class documentation + @render_capitalize + def cap_upper(): return input.caption() - @sub_barret_renderer(a=2) - def barret_sub_renderer_paren(): + # Hovering over `@render_capitalize` will display the `__init__()` documentation + @render_capitalize(to_case="lower") + def cap_lower(): return input.caption() From 533f0f93eef6a6d2e2d5d400dd0c734feffe6d92 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 9 Jan 2024 16:34:01 -0500 Subject: [PATCH 69/77] Update comments --- shiny/api-examples/output_transformer/app.py | 4 ++++ shiny/express/_output.py | 2 +- shiny/render/renderer/_renderer.py | 16 ++++++++-------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/shiny/api-examples/output_transformer/app.py b/shiny/api-examples/output_transformer/app.py index 568b102b2..b707fcb8e 100644 --- a/shiny/api-examples/output_transformer/app.py +++ b/shiny/api-examples/output_transformer/app.py @@ -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. # diff --git a/shiny/express/_output.py b/shiny/express/_output.py index 5a3f5ccc9..6ab1362d8 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -21,7 +21,7 @@ CallableT = TypeVar("CallableT", bound=Callable[..., object]) -# TODO-barret; quartodoc entry? +# TODO-barret-future; quartodoc entry? def ui_kwargs( **kwargs: object, ) -> Callable[[RendererBaseT], RendererBaseT]: diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 62d78d403..b68e19bfe 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -21,7 +21,7 @@ from ..._utils import is_async_callable, wrap_async from ._auto_register import AutoRegisterMixin -# TODO-barret; POST-merge; shinywidgets should not call `resolve_value_fn` +# TODO-barret; POST-merge; Update shinywidgets # TODO-future: docs; missing first paragraph from some classes: Example: TransformerMetadata. @@ -105,7 +105,7 @@ class RendererBase(AutoRegisterMixin, ABC): """ Base class for all renderers. - TODO-barret - docs + TODO-barret-docs """ __name__: str @@ -262,7 +262,7 @@ class Renderer(RendererBase, Generic[IT]): """ Renderer cls docs here - TODO-barret - docs + TODO-barret-docs """ value_fn: AsyncValueFn[IT | None] @@ -276,7 +276,7 @@ def __call__(self, value_fn: ValueFnApp[IT | None]) -> Self: """ Renderer __call__ docs here; Sets app's value function - TODO-barret - docs + TODO-barret-docs """ if not callable(value_fn): @@ -312,20 +312,20 @@ async def transform(self, value: IT) -> JSONifiable: """ Renderer - transform docs here - TODO-barret - docs + TODO-barret-docs """ 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 a value is retrieved and utilized. For full control, use this method. (rare)" - "\n By default, the `render` retrieves the value and then calls `transform` method on non-`None` values." + "* `render(self)` method has full control of how an App author's value is retrieved (`self.value_fn()`) and utilized. (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 - TODO-barret - docs + TODO-barret-docs """ value = await self.value_fn() if value is None: From 19c2ea4f871c3f501a36588410843393d12a72a8 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 9 Jan 2024 16:35:16 -0500 Subject: [PATCH 70/77] Update to latest shiny bindings pkgs --- .../custom_component/custom_component.py | 43 +++++++++++-------- .../js-output/package-lock.json | 8 ++-- .../package-templates/js-output/package.json | 2 +- .../js-output/srcts/index.ts | 5 ++- .../custom_component/custom_component.py | 41 +++++++++--------- .../js-react/example-app/app.py | 10 +++-- .../js-react/package-lock.json | 14 +++--- .../package-templates/js-react/package.json | 2 +- .../js-react/srcts/index.tsx | 10 ++--- 9 files changed, 73 insertions(+), 62 deletions(-) diff --git a/shiny/templates/package-templates/js-output/custom_component/custom_component.py b/shiny/templates/package-templates/js-output/custom_component/custom_component.py index d2f9fda07..1d7397c8a 100644 --- a/shiny/templates/package-templates/js-output/custom_component/custom_component.py +++ b/shiny/templates/package-templates/js-output/custom_component/custom_component.py @@ -4,7 +4,7 @@ from htmltools import HTMLDependency, Tag from shiny.module import resolve_id -from shiny.render.transformer import TransformerMetadata, ValueFn, output_transformer +from shiny.render.renderer import JSONifiable, Renderer, ValueFn # This object is used to let Shiny know where the dependencies needed to run # our component all live. In this case, we're just using a single javascript @@ -20,28 +20,35 @@ ) -@output_transformer() -async def render_custom_component( - _meta: TransformerMetadata, - _fn: ValueFn[int | None], -): - res = await _fn() - if res is None: - return None +class render_custom_component(Renderer[int]): + """ + Render a value in a custom component. + """ - if not isinstance(res, int): - # Throw an error if the value is not an integer. - raise TypeError(f"Expected a integer, got {type(res)}. ") + # The UI used within Shiny Express mode + def default_ui(self, id: str) -> Tag: + return custom_component(id, height=self.height) - # Send the results to the client. Make sure that this is a serializable - # object and matches what is expected in the javascript code. - return {"value": res} + # The init method is used to set up the renderer's parameters. + # If no parameters are needed, then the `__init__()` method can be omitted. + def __init__(self, _value_fn: ValueFn[int] = None, *, height: str = "200px"): + super().__init__(_value_fn) + self.height: str = height + # Transforms non-`None` values into a JSONifiable object. + # If you'd like more control on when and how the value is resolved, + # please use the `async def resolve(self)` method. + async def transform(self, value: int) -> JSONifiable: + # Send the results to the client. Make sure that this is a serializable + # object and matches what is expected in the javascript code. + return {"value": int(value)} -def custom_component(id: str, height: str = "200px"): + +def custom_component(id: str, height: str = "200px") -> Tag: """ - A shiny output. To be paired with - `render_custom_component` decorator. + A shiny UI output. + + To be paired with `render_custom_component` decorator within the Shiny server. """ return Tag( "custom-component", diff --git a/shiny/templates/package-templates/js-output/package-lock.json b/shiny/templates/package-templates/js-output/package-lock.json index b14fe37f3..3f87e6495 100644 --- a/shiny/templates/package-templates/js-output/package-lock.json +++ b/shiny/templates/package-templates/js-output/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@posit-dev/shiny-bindings-core": "^0.0.3", + "@posit-dev/shiny-bindings-core": "^0.1.0", "lit": "^3.0.2" }, "devDependencies": { @@ -383,9 +383,9 @@ } }, "node_modules/@posit-dev/shiny-bindings-core": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@posit-dev/shiny-bindings-core/-/shiny-bindings-core-0.0.3.tgz", - "integrity": "sha512-G4Zd916Y9YkvuQHRJtRceQBwJD51pBsEyYZFpkIwHiyR56nGGbX0POqHSE39ZQMxa+ewhiBhd4FvK5RgGOoVCA==" + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@posit-dev/shiny-bindings-core/-/shiny-bindings-core-0.1.0.tgz", + "integrity": "sha512-va6csvzr1XTyaaW15Ak6aNhv9Lp/iQ6gxSRCqnRjkFeAjJkUXmlYV6nYAnywegkbnGfhqDkft7AXQK8POZAuWA==" }, "node_modules/@types/trusted-types": { "version": "2.0.7", diff --git a/shiny/templates/package-templates/js-output/package.json b/shiny/templates/package-templates/js-output/package.json index a3cf5f842..bd3bb563e 100644 --- a/shiny/templates/package-templates/js-output/package.json +++ b/shiny/templates/package-templates/js-output/package.json @@ -14,7 +14,7 @@ "typescript": "^5.2.2" }, "dependencies": { - "@posit-dev/shiny-bindings-core": "^0.0.3", + "@posit-dev/shiny-bindings-core": "^0.1.0", "lit": "^3.0.2" } } diff --git a/shiny/templates/package-templates/js-output/srcts/index.ts b/shiny/templates/package-templates/js-output/srcts/index.ts index 6e0c00b97..7a2acd0cc 100644 --- a/shiny/templates/package-templates/js-output/srcts/index.ts +++ b/shiny/templates/package-templates/js-output/srcts/index.ts @@ -1,7 +1,7 @@ import { LitElement, html, css } from "lit"; import { property } from "lit/decorators.js"; -import { makeOutputBinding } from "@posit-dev/shiny-bindings-core"; +import { makeOutputBindingWebComponent } from "@posit-dev/shiny-bindings-core"; // What the server-side output binding will send to the client. It's important // to make sure this matches what the python code is sending. @@ -41,4 +41,5 @@ export class CustomComponentEl extends LitElement { } // Setup output binding. This also registers the custom element. -makeOutputBinding("custom-component", CustomComponentEl); + +makeOutputBindingWebComponent("custom-component", CustomComponentEl); diff --git a/shiny/templates/package-templates/js-react/custom_component/custom_component.py b/shiny/templates/package-templates/js-react/custom_component/custom_component.py index 91529e1f6..4c479b760 100644 --- a/shiny/templates/package-templates/js-react/custom_component/custom_component.py +++ b/shiny/templates/package-templates/js-react/custom_component/custom_component.py @@ -3,12 +3,7 @@ from htmltools import HTMLDependency, Tag from shiny.module import resolve_id -from shiny.render.transformer import ( - TransformerMetadata, - ValueFn, - output_transformer, - resolve_value_fn, -) +from shiny.render.renderer import JSONifiable, Renderer # This object is used to let Shiny know where the dependencies needed to run # our component all live. In this case, we're just using a single javascript @@ -38,24 +33,28 @@ def input_custom_component(id: str): # Output component +class render_custom_component(Renderer[str]): + """ + Render a value in a custom component. + """ + # The UI used within Shiny Express mode + def default_ui(self, id: str) -> Tag: + return output_custom_component(id) -@output_transformer() -async def render_custom_component( - _meta: TransformerMetadata, - _fn: ValueFn[str | None], -): - res = await resolve_value_fn(_fn) - if res is None: - return None - - if not isinstance(res, str): - # Throw an error if the value is not a string - raise TypeError(f"Expected a string, got {type(res)}. ") + # # There are no parameters being supplied to the `output_custom_component` rendering function. + # # Therefore, we can omit the `__init__()` method. + # def __init__(self, _value_fn: ValueFn[int] = None, *, extra_arg: str = "bar"): + # super().__init__(_value_fn) + # self.extra_arg: str = extra_arg - # Send the results to the client. Make sure that this is a serializable - # object and matches what is expected in the javascript code. - return {"value": res} + # Transforms non-`None` values into a JSONifiable object. + # If you'd like more control on when and how the value is resolved, + # please use the `async def resolve(self)` method. + async def transform(self, value: str) -> JSONifiable: + # Send the results to the client. Make sure that this is a serializable + # object and matches what is expected in the javascript code. + return {"value": str(value)} def output_custom_component(id: str): diff --git a/shiny/templates/package-templates/js-react/example-app/app.py b/shiny/templates/package-templates/js-react/example-app/app.py index a87660408..a2464da98 100644 --- a/shiny/templates/package-templates/js-react/example-app/app.py +++ b/shiny/templates/package-templates/js-react/example-app/app.py @@ -9,15 +9,19 @@ from shiny import App, ui app_ui = ui.page_fluid( + ui.h2("Color picker"), input_custom_component("color"), - output_custom_component("valueOut"), + ui.br(), + ui.h2("Output color"), + output_custom_component("value"), ) def server(input, output, session): @render_custom_component - def valueOut(): + def value(): + print("Calculating value") return input.color() -app = App(app_ui, server) +app = App(app_ui, server, debug=True) diff --git a/shiny/templates/package-templates/js-react/package-lock.json b/shiny/templates/package-templates/js-react/package-lock.json index dc7eb9191..f42f1b17a 100644 --- a/shiny/templates/package-templates/js-react/package-lock.json +++ b/shiny/templates/package-templates/js-react/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@posit-dev/shiny-bindings-react": "^0.0.3", + "@posit-dev/shiny-bindings-react": "^0.1.0", "react": "^18.2.0", "react-color": "^2.19.3", "react-dom": "^18.2.0" @@ -382,14 +382,14 @@ } }, "node_modules/@posit-dev/shiny-bindings-core": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@posit-dev/shiny-bindings-core/-/shiny-bindings-core-0.0.2.tgz", - "integrity": "sha512-uJ1cUAjtIZVFqU7bXqjZm8HX72FrM3BVfCtReppTUrtqE2SJnOQNlUZpc+xkV+3WkPeqnpn0NS7SH880mkcrPQ==" + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@posit-dev/shiny-bindings-core/-/shiny-bindings-core-0.1.0.tgz", + "integrity": "sha512-va6csvzr1XTyaaW15Ak6aNhv9Lp/iQ6gxSRCqnRjkFeAjJkUXmlYV6nYAnywegkbnGfhqDkft7AXQK8POZAuWA==" }, "node_modules/@posit-dev/shiny-bindings-react": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@posit-dev/shiny-bindings-react/-/shiny-bindings-react-0.0.3.tgz", - "integrity": "sha512-zarfRZ3/dUFBf11Vc2fUM4grpp6xaJQAkqZRj9W/Xbtax3LJ4PElzPzjZicN9PdDBL72cux8XJ+U+0G3b6G8Nw==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@posit-dev/shiny-bindings-react/-/shiny-bindings-react-0.1.0.tgz", + "integrity": "sha512-zmGY/H8aLVODr2NiaqUBgB+YJURtiUHqY8+x/QRlcmD9T3eWrdVboNd8PdZJyX9IO3qDJUSEq01npuGzdEFOQA==", "dependencies": { "@posit-dev/shiny-bindings-core": "*", "@types/react": "^18.2.38", diff --git a/shiny/templates/package-templates/js-react/package.json b/shiny/templates/package-templates/js-react/package.json index 6ac2fbb2c..2af72278a 100644 --- a/shiny/templates/package-templates/js-react/package.json +++ b/shiny/templates/package-templates/js-react/package.json @@ -16,7 +16,7 @@ "typescript": "^5.2.2" }, "dependencies": { - "@posit-dev/shiny-bindings-react": "^0.0.3", + "@posit-dev/shiny-bindings-react": "^0.1.0", "react": "^18.2.0", "react-color": "^2.19.3", "react-dom": "^18.2.0" diff --git a/shiny/templates/package-templates/js-react/srcts/index.tsx b/shiny/templates/package-templates/js-react/srcts/index.tsx index 1b35e7257..3d6fc11c0 100644 --- a/shiny/templates/package-templates/js-react/srcts/index.tsx +++ b/shiny/templates/package-templates/js-react/srcts/index.tsx @@ -11,10 +11,10 @@ import { makeReactInput({ name: "custom-component-input", initialValue: "#fff", - renderComp: ({ initialValue, onNewValue }) => ( + renderComp: ({ initialValue, updateValue }) => ( onNewValue(color)} + updateValue={(color) => updateValue(color)} /> ), }); @@ -22,10 +22,10 @@ makeReactInput({ // Color Picker React component function ColorPickerReact({ initialValue, - onNewValue, + updateValue, }: { initialValue: string; - onNewValue: (x: string) => void; + updateValue: (x: string) => void; }) { const [currentColor, setCurrentColor] = React.useState(initialValue); @@ -34,7 +34,7 @@ function ColorPickerReact({ color={currentColor} onChange={(color) => { setCurrentColor(color.hex); - onNewValue(color.hex); + updateValue(color.hex); }} /> ); From bc65c4da623c2c94017f99478e6459b039ae7a1e Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 9 Jan 2024 16:35:39 -0500 Subject: [PATCH 71/77] Use `selector` for workaround Related: https://github.com/posit-dev/shiny-bindings/issues/4 --- shiny/templates/package-templates/js-react/srcts/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shiny/templates/package-templates/js-react/srcts/index.tsx b/shiny/templates/package-templates/js-react/srcts/index.tsx index 3d6fc11c0..88f0b7095 100644 --- a/shiny/templates/package-templates/js-react/srcts/index.tsx +++ b/shiny/templates/package-templates/js-react/srcts/index.tsx @@ -10,6 +10,7 @@ import { // into the root of the webcomponent. makeReactInput({ name: "custom-component-input", + selector: "custom-component-input", initialValue: "#fff", renderComp: ({ initialValue, updateValue }) => ( ({ name: "custom-component-output", + selector: "custom-component-output", renderComp: ({ value }) => (
Date: Tue, 9 Jan 2024 16:51:45 -0500 Subject: [PATCH 72/77] Move mixin back in main class. Move internal variable up in definition --- shiny/render/renderer/_auto_register.py | 47 --------------------- shiny/render/renderer/_renderer.py | 54 +++++++++++++++++++++---- 2 files changed, 47 insertions(+), 54 deletions(-) delete mode 100644 shiny/render/renderer/_auto_register.py diff --git a/shiny/render/renderer/_auto_register.py b/shiny/render/renderer/_auto_register.py deleted file mode 100644 index dda732503..000000000 --- a/shiny/render/renderer/_auto_register.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -from typing import Protocol, cast - -# from ...session import require_active_session - - -class AutoRegisterP(Protocol): - __name__: str - _auto_registered: bool = False - - -class AutoRegisterMixin: - """ - 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()`. - """ - - _auto_registered: bool = False - - def _on_register(self: AutoRegisterP) -> None: - if self._auto_registered: - # We're being explicitly registered now. Undo the auto-registration. - # (w/ module support) - from ...session import require_active_session - - session = require_active_session(None) - ns_name = session.output._ns(self.__name__) - session.output.remove(ns_name) - self._auto_registered = False - - def _auto_register(self) -> None: - # If in Express mode, register the output - if not self._auto_registered: - from ...session import get_current_session - - s = get_current_session() - if s is not None: - from ._renderer import RendererBase - - # Cast to avoid circular import as this mixin is ONLY used within RendererBase - renderer_self = cast(RendererBase, self) - s.output(renderer_self) - # We mark the fact that we're auto-registered so that, if an explicit - # registration now occurs, we can undo this auto-registration. - self._auto_registered = True diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index b68e19bfe..04e3a5818 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -101,13 +101,19 @@ ValueFn = Optional[ValueFnApp[Union[IT, None]]] -class RendererBase(AutoRegisterMixin, ABC): +class RendererBase(ABC): """ Base class for all renderers. TODO-barret-docs """ + # Q: Could we do this with typing without putting `P` in the Generic? + # A: No. Even if we had a `P` in the Generic, the calling decorator would not have access to it. + # Idea: Possibly use a chained method of `.ui_kwargs()`? https://github.com/posit-dev/py-shiny/issues/971 + _default_ui_kwargs: dict[str, Any] = dict() + # _default_ui_args: tuple[Any, ...] = tuple() + __name__: str """ Name of output function supplied. (The value will not contain any module prefix.) @@ -152,13 +158,11 @@ async def render(self) -> JSONifiable: def __init__(self) -> None: super().__init__() + self._auto_registered: bool = False - # Q: Could we do this with typing without putting `P` in the Generic? - # A: No. Even if we had a `P` in the Generic, the calling decorator would not have access to it. - # Idea: Possibly use a chained method of `.ui_kwargs()`? https://github.com/posit-dev/py-shiny/issues/971 - _default_ui_kwargs: dict[str, Any] = dict() - # _default_ui_args: tuple[Any, ...] = tuple() - + # ###### + # Tagify-like methods + # ###### def _repr_html_(self) -> str | None: rendered_ui = self._render_default_ui() if rendered_ui is None: @@ -181,6 +185,42 @@ def _render_default_ui(self) -> DefaultUIFnResultOrNone: **self._default_ui_kwargs, ) + # ###### + # 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: + # We're being explicitly registered now. Undo the auto-registration. + # (w/ module support) + from ...session import require_active_session + + session = require_active_session(None) + ns_name = session.output._ns(self.__name__) + session.output.remove(ns_name) + self._auto_registered = False + + def _auto_register(self) -> None: + # If in Express mode, register the output + if not self._auto_registered: + from ...session import get_current_session + + s = get_current_session() + if s is not None: + from ._renderer import RendererBase + + # Cast to avoid circular import as this mixin is ONLY used within RendererBase + renderer_self = cast(RendererBase, self) + s.output(renderer_self) + # We mark the fact that we're auto-registered so that, if an explicit + # registration now occurs, we can undo this auto-registration. + 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]):` From f7720c6a8576185e0d1a162135e85a0c907d5f25 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 9 Jan 2024 16:51:47 -0500 Subject: [PATCH 73/77] Update CHANGELOG.md --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efb940baa..9065dbd32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,15 +18,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`) you expect to be returned from the App author. (#964) +* 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.output_transformer`. (#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. (#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 From 075c0d649b3646810cac6d2d61c12aab8446ce28 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 9 Jan 2024 16:53:34 -0500 Subject: [PATCH 74/77] `Renderer.output_name` -> `Renderer.output_id` --- shiny/render/_render.py | 2 +- shiny/render/renderer/_renderer.py | 12 ++---------- shiny/render/transformer/_transformer.py | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 60cb3fbb8..17ff02061 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -180,7 +180,7 @@ def __init__( async def render(self) -> dict[str, JSONifiable] | JSONifiable | None: is_userfn_async = self.value_fn.is_async - name = self.output_name + name = self.output_id session = require_active_session(None) width = self.width height = self.height diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 04e3a5818..ae8985d07 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -122,7 +122,7 @@ class RendererBase(ABC): """ # Meta - output_name: str + output_id: str """ Output function name or ID (provided to `@output(id=)`). This value will contain any module prefix. @@ -142,7 +142,7 @@ def _set_output_metadata( output_name : str Output function name or ID (provided to `@output(id=)`). This value will contain any module prefix. """ - self.output_name = output_name + self.output_id = output_name def default_ui( self, @@ -290,14 +290,6 @@ def sync_fn(self) -> Callable[[], IT]: return sync_fn -# class RendererShim(RendererBase, Generic[IT, P]): -# def default_ui( -# self, id: str, *args: P.args, **kwargs: P.kwargs -# ) -> DefaultUIFnResultOrNone: -# return super().default_ui(id) - - -# class Renderer(RendererShim[IT, ...], Generic[IT]): class Renderer(RendererBase, Generic[IT]): """ Renderer cls docs here diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index bd6db7a26..c2a49ce86 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -284,7 +284,7 @@ def _meta(self) -> TransformerMetadata: session = require_active_session(None) return TransformerMetadata( session=session, - name=self.output_name, + name=self.output_id, ) async def _run(self) -> OT: From 3e49d50e6e3c9502f31702ef5afc8a2ff7467d22 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 9 Jan 2024 16:57:13 -0500 Subject: [PATCH 75/77] Remove `@property` from `AsyncValueFn` class --- shiny/render/_display.py | 7 ++++--- shiny/render/_render.py | 2 +- shiny/render/renderer/_renderer.py | 8 ++------ shiny/render/transformer/_transformer.py | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/shiny/render/_display.py b/shiny/render/_display.py index 8c033883b..b91eaac2f 100644 --- a/shiny/render/_display.py +++ b/shiny/render/_display.py @@ -45,7 +45,7 @@ def __call__(self, fn: ValueFn[None]) -> Self: raise TypeError("@render.display requires a function when called") async_fn = AsyncValueFn(fn) - if async_fn.is_async: + if async_fn.is_async(): raise TypeError( "@render.display does not support async functions. Use @render.ui instead." ) @@ -83,14 +83,15 @@ async def render(self) -> JSONifiable_dict | None: orig_displayhook = sys.displayhook sys.displayhook = wrap_displayhook_handler(results.append) - if self.value_fn.is_async: + if self.value_fn.is_async(): raise TypeError( "@render.display does not support async functions. Use @render.ui instead." ) try: # Run synchronously - ret = self.value_fn.sync_fn() + sync_value_fn = self.value_fn.get_sync_fn() + ret = sync_value_fn() if ret is not None: raise RuntimeError( "@render.display functions should not return values. (`None` is allowed)." diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 17ff02061..403a7f989 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -179,7 +179,7 @@ def __init__( self.kwargs = kwargs async def render(self) -> dict[str, JSONifiable] | JSONifiable | None: - is_userfn_async = self.value_fn.is_async + is_userfn_async = self.value_fn.is_async() name = self.output_id session = require_active_session(None) width = self.width diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index ae8985d07..6af1ca244 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -19,7 +19,6 @@ from ..._typing_extensions import Self from ..._utils import is_async_callable, wrap_async -from ._auto_register import AutoRegisterMixin # TODO-barret; POST-merge; Update shinywidgets @@ -246,7 +245,6 @@ async def __call__(self) -> IT: """ return await self._fn() - @property def is_async(self) -> bool: """ Was the original function asynchronous? @@ -258,8 +256,7 @@ def is_async(self) -> bool: """ return self._is_async - @property - def async_fn(self) -> Callable[[], Awaitable[IT]]: + def get_async_fn(self) -> Callable[[], Awaitable[IT]]: """ Return the async value function. @@ -270,8 +267,7 @@ def async_fn(self) -> Callable[[], Awaitable[IT]]: """ return self._fn - @property - def sync_fn(self) -> Callable[[], IT]: + def get_sync_fn(self) -> Callable[[], IT]: """ Retrieve the original, synchronous value function function. diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index c2a49ce86..6b340932c 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -260,7 +260,7 @@ def __init__( # -> It is faster to always call an async function than to always check if it is async # Always being async simplifies the execution self._value_fn = AsyncValueFn(value_fn) - self._value_fn_is_async = self._value_fn.is_async # legacy key + self._value_fn_is_async = self._value_fn.is_async() # legacy key self.__name__ = value_fn.__name__ self._transformer = transform_fn From 1755fdf6ff58e5b7ed481a89d3cf0dfcfcf5346a Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 9 Jan 2024 17:00:07 -0500 Subject: [PATCH 76/77] `JSONifiable` -> `Jsonifiable` --- docs/_quartodoc.yml | 2 +- shiny/api-examples/Renderer/app.py | 2 +- shiny/render/_dataframe.py | 10 +++++----- shiny/render/_display.py | 4 ++-- shiny/render/_render.py | 14 +++++++------- shiny/render/renderer/__init__.py | 4 ++-- shiny/render/renderer/_renderer.py | 18 +++++++++--------- shiny/render/renderer/_utils.py | 12 ++++++------ shiny/render/transformer/_transformer.py | 10 +++++----- shiny/session/_session.py | 4 ++-- .../custom_component/custom_component.py | 6 +++--- .../custom_component/custom_component.py | 6 +++--- 12 files changed, 46 insertions(+), 46 deletions(-) diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index b7c903ede..967abf63c 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -177,7 +177,7 @@ quartodoc: contents: - render.renderer.Renderer - render.renderer.RendererBase - - render.renderer.JSONifiable + - render.renderer.Jsonifiable - render.renderer.ValueFn - render.renderer.AsyncValueFn - title: Reactive programming diff --git a/shiny/api-examples/Renderer/app.py b/shiny/api-examples/Renderer/app.py index 76c347115..39caefc31 100644 --- a/shiny/api-examples/Renderer/app.py +++ b/shiny/api-examples/Renderer/app.py @@ -116,7 +116,7 @@ async def transform(self, value: str) -> str: Returns ------- str - The transformed value. (Must be a subset of `JSONifiable`.) + The transformed value. (Must be a subset of `Jsonifiable`.) """ return str(value).upper() diff --git a/shiny/render/_dataframe.py b/shiny/render/_dataframe.py index 255f1d39c..246aab89f 100644 --- a/shiny/render/_dataframe.py +++ b/shiny/render/_dataframe.py @@ -9,7 +9,7 @@ from .. import ui from .._docstring import add_example from ._dataframe_unsafe import serialize_numpy_dtypes -from .renderer import JSONifiable, Renderer +from .renderer import Jsonifiable, Renderer if TYPE_CHECKING: import pandas as pd @@ -17,7 +17,7 @@ class AbstractTabularData(abc.ABC): @abc.abstractmethod - def to_payload(self) -> JSONifiable: + def to_payload(self) -> Jsonifiable: ... @@ -94,7 +94,7 @@ def __init__( self.filters = filters self.row_selection_mode = row_selection_mode - def to_payload(self) -> JSONifiable: + def to_payload(self) -> Jsonifiable: res = serialize_pandas_df(self.data) res["options"] = dict( width=self.width, @@ -182,7 +182,7 @@ def __init__( self.filters = filters self.row_selection_mode = row_selection_mode - def to_payload(self) -> JSONifiable: + def to_payload(self) -> Jsonifiable: res = serialize_pandas_df(self.data) res["options"] = dict( width=self.width, @@ -259,7 +259,7 @@ class data_frame(Renderer[DataFrameResult]): def default_ui(self, id: str) -> Tag: return ui.output_data_frame(id=id) - async def transform(self, value: DataFrameResult) -> JSONifiable: + async def transform(self, value: DataFrameResult) -> Jsonifiable: if not isinstance(value, AbstractTabularData): value = DataGrid( cast_to_pandas( diff --git a/shiny/render/_display.py b/shiny/render/_display.py index b91eaac2f..ca19efd2f 100644 --- a/shiny/render/_display.py +++ b/shiny/render/_display.py @@ -11,7 +11,7 @@ from ..types import MISSING, MISSING_TYPE from .renderer import AsyncValueFn, Renderer, ValueFn from .renderer._utils import ( - JSONifiable_dict, + JsonifiableDict, rendered_deps_to_jsonifiable, set_kwargs_value, ) @@ -78,7 +78,7 @@ def __init__( self.fillable: bool = fillable self.kwargs: dict[str, TagAttrValue] = kwargs - async def render(self) -> JSONifiable_dict | None: + async def render(self) -> JsonifiableDict | None: results: list[object] = [] orig_displayhook = sys.displayhook sys.displayhook = wrap_displayhook_handler(results.append) diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 403a7f989..e574535f2 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -34,7 +34,7 @@ try_render_pil, try_render_plotnine, ) -from .renderer import JSONifiable, Renderer, ValueFn +from .renderer import Jsonifiable, Renderer, ValueFn from .renderer._utils import ( imgdata_to_jsonifiable, rendered_deps_to_jsonifiable, @@ -78,7 +78,7 @@ def default_ui(self, id: str, placeholder: bool | MISSING_TYPE = MISSING) -> Tag set_kwargs_value(kwargs, "placeholder", placeholder, None) return _ui.output_text_verbatim(id, **kwargs) - async def transform(self, value: str) -> JSONifiable: + async def transform(self, value: str) -> Jsonifiable: return str(value) @@ -178,7 +178,7 @@ def __init__( self.height = height self.kwargs = kwargs - async def render(self) -> dict[str, JSONifiable] | JSONifiable | None: + async def render(self) -> dict[str, Jsonifiable] | Jsonifiable | None: is_userfn_async = self.value_fn.is_async() name = self.output_id session = require_active_session(None) @@ -239,7 +239,7 @@ def container_size(dimension: Literal["width", "height"]) -> float: ok: bool result: ImgData | None - def cast_result(result: ImgData | None) -> dict[str, JSONifiable] | None: + def cast_result(result: ImgData | None) -> dict[str, Jsonifiable] | None: if result is None: return None return imgdata_to_jsonifiable(result) @@ -332,7 +332,7 @@ def __init__( super().__init__(fn) self.delete_file: bool = delete_file - async def transform(self, value: ImgData) -> dict[str, JSONifiable] | None: + async def transform(self, value: ImgData) -> dict[str, Jsonifiable] | None: src: str = value.get("src") try: with open(src, "rb") as f: @@ -426,7 +426,7 @@ def __init__( self.border: int = border self.kwargs: dict[str, object] = kwargs - async def transform(self, value: TableResult) -> dict[str, JSONifiable]: + async def transform(self, value: TableResult) -> dict[str, Jsonifiable]: import pandas import pandas.io.formats.style @@ -487,7 +487,7 @@ class ui(Renderer[TagChild]): def default_ui(self, id: str) -> Tag: return _ui.output_ui(id) - async def transform(self, value: TagChild) -> JSONifiable: + async def transform(self, value: TagChild) -> Jsonifiable: session = require_active_session(None) return rendered_deps_to_jsonifiable( session._process_ui(value), diff --git a/shiny/render/renderer/__init__.py b/shiny/render/renderer/__init__.py index ebe018f88..2afc8fe4e 100644 --- a/shiny/render/renderer/__init__.py +++ b/shiny/render/renderer/__init__.py @@ -2,7 +2,7 @@ RendererBase, Renderer, ValueFn, - JSONifiable, + Jsonifiable, RendererBaseT, # pyright: ignore[reportUnusedImport] ValueFnApp, # pyright: ignore[reportUnusedImport] ValueFnSync, # pyright: ignore[reportUnusedImport] @@ -16,6 +16,6 @@ "RendererBase", "Renderer", "ValueFn", - "JSONifiable", + "Jsonifiable", "AsyncValueFn", ) diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index 6af1ca244..ade9db1cf 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -33,7 +33,7 @@ "Renderer", "RendererBase", "ValueFn", - "JSONifiable", + "Jsonifiable", "AsyncValueFn", ) @@ -66,15 +66,15 @@ # +-------------------+---------------+ # | None | null | # +-------------------+---------------+ -JSONifiable = Union[ +Jsonifiable = Union[ str, int, float, bool, None, - List["JSONifiable"], - Tuple["JSONifiable"], - Dict[str, "JSONifiable"], + List["Jsonifiable"], + Tuple["Jsonifiable"], + Dict[str, "Jsonifiable"], ] @@ -152,7 +152,7 @@ def default_ui( return None @abstractmethod - async def render(self) -> JSONifiable: + async def render(self) -> Jsonifiable: ... def __init__(self) -> None: @@ -336,7 +336,7 @@ def __init__( # Register the value function self(value_fn) - async def transform(self, value: IT) -> JSONifiable: + async def transform(self, value: IT) -> Jsonifiable: """ Renderer - transform docs here @@ -344,12 +344,12 @@ async def transform(self, value: IT) -> JSONifiable: """ 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" + "* `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.value_fn()`) and utilized. (rare)\n" "By default, the `render` retrieves the value and then calls `transform` method on non-`None` values." ) - async def render(self) -> JSONifiable: + async def render(self) -> Jsonifiable: """ Renderer - render docs here diff --git a/shiny/render/renderer/_utils.py b/shiny/render/renderer/_utils.py index 2b6b846a8..8bed1c415 100644 --- a/shiny/render/renderer/_utils.py +++ b/shiny/render/renderer/_utils.py @@ -6,17 +6,17 @@ from ...session._utils import RenderedDeps from ...types import MISSING_TYPE, ImgData -from ._renderer import JSONifiable +from ._renderer import Jsonifiable -JSONifiable_dict = Dict[str, JSONifiable] +JsonifiableDict = Dict[str, Jsonifiable] -def rendered_deps_to_jsonifiable(rendered_deps: RenderedDeps) -> JSONifiable_dict: - return cast(JSONifiable_dict, dict(rendered_deps)) +def rendered_deps_to_jsonifiable(rendered_deps: RenderedDeps) -> JsonifiableDict: + return cast(JsonifiableDict, dict(rendered_deps)) -def imgdata_to_jsonifiable(imgdata: ImgData) -> JSONifiable_dict: - return cast(JSONifiable_dict, dict(imgdata)) +def imgdata_to_jsonifiable(imgdata: ImgData) -> JsonifiableDict: + return cast(JsonifiableDict, dict(imgdata)) def set_kwargs_value( diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 6b340932c..157452fbc 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -31,7 +31,7 @@ overload, ) -from ..renderer import AsyncValueFn, JSONifiable, RendererBase +from ..renderer import AsyncValueFn, Jsonifiable, RendererBase from ..renderer._renderer import DefaultUIFn, DefaultUIFnResultOrNone if TYPE_CHECKING: @@ -333,12 +333,12 @@ def default_ui( return self._default_ui(id, *self._default_ui_args, **kwargs) - async def render(self) -> JSONifiable: + async def render(self) -> Jsonifiable: ret = await self._run() - # Really, OT should be bound by JSONifiable. - # But we can't do that now as types like TypedDict break on JSONifiable + # Really, OT should be bound by Jsonifiable. + # But we can't do that now as types like TypedDict break on Jsonifiable # (We also don't really care as we're moving to `Renderer` class) - jsonifiable_ret = cast(JSONifiable, ret) + jsonifiable_ret = cast(Jsonifiable, ret) return jsonifiable_ret diff --git a/shiny/session/_session.py b/shiny/session/_session.py index 8390fbd09..79ffdd8b7 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -48,7 +48,7 @@ from ..input_handler import input_handlers from ..reactive import Effect_, Value, effect, flush, isolate from ..reactive._core import lock, on_flushed -from ..render.renderer import JSONifiable, RendererBase, RendererBaseT +from ..render.renderer import Jsonifiable, RendererBase, RendererBaseT from ..types import SafeException, SilentCancelOutputException, SilentException from ._utils import RenderedDeps, read_thunk_opt, session_context @@ -1017,7 +1017,7 @@ async def output_obs(): {"recalculating": {"name": output_name, "status": "recalculating"}} ) - message: dict[str, JSONifiable] = {} + message: dict[str, Jsonifiable] = {} try: message[output_name] = await renderer.render() except SilentCancelOutputException: diff --git a/shiny/templates/package-templates/js-output/custom_component/custom_component.py b/shiny/templates/package-templates/js-output/custom_component/custom_component.py index 1d7397c8a..a19e41ed5 100644 --- a/shiny/templates/package-templates/js-output/custom_component/custom_component.py +++ b/shiny/templates/package-templates/js-output/custom_component/custom_component.py @@ -4,7 +4,7 @@ from htmltools import HTMLDependency, Tag from shiny.module import resolve_id -from shiny.render.renderer import JSONifiable, Renderer, ValueFn +from shiny.render.renderer import Jsonifiable, Renderer, ValueFn # This object is used to let Shiny know where the dependencies needed to run # our component all live. In this case, we're just using a single javascript @@ -35,10 +35,10 @@ def __init__(self, _value_fn: ValueFn[int] = None, *, height: str = "200px"): super().__init__(_value_fn) self.height: str = height - # Transforms non-`None` values into a JSONifiable object. + # Transforms non-`None` values into a `Jsonifiable` object. # If you'd like more control on when and how the value is resolved, # please use the `async def resolve(self)` method. - async def transform(self, value: int) -> JSONifiable: + async def transform(self, value: int) -> Jsonifiable: # Send the results to the client. Make sure that this is a serializable # object and matches what is expected in the javascript code. return {"value": int(value)} diff --git a/shiny/templates/package-templates/js-react/custom_component/custom_component.py b/shiny/templates/package-templates/js-react/custom_component/custom_component.py index 4c479b760..3a7e1b0d5 100644 --- a/shiny/templates/package-templates/js-react/custom_component/custom_component.py +++ b/shiny/templates/package-templates/js-react/custom_component/custom_component.py @@ -3,7 +3,7 @@ from htmltools import HTMLDependency, Tag from shiny.module import resolve_id -from shiny.render.renderer import JSONifiable, Renderer +from shiny.render.renderer import Jsonifiable, Renderer # This object is used to let Shiny know where the dependencies needed to run # our component all live. In this case, we're just using a single javascript @@ -48,10 +48,10 @@ def default_ui(self, id: str) -> Tag: # super().__init__(_value_fn) # self.extra_arg: str = extra_arg - # Transforms non-`None` values into a JSONifiable object. + # Transforms non-`None` values into a `Jsonifiable` object. # If you'd like more control on when and how the value is resolved, # please use the `async def resolve(self)` method. - async def transform(self, value: str) -> JSONifiable: + async def transform(self, value: str) -> Jsonifiable: # Send the results to the client. Make sure that this is a serializable # object and matches what is expected in the javascript code. return {"value": str(value)} From 2fb222e3869fa393846f56c692a01a8f4d17570e Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 9 Jan 2024 17:01:48 -0500 Subject: [PATCH 77/77] Update _transformer.py --- shiny/render/transformer/_transformer.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 157452fbc..889aab53a 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -239,7 +239,7 @@ def __init__( super().__init__() warn_deprecated( - "`shiny.render.transformer.output_transformer()` and `shiny.render.transformer.OutputRenderer()` output render function utiltities have been superceded by `shiny.render.renderer.Renderer` and will be removed in a near future release." + "`shiny.render.transformer.output_transformer()` and `shiny.render.transformer.OutputRenderer()` output render function utilities have been superseded by `shiny.render.renderer.Renderer` and will be removed in a near future release." ) # Copy over function name as it is consistent with how Session and Output @@ -248,10 +248,9 @@ def __init__( if not is_async_callable(transform_fn): raise TypeError( - """\ - OutputRenderer requires an async tranformer function (`transform_fn`). - Please define your transform function as asynchronous. Ex `async def my_transformer(....` - """ + "OutputRenderer requires an async tranformer function (`transform_fn`)." + " Please define your transform function as asynchronous." + " Ex `async def my_transformer(....`" ) # Upgrade value function to be async;