Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move @render_widget to output_transformer infrastructure #110

Merged
merged 5 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

* Moved `@render_widget` to output_transformer infrastructure, and as a result, it works more seamlessly in `shiny.express` mode. (#110)
cpsievert marked this conversation as resolved.
Show resolved Hide resolved
* Closed #104: Officially support for Python 3.7.

## [0.2.1] - 2023-05-15
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ setup_requires =
install_requires =
ipywidgets>=7.6.5
jupyter_core
shiny>=0.3.0
shiny>=0.5.1.9003
python-dateutil>=2.8.2
# Needed because of https://github.com/python/importlib_metadata/issues/411
importlib-metadata>=4.8.3,<5; python_version < "3.8"
Expand Down
72 changes: 21 additions & 51 deletions shinywidgets/_shinywidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
from shiny.http_staticfiles import StaticFiles
from shiny.module import resolve_id
from shiny.render import RenderFunction, RenderFunctionAsync
from shiny.render.transformer import (
TransformerMetadata,
ValueFn,
output_transformer,
resolve_value_fn,
)
from shiny.session import get_current_session, require_active_session

from ._as_widget import as_widget
Expand Down Expand Up @@ -182,71 +188,35 @@ def _restore_state():

# --------------------------------------------------------------------------------------------
# Implement @render_widget()
# TODO: shiny should probably make this simpler
# --------------------------------------------------------------------------------------------

IPyWidgetRenderFunc = Callable[[], Widget]
IPyWidgetRenderFuncAsync = Callable[[], Awaitable[Widget]]


class IPyWidget(RenderFunction[Widget, object]):
def __init__(self, fn: IPyWidgetRenderFunc) -> None:
super().__init__(fn)
self._fn: IPyWidgetRenderFuncAsync = wrap_async(fn)

def __call__(self) -> object:
return run_coro_sync(self.run())

async def run(self) -> object:
x = await self._fn()
if x is None:
return None
widget = as_widget(x)
return {"model_id": widget.model_id} # type: ignore


class IPyWidgetAsync(IPyWidget, RenderFunctionAsync[Widget, object]):
def __init__(self, fn: IPyWidgetRenderFuncAsync) -> None:
if not inspect.iscoroutinefunction(fn):
raise TypeError("IPyWidgetAsync requires an async function")
super().__init__(cast(IPyWidgetRenderFunc, fn))

async def __call__(self) -> object:
return await self.run()
@output_transformer(default_ui=output_widget)
async def WidgetTransformer(
_meta: TransformerMetadata,
_fn: ValueFn[object | None],
) -> dict[str, Any] | None:
value = await resolve_value_fn(_fn)
if value is None:
return None
widget = as_widget(value)
return {"model_id": widget.model_id} # type: ignore


@overload
def render_widget(
fn: Union[IPyWidgetRenderFunc, IPyWidgetRenderFuncAsync]
) -> IPyWidget:
def render_widget(fn: WidgetTransformer.ValueFn) -> WidgetTransformer.OutputRenderer:
...


@overload
def render_widget() -> (
Callable[[Union[IPyWidgetRenderFunc, IPyWidgetRenderFuncAsync]], IPyWidget]
):
def render_widget() -> WidgetTransformer.OutputRendererDecorator:
...


def render_widget(
fn: Optional[Union[IPyWidgetRenderFunc, IPyWidgetRenderFuncAsync]] = None
) -> Union[
IPyWidget,
Callable[[Union[IPyWidgetRenderFunc, IPyWidgetRenderFuncAsync]], IPyWidget],
]:
def wrapper(fn: Union[IPyWidgetRenderFunc, IPyWidgetRenderFuncAsync]) -> IPyWidget:
if inspect.iscoroutinefunction(fn):
fn = cast(IPyWidgetRenderFuncAsync, fn)
return IPyWidgetAsync(fn)
else:
fn = cast(IPyWidgetRenderFunc, fn)
return IPyWidget(fn)

if fn is None:
return wrapper
else:
return wrapper(fn)
fn: WidgetTransformer.ValueFn | None = None,
) -> WidgetTransformer.OutputRenderer | WidgetTransformer.OutputRendererDecorator:
return WidgetTransformer(fn)


def reactive_read(widget: Widget, names: Union[str, Sequence[str]]) -> Any:
Expand Down