From 96d88fdd152098f6f5987f3cecbd415c9f9702f1 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 15 Nov 2023 17:27:55 -0600 Subject: [PATCH] render_widget now attaches it's return value (and as_widget() equivalent) to the decorated function --- examples/ipyleaflet/app.py | 40 ++++++++++++---------- examples/ipywidgets/app.py | 41 ++++++++++++----------- examples/plotly/app.py | 46 +++++++++++++------------ examples/pydeck/app.py | 63 ++++++++++++++++++----------------- shinywidgets/_shinywidgets.py | 31 +++++++++++++++-- 5 files changed, 128 insertions(+), 93 deletions(-) diff --git a/examples/ipyleaflet/app.py b/examples/ipyleaflet/app.py index 6fbffdc..32a6cc4 100644 --- a/examples/ipyleaflet/app.py +++ b/examples/ipyleaflet/app.py @@ -1,46 +1,50 @@ import ipyleaflet as L -from htmltools import css -from shiny import * +from shiny import App, reactive, render, req, ui -from shinywidgets import output_widget, reactive_read, register_widget +from shinywidgets import output_widget, reactive_read, render_widget -app_ui = ui.page_fillable( - ui.div( +app_ui = ui.page_sidebar( + ui.sidebar( ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10), + ), + ui.card( ui.output_text("map_bounds"), - style=css( - display="flex", justify_content="center", align_items="center", gap="2rem" - ), + fill=False + ), + ui.card( + output_widget("lmap") ), - output_widget("map"), + title="ipyleaflet demo" ) def server(input, output, session): - # Initialize and display when the session starts (1) - map = L.Map(center=(52, 360), zoom=4) - register_widget("map", map) + @output + @render_widget + def lmap(): + return L.Map(center=(52, 360), zoom=4) # When the slider changes, update the map's zoom attribute (2) @reactive.Effect def _(): - map.zoom = input.zoom() + lmap.widget.zoom = input.zoom() # When zooming directly on the map, update the slider's value (2 and 3) @reactive.Effect def _(): - ui.update_slider("zoom", value=reactive_read(map, "zoom")) + zoom = reactive_read(lmap.widget, "zoom") + ui.update_slider("zoom", value=zoom) # Everytime the map's bounds change, update the output message (3) @output @render.text def map_bounds(): - b = reactive_read(map, "bounds") + b = reactive_read(lmap.widget, "bounds") req(b) - lat = [b[0][0], b[0][1]] - lon = [b[1][0], b[1][1]] - return f"The current latitude is {lat} and longitude is {lon}" + lat = [round(x) for x in [b[0][0], b[0][1]]] + lon = [round(x) for x in [b[1][0], b[1][1]]] + return f"The map bounds is currently {lat} / {lon}" app = App(app_ui, server) diff --git a/examples/ipywidgets/app.py b/examples/ipywidgets/app.py index fea36f2..d7b51f2 100644 --- a/examples/ipywidgets/app.py +++ b/examples/ipywidgets/app.py @@ -4,29 +4,32 @@ from shinywidgets import * -app_ui = ui.page_fluid(output_widget("slider"), ui.output_text("value")) +app_ui = ui.page_fluid(output_widget("slider", fillable=False, fill=False), ui.output_text("slider_val")) def server(input: Inputs, output: Outputs, session: Session): - s: Widget = ipy.IntSlider( - value=7, - min=0, - max=10, - step=1, - description="Test:", - disabled=False, - continuous_update=False, - orientation="horizontal", - readout=True, - readout_format="d", - ) - - register_widget("slider", s) - - @output(id="value") + + @output + @render_widget + def slider(): + return ipy.IntSlider( + value=7, + min=0, + max=10, + step=1, + description="Test:", + disabled=False, + continuous_update=False, + orientation="horizontal", + readout=True, + readout_format="d", + ) + + @output @render.text - def _(): - return f"The value of the slider is: {reactive_read(s, 'value')}" + def slider_val(): + val = reactive_read(slider.widget, "value") + return f"The value of the slider is: {val}" app = App(app_ui, server, debug=True) diff --git a/examples/plotly/app.py b/examples/plotly/app.py index 568dcc6..858e155 100644 --- a/examples/plotly/app.py +++ b/examples/plotly/app.py @@ -3,7 +3,7 @@ from shiny import * from sklearn.linear_model import LinearRegression -from shinywidgets import output_widget, register_widget +from shinywidgets import output_widget, render_widget # Generate some data and fit a linear regression n = 10000 @@ -15,35 +15,37 @@ app_ui = ui.page_fillable( ui.input_checkbox("show_fit", "Show fitted line", value=True), - output_widget("scatterplot"), + output_widget("scatterplot") ) def server(input, output, session): - scatterplot = go.FigureWidget( - data=[ - go.Scattergl( - x=x, - y=y, - mode="markers", - marker=dict(color="rgba(0, 0, 0, 0.05)", size=5), - ), - go.Scattergl( - x=xgrid, - y=fit.intercept_ + fit.coef_[0] * xgrid, - mode="lines", - line=dict(color="red", width=2), - ), - ], - layout={"showlegend": False}, - ) - - register_widget("scatterplot", scatterplot) + @output + @render_widget + def scatterplot(): + return go.FigureWidget( + data=[ + go.Scattergl( + x=x, + y=y, + mode="markers", + marker=dict(color="rgba(0, 0, 0, 0.05)", size=5), + ), + go.Scattergl( + x=xgrid, + y=fit.intercept_ + fit.coef_[0] * xgrid, + mode="lines", + line=dict(color="red", width=2), + ), + ], + layout={"showlegend": False}, + ) @reactive.Effect def _(): - scatterplot.data[1].visible = input.show_fit() + plt.data[1].visible = input.show_fit() + app = App(app_ui, server) diff --git a/examples/pydeck/app.py b/examples/pydeck/app.py index a285ac5..1933d4b 100644 --- a/examples/pydeck/app.py +++ b/examples/pydeck/app.py @@ -5,44 +5,45 @@ app_ui = ui.page_fillable( ui.input_slider("zoom", "Zoom", 0, 20, 6, step=1), - output_widget("pydeck") + output_widget("deckmap") ) def server(input: Inputs, output: Outputs, session: Session): - UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv" - - layer = pdk.Layer( - "HexagonLayer", # `type` positional argument is here - UK_ACCIDENTS_DATA, - get_position=["lng", "lat"], - auto_highlight=True, - elevation_scale=50, - pickable=True, - elevation_range=[0, 3000], - extruded=True, - coverage=1, - ) - - view_state = pdk.ViewState( - longitude=-1.415, - latitude=52.2323, - zoom=6, - min_zoom=5, - max_zoom=15, - pitch=40.5, - bearing=-27.36, - ) - - deck = pdk.Deck(layers=[layer], initial_view_state=view_state) - - # Register either the deck (or deck_widget) instance - register_widget("pydeck", deck) + @output + @render_widget + def deckmap(): + + UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv" + + layer = pdk.Layer( + "HexagonLayer", # `type` positional argument is here + UK_ACCIDENTS_DATA, + get_position=["lng", "lat"], + auto_highlight=True, + elevation_scale=50, + pickable=True, + elevation_range=[0, 3000], + extruded=True, + coverage=1, + ) + + view_state = pdk.ViewState( + longitude=-1.415, + latitude=52.2323, + zoom=6, + min_zoom=5, + max_zoom=15, + pitch=40.5, + bearing=-27.36, + ) + + return pdk.Deck(layers=[layer], initial_view_state=view_state) @reactive.Effect() def _(): - deck.initial_view_state.zoom = input.zoom() - deck.update() + deckmap.value.initial_view_state.zoom = input.zoom() + deckmap.value.update() app = App(app_ui, server) diff --git a/shinywidgets/_shinywidgets.py b/shinywidgets/_shinywidgets.py index 6b69235..a61dc78 100644 --- a/shinywidgets/_shinywidgets.py +++ b/shinywidgets/_shinywidgets.py @@ -26,7 +26,7 @@ from ipywidgets.widgets.widget import ( _remove_buffers, # pyright: ignore[reportUnknownVariableType, reportGeneralTypeIssues] ) -from shiny import Session, reactive +from shiny import Session, reactive, req from shiny.http_staticfiles import StaticFiles from shiny.module import resolve_id from shiny.render.transformer import ( @@ -216,10 +216,13 @@ async def WidgetTransformer( _fn: ValueFn[object | None], ) -> dict[str, Any] | None: value = await resolve_value_fn(_fn) + _fn.value = value # type: ignore + _fn.widget = None # type: ignore if value is None: return None widget = as_widget(value) widget, fill = set_layout_defaults(widget) + _fn.widget = widget # type: ignore return {"model_id": widget.model_id, "fill": fill} # type: ignore @@ -232,12 +235,34 @@ def render_widget(fn: WidgetTransformer.ValueFn) -> WidgetTransformer.OutputRend def render_widget() -> WidgetTransformer.OutputRendererDecorator: ... - def render_widget( fn: WidgetTransformer.ValueFn | None = None, ) -> WidgetTransformer.OutputRenderer | WidgetTransformer.OutputRendererDecorator: - return WidgetTransformer(fn) + res = WidgetTransformer(fn) + + # Make the `res._value_fn.widget` attribute that we set in WidgetTransformer + # accessible via `res.widget` + def get_widget(*_: object) -> Optional[Widget]: + w = res._value_fn.widget # type: ignore + if w is None: + req(False) + return None + return w + + def set_widget(*_: object): + raise RuntimeError("The widget attribute of a @render_widget function is read only.") + + setattr(res.__class__, "widget", property(get_widget, set_widget)) + def get_value(*_: object) -> object | None: + return res._value_fn.value # type: ignore + + def set_value(*_: object): + raise RuntimeError("The value attribute of a @render_widget function is read only.") + + setattr(res.__class__, "value", property(get_value, set_value)) + + return res def reactive_read(widget: Widget, names: Union[str, Sequence[str]]) -> Any: reactive_depend(widget, names)