Skip to content

Commit 2affb72

Browse files
committed
render_widget now attaches it's return value (and as_widget() equivalent) to the decorated function
1 parent ed641c3 commit 2affb72

File tree

5 files changed

+128
-93
lines changed

5 files changed

+128
-93
lines changed

examples/ipyleaflet/app.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,50 @@
11
import ipyleaflet as L
2-
from htmltools import css
3-
from shiny import *
2+
from shiny import App, reactive, render, req, ui
43

5-
from shinywidgets import output_widget, reactive_read, register_widget
4+
from shinywidgets import output_widget, reactive_read, render_widget
65

7-
app_ui = ui.page_fillable(
8-
ui.div(
6+
app_ui = ui.page_sidebar(
7+
ui.sidebar(
98
ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10),
9+
),
10+
ui.card(
1011
ui.output_text("map_bounds"),
11-
style=css(
12-
display="flex", justify_content="center", align_items="center", gap="2rem"
13-
),
12+
fill=False
13+
),
14+
ui.card(
15+
output_widget("lmap")
1416
),
15-
output_widget("map"),
17+
title="ipyleaflet demo"
1618
)
1719

1820

1921
def server(input, output, session):
2022

21-
# Initialize and display when the session starts (1)
22-
map = L.Map(center=(52, 360), zoom=4)
23-
register_widget("map", map)
23+
@output
24+
@render_widget
25+
def lmap():
26+
return L.Map(center=(52, 360), zoom=4)
2427

2528
# When the slider changes, update the map's zoom attribute (2)
2629
@reactive.Effect
2730
def _():
28-
map.zoom = input.zoom()
31+
lmap.widget.zoom = input.zoom()
2932

3033
# When zooming directly on the map, update the slider's value (2 and 3)
3134
@reactive.Effect
3235
def _():
33-
ui.update_slider("zoom", value=reactive_read(map, "zoom"))
36+
zoom = reactive_read(lmap.widget, "zoom")
37+
ui.update_slider("zoom", value=zoom)
3438

3539
# Everytime the map's bounds change, update the output message (3)
3640
@output
3741
@render.text
3842
def map_bounds():
39-
b = reactive_read(map, "bounds")
43+
b = reactive_read(lmap.widget, "bounds")
4044
req(b)
41-
lat = [b[0][0], b[0][1]]
42-
lon = [b[1][0], b[1][1]]
43-
return f"The current latitude is {lat} and longitude is {lon}"
45+
lat = [round(x) for x in [b[0][0], b[0][1]]]
46+
lon = [round(x) for x in [b[1][0], b[1][1]]]
47+
return f"The map bounds is currently {lat} / {lon}"
4448

4549

4650
app = App(app_ui, server)

examples/ipywidgets/app.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,32 @@
44

55
from shinywidgets import *
66

7-
app_ui = ui.page_fluid(output_widget("slider"), ui.output_text("value"))
7+
app_ui = ui.page_fluid(output_widget("slider", fillable=False, fill=False), ui.output_text("slider_val"))
88

99

1010
def server(input: Inputs, output: Outputs, session: Session):
11-
s: Widget = ipy.IntSlider(
12-
value=7,
13-
min=0,
14-
max=10,
15-
step=1,
16-
description="Test:",
17-
disabled=False,
18-
continuous_update=False,
19-
orientation="horizontal",
20-
readout=True,
21-
readout_format="d",
22-
)
23-
24-
register_widget("slider", s)
25-
26-
@output(id="value")
11+
12+
@output
13+
@render_widget
14+
def slider():
15+
return ipy.IntSlider(
16+
value=7,
17+
min=0,
18+
max=10,
19+
step=1,
20+
description="Test:",
21+
disabled=False,
22+
continuous_update=False,
23+
orientation="horizontal",
24+
readout=True,
25+
readout_format="d",
26+
)
27+
28+
@output
2729
@render.text
28-
def _():
29-
return f"The value of the slider is: {reactive_read(s, 'value')}"
30+
def slider_val():
31+
val = reactive_read(slider.widget, "value")
32+
return f"The value of the slider is: {val}"
3033

3134

3235
app = App(app_ui, server, debug=True)

examples/plotly/app.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from shiny import *
44
from sklearn.linear_model import LinearRegression
55

6-
from shinywidgets import output_widget, register_widget
6+
from shinywidgets import output_widget, render_widget
77

88
# Generate some data and fit a linear regression
99
n = 10000
@@ -15,35 +15,37 @@
1515

1616
app_ui = ui.page_fillable(
1717
ui.input_checkbox("show_fit", "Show fitted line", value=True),
18-
output_widget("scatterplot"),
18+
output_widget("scatterplot")
1919
)
2020

2121

2222
def server(input, output, session):
2323

24-
scatterplot = go.FigureWidget(
25-
data=[
26-
go.Scattergl(
27-
x=x,
28-
y=y,
29-
mode="markers",
30-
marker=dict(color="rgba(0, 0, 0, 0.05)", size=5),
31-
),
32-
go.Scattergl(
33-
x=xgrid,
34-
y=fit.intercept_ + fit.coef_[0] * xgrid,
35-
mode="lines",
36-
line=dict(color="red", width=2),
37-
),
38-
],
39-
layout={"showlegend": False},
40-
)
41-
42-
register_widget("scatterplot", scatterplot)
24+
@output
25+
@render_widget
26+
def scatterplot():
27+
return go.FigureWidget(
28+
data=[
29+
go.Scattergl(
30+
x=x,
31+
y=y,
32+
mode="markers",
33+
marker=dict(color="rgba(0, 0, 0, 0.05)", size=5),
34+
),
35+
go.Scattergl(
36+
x=xgrid,
37+
y=fit.intercept_ + fit.coef_[0] * xgrid,
38+
mode="lines",
39+
line=dict(color="red", width=2),
40+
),
41+
],
42+
layout={"showlegend": False},
43+
)
4344

4445
@reactive.Effect
4546
def _():
46-
scatterplot.data[1].visible = input.show_fit()
47+
plt.data[1].visible = input.show_fit()
48+
4749

4850

4951
app = App(app_ui, server)

examples/pydeck/app.py

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,45 @@
55

66
app_ui = ui.page_fillable(
77
ui.input_slider("zoom", "Zoom", 0, 20, 6, step=1),
8-
output_widget("pydeck")
8+
output_widget("deckmap")
99
)
1010

1111
def server(input: Inputs, output: Outputs, session: Session):
1212

13-
UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv"
14-
15-
layer = pdk.Layer(
16-
"HexagonLayer", # `type` positional argument is here
17-
UK_ACCIDENTS_DATA,
18-
get_position=["lng", "lat"],
19-
auto_highlight=True,
20-
elevation_scale=50,
21-
pickable=True,
22-
elevation_range=[0, 3000],
23-
extruded=True,
24-
coverage=1,
25-
)
26-
27-
view_state = pdk.ViewState(
28-
longitude=-1.415,
29-
latitude=52.2323,
30-
zoom=6,
31-
min_zoom=5,
32-
max_zoom=15,
33-
pitch=40.5,
34-
bearing=-27.36,
35-
)
36-
37-
deck = pdk.Deck(layers=[layer], initial_view_state=view_state)
38-
39-
# Register either the deck (or deck_widget) instance
40-
register_widget("pydeck", deck)
13+
@output
14+
@render_widget
15+
def deckmap():
16+
17+
UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv"
18+
19+
layer = pdk.Layer(
20+
"HexagonLayer", # `type` positional argument is here
21+
UK_ACCIDENTS_DATA,
22+
get_position=["lng", "lat"],
23+
auto_highlight=True,
24+
elevation_scale=50,
25+
pickable=True,
26+
elevation_range=[0, 3000],
27+
extruded=True,
28+
coverage=1,
29+
)
30+
31+
view_state = pdk.ViewState(
32+
longitude=-1.415,
33+
latitude=52.2323,
34+
zoom=6,
35+
min_zoom=5,
36+
max_zoom=15,
37+
pitch=40.5,
38+
bearing=-27.36,
39+
)
40+
41+
return pdk.Deck(layers=[layer], initial_view_state=view_state)
4142

4243
@reactive.Effect()
4344
def _():
44-
deck.initial_view_state.zoom = input.zoom()
45-
deck.update()
45+
deckmap.value.initial_view_state.zoom = input.zoom()
46+
deckmap.value.update()
4647

4748

4849
app = App(app_ui, server)

shinywidgets/_shinywidgets.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from ipywidgets.widgets.widget import (
2727
_remove_buffers, # pyright: ignore[reportUnknownVariableType, reportGeneralTypeIssues]
2828
)
29-
from shiny import Session, reactive
29+
from shiny import Session, reactive, req
3030
from shiny.http_staticfiles import StaticFiles
3131
from shiny.module import resolve_id
3232
from shiny.render.transformer import (
@@ -216,10 +216,13 @@ async def WidgetTransformer(
216216
_fn: ValueFn[object | None],
217217
) -> dict[str, Any] | None:
218218
value = await resolve_value_fn(_fn)
219+
_fn.value = value # type: ignore
220+
_fn.widget = None # type: ignore
219221
if value is None:
220222
return None
221223
widget = as_widget(value)
222224
widget, fill = set_layout_defaults(widget)
225+
_fn.widget = widget # type: ignore
223226
return {"model_id": widget.model_id, "fill": fill} # type: ignore
224227

225228

@@ -232,12 +235,34 @@ def render_widget(fn: WidgetTransformer.ValueFn) -> WidgetTransformer.OutputRend
232235
def render_widget() -> WidgetTransformer.OutputRendererDecorator:
233236
...
234237

235-
236238
def render_widget(
237239
fn: WidgetTransformer.ValueFn | None = None,
238240
) -> WidgetTransformer.OutputRenderer | WidgetTransformer.OutputRendererDecorator:
239-
return WidgetTransformer(fn)
241+
res = WidgetTransformer(fn)
242+
243+
# Make the `res._value_fn.widget` attribute that we set in WidgetTransformer
244+
# accessible via `res.widget`
245+
def get_widget(*_: object) -> Optional[Widget]:
246+
w = res._value_fn.widget # type: ignore
247+
if w is None:
248+
req(False)
249+
return None
250+
return w
251+
252+
def set_widget(*_: object):
253+
raise RuntimeError("The widget attribute of a @render_widget function is read only.")
254+
255+
setattr(res.__class__, "widget", property(get_widget, set_widget))
240256

257+
def get_value(*_: object) -> object | None:
258+
return res._value_fn.value # type: ignore
259+
260+
def set_value(*_: object):
261+
raise RuntimeError("The value attribute of a @render_widget function is read only.")
262+
263+
setattr(res.__class__, "value", property(get_value, set_value))
264+
265+
return res
241266

242267
def reactive_read(widget: Widget, names: Union[str, Sequence[str]]) -> Any:
243268
reactive_depend(widget, names)

0 commit comments

Comments
 (0)