From 7a4911e1c53fbecdbac8d7a0d64b3a15e284f8a3 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Tue, 27 Feb 2024 17:49:00 -0600 Subject: [PATCH 01/35] `update_dark_mode()` now sends it's message synchronously (#1162) --- shiny/api-examples/input_dark_mode/app-core.py | 8 ++++---- shiny/api-examples/input_dark_mode/app-express.py | 8 ++++---- shiny/ui/_input_dark_mode.py | 5 ++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/shiny/api-examples/input_dark_mode/app-core.py b/shiny/api-examples/input_dark_mode/app-core.py index 0e298bd3b..30539ac30 100644 --- a/shiny/api-examples/input_dark_mode/app-core.py +++ b/shiny/api-examples/input_dark_mode/app-core.py @@ -35,13 +35,13 @@ def server(input: Inputs, output: Outputs, session: Session): @reactive.effect @reactive.event(input.make_light) - async def _(): - await ui.update_dark_mode("light") + def _(): + ui.update_dark_mode("light") @reactive.effect @reactive.event(input.make_dark) - async def _(): - await ui.update_dark_mode("dark") + def _(): + ui.update_dark_mode("dark") @render.plot(alt="A histogram") def plot() -> object: diff --git a/shiny/api-examples/input_dark_mode/app-express.py b/shiny/api-examples/input_dark_mode/app-express.py index a6e05380a..063f215d8 100644 --- a/shiny/api-examples/input_dark_mode/app-express.py +++ b/shiny/api-examples/input_dark_mode/app-express.py @@ -50,11 +50,11 @@ def plot() -> object: @reactive.effect @reactive.event(input.make_light) -async def _(): - await ui.update_dark_mode("light") +def _(): + ui.update_dark_mode("light") @reactive.effect @reactive.event(input.make_dark) -async def _(): - await ui.update_dark_mode("dark") +def _(): + ui.update_dark_mode("dark") diff --git a/shiny/ui/_input_dark_mode.py b/shiny/ui/_input_dark_mode.py index 0c8523b3e..2439000d7 100644 --- a/shiny/ui/_input_dark_mode.py +++ b/shiny/ui/_input_dark_mode.py @@ -77,7 +77,7 @@ def validate_dark_mode_option(mode: BootstrapColorMode) -> BootstrapColorMode: @no_example() -async def update_dark_mode( +def update_dark_mode( mode: BootstrapColorMode, *, session: Optional[Session] = None ) -> None: session = require_active_session(session) @@ -88,5 +88,4 @@ async def update_dark_mode( "method": "toggle", "value": mode, } - - await session.send_custom_message("bslib.toggle-dark-mode", msg) + session._send_message_sync({"custom": {"bslib.toggle-dark-mode": msg}}) From 7dd5587a7b2f0da7090194254e4899c435fb0754 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 27 Feb 2024 23:47:22 -0600 Subject: [PATCH 02/35] Update examples to use lower case reactive.effect and .calc --- examples/airmass/app.py | 12 ++++++------ examples/airmass/location.py | 8 ++++---- examples/annotation-export/app.py | 4 ++-- examples/brownian/app.py | 10 +++++----- examples/cpuinfo/app.py | 6 +++--- examples/dataframe/app.py | 4 ++-- examples/duckdb/app.py | 6 +++--- examples/duckdb/query.py | 2 +- examples/event/app.py | 12 ++++++------ examples/express/shared_app.py | 2 +- examples/inputs-update/app.py | 2 +- examples/model-score/app.py | 10 +++++----- examples/model-score/plotly_streaming.py | 6 +++--- examples/moduleapp/app.py | 2 +- examples/penguins/app.py | 2 +- examples/req/app.py | 4 ++-- examples/static_plots/app.py | 2 +- examples/typed_inputs/app.py | 2 +- shiny/api-examples/Module/app-core.py | 2 +- shiny/api-examples/accordion_panel/app-core.py | 2 +- shiny/api-examples/close/app-core.py | 2 +- shiny/api-examples/dynamic_route/app-core.py | 2 +- shiny/api-examples/effect/app-core.py | 2 +- shiny/api-examples/event/app-core.py | 10 +++++----- shiny/api-examples/extended_task/app-express.py | 4 ++-- shiny/api-examples/input_file/app-core.py | 2 +- shiny/api-examples/input_file/app-express.py | 2 +- .../api-examples/insert_accordion_panel/app-core.py | 2 +- shiny/api-examples/insert_ui/app-core.py | 2 +- shiny/api-examples/insert_ui/app-express.py | 2 +- shiny/api-examples/modal/app-core.py | 2 +- shiny/api-examples/nav_panel/app-core.py | 2 +- shiny/api-examples/navset_hidden/app-core.py | 2 +- shiny/api-examples/notification_show/app-core.py | 4 ++-- shiny/api-examples/on_ended/app-core.py | 2 +- .../api-examples/remove_accordion_panel/app-core.py | 2 +- shiny/api-examples/remove_ui/app-core.py | 2 +- shiny/api-examples/remove_ui/app-express.py | 2 +- shiny/api-examples/req/app-core.py | 4 ++-- shiny/api-examples/send_custom_message/app-core.py | 2 +- shiny/api-examples/todo_list/app-core.py | 8 ++++---- shiny/api-examples/update_accordion/app-core.py | 2 +- .../api-examples/update_accordion_panel/app-core.py | 2 +- shiny/api-examples/update_action_button/app-core.py | 2 +- .../api-examples/update_action_button/app-express.py | 2 +- shiny/api-examples/update_checkbox/app-core.py | 2 +- shiny/api-examples/update_checkbox_group/app-core.py | 2 +- shiny/api-examples/update_date/app-core.py | 2 +- shiny/api-examples/update_date/app-express.py | 2 +- shiny/api-examples/update_date_range/app-core.py | 2 +- shiny/api-examples/update_navs/app-core.py | 2 +- shiny/api-examples/update_numeric/app-core.py | 2 +- shiny/api-examples/update_radio_buttons/app-core.py | 2 +- shiny/api-examples/update_select/app-core.py | 2 +- shiny/api-examples/update_selectize/app-core.py | 2 +- shiny/api-examples/update_sidebar/app-core.py | 4 ++-- shiny/templates/app-templates/dashboard/app-core.py | 2 +- .../templates/app-templates/dashboard/app-express.py | 2 +- shiny/templates/app-templates/multi-page/app-core.py | 2 +- shiny/types.py | 2 +- tests/playwright/deploys/plotly/app.py | 4 ++-- tests/playwright/shiny/bugs/0696-resolve-id/app.py | 2 +- .../playwright/shiny/inputs/input_task_button/app.py | 6 +++--- 63 files changed, 107 insertions(+), 107 deletions(-) diff --git a/examples/airmass/app.py b/examples/airmass/app.py index 005b19a73..54e333104 100644 --- a/examples/airmass/app.py +++ b/examples/airmass/app.py @@ -61,17 +61,17 @@ def server(input: Inputs, output: Outputs, session: Session): loc = location_server("location") time_padding = datetime.timedelta(hours=1.5) - @reactive.Calc + @reactive.calc def obj_names() -> List[str]: """Returns a split and *slightly* cleaned-up list of object names""" req(input.objects()) return [x.strip() for x in input.objects().split(",") if x.strip() != ""] - @reactive.Calc + @reactive.calc def obj_coords() -> List[SkyCoord]: return [SkyCoord.from_name(name) for name in obj_names()] - @reactive.Calc + @reactive.calc def times_utc() -> Tuple[datetime.datetime, datetime.datetime]: req(input.date()) lat, long = loc() @@ -81,18 +81,18 @@ def times_utc() -> Tuple[datetime.datetime, datetime.datetime]: sun.get_sunrise_time(input.date() + datetime.timedelta(days=1)), ) - @reactive.Calc + @reactive.calc def timezone() -> Optional[str]: lat, long = loc() return timezonefinder.TimezoneFinder().timezone_at(lat=lat, lng=long) - @reactive.Calc + @reactive.calc def times_at_loc(): start, end = times_utc() tz = pytz.timezone(timezone()) return (start.astimezone(tz), end.astimezone(tz)) - @reactive.Calc + @reactive.calc def df() -> Dict[str, pd.DataFrame]: start, end = times_at_loc() times = pd.date_range( diff --git a/examples/airmass/location.py b/examples/airmass/location.py index b3ced3b2b..5a8cf926c 100644 --- a/examples/airmass/location.py +++ b/examples/airmass/location.py @@ -100,24 +100,24 @@ def on_map_interaction(**kwargs): register_widget("map", map) - @reactive.Effect + @reactive.effect def _(): coords = reactive_read(marker, "location") if coords: update_text_inputs(coords[0], coords[1]) - @reactive.Effect + @reactive.effect def sync_autolocate(): coords = input.here() ui.notification_remove("searching") if coords and not input.lat() and not input.long(): update_text_inputs(coords["latitude"], coords["longitude"]) - @reactive.Effect + @reactive.effect def sync_inputs_to_marker(): update_marker(input.lat(), input.long()) - @reactive.Calc + @reactive.calc def location(): """Returns tuple of (lat,long) floats--or throws silent error if no lat/long is selected""" diff --git a/examples/annotation-export/app.py b/examples/annotation-export/app.py index 3d6e2ce12..0f4e20148 100644 --- a/examples/annotation-export/app.py +++ b/examples/annotation-export/app.py @@ -41,12 +41,12 @@ def server(input: Inputs, output: Outputs, session: Session): annotated_data = reactive.Value(weather_df) - @reactive.Calc + @reactive.calc def selected_data(): out = brushed_points(annotated_data(), input.time_series_brush(), xvar="date") return out - @reactive.Effect + @reactive.effect @reactive.event(input.annotate_button) def _(): selected = selected_data() diff --git a/examples/brownian/app.py b/examples/brownian/app.py index 47859ea79..e2ae7d3c2 100644 --- a/examples/brownian/app.py +++ b/examples/brownian/app.py @@ -44,7 +44,7 @@ def server(input, output, session): # BROWNIAN MOTION ==== - @reactive.Calc + @reactive.calc def random_walk(): """Generates brownian data whenever 'New Data' is clicked""" input.data_btn() @@ -54,7 +54,7 @@ def random_walk(): widget = brownian_widget(600, 600) register_widget("plot", widget) - @reactive.Effect + @reactive.effect def update_plotly_data(): walk = random_walk() layer = widget.data[0] @@ -65,7 +65,7 @@ def update_plotly_data(): # HAND TRACKING ==== - @reactive.Calc + @reactive.calc def camera_eye(): """The eye position, as reflected by the hand input""" hand_val = input.hand() @@ -78,7 +78,7 @@ def camera_eye(): # The raw data is a little jittery. Smooth it out by averaging a few samples smooth_camera_eye = reactive_smooth(n_samples=5, smoother=xyz_mean)(camera_eye) - @reactive.Effect + @reactive.effect def update_plotly_camera(): """Update Plotly camera using the hand tracking""" widget.layout.scene.camera.eye = smooth_camera_eye() @@ -114,7 +114,7 @@ def wrapper(calc): buffer = [] # Ring buffer of capacity `n_samples` result = reactive.Value(None) # Holds the most recent smoothed result - @reactive.Effect + @reactive.effect def _(): # Get latest value. Because this is happening in a reactive Effect, we'll # automatically take a reactive dependency on whatever is happening in the diff --git a/examples/cpuinfo/app.py b/examples/cpuinfo/app.py index 75607657b..3cf4db472 100644 --- a/examples/cpuinfo/app.py +++ b/examples/cpuinfo/app.py @@ -102,7 +102,7 @@ ) -@reactive.Calc +@reactive.calc def cpu_current(): reactive.invalidate_later(SAMPLE_PERIOD) return cpu_percent(percpu=True) @@ -111,7 +111,7 @@ def cpu_current(): def server(input: Inputs, output: Outputs, session: Session): cpu_history = reactive.Value(None) - @reactive.Calc + @reactive.calc def cpu_history_with_hold(): # If "hold" is on, grab an isolated snapshot of cpu_history; if not, then do a # regular read @@ -123,7 +123,7 @@ def cpu_history_with_hold(): with reactive.isolate(): return cpu_history() - @reactive.Effect + @reactive.effect def collect_cpu_samples(): """cpu_percent() reports just the current CPU usage sample; this Effect gathers them up and stores them in the cpu_history reactive value, in a numpy 2D array diff --git a/examples/dataframe/app.py b/examples/dataframe/app.py index 09b694827..9ba99a770 100644 --- a/examples/dataframe/app.py +++ b/examples/dataframe/app.py @@ -57,7 +57,7 @@ def light_dark_switcher(dark): def server(input: Inputs, output: Outputs, session: Session): df: reactive.Value[pd.DataFrame] = reactive.Value() - @reactive.Effect + @reactive.effect def update_df(): return df.set(sns.load_dataset(req(input.dataset()))) @@ -82,7 +82,7 @@ def grid(): row_selection_mode=input.selection_mode(), ) - @reactive.Effect + @reactive.effect @reactive.event(input.grid_cell_edit) def handle_edit(): edit = input.grid_cell_edit() diff --git a/examples/duckdb/app.py b/examples/duckdb/app.py index 278cf3779..49898cca8 100644 --- a/examples/duckdb/app.py +++ b/examples/duckdb/app.py @@ -71,7 +71,7 @@ def server(input, output, session): query_output_server("initial_query", con=con, remove_id="initial_query") - @reactive.Effect + @reactive.effect @reactive.event(input.add_query) def _(): counter = mod_counter.get() + 1 @@ -84,7 +84,7 @@ def _(): ) query_output_server(id, con=con, remove_id=id) - @reactive.Effect + @reactive.effect @reactive.event(input.show_meta) def _(): counter = mod_counter.get() + 1 @@ -99,7 +99,7 @@ def _(): ) query_output_server(id, con=con, remove_id=id) - @reactive.Effect + @reactive.effect @reactive.event(input.rmv) def _(): ui.remove_ui(selector="div:has(> #txt)") diff --git a/examples/duckdb/query.py b/examples/duckdb/query.py index 95a5b768d..7de72f1a5 100644 --- a/examples/duckdb/query.py +++ b/examples/duckdb/query.py @@ -57,7 +57,7 @@ def results(): return result - @reactive.Effect + @reactive.effect @reactive.event(input.rmv) def _(): ui.remove_ui(selector=f"div#{remove_id}") diff --git a/examples/event/app.py b/examples/event/app.py index ade6105a3..683ffdded 100644 --- a/examples/event/app.py +++ b/examples/event/app.py @@ -27,17 +27,17 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.btn) def _(): print("@effect() event: ", str(input.btn())) - @reactive.Calc + @reactive.calc @reactive.event(input.btn) def btn() -> int: return input.btn() - @reactive.Effect + @reactive.effect def _(): print("@calc() event: ", str(btn())) @@ -49,19 +49,19 @@ def btn_value(): # ----------------------------------------------------------------------------- # Async # ----------------------------------------------------------------------------- - @reactive.Effect + @reactive.effect @reactive.event(input.btn_async) async def _(): await asyncio.sleep(0) print("async @effect() event: ", str(input.btn_async())) - @reactive.Calc + @reactive.calc @reactive.event(input.btn_async) async def btn_async_r() -> int: await asyncio.sleep(0) return input.btn_async() - @reactive.Effect + @reactive.effect async def _(): val = await btn_async_r() print("async @calc() event: ", str(val)) diff --git a/examples/express/shared_app.py b/examples/express/shared_app.py index 439d325c3..89c587a1c 100644 --- a/examples/express/shared_app.py +++ b/examples/express/shared_app.py @@ -20,7 +20,7 @@ def histogram(): ui.input_slider("n", "N", 1, 100, 50) -@reactive.Effect +@reactive.effect def _(): shared.rv.set(input.n()) diff --git a/examples/inputs-update/app.py b/examples/inputs-update/app.py index be210a807..092a1d2c6 100644 --- a/examples/inputs-update/app.py +++ b/examples/inputs-update/app.py @@ -88,7 +88,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): # We'll use these multiple times, so use short var names for # convenience. diff --git a/examples/model-score/app.py b/examples/model-score/app.py index 1b50fcbfb..ef84aeb5e 100644 --- a/examples/model-score/app.py +++ b/examples/model-score/app.py @@ -134,7 +134,7 @@ def app_ui(req): def server(input: Inputs, output: Outputs, session: Session): - @reactive.Calc + @reactive.calc def recent_df(): """ Returns the most recent rows from the database, at the refresh interval @@ -152,7 +152,7 @@ def recent_df(): with reactive.isolate(): return df() - @reactive.Calc + @reactive.calc def timeframe_df(): """ Returns rows from the database within the specified time range. Notice that we @@ -162,7 +162,7 @@ def timeframe_df(): start, end = input.timerange() return read_time_period(start, end) - @reactive.Calc + @reactive.calc def filtered_df(): """ Return the data frame that should be displayed in the app, based on the user's @@ -174,7 +174,7 @@ def filtered_df(): # Filter the rows so we only include the desired models return data[data["model"].isin(input.models())] - @reactive.Calc + @reactive.calc def filtered_model_names(): return filtered_df()["model"].unique() @@ -282,7 +282,7 @@ def plot_dist(): return fig - @reactive.Effect + @reactive.effect def update_time_range(): """ Every 5 seconds, update the custom time range slider's min and max values to diff --git a/examples/model-score/plotly_streaming.py b/examples/model-score/plotly_streaming.py index d409d3bfd..71f4c5733 100644 --- a/examples/model-score/plotly_streaming.py +++ b/examples/model-score/plotly_streaming.py @@ -62,7 +62,7 @@ def wrapper(): fig = func() widget = go.FigureWidget(fig) - @reactive.Effect + @reactive.effect def update_plotly_data(): f_new = func() with widget.batch_update(): @@ -85,14 +85,14 @@ def deduplicate(func): with reactive.isolate(): rv = reactive.Value(func()) - @reactive.Effect + @reactive.effect def update(): x = func() with reactive.isolate(): if x != rv(): rv.set(x) - @reactive.Calc + @reactive.calc @functools.wraps(func) def wrapper(): return rv() diff --git a/examples/moduleapp/app.py b/examples/moduleapp/app.py index 3cb185638..090eb8695 100644 --- a/examples/moduleapp/app.py +++ b/examples/moduleapp/app.py @@ -20,7 +20,7 @@ def counter_server( ): count: reactive.Value[int] = reactive.Value(starting_value) - @reactive.Effect + @reactive.effect @reactive.event(input.button) def _(): count.set(count() + 1) diff --git a/examples/penguins/app.py b/examples/penguins/app.py index 9ab78d6b5..378f49cea 100644 --- a/examples/penguins/app.py +++ b/examples/penguins/app.py @@ -53,7 +53,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Calc + @reactive.calc def filtered_df() -> pd.DataFrame: """Returns a Pandas data frame that includes only the desired rows""" diff --git a/examples/req/app.py b/examples/req/app.py index 80d70ebb0..6e0f6e5e2 100644 --- a/examples/req/app.py +++ b/examples/req/app.py @@ -16,7 +16,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Calc + @reactive.calc def safe_click(): req(input.safe()) return input.safe() @@ -30,7 +30,7 @@ def unsafe(): req(input.unsafe()) raise Exception(f"Super secret number of clicks: {str(input.unsafe())}") - @reactive.Effect + @reactive.effect def _(): req(input.unsafe()) print("unsafe clicks:", input.unsafe()) diff --git a/examples/static_plots/app.py b/examples/static_plots/app.py index 52736f56e..b5c928169 100644 --- a/examples/static_plots/app.py +++ b/examples/static_plots/app.py @@ -54,7 +54,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Calc + @reactive.calc def fake_data(): n = 5000 mean = [0, 0] diff --git a/examples/typed_inputs/app.py b/examples/typed_inputs/app.py index 1356fef51..4759e8b75 100644 --- a/examples/typed_inputs/app.py +++ b/examples/typed_inputs/app.py @@ -32,7 +32,7 @@ def server(input: Inputs, output: Outputs, session: Session): # The type checker knows that r() returns an int, which you can see if you hover # over it. - @reactive.Calc + @reactive.calc def r(): if input.n() is None: return 0 diff --git a/shiny/api-examples/Module/app-core.py b/shiny/api-examples/Module/app-core.py index 12f27078f..87a335fe7 100644 --- a/shiny/api-examples/Module/app-core.py +++ b/shiny/api-examples/Module/app-core.py @@ -20,7 +20,7 @@ def counter_server( ): count: reactive.Value[int] = reactive.Value(starting_value) - @reactive.Effect + @reactive.effect @reactive.event(input.button) def _(): count.set(count() + 1) diff --git a/shiny/api-examples/accordion_panel/app-core.py b/shiny/api-examples/accordion_panel/app-core.py index b4d7d2bf8..c53697eb0 100644 --- a/shiny/api-examples/accordion_panel/app-core.py +++ b/shiny/api-examples/accordion_panel/app-core.py @@ -14,7 +14,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): print(input.acc()) diff --git a/shiny/api-examples/close/app-core.py b/shiny/api-examples/close/app-core.py index e87f94a65..ddb12eb3b 100644 --- a/shiny/api-examples/close/app-core.py +++ b/shiny/api-examples/close/app-core.py @@ -19,7 +19,7 @@ def log(): session.on_ended(log) - @reactive.Effect + @reactive.effect @reactive.event(input.close) async def _(): await session.close() diff --git a/shiny/api-examples/dynamic_route/app-core.py b/shiny/api-examples/dynamic_route/app-core.py index ae29ec54c..e9fe50ee5 100644 --- a/shiny/api-examples/dynamic_route/app-core.py +++ b/shiny/api-examples/dynamic_route/app-core.py @@ -9,7 +9,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.serve) def _(): async def my_handler(request: Request) -> JSONResponse: diff --git a/shiny/api-examples/effect/app-core.py b/shiny/api-examples/effect/app-core.py index 8b9c2c599..01ac2e272 100644 --- a/shiny/api-examples/effect/app-core.py +++ b/shiny/api-examples/effect/app-core.py @@ -4,7 +4,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.btn) def _(): ui.insert_ui( diff --git a/shiny/api-examples/event/app-core.py b/shiny/api-examples/event/app-core.py index d49010fc0..b1a7bd53f 100644 --- a/shiny/api-examples/event/app-core.py +++ b/shiny/api-examples/event/app-core.py @@ -6,8 +6,8 @@ ui.markdown( f""" This example demonstrates how `@reactive.event()` can be used to restrict - execution of: (1) a `@render` function, (2) `@reactive.Calc`, or (3) - `@reactive.Effect`. + execution of: (1) a `@render` function, (2) `@reactive.calc`, or (3) + `@reactive.effect`. In all three cases, the output is dependent on a random value that gets updated every 0.5 seconds (currently, it is {ui.output_ui("number", inline=True)}), but @@ -38,7 +38,7 @@ def server(input: Inputs, output: Outputs, session: Session): # Update a random number every second val = reactive.Value(random.randint(0, 1000)) - @reactive.Effect + @reactive.effect def _(): reactive.invalidate_later(0.5) val.set(random.randint(0, 1000)) @@ -56,7 +56,7 @@ def number(): def out_out(): return str(val.get()) - @reactive.Calc + @reactive.calc @reactive.event(input.btn_calc) def calc(): return 1 / val.get() @@ -65,7 +65,7 @@ def calc(): def out_calc(): return str(calc()) - @reactive.Effect + @reactive.effect @reactive.event(input.btn_effect) def _(): ui.insert_ui( diff --git a/shiny/api-examples/extended_task/app-express.py b/shiny/api-examples/extended_task/app-express.py index 6fa9cbe8b..f7c981fc9 100644 --- a/shiny/api-examples/extended_task/app-express.py +++ b/shiny/api-examples/extended_task/app-express.py @@ -31,13 +31,13 @@ async def slow_compute(a: int, b: int) -> int: ui.input_task_button("btn", "Compute, slowly") ui.input_action_button("btn_cancel", "Cancel") - @reactive.Effect + @reactive.effect @reactive.event(input.btn, ignore_none=False) def handle_click(): # slow_compute.cancel() slow_compute(input.x(), input.y()) - @reactive.Effect + @reactive.effect @reactive.event(input.btn_cancel) def handle_cancel(): slow_compute.cancel() diff --git a/shiny/api-examples/input_file/app-core.py b/shiny/api-examples/input_file/app-core.py index fa34f804c..4cd0d99ee 100644 --- a/shiny/api-examples/input_file/app-core.py +++ b/shiny/api-examples/input_file/app-core.py @@ -16,7 +16,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Calc + @reactive.calc def parsed_file(): file: list[FileInfo] | None = input.file1() if file is None: diff --git a/shiny/api-examples/input_file/app-express.py b/shiny/api-examples/input_file/app-express.py index 81222e4f7..6a035a555 100644 --- a/shiny/api-examples/input_file/app-express.py +++ b/shiny/api-examples/input_file/app-express.py @@ -13,7 +13,7 @@ ) -@reactive.Calc +@reactive.calc def parsed_file(): file: list[FileInfo] | None = input.file1() if file is None: diff --git a/shiny/api-examples/insert_accordion_panel/app-core.py b/shiny/api-examples/insert_accordion_panel/app-core.py index 172460646..c9a2d0f98 100644 --- a/shiny/api-examples/insert_accordion_panel/app-core.py +++ b/shiny/api-examples/insert_accordion_panel/app-core.py @@ -18,7 +18,7 @@ def make_panel(letter: str) -> ui.AccordionPanel: def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.add_panel) def _(): ui.insert_accordion_panel("acc", make_panel(str(random.randint(0, 10000)))) diff --git a/shiny/api-examples/insert_ui/app-core.py b/shiny/api-examples/insert_ui/app-core.py index d8b9139f2..98cc7d49e 100644 --- a/shiny/api-examples/insert_ui/app-core.py +++ b/shiny/api-examples/insert_ui/app-core.py @@ -6,7 +6,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.add) def _(): ui.insert_ui( diff --git a/shiny/api-examples/insert_ui/app-express.py b/shiny/api-examples/insert_ui/app-express.py index 37a0b72f9..4e6f45291 100644 --- a/shiny/api-examples/insert_ui/app-express.py +++ b/shiny/api-examples/insert_ui/app-express.py @@ -4,7 +4,7 @@ ui.input_action_button("add", "Add UI") -@reactive.Effect +@reactive.effect @reactive.event(input.add) def _(): ui.insert_ui( diff --git a/shiny/api-examples/modal/app-core.py b/shiny/api-examples/modal/app-core.py index d054a3083..8fb35ae1d 100644 --- a/shiny/api-examples/modal/app-core.py +++ b/shiny/api-examples/modal/app-core.py @@ -6,7 +6,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.show) def _(): m = ui.modal( diff --git a/shiny/api-examples/nav_panel/app-core.py b/shiny/api-examples/nav_panel/app-core.py index 269c6a18e..db7650f65 100644 --- a/shiny/api-examples/nav_panel/app-core.py +++ b/shiny/api-examples/nav_panel/app-core.py @@ -66,7 +66,7 @@ def nav_controls(prefix: str) -> List[NavSetArg]: def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): print("Current navbar page: ", input.navbar_id()) diff --git a/shiny/api-examples/navset_hidden/app-core.py b/shiny/api-examples/navset_hidden/app-core.py index c1da6c78d..8f537160d 100644 --- a/shiny/api-examples/navset_hidden/app-core.py +++ b/shiny/api-examples/navset_hidden/app-core.py @@ -16,7 +16,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.controller) def _(): ui.update_navs("hidden_tabs", selected="panel" + str(input.controller())) diff --git a/shiny/api-examples/notification_show/app-core.py b/shiny/api-examples/notification_show/app-core.py index e83b6b0c4..2f8e3a056 100644 --- a/shiny/api-examples/notification_show/app-core.py +++ b/shiny/api-examples/notification_show/app-core.py @@ -11,7 +11,7 @@ def server(input: Inputs, output: Outputs, session: Session): ids: list[str] = [] n: int = 0 - @reactive.Effect + @reactive.effect @reactive.event(input.show) def _(): nonlocal ids @@ -21,7 +21,7 @@ def _(): ids.append(id) n += 1 - @reactive.Effect + @reactive.effect @reactive.event(input.remove) def _(): nonlocal ids diff --git a/shiny/api-examples/on_ended/app-core.py b/shiny/api-examples/on_ended/app-core.py index a48a939b5..2c4a4bbf8 100644 --- a/shiny/api-examples/on_ended/app-core.py +++ b/shiny/api-examples/on_ended/app-core.py @@ -13,7 +13,7 @@ def log(): session.on_ended(log) - @reactive.Effect + @reactive.effect @reactive.event(input.close) async def _(): await session.close() diff --git a/shiny/api-examples/remove_accordion_panel/app-core.py b/shiny/api-examples/remove_accordion_panel/app-core.py index b04785498..00e4eb844 100644 --- a/shiny/api-examples/remove_accordion_panel/app-core.py +++ b/shiny/api-examples/remove_accordion_panel/app-core.py @@ -29,7 +29,7 @@ def server(input: Inputs, output: Outputs, session: Session): # Copy the list for user user_choices = [choice for choice in choices] - @reactive.Effect + @reactive.effect @reactive.event(input.remove_panel) def _(): if len(user_choices) == 0: diff --git a/shiny/api-examples/remove_ui/app-core.py b/shiny/api-examples/remove_ui/app-core.py index 61af22a8c..2ae33b8f4 100644 --- a/shiny/api-examples/remove_ui/app-core.py +++ b/shiny/api-examples/remove_ui/app-core.py @@ -7,7 +7,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.rmv) def _(): ui.remove_ui(selector="div:has(> #txt)") diff --git a/shiny/api-examples/remove_ui/app-express.py b/shiny/api-examples/remove_ui/app-express.py index b491abe4b..93656b60e 100644 --- a/shiny/api-examples/remove_ui/app-express.py +++ b/shiny/api-examples/remove_ui/app-express.py @@ -5,7 +5,7 @@ ui.input_text("txt", "Click button above to remove me") -@reactive.Effect +@reactive.effect @reactive.event(input.rmv) def _(): ui.remove_ui(selector="div:has(> #txt)") diff --git a/shiny/api-examples/req/app-core.py b/shiny/api-examples/req/app-core.py index adf764625..e7246df2d 100644 --- a/shiny/api-examples/req/app-core.py +++ b/shiny/api-examples/req/app-core.py @@ -15,7 +15,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Calc + @reactive.calc def safe_click(): req(input.safe()) return input.safe() @@ -29,7 +29,7 @@ def unsafe(): req(input.unsafe()) raise Exception(f"Super secret number of clicks: {str(input.unsafe())}") - @reactive.Effect + @reactive.effect def _(): req(input.unsafe()) print("unsafe clicks:", input.unsafe()) diff --git a/shiny/api-examples/send_custom_message/app-core.py b/shiny/api-examples/send_custom_message/app-core.py index 2b0d610a5..7f199fd70 100644 --- a/shiny/api-examples/send_custom_message/app-core.py +++ b/shiny/api-examples/send_custom_message/app-core.py @@ -19,7 +19,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.submit) async def _(): await session.send_custom_message("append_msg", {"msg": input.msg()}) diff --git a/shiny/api-examples/todo_list/app-core.py b/shiny/api-examples/todo_list/app-core.py index 9ee61fd39..d356ac071 100644 --- a/shiny/api-examples/todo_list/app-core.py +++ b/shiny/api-examples/todo_list/app-core.py @@ -28,7 +28,7 @@ def server(input, output, session): def cleared_tasks(): return f"Finished tasks: {finished_tasks()}" - @reactive.Effect + @reactive.effect @reactive.event(input.add) def add(): counter = task_counter.get() + 1 @@ -47,7 +47,7 @@ def add(): # event within the `add` closure, so nesting the reactive effects # means that we don't have to worry about conflicting with # finish events from other task elements. - @reactive.Effect + @reactive.effect @reactive.event(finish) def iterate_counter(): finished_tasks.set(finished_tasks.get() + 1) @@ -82,12 +82,12 @@ def button_row(): style=css(text_decoration="line-through" if finished() else None), ) - @reactive.Effect + @reactive.effect @reactive.event(input.finish) def finish_task(): finished.set(True) - @reactive.Effect + @reactive.effect @reactive.event(input.clear) def clear_task(): ui.remove_ui(selector=f"div#{session.ns('button_row')}") diff --git a/shiny/api-examples/update_accordion/app-core.py b/shiny/api-examples/update_accordion/app-core.py index 2ae14d64f..0cf6b58e4 100644 --- a/shiny/api-examples/update_accordion/app-core.py +++ b/shiny/api-examples/update_accordion/app-core.py @@ -13,7 +13,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.set_acc) def _(): ui.update_accordion("acc", show=["Section A", "Section C", "Section E"]) diff --git a/shiny/api-examples/update_accordion_panel/app-core.py b/shiny/api-examples/update_accordion_panel/app-core.py index f96fce999..b02d257f6 100644 --- a/shiny/api-examples/update_accordion_panel/app-core.py +++ b/shiny/api-examples/update_accordion_panel/app-core.py @@ -18,7 +18,7 @@ def make_panel(letter: str) -> ui.AccordionPanel: def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.update_panel) def _(): txt = " (updated)" if input.update_panel() else "" diff --git a/shiny/api-examples/update_action_button/app-core.py b/shiny/api-examples/update_action_button/app-core.py index 0b08cc34a..021b65c53 100644 --- a/shiny/api-examples/update_action_button/app-core.py +++ b/shiny/api-examples/update_action_button/app-core.py @@ -14,7 +14,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): req(input.update()) # Updates goButton's label and icon diff --git a/shiny/api-examples/update_action_button/app-express.py b/shiny/api-examples/update_action_button/app-express.py index d93b21ca6..d479524f5 100644 --- a/shiny/api-examples/update_action_button/app-express.py +++ b/shiny/api-examples/update_action_button/app-express.py @@ -11,7 +11,7 @@ ui.input_action_link("goLink", "Go Link") -@reactive.Effect +@reactive.effect def _(): req(input.update()) # Updates goButton's label and icon diff --git a/shiny/api-examples/update_checkbox/app-core.py b/shiny/api-examples/update_checkbox/app-core.py index 036a4034f..be8443fec 100644 --- a/shiny/api-examples/update_checkbox/app-core.py +++ b/shiny/api-examples/update_checkbox/app-core.py @@ -7,7 +7,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): # True if controller is odd, False if even. x_even = input.controller() % 2 == 1 diff --git a/shiny/api-examples/update_checkbox_group/app-core.py b/shiny/api-examples/update_checkbox_group/app-core.py index f4b567ee8..e842532a5 100644 --- a/shiny/api-examples/update_checkbox_group/app-core.py +++ b/shiny/api-examples/update_checkbox_group/app-core.py @@ -12,7 +12,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): x = input.inCheckboxGroup() diff --git a/shiny/api-examples/update_date/app-core.py b/shiny/api-examples/update_date/app-core.py index 25f9be46d..b6785b522 100644 --- a/shiny/api-examples/update_date/app-core.py +++ b/shiny/api-examples/update_date/app-core.py @@ -9,7 +9,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): d = date(2013, 4, input.n()) ui.update_date( diff --git a/shiny/api-examples/update_date/app-express.py b/shiny/api-examples/update_date/app-express.py index cc83e2788..0ab8bbca0 100644 --- a/shiny/api-examples/update_date/app-express.py +++ b/shiny/api-examples/update_date/app-express.py @@ -7,7 +7,7 @@ ui.input_date("inDate", "Input date") -@reactive.Effect +@reactive.effect def _(): d = date(2013, 4, input.n()) ui.update_date( diff --git a/shiny/api-examples/update_date_range/app-core.py b/shiny/api-examples/update_date_range/app-core.py index d7d5cd969..b68cbba6e 100644 --- a/shiny/api-examples/update_date_range/app-core.py +++ b/shiny/api-examples/update_date_range/app-core.py @@ -9,7 +9,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): d = date(2013, 4, input.n()) ui.update_date_range( diff --git a/shiny/api-examples/update_navs/app-core.py b/shiny/api-examples/update_navs/app-core.py index c2d807d0d..d788e0efa 100644 --- a/shiny/api-examples/update_navs/app-core.py +++ b/shiny/api-examples/update_navs/app-core.py @@ -12,7 +12,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): ui.update_navs("inTabset", selected="panel" + str(input.controller())) diff --git a/shiny/api-examples/update_numeric/app-core.py b/shiny/api-examples/update_numeric/app-core.py index 3f601310b..84368a9ba 100644 --- a/shiny/api-examples/update_numeric/app-core.py +++ b/shiny/api-examples/update_numeric/app-core.py @@ -8,7 +8,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): x = input.controller() ui.update_numeric("inNumber", value=x) diff --git a/shiny/api-examples/update_radio_buttons/app-core.py b/shiny/api-examples/update_radio_buttons/app-core.py index 05cf05217..9cf6168d4 100644 --- a/shiny/api-examples/update_radio_buttons/app-core.py +++ b/shiny/api-examples/update_radio_buttons/app-core.py @@ -12,7 +12,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): x = input.inRadioButtons() diff --git a/shiny/api-examples/update_select/app-core.py b/shiny/api-examples/update_select/app-core.py index a1c81149a..237efbbdc 100644 --- a/shiny/api-examples/update_select/app-core.py +++ b/shiny/api-examples/update_select/app-core.py @@ -10,7 +10,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): x = input.inCheckboxGroup() diff --git a/shiny/api-examples/update_selectize/app-core.py b/shiny/api-examples/update_selectize/app-core.py index 4428851af..31b83a501 100644 --- a/shiny/api-examples/update_selectize/app-core.py +++ b/shiny/api-examples/update_selectize/app-core.py @@ -6,7 +6,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect def _(): ui.update_selectize( "x", diff --git a/shiny/api-examples/update_sidebar/app-core.py b/shiny/api-examples/update_sidebar/app-core.py index 4c27ae57a..9dc0a155d 100644 --- a/shiny/api-examples/update_sidebar/app-core.py +++ b/shiny/api-examples/update_sidebar/app-core.py @@ -12,12 +12,12 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect @reactive.event(input.open_sidebar) def _(): ui.update_sidebar("sidebar", show=True) - @reactive.Effect + @reactive.effect @reactive.event(input.close_sidebar) def _(): ui.update_sidebar("sidebar", show=False) diff --git a/shiny/templates/app-templates/dashboard/app-core.py b/shiny/templates/app-templates/dashboard/app-core.py index 341dda69d..6514ba5f4 100644 --- a/shiny/templates/app-templates/dashboard/app-core.py +++ b/shiny/templates/app-templates/dashboard/app-core.py @@ -46,7 +46,7 @@ def make_value_box(penguin): def server(input: Inputs, output: Outputs, session: Session): - @reactive.Calc + @reactive.calc def filtered_df() -> pd.DataFrame: filt_df = df[df["Species"].isin(input.species())] filt_df = filt_df.loc[filt_df["Body Mass (g)"] > input.mass()] diff --git a/shiny/templates/app-templates/dashboard/app-express.py b/shiny/templates/app-templates/dashboard/app-express.py index b5e0f7153..5ad41f0c7 100644 --- a/shiny/templates/app-templates/dashboard/app-express.py +++ b/shiny/templates/app-templates/dashboard/app-express.py @@ -22,7 +22,7 @@ def count_species(df, species): ui.input_checkbox_group("species", "Filter by species", species, selected=species) -@reactive.Calc +@reactive.calc def filtered_df() -> pd.DataFrame: filt_df = df[df["Species"].isin(input.species())] filt_df = filt_df.loc[filt_df["Body Mass (g)"] > input.mass()] diff --git a/shiny/templates/app-templates/multi-page/app-core.py b/shiny/templates/app-templates/multi-page/app-core.py index 271964cbf..01beb0d2f 100644 --- a/shiny/templates/app-templates/multi-page/app-core.py +++ b/shiny/templates/app-templates/multi-page/app-core.py @@ -32,7 +32,7 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Calc() + @reactive.calc() def filtered_data() -> pd.DataFrame: return df.loc[df["account"] == input.account()] diff --git a/shiny/types.py b/shiny/types.py index e44141725..89a22ac68 100644 --- a/shiny/types.py +++ b/shiny/types.py @@ -103,7 +103,7 @@ class SilentException(Exception): - Displayed to the user (as a big red error message) - This happens when the exception is raised from an output context (e.g., :class:`shiny.render.ui`) - Crashes the application - - This happens when the exception is raised from an :func:`shiny.reactive.Effect` + - This happens when the exception is raised from an :func:`shiny.reactive.effect` This exception is used to silently throw inside a reactive context, meaning that execution is paused, and no output is shown to users (or the python console). diff --git a/tests/playwright/deploys/plotly/app.py b/tests/playwright/deploys/plotly/app.py index f3b8b78e5..05b4885fd 100644 --- a/tests/playwright/deploys/plotly/app.py +++ b/tests/playwright/deploys/plotly/app.py @@ -59,7 +59,7 @@ def summary_data(): height="100%", ) - @reactive.Calc + @reactive.calc def filtered_df(): req(summary_data.input_selected_rows()) @@ -116,7 +116,7 @@ def synchronize_size(output_id): def wrapper(func): input = session.get_current_session().input - @reactive.Effect + @reactive.effect def size_updater(): func( input[f".clientdata_output_{output_id}_width"](), diff --git a/tests/playwright/shiny/bugs/0696-resolve-id/app.py b/tests/playwright/shiny/bugs/0696-resolve-id/app.py index e59d7cbde..1ed568c4e 100644 --- a/tests/playwright/shiny/bugs/0696-resolve-id/app.py +++ b/tests/playwright/shiny/bugs/0696-resolve-id/app.py @@ -498,7 +498,7 @@ def _(): # # ## Debug - # @reactive.Effect + # @reactive.effect # def _(): # print("here") # reactive.invalidate_later(1) diff --git a/tests/playwright/shiny/inputs/input_task_button/app.py b/tests/playwright/shiny/inputs/input_task_button/app.py index b5feb7fb8..fa73e8d5a 100644 --- a/tests/playwright/shiny/inputs/input_task_button/app.py +++ b/tests/playwright/shiny/inputs/input_task_button/app.py @@ -37,19 +37,19 @@ async def slow_input_compute(a: int, b: int) -> int: ui.input_task_button("btn_block", "Block compute", label_busy="Blocking...") ui.input_action_button("btn_cancel", "Cancel") - @reactive.Effect + @reactive.effect @reactive.event(input.btn_task, ignore_none=False) def handle_click(): # slow_compute.cancel() slow_compute(input.x(), input.y()) - @reactive.Effect + @reactive.effect @reactive.event(input.btn_block, ignore_none=False) async def handle_click2(): # slow_compute.cancel() await slow_input_compute(input.x(), input.y()) - @reactive.Effect + @reactive.effect @reactive.event(input.btn_cancel) def handle_cancel(): slow_compute.cancel() From 20781bdb3e023b2747c6c106b61679c910b3d4c4 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 28 Feb 2024 00:01:30 -0600 Subject: [PATCH 03/35] Update examples to use lower case reactive.value --- examples/annotation-export/app.py | 2 +- examples/brownian/app.py | 2 +- examples/cpuinfo/app.py | 2 +- examples/dataframe/app.py | 2 +- examples/duckdb/app.py | 2 +- examples/express/shared.py | 2 +- examples/model-score/plotly_streaming.py | 2 +- examples/moduleapp/app.py | 2 +- examples/typed_inputs/app.py | 4 ++-- shiny/api-examples/Module/app-core.py | 2 +- shiny/api-examples/Value/app-core.py | 2 +- shiny/api-examples/Value/app-express.py | 2 +- shiny/api-examples/event/app-core.py | 2 +- shiny/api-examples/todo_list/app-core.py | 6 +++--- shiny/reactive/_core.py | 7 ++++--- tests/playwright/shiny/inputs/input_task_button2/app.py | 2 +- 16 files changed, 22 insertions(+), 21 deletions(-) diff --git a/examples/annotation-export/app.py b/examples/annotation-export/app.py index 0f4e20148..e3835a05e 100644 --- a/examples/annotation-export/app.py +++ b/examples/annotation-export/app.py @@ -39,7 +39,7 @@ def server(input: Inputs, output: Outputs, session: Session): - annotated_data = reactive.Value(weather_df) + annotated_data = reactive.value(weather_df) @reactive.calc def selected_data(): diff --git a/examples/brownian/app.py b/examples/brownian/app.py index e2ae7d3c2..998506e9c 100644 --- a/examples/brownian/app.py +++ b/examples/brownian/app.py @@ -112,7 +112,7 @@ def reactive_smooth(n_samples, smoother, *, filter_none=True): def wrapper(calc): buffer = [] # Ring buffer of capacity `n_samples` - result = reactive.Value(None) # Holds the most recent smoothed result + result = reactive.value(None) # Holds the most recent smoothed result @reactive.effect def _(): diff --git a/examples/cpuinfo/app.py b/examples/cpuinfo/app.py index 3cf4db472..b898672af 100644 --- a/examples/cpuinfo/app.py +++ b/examples/cpuinfo/app.py @@ -109,7 +109,7 @@ def cpu_current(): def server(input: Inputs, output: Outputs, session: Session): - cpu_history = reactive.Value(None) + cpu_history = reactive.value(None) @reactive.calc def cpu_history_with_hold(): diff --git a/examples/dataframe/app.py b/examples/dataframe/app.py index 9ba99a770..e1d527534 100644 --- a/examples/dataframe/app.py +++ b/examples/dataframe/app.py @@ -55,7 +55,7 @@ def light_dark_switcher(dark): def server(input: Inputs, output: Outputs, session: Session): - df: reactive.Value[pd.DataFrame] = reactive.Value() + df: reactive.value[pd.DataFrame] = reactive.value() @reactive.effect def update_df(): diff --git a/examples/duckdb/app.py b/examples/duckdb/app.py index 49898cca8..c18ed967a 100644 --- a/examples/duckdb/app.py +++ b/examples/duckdb/app.py @@ -67,7 +67,7 @@ def load_csv(con, csv_name, table_name): def server(input, output, session): - mod_counter = reactive.Value(0) + mod_counter = reactive.value(0) query_output_server("initial_query", con=con, remove_id="initial_query") diff --git a/examples/express/shared.py b/examples/express/shared.py index 1628fdcb1..945c79c3e 100644 --- a/examples/express/shared.py +++ b/examples/express/shared.py @@ -16,4 +16,4 @@ # This reactive value can be used by multiple sessions; if it is invalidated (in # other words, if the value is changed), it will trigger invalidations in all of # those sessions. - rv = reactive.Value(50) + rv = reactive.value(50) diff --git a/examples/model-score/plotly_streaming.py b/examples/model-score/plotly_streaming.py index 71f4c5733..e18c1439b 100644 --- a/examples/model-score/plotly_streaming.py +++ b/examples/model-score/plotly_streaming.py @@ -83,7 +83,7 @@ def update_plotly_data(): def deduplicate(func): with reactive.isolate(): - rv = reactive.Value(func()) + rv = reactive.value(func()) @reactive.effect def update(): diff --git a/examples/moduleapp/app.py b/examples/moduleapp/app.py index 090eb8695..0454e9420 100644 --- a/examples/moduleapp/app.py +++ b/examples/moduleapp/app.py @@ -18,7 +18,7 @@ def counter_ui(label: str = "Increment counter") -> ui.TagChild: def counter_server( input: Inputs, output: Outputs, session: Session, starting_value: int = 0 ): - count: reactive.Value[int] = reactive.Value(starting_value) + count: reactive.value[int] = reactive.value(starting_value) @reactive.effect @reactive.event(input.button) diff --git a/examples/typed_inputs/app.py b/examples/typed_inputs/app.py index 4759e8b75..bd1035f1b 100644 --- a/examples/typed_inputs/app.py +++ b/examples/typed_inputs/app.py @@ -20,8 +20,8 @@ # But it is possible to specify the type of the input value, by creating a subclass of # Inputs. We'll do that for input.n2() and input.checkbox(): class ShinyInputs(Inputs): - n2: reactive.Value[int] - check: reactive.Value[bool] + n2: reactive.value[int] + check: reactive.value[bool] def server(input: Inputs, output: Outputs, session: Session): diff --git a/shiny/api-examples/Module/app-core.py b/shiny/api-examples/Module/app-core.py index 87a335fe7..81d980340 100644 --- a/shiny/api-examples/Module/app-core.py +++ b/shiny/api-examples/Module/app-core.py @@ -18,7 +18,7 @@ def counter_ui(label: str = "Increment counter") -> ui.TagChild: def counter_server( input: Inputs, output: Outputs, session: Session, starting_value: int = 0 ): - count: reactive.Value[int] = reactive.Value(starting_value) + count: reactive.value[int] = reactive.value(starting_value) @reactive.effect @reactive.event(input.button) diff --git a/shiny/api-examples/Value/app-core.py b/shiny/api-examples/Value/app-core.py index 04479492a..bd854117e 100644 --- a/shiny/api-examples/Value/app-core.py +++ b/shiny/api-examples/Value/app-core.py @@ -10,7 +10,7 @@ def server(input: Inputs, output: Outputs, session: Session): - val = reactive.Value(0) + val = reactive.value(0) @reactive.effect @reactive.event(input.minus) diff --git a/shiny/api-examples/Value/app-express.py b/shiny/api-examples/Value/app-express.py index 80a55e165..bdc227155 100644 --- a/shiny/api-examples/Value/app-express.py +++ b/shiny/api-examples/Value/app-express.py @@ -1,7 +1,7 @@ from shiny import reactive from shiny.express import input, render, ui -val = reactive.Value(0) +val = reactive.value(0) @reactive.effect diff --git a/shiny/api-examples/event/app-core.py b/shiny/api-examples/event/app-core.py index b1a7bd53f..02f7a03b0 100644 --- a/shiny/api-examples/event/app-core.py +++ b/shiny/api-examples/event/app-core.py @@ -36,7 +36,7 @@ def server(input: Inputs, output: Outputs, session: Session): # Update a random number every second - val = reactive.Value(random.randint(0, 1000)) + val = reactive.value(random.randint(0, 1000)) @reactive.effect def _(): diff --git a/shiny/api-examples/todo_list/app-core.py b/shiny/api-examples/todo_list/app-core.py index d356ac071..fed00a20b 100644 --- a/shiny/api-examples/todo_list/app-core.py +++ b/shiny/api-examples/todo_list/app-core.py @@ -21,8 +21,8 @@ def server(input, output, session): - finished_tasks = reactive.Value(0) - task_counter = reactive.Value(0) + finished_tasks = reactive.value(0) + task_counter = reactive.value(0) @render.text def cleared_tasks(): @@ -65,7 +65,7 @@ def task_ui(): @module.server def task_server(input, output, session, text): - finished = reactive.Value(False) + finished = reactive.value(False) @render.ui def button_row(): diff --git a/shiny/reactive/_core.py b/shiny/reactive/_core.py index a20c49968..b961cbcdc 100644 --- a/shiny/reactive/_core.py +++ b/shiny/reactive/_core.py @@ -293,9 +293,10 @@ def lock() -> asyncio.Lock: """ A lock that should be held whenever manipulating the reactive graph. - For example, :func:`~shiny.reactive.lock` makes it safe to set a :class:`~reactive.Value` and call - :func:`~shiny.reactive.flush` from a different :class:`~asyncio.Task` than the one that - is running the Shiny :class:`~shiny.Session`. + For example, :func:`~shiny.reactive.lock` makes it safe to set a + :class:`~reactive.value` and call :func:`~shiny.reactive.flush` from a different + :class:`~asyncio.Task` than the one that is running the Shiny + :class:`~shiny.Session`. """ return _reactive_environment.lock diff --git a/tests/playwright/shiny/inputs/input_task_button2/app.py b/tests/playwright/shiny/inputs/input_task_button2/app.py index a89f51216..03a97b7ff 100644 --- a/tests/playwright/shiny/inputs/input_task_button2/app.py +++ b/tests/playwright/shiny/inputs/input_task_button2/app.py @@ -13,7 +13,7 @@ def button_ui(): @module.server def button_server(input: Inputs, output: Outputs, session: Session): - counter = reactive.Value(0) + counter = reactive.value(0) @render.text def text_counter(): From 9bf972b9fb04debe0b8b76cbf0dced4c9f4e6474 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 28 Feb 2024 17:39:46 -0600 Subject: [PATCH 04/35] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee71d4635..de2bd730d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The sidebar's collapse toggle now has a high `z-index` value to ensure it always appears above elements in the main content area of `ui.layout_sidebar()`. The sidebar overlay also now receives the same high `z-index` on mobile layouts. (#1129) +* Updated example apps to use lower-case versions of `reactive.Calc`->`reactive.calc`, `reactive.Effect`->`reactive.effect`, and `reactive.Value`->`reactive.value`. (#1164) + ### Bug fixes * Fixed `input_task_button` not working in a Shiny module. (#1108) From 0ac8599aa4e33475b83770372b05f331d49c3917 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 1 Mar 2024 13:46:53 -0600 Subject: [PATCH 05/35] Add python 3.12 to test matrix (#1168) --- .github/workflows/pytest.yaml | 4 ++-- examples/cpuinfo/app.py | 2 +- setup.cfg | 1 + shiny/api-examples/download/app-core.py | 2 +- shiny/api-examples/download/app-express.py | 2 +- shiny/api-examples/download_button/app-core.py | 2 +- shiny/api-examples/download_button/app-express.py | 2 +- shiny/api-examples/download_link/app-core.py | 2 +- shiny/api-examples/remove_accordion_panel/app-core.py | 2 +- shiny/api-examples/remove_accordion_panel/app-express.py | 2 +- shiny/quarto.py | 6 +++--- 11 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 6108b3055..651c2b78f 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -16,7 +16,7 @@ jobs: strategy: matrix: # "3.10" must be a string; otherwise it is interpreted as 3.1. - python-version: ["3.11", "3.10", "3.9", "3.8"] + python-version: ["3.12", "3.11", "3.10", "3.9", "3.8"] os: [ubuntu-latest, windows-latest, macOS-latest] fail-fast: false @@ -53,7 +53,7 @@ jobs: if: github.event_name != 'release' strategy: matrix: - python-version: ["3.11", "3.10", "3.9", "3.8"] + python-version: ["3.12", "3.11", "3.10", "3.9", "3.8"] os: [ubuntu-latest] fail-fast: false diff --git a/examples/cpuinfo/app.py b/examples/cpuinfo/app.py index b898672af..7cf934283 100644 --- a/examples/cpuinfo/app.py +++ b/examples/cpuinfo/app.py @@ -50,7 +50,7 @@ text-align: center; } """ - % f"{ncpu*4}em" + % f"{ncpu * 4}em" ), ui.h3("CPU Usage %", class_="mt-2"), ui.layout_sidebar( diff --git a/setup.cfg b/setup.cfg index 04ffd8517..2658862b0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 project_urls = Bug Tracker = https://github.com/posit-dev/py-shiny/issues Documentation = https://shiny.posit.co/py/ diff --git a/shiny/api-examples/download/app-core.py b/shiny/api-examples/download/app-core.py index b88161867..74edfa82d 100644 --- a/shiny/api-examples/download/app-core.py +++ b/shiny/api-examples/download/app-core.py @@ -108,7 +108,7 @@ def download2(): yield buf.getvalue() @render.download( - filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100,999)}.csv" + filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100, 999)}.csv" ) async def download3(): await asyncio.sleep(0.25) diff --git a/shiny/api-examples/download/app-express.py b/shiny/api-examples/download/app-express.py index 748dce7c4..9eaa60647 100644 --- a/shiny/api-examples/download/app-express.py +++ b/shiny/api-examples/download/app-express.py @@ -57,7 +57,7 @@ def download2(): @render.download( label="Download filename", - filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100,999)}.csv", + filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100, 999)}.csv", ) async def download3(): await asyncio.sleep(0.25) diff --git a/shiny/api-examples/download_button/app-core.py b/shiny/api-examples/download_button/app-core.py index 520fa934d..ed3cab273 100644 --- a/shiny/api-examples/download_button/app-core.py +++ b/shiny/api-examples/download_button/app-core.py @@ -11,7 +11,7 @@ def server(input: Inputs, output: Outputs, session: Session): @render.download( - filename=lambda: f"新型-{date.today().isoformat()}-{random.randint(100,999)}.csv" + filename=lambda: f"新型-{date.today().isoformat()}-{random.randint(100, 999)}.csv" ) async def downloadData(): await asyncio.sleep(0.25) diff --git a/shiny/api-examples/download_button/app-express.py b/shiny/api-examples/download_button/app-express.py index 96bd64273..c45fade5d 100644 --- a/shiny/api-examples/download_button/app-express.py +++ b/shiny/api-examples/download_button/app-express.py @@ -6,7 +6,7 @@ @render.download( - filename=lambda: f"新型-{date.today().isoformat()}-{random.randint(100,999)}.csv" + filename=lambda: f"新型-{date.today().isoformat()}-{random.randint(100, 999)}.csv" ) async def downloadData(): await asyncio.sleep(0.25) diff --git a/shiny/api-examples/download_link/app-core.py b/shiny/api-examples/download_link/app-core.py index 408b38761..f8bc61ac2 100644 --- a/shiny/api-examples/download_link/app-core.py +++ b/shiny/api-examples/download_link/app-core.py @@ -11,7 +11,7 @@ def server(input: Inputs, output: Outputs, session: Session): @render.download( - filename=lambda: f"新型-{date.today().isoformat()}-{random.randint(100,999)}.csv" + filename=lambda: f"新型-{date.today().isoformat()}-{random.randint(100, 999)}.csv" ) async def downloadData(): await asyncio.sleep(0.25) diff --git a/shiny/api-examples/remove_accordion_panel/app-core.py b/shiny/api-examples/remove_accordion_panel/app-core.py index 00e4eb844..fa51fd56d 100644 --- a/shiny/api-examples/remove_accordion_panel/app-core.py +++ b/shiny/api-examples/remove_accordion_panel/app-core.py @@ -37,7 +37,7 @@ def _(): return # Remove panel - ui.remove_accordion_panel("acc", f"Section { user_choices.pop() }") + ui.remove_accordion_panel("acc", f"Section {user_choices.pop()}") label = "No more panels to remove!" if len(user_choices) > 0: diff --git a/shiny/api-examples/remove_accordion_panel/app-express.py b/shiny/api-examples/remove_accordion_panel/app-express.py index 04c9a087a..d01f643e6 100644 --- a/shiny/api-examples/remove_accordion_panel/app-express.py +++ b/shiny/api-examples/remove_accordion_panel/app-express.py @@ -30,7 +30,7 @@ def _(): ui.notification_show("No more panels to remove!") return - ui.remove_accordion_panel("acc", f"Section { user_choices.pop() }") + ui.remove_accordion_panel("acc", f"Section {user_choices.pop()}") label = "No more panels to remove!" if len(user_choices) > 0: diff --git a/shiny/quarto.py b/shiny/quarto.py index 394c1d3f9..5f0d5efc2 100644 --- a/shiny/quarto.py +++ b/shiny/quarto.py @@ -74,11 +74,11 @@ def convert_code_cells_to_app_py(json_file: str | Path, app_file: str | Path) -> from pathlib import Path from shiny import App, Inputs, Outputs, Session, ui -{ "".join(global_code_cell_texts) } +{"".join(global_code_cell_texts)} def server(input: Inputs, output: Outputs, session: Session) -> None: -{ "".join(session_code_cell_texts) } +{"".join(session_code_cell_texts)} return None @@ -87,7 +87,7 @@ def server(input: Inputs, output: Outputs, session: Session) -> None: _static_assets = {{"/" + sa: Path(__file__).parent / sa for sa in _static_assets}} app = App( - Path(__file__).parent / "{ data["html_file"] }", + Path(__file__).parent / "{data["html_file"]}", server, static_assets=_static_assets, ) From 102547121797d9bed97806c2246cdf336a8dff32 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 1 Mar 2024 14:07:19 -0600 Subject: [PATCH 06/35] Consolidate component dependency assets into a single dependency that gets included more generally (#1160) --- shiny/ui/_accordion.py | 4 ++-- shiny/ui/_card.py | 4 ++-- shiny/ui/_html_deps_shinyverse.py | 19 +++++-------------- shiny/ui/_input_check_radio.py | 4 ++-- shiny/ui/_input_task_button.py | 5 ++--- shiny/ui/_layout.py | 4 ++-- shiny/ui/_layout_columns.py | 4 ++-- shiny/ui/_navs.py | 4 ++-- shiny/ui/_page.py | 4 ++-- shiny/ui/_sidebar.py | 4 ++-- shiny/ui/_web_component.py | 4 ++-- 11 files changed, 25 insertions(+), 35 deletions(-) diff --git a/shiny/ui/_accordion.py b/shiny/ui/_accordion.py index 4956b8e31..454ebefd6 100644 --- a/shiny/ui/_accordion.py +++ b/shiny/ui/_accordion.py @@ -9,7 +9,7 @@ from .._utils import drop_none, private_random_id from ..session import require_active_session from ..types import MISSING, MISSING_TYPE -from ._html_deps_shinyverse import components_dependency +from ._html_deps_shinyverse import components_dependencies from ._tag import consolidate_attrs from .css._css_unit import CssUnit, as_css_unit @@ -287,7 +287,7 @@ def accordion( # just for ease of identifying autoclosing client-side {"class": "autoclose"} if not multiple else None, binding_class_value, - components_dependency(), + components_dependencies(), attrs, *panel_tags, ) diff --git a/shiny/ui/_card.py b/shiny/ui/_card.py index 4950bcfc8..31979b7a8 100644 --- a/shiny/ui/_card.py +++ b/shiny/ui/_card.py @@ -18,7 +18,7 @@ from .._docstring import add_example from .._utils import private_random_id from ..types import MISSING, MISSING_TYPE -from ._html_deps_shinyverse import components_dependency +from ._html_deps_shinyverse import components_dependencies from ._tag import consolidate_attrs from ._tooltip import tooltip from .css._css_unit import CssUnit, as_css_padding, as_css_unit @@ -148,7 +148,7 @@ def _card_impl( *children, attrs, _full_screen_toggle(attrs["id"]) if full_screen else None, - components_dependency(), + components_dependencies(), _card_js_init(), ) if fill: diff --git a/shiny/ui/_html_deps_shinyverse.py b/shiny/ui/_html_deps_shinyverse.py index 57348cecd..2fe42cbea 100644 --- a/shiny/ui/_html_deps_shinyverse.py +++ b/shiny/ui/_html_deps_shinyverse.py @@ -38,7 +38,7 @@ def fill_dependency() -> HTMLDependency: # -- bslib ------------------------- -def components_dependency() -> HTMLDependency: +def components_dependencies() -> HTMLDependency: return HTMLDependency( name="bslib-components", version=bslib_version, @@ -46,18 +46,9 @@ def components_dependency() -> HTMLDependency: "package": "shiny", "subdir": f"{_components_path}", }, - script={"src": "components.min.js"}, + script=[ + {"src": "components.min.js"}, + {"src": "web-components.min.js", "type": "module"}, + ], stylesheet={"href": "components.css"}, ) - - -def web_component_dependency() -> HTMLDependency: - return HTMLDependency( - name="bslib-web-components", - version=bslib_version, - source={ - "package": "shiny", - "subdir": f"{_components_path}", - }, - script={"src": "web-components.min.js", "type": "module"}, - ) diff --git a/shiny/ui/_input_check_radio.py b/shiny/ui/_input_check_radio.py index ffaecc098..5f52f7cd6 100644 --- a/shiny/ui/_input_check_radio.py +++ b/shiny/ui/_input_check_radio.py @@ -12,7 +12,7 @@ from .._docstring import add_example from .._namespaces import resolve_id -from ._html_deps_shinyverse import components_dependency +from ._html_deps_shinyverse import components_dependencies from ._utils import shiny_input_label # Canonical format for representing select options. @@ -160,7 +160,7 @@ def _bslib_input_checkbox( ), class_=class_, ), - components_dependency(), + components_dependencies(), class_="form-group shiny-input-container", style=css(width=width), ) diff --git a/shiny/ui/_input_task_button.py b/shiny/ui/_input_task_button.py index 20acf1ebe..76229e915 100644 --- a/shiny/ui/_input_task_button.py +++ b/shiny/ui/_input_task_button.py @@ -15,7 +15,7 @@ from ..reactive._extended_task import ExtendedTask from ..reactive._reactives import effect from ._html_deps_py_shiny import spin_dependency -from ._html_deps_shinyverse import components_dependency, web_component_dependency +from ._html_deps_shinyverse import components_dependencies P = ParamSpec("P") R = TypeVar("R") @@ -146,8 +146,7 @@ def input_task_button( *args, case="ready", ), - components_dependency(), - web_component_dependency(), + components_dependencies(), spin_dependency(), id=resolve_id(id), type="button", diff --git a/shiny/ui/_layout.py b/shiny/ui/_layout.py index 7a877f2ea..4d44cca6f 100644 --- a/shiny/ui/_layout.py +++ b/shiny/ui/_layout.py @@ -7,7 +7,7 @@ from .._deprecated import warn_deprecated from .._docstring import add_example from ..types import MISSING, MISSING_TYPE -from ._html_deps_shinyverse import components_dependency +from ._html_deps_shinyverse import components_dependencies from ._tag import consolidate_attrs from ._utils import is_01_scalar from .css import CssUnit, as_css_unit @@ -155,7 +155,7 @@ def layout_column_wrap( }, attrs, *wrap_all_in_gap_spaced_container(children, fillable), - components_dependency(), + components_dependencies(), ) if fill: tag = as_fill_item(tag) diff --git a/shiny/ui/_layout_columns.py b/shiny/ui/_layout_columns.py index 19ba3eac6..d2de2d38e 100644 --- a/shiny/ui/_layout_columns.py +++ b/shiny/ui/_layout_columns.py @@ -7,7 +7,7 @@ from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, css from .._docstring import add_example -from ._html_deps_shinyverse import web_component_dependency +from ._html_deps_shinyverse import components_dependencies from ._layout import wrap_all_in_gap_spaced_container from ._tag import consolidate_attrs from .css import CssUnit, as_css_unit @@ -135,7 +135,7 @@ def layout_columns( attrs, row_heights_attrs(row_heights), *wrap_all_in_gap_spaced_container(children, fillable, "bslib-grid-item"), - web_component_dependency(), + components_dependencies(), ) # Apply fill to the outer layout (fillable is applied to the children) diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index 7f9674b06..4f2b74e1e 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -34,7 +34,7 @@ from ..types import NavSetArg from ._bootstrap import column, row from ._card import CardItem, WrapperCallable, card, card_body, card_footer, card_header -from ._html_deps_shinyverse import components_dependency +from ._html_deps_shinyverse import components_dependencies from ._sidebar import Sidebar, layout_sidebar from .css import CssUnit, as_css_padding, as_css_unit from .fill import as_fill_item, as_fillable_container @@ -215,7 +215,7 @@ def nav_spacer() -> NavPanel: See :func:`~shiny.ui.nav_panel` """ - return NavPanel(tags.li(components_dependency(), class_="bslib-nav-spacer")) + return NavPanel(tags.li(components_dependencies(), class_="bslib-nav-spacer")) class NavMenu: diff --git a/shiny/ui/_page.py b/shiny/ui/_page.py index d0d454853..e563b0794 100644 --- a/shiny/ui/_page.py +++ b/shiny/ui/_page.py @@ -33,7 +33,7 @@ from ._bootstrap import panel_title from ._html_deps_external import bootstrap_deps from ._html_deps_py_shiny import page_output_dependency -from ._html_deps_shinyverse import components_dependency +from ._html_deps_shinyverse import components_dependencies from ._navs import NavMenu, NavPanel, navset_bar from ._sidebar import Sidebar, SidebarOpen, layout_sidebar from ._tag import consolidate_attrs @@ -325,7 +325,7 @@ def page_fillable( {"class": "bslib-flow-mobile"} if not fillable_mobile else None, attrs, *children, - components_dependency(), + components_dependencies(), title=title, lang=lang, ) diff --git a/shiny/ui/_sidebar.py b/shiny/ui/_sidebar.py index dd287a4cc..bff18727e 100644 --- a/shiny/ui/_sidebar.py +++ b/shiny/ui/_sidebar.py @@ -23,7 +23,7 @@ from ..session import require_active_session from ..types import MISSING, MISSING_TYPE from ._card import CardItem -from ._html_deps_shinyverse import components_dependency +from ._html_deps_shinyverse import components_dependencies from ._tag import consolidate_attrs, trinary from ._utils import css_no_sub from .css import CssUnit, as_css_padding, as_css_unit @@ -652,7 +652,7 @@ def layout_sidebar( {"class": "sidebar-collapsed"} if sidebar.open().desktop == "closed" else None, main, sidebar, - components_dependency(), + components_dependencies(), _sidebar_init_js(), data_bslib_sidebar_init="true", data_open_desktop=sidebar.open().desktop, diff --git a/shiny/ui/_web_component.py b/shiny/ui/_web_component.py index 1d2ed36cd..03b7a8893 100644 --- a/shiny/ui/_web_component.py +++ b/shiny/ui/_web_component.py @@ -2,7 +2,7 @@ from htmltools import Tag, TagAttrs, TagAttrValue, TagChild -from ._html_deps_shinyverse import web_component_dependency +from ._html_deps_shinyverse import components_dependencies def web_component( @@ -12,7 +12,7 @@ def web_component( ) -> Tag: return Tag( tag_name, - web_component_dependency(), + components_dependencies(), *args, _add_ws=False, **kwargs, From d10236c14e2c3e3394786057aeefb2ba17805ad3 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 1 Mar 2024 14:13:05 -0600 Subject: [PATCH 07/35] Remove redundant checks in GHA workflow (#1169) --- .github/workflows/pytest.yaml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 651c2b78f..f7c9085d2 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -31,22 +31,27 @@ jobs: - name: Run unit tests if: steps.install.outcome == 'success' && (success() || failure()) run: | - make test + make check-tests - name: Type check with pyright if: steps.install.outcome == 'success' && (success() || failure()) run: | - make pyright + make check-types - name: Lint with flake8 if: steps.install.outcome == 'success' && (success() || failure()) run: | - make lint + make check-lint - - name: black and isort + - name: black if: steps.install.outcome == 'success' && (success() || failure()) run: | - make check + make check-black + + - name: isort + if: steps.install.outcome == 'success' && (success() || failure()) + run: | + make check-isort playwright-shiny: runs-on: ${{ matrix.os }} From af891a638f65c3a74a6fd4e4b8fbfb839854442f Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 1 Mar 2024 15:26:26 -0600 Subject: [PATCH 08/35] Add `has_docstring` param to `expressify()` (#1163) --- CHANGELOG.md | 2 ++ .../expressify_decorator/_expressify.py | 34 ++++++++++++++----- .../_node_transformers.py | 17 +++++++++- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de2bd730d..22c8c7de4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Updated example apps to use lower-case versions of `reactive.Calc`->`reactive.calc`, `reactive.Effect`->`reactive.effect`, and `reactive.Value`->`reactive.value`. (#1164) +* Closed #1081: The `@expressify()` function now has an option `has_docstring`. This allows the decorator to be used with functions that contain a docstring. (#1163) + ### Bug fixes * Fixed `input_task_button` not working in a Shiny module. (#1108) diff --git a/shiny/express/expressify_decorator/_expressify.py b/shiny/express/expressify_decorator/_expressify.py index 440cae86d..264318df0 100644 --- a/shiny/express/expressify_decorator/_expressify.py +++ b/shiny/express/expressify_decorator/_expressify.py @@ -97,11 +97,15 @@ def expressify(fn: TFunc) -> TFunc: ... @overload -def expressify() -> Callable[[TFunc], TFunc]: ... +def expressify(*, has_docstring: bool = False) -> Callable[[TFunc], TFunc]: ... @no_example() -def expressify(fn: TFunc | None = None) -> TFunc | Callable[[TFunc], TFunc]: +def expressify( + fn: TFunc | None = None, + *, + has_docstring: bool = False, +) -> TFunc | Callable[[TFunc], TFunc]: """ Decorate a function so that output is captured as in Shiny Express @@ -115,6 +119,10 @@ def expressify(fn: TFunc | None = None) -> TFunc | Callable[[TFunc], TFunc]: ---------- fn : The function to decorate. If not provided, this is a decorator factory. + has_docstring : + Whether the function has a docstring. Set this to `True` if the function to + decorate has a docstring. This tells `expressify()` to *not* capture the + docstring and display it in the UI. Returns ------- @@ -133,13 +141,13 @@ def decorator(fn: TFunc) -> TFunc: # Disable code caching on Pyodide due to bug in hashing bytecode in 0.22.1. # When Pyodide is updated to a newer version, this will be not be needed. # https://github.com/posit-dev/py-shiny/issues/1042#issuecomment-1901945787 - fcode = _transform_body(cast(types.FunctionType, fn)) + fcode = _transform_body(cast(types.FunctionType, fn), has_docstring) else: if fn.__code__ in code_cache: fcode = code_cache[fn.__code__] else: # Save for next time - fcode = _transform_body(cast(types.FunctionType, fn)) + fcode = _transform_body(cast(types.FunctionType, fn), has_docstring) code_cache[fn.__code__] = fcode # Create a new function from the code object @@ -169,7 +177,10 @@ def decorator(fn: TFunc) -> TFunc: return decorator -def _transform_body(fn: types.FunctionType) -> types.CodeType: +def _transform_body( + fn: types.FunctionType, + has_docstring: bool = False, +) -> types.CodeType: # The approach we take here is much more complicated than what you'd expect. # # The simple approach is to use ast.parse() to get an AST for the function, @@ -197,10 +208,14 @@ def _transform_body(fn: types.FunctionType) -> types.CodeType: " This should never happen, please file an issue!" ) + # A wrapper for _transform_function_ast that conveys the value of has_docstring. + def transform_function_ast_local(node: ast.AST) -> ast.AST: + return _transform_function_ast(node, has_docstring) + tft = TargetFunctionTransformer( fn, # If we find `fn` in the AST, use transform its body to use displayhook - _transform_function_ast, + transform_function_ast_local, ) new_ast = tft.visit(parsed_ast) @@ -248,10 +263,13 @@ def read_ast(filename: str) -> ast.Module | None: return ast.parse("".join(lines), filename=filename) -def _transform_function_ast(node: ast.AST) -> ast.AST: +def _transform_function_ast(node: ast.AST, has_docstring: bool = False) -> ast.AST: if not isinstance(node, ast.FunctionDef): return node - func_node = cast(ast.FunctionDef, FuncBodyDisplayHookTransformer().visit(node)) + func_node = cast( + ast.FunctionDef, + FuncBodyDisplayHookTransformer(has_docstring).visit(node), + ) func_node.body = [ DisplayFuncsTransformer().visit(child) for child in func_node.body ] diff --git a/shiny/express/expressify_decorator/_node_transformers.py b/shiny/express/expressify_decorator/_node_transformers.py index f7e362e0e..b80333f7e 100644 --- a/shiny/express/expressify_decorator/_node_transformers.py +++ b/shiny/express/expressify_decorator/_node_transformers.py @@ -59,8 +59,10 @@ class FuncBodyDisplayHookTransformer(TopLevelTransformer): FunctionDef it finds will be transformed. """ - def __init__(self): + def __init__(self, has_docstring: bool = False): self.saw_func = False + self.has_docstring = has_docstring + self.has_visited_first_node = False def visit_FunctionDef(self, node: ast.FunctionDef) -> object: """ @@ -78,6 +80,19 @@ def visit_Expr(self, node: ast.Expr) -> object: Note that we don't descend into the node, so child expressions will not be affected. """ + if not self.has_visited_first_node: + self.has_visited_first_node = True + + # If the first node is meant to be treated as a docstring, first make sure + # it actually is a static string, and return it without wrapping it with the + # displayhook. + if ( + self.has_docstring + and isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + ): + return node + return ast.Expr( value=ast.Call( func=ast.Name(id=sys_alias, ctx=ast.Load()), From 84f872b0c423b8a362fcf19419a48feb04bd2a76 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 1 Mar 2024 15:49:40 -0600 Subject: [PATCH 09/35] Allow serving static assets in Shiny Express apps (#1170) --- CHANGELOG.md | 4 ++ docs/_quartodoc-express.yml | 4 ++ shiny/_app.py | 8 ++- shiny/_utils.py | 8 +++ shiny/express/__init__.py | 3 +- shiny/express/_mock_session.py | 22 +++++-- shiny/express/_run.py | 114 +++++++++++++++++++++++++++++++-- shiny/reactive/_reactives.py | 8 +-- shiny/render/_render.py | 4 +- 9 files changed, 157 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c8c7de4..315365785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `ui.sidebar(open=)` now accepts a dictionary with keys `desktop` and `mobile`, allowing you to independently control the initial state of the sidebar at desktop and mobile screen sizes. (#1129) +* Closed #984: In Shiny Express apps, if there is a `"www"` subdirectory in the app's directory, Shiny will serve the files in that directory as static assets, mounted at `/`. (#1170) + +* For Shiny Express apps, added `express.app_opts()`, which allows setting application-level options, like `static_assets` and `debug`. (#1170) + ### Other changes * `@render.data_frame` now properly fills its container by default. (#1126) diff --git a/docs/_quartodoc-express.yml b/docs/_quartodoc-express.yml index e27a6d754..410a609cf 100644 --- a/docs/_quartodoc-express.yml +++ b/docs/_quartodoc-express.yml @@ -177,6 +177,10 @@ quartodoc: # - express.ui.fill.remove_all_fill # - express.ui.css.as_css_unit # - express.ui.css.as_css_padding + - title: Application-level settings + desc: Set application-level settings. + contents: + - express.app_opts - title: Express developer tooling desc: contents: diff --git a/shiny/_app.py b/shiny/_app.py index 5432a66ed..8a23c5fca 100644 --- a/shiny/_app.py +++ b/shiny/_app.py @@ -29,7 +29,7 @@ from ._connection import Connection, StarletteConnection from ._error import ErrorMiddleware from ._shinyenv import is_pyodide -from ._utils import guess_mime_type, is_async_callable +from ._utils import guess_mime_type, is_async_callable, sort_keys_length from .html_dependencies import jquery_deps, require_deps, shiny_deps from .http_staticfiles import FileResponse, StaticFiles from .session._session import Inputs, Outputs, Session, session_context @@ -149,6 +149,12 @@ def __init__( ) static_assets = {"/": Path(static_assets)} + # Sort the static assets keys by descending length, to ensure that the most + # specific paths are mounted first. Suppose there are mounts "/foo" and "/". If + # "/" is first in the dict, then requests to "/foo/file.html" will never reach + # the second mount. We need to put "/foo" first and "/" second so that it will + # actually look in the "/foo" mount. + static_assets = sort_keys_length(static_assets, descending=True) self._static_assets: dict[str, Path] = static_assets self._sessions: dict[str, Session] = {} diff --git a/shiny/_utils.py b/shiny/_utils.py index 99bf761d9..5357c91a5 100644 --- a/shiny/_utils.py +++ b/shiny/_utils.py @@ -17,6 +17,8 @@ CancelledError = asyncio.CancelledError +T = TypeVar("T") + # ============================================================================== # Misc utility functions @@ -49,6 +51,12 @@ def lists_to_tuples(x: object) -> object: return x +# Given a dictionary, return a new dictionary with the keys sorted by length. +def sort_keys_length(x: dict[str, T], descending: bool = False) -> dict[str, T]: + sorted_keys = sorted(x.keys(), key=len, reverse=descending) + return {key: x[key] for key in sorted_keys} + + def guess_mime_type( url: "str | os.PathLike[str]", default: str = "application/octet-stream", diff --git a/shiny/express/__init__.py b/shiny/express/__init__.py index ca72a1858..aa9cd1fa0 100644 --- a/shiny/express/__init__.py +++ b/shiny/express/__init__.py @@ -15,7 +15,7 @@ output_args, # pyright: ignore[reportUnusedImport] suspend_display, # pyright: ignore[reportUnusedImport] - Deprecated ) -from ._run import wrap_express_app +from ._run import app_opts, wrap_express_app from .expressify_decorator import expressify @@ -25,6 +25,7 @@ "output", "session", "is_express_app", + "app_opts", "wrap_express_app", "ui", "expressify", diff --git a/shiny/express/_mock_session.py b/shiny/express/_mock_session.py index 6eab21e9e..9478dc078 100644 --- a/shiny/express/_mock_session.py +++ b/shiny/express/_mock_session.py @@ -1,22 +1,34 @@ from __future__ import annotations import textwrap -from typing import Awaitable, Callable, cast +from typing import TYPE_CHECKING, Awaitable, Callable, cast from .._namespaces import Root from ..session import Inputs, Outputs, Session -all = ("MockSession",) +if TYPE_CHECKING: + from ._run import AppOpts +all = ("ExpressMockSession",) + + +class ExpressMockSession: + """ + A very bare-bones mock session class that is used only in shiny.express's UI + rendering phase. + + Note that this class is also used to hold application-level options that are set via + the `app_opts()` function. + """ -# A very bare-bones mock session class that is used only in shiny.express's UI rendering -# phase. -class MockSession: def __init__(self): self.ns = Root self.input = Inputs({}) self.output = Outputs(cast(Session, self), self.ns, {}, {}) + # Application-level (not session-level) options that may be set via app_opts(). + self.app_opts: AppOpts = {} + # This is needed so that Outputs don't throw an error. def _is_hidden(self, name: str) -> bool: return False diff --git a/shiny/express/_run.py b/shiny/express/_run.py index 17cdbbf21..272b18537 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -1,6 +1,7 @@ from __future__ import annotations import ast +import os import sys from pathlib import Path from typing import cast @@ -9,8 +10,10 @@ from .._app import App from .._docstring import no_example -from ..session import Inputs, Outputs, Session, session_context -from ._mock_session import MockSession +from .._typing_extensions import NotRequired, TypedDict +from ..session import Inputs, Outputs, Session, get_current_session, session_context +from ..types import MISSING, MISSING_TYPE +from ._mock_session import ExpressMockSession from ._recall_context import RecallContextManager from .expressify_decorator._func_displayhook import _expressify_decorator_function_def from .expressify_decorator._node_transformers import ( @@ -18,7 +21,10 @@ expressify_decorator_func_name, ) -__all__ = ("wrap_express_app",) +__all__ = ( + "app_opts", + "wrap_express_app", +) @no_example() @@ -35,8 +41,10 @@ def wrap_express_app(file: Path) -> App: : A :class:`shiny.App` object. """ + try: - with session_context(cast(Session, MockSession())): + mock_session = ExpressMockSession() + with session_context(cast(Session, mock_session)): # We tagify here, instead of waiting for the App object to do it when it wraps # the UI in a HTMLDocument and calls render() on it. This is because # AttributeErrors can be thrown during the tagification process, and we need to @@ -59,7 +67,20 @@ def express_server(input: Inputs, output: Outputs, session: Session): traceback.print_exception(*sys.exc_info()) raise - app = App(app_ui, express_server) + app_opts: AppOpts = {} + + www_dir = file.parent / "www" + if www_dir.is_dir(): + app_opts["static_assets"] = {"/": www_dir} + + app_opts = _merge_app_opts(app_opts, mock_session.app_opts) + app_opts = _normalize_app_opts(app_opts, file.parent) + + app = App( + app_ui, + express_server, + **app_opts, + ) return app @@ -164,3 +185,86 @@ def __getattr__(self, name: str): "Tried to access `input`, but it was not imported. " "Perhaps you need `from shiny.express import input`?" ) + + +class AppOpts(TypedDict): + static_assets: NotRequired[dict[str, Path]] + debug: NotRequired[bool] + + +@no_example() +def app_opts( + static_assets: ( + str | os.PathLike[str] | dict[str, str | Path] | MISSING_TYPE + ) = MISSING, + debug: bool | MISSING_TYPE = MISSING, +): + """ + Set App-level options in Shiny Express + + This function sets application-level options for Shiny Express. These options are + the same as those from the :class:`shiny.App` constructor. + + Parameters + ---------- + static_assets + Static files to be served by the app. If this is a string or Path object, it + must be a directory, and it will be mounted at `/`. If this is a dictionary, + each key is a mount point and each value is a file or directory to be served at + that mount point. In Shiny Express, if there is a `www` subdirectory of the + directory containing the app file, it will automatically be mounted at `/`, even + without needing to set the option here. + debug + Whether to enable debug mode. + """ + + # Store these options only if we're in the UI-rendering phase of Shiny Express. + mock_session = get_current_session() + if not isinstance(mock_session, ExpressMockSession): + return + + if not isinstance(static_assets, MISSING_TYPE): + if isinstance(static_assets, (str, os.PathLike)): + static_assets = {"/": Path(static_assets)} + + # Convert string values to Paths. (Need new var name to help type checker.) + static_assets_paths = {k: Path(v) for k, v in static_assets.items()} + + mock_session.app_opts["static_assets"] = static_assets_paths + + if not isinstance(debug, MISSING_TYPE): + mock_session.app_opts["debug"] = debug + + +def _merge_app_opts(app_opts: AppOpts, app_opts_new: AppOpts) -> AppOpts: + """ + Merge a set of app options into an existing set of app options. The values from + `app_opts_new` take precedence. This will alter the original app_opts and return it. + """ + + # We can't just do a `app_opts.update(app_opts_new)` because we need to handle the + # case where app_opts["static_assets"] and app_opts_new["static_assets"] are + # dictionaries, and we need to merge those dictionaries. + if "static_assets" in app_opts and "static_assets" in app_opts_new: + app_opts["static_assets"].update(app_opts_new["static_assets"]) + elif "static_assets" in app_opts_new: + app_opts["static_assets"] = app_opts_new["static_assets"].copy() + + if "debug" in app_opts_new: + app_opts["debug"] = app_opts_new["debug"] + + return app_opts + + +def _normalize_app_opts(app_opts: AppOpts, parent_dir: Path) -> AppOpts: + """ + Normalize the app options, ensuring that all paths in static_assets are absolute. + Modifies the original in place. + """ + if "static_assets" in app_opts: + for mount_point, path in app_opts["static_assets"].items(): + if not path.is_absolute(): + path = parent_dir / path + app_opts["static_assets"][mount_point] = path + + return app_opts diff --git a/shiny/reactive/_reactives.py b/shiny/reactive/_reactives.py index cc75a419d..f82658d09 100644 --- a/shiny/reactive/_reactives.py +++ b/shiny/reactive/_reactives.py @@ -477,7 +477,7 @@ def __init__( self.__name__ = fn.__name__ self.__doc__ = fn.__doc__ - from ..express._mock_session import MockSession + from ..express._mock_session import ExpressMockSession from ..render.renderer import Renderer if isinstance(fn, Renderer): @@ -513,9 +513,9 @@ def __init__( # could be None if outside of a session). session = get_current_session() - if isinstance(session, MockSession): - # If we're in a MockSession, then don't actually set up this Effect -- we - # don't want it to try to run later. + if isinstance(session, ExpressMockSession): + # If we're in an ExpressMockSession, then don't actually set up this effect + # -- we don't want it to try to run later. return self._session = session diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 20f793a54..cdf2a1cfd 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -30,7 +30,7 @@ from .._docstring import add_example, no_example from .._namespaces import ResolvedId from .._typing_extensions import Self -from ..express._mock_session import MockSession +from ..express._mock_session import ExpressMockSession from ..session import get_current_session, require_active_session from ..session._session import DownloadHandler, DownloadInfo from ..types import MISSING, MISSING_TYPE, ImgData @@ -690,7 +690,7 @@ def url() -> str: # not being None is because in Express, when the UI is rendered, this function # `render.download()()` called once before any sessions have been started. session = get_current_session() - if session is not None and not isinstance(session, MockSession): + if session is not None and not isinstance(session, ExpressMockSession): session._downloads[self.output_id] = DownloadInfo( filename=self.filename, content_type=self.media_type, From 9084a130ac458a6e7cbec9137b27a2c6f27753cf Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 1 Mar 2024 16:19:50 -0600 Subject: [PATCH 10/35] Update doc section description --- docs/_quartodoc-express.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_quartodoc-express.yml b/docs/_quartodoc-express.yml index 410a609cf..6dfeb31f1 100644 --- a/docs/_quartodoc-express.yml +++ b/docs/_quartodoc-express.yml @@ -178,7 +178,7 @@ quartodoc: # - express.ui.css.as_css_unit # - express.ui.css.as_css_padding - title: Application-level settings - desc: Set application-level settings. + desc: contents: - express.app_opts - title: Express developer tooling From faa7648f1ed08a449b0d182c29ff2313a5a8df58 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 1 Mar 2024 23:07:48 -0600 Subject: [PATCH 11/35] Automatically load `globals.py` for Express apps (#1172) --- CHANGELOG.md | 2 ++ shiny/_utils.py | 46 +++++++++++++++++++++++++++++++++++++++++++ shiny/express/_run.py | 6 ++++++ 3 files changed, 54 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 315365785..95e32db89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * For Shiny Express apps, added `express.app_opts()`, which allows setting application-level options, like `static_assets` and `debug`. (#1170) +* Closed #1079: For Shiny Express apps, automatically run a `globals.py` file in the same directory as the app file, if it exists. The code in `globals.py` will be run with the session context set to `None`. (#1172) + ### Other changes * `@render.data_frame` now properly fills its container by default. (#1126) diff --git a/shiny/_utils.py b/shiny/_utils.py index 5357c91a5..f9a180b28 100644 --- a/shiny/_utils.py +++ b/shiny/_utils.py @@ -10,7 +10,11 @@ import random import secrets import socketserver +import sys import tempfile +import warnings +from pathlib import Path +from types import ModuleType from typing import Any, Awaitable, Callable, Generator, Optional, TypeVar, cast from ._typing_extensions import ParamSpec, TypeGuard @@ -537,3 +541,45 @@ def package_dir(package: str) -> str: if pkg_file is None: raise RuntimeError(f"Could not find package dir for '{package}'") return os.path.dirname(pkg_file) + + +class ModuleImportWarning(ImportWarning): + pass + + +warnings.simplefilter("always", ModuleImportWarning) + + +def import_module_from_path(module_name: str, path: Path): + import importlib.util + + if not path.is_absolute(): + raise ValueError("Path must be absolute") + + spec = importlib.util.spec_from_file_location(module_name, path) + if spec is None or spec.loader is None: + raise ImportError(f"Could not import module {module_name} from path: {path}") + + module = importlib.util.module_from_spec(spec) + + prev_module: ModuleType | None = None + + if module_name in sys.modules: + prev_module = sys.modules[module_name] + warnings.warn( + f"A module named {module_name} is already loaded, but is being loaded again.", + ModuleImportWarning, + stacklevel=1, + ) + + sys.modules[module_name] = module + + try: + spec.loader.exec_module(module) + except Exception: + if prev_module is None: + del sys.modules[module_name] + else: + sys.modules[module_name] = prev_module + raise + return module diff --git a/shiny/express/_run.py b/shiny/express/_run.py index 272b18537..be8fa4314 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -11,6 +11,7 @@ from .._app import App from .._docstring import no_example from .._typing_extensions import NotRequired, TypedDict +from .._utils import import_module_from_path from ..session import Inputs, Outputs, Session, get_current_session, session_context from ..types import MISSING, MISSING_TYPE from ._mock_session import ExpressMockSession @@ -43,6 +44,11 @@ def wrap_express_app(file: Path) -> App: """ try: + globals_file = file.parent / "globals.py" + if globals_file.is_file(): + with session_context(None): + import_module_from_path("globals", globals_file) + mock_session = ExpressMockSession() with session_context(cast(Session, mock_session)): # We tagify here, instead of waiting for the App object to do it when it wraps From 7579f24dfe63a1b7b6d552bb2bcb48378a40d43f Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 1 Mar 2024 23:32:06 -0600 Subject: [PATCH 12/35] Install shiny before shinylive in GHA --- .github/py-shiny/setup/action.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/py-shiny/setup/action.yaml b/.github/py-shiny/setup/action.yaml index 1fd5b6253..aace55be3 100644 --- a/.github/py-shiny/setup/action.yaml +++ b/.github/py-shiny/setup/action.yaml @@ -25,6 +25,10 @@ runs: shell: bash run: | pip install https://github.com/rstudio/py-htmltools/tarball/main + # Install shiny _before_ shinylive; otherwise shinylive will install cause + # shiny to be installed from PyPI, and may bring in old versions of + # dependencies. + pip install -e . pip install https://github.com/posit-dev/py-shinylive/tarball/main make install-deps From f0a05faff68676a3c027d4592c1ffbf466fca59b Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Sat, 2 Mar 2024 01:20:56 -0500 Subject: [PATCH 13/35] Update `InjectAutoreloadMiddleware` to be compatible with starlette >= 0.35.0 (#1013) Co-authored-by: Winston Chang --- shiny/_autoreload.py | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/shiny/_autoreload.py b/shiny/_autoreload.py index 40d836525..b4079d990 100644 --- a/shiny/_autoreload.py +++ b/shiny/_autoreload.py @@ -8,8 +8,9 @@ import secrets import threading import webbrowser -from typing import Callable, Optional +from typing import Callable, Optional, cast +import starlette.types from asgiref.typing import ( ASGI3Application, ASGIReceiveCallable, @@ -90,8 +91,19 @@ class InjectAutoreloadMiddleware: because we want autoreload to be effective even when displaying an error page. """ - def __init__(self, app: ASGI3Application): - self.app = app + def __init__( + self, + app: starlette.types.ASGIApp | ASGI3Application, + *args: object, + **kwargs: object, + ): + if len(args) > 0 or len(kwargs) > 0: + raise TypeError( + f"InjectAutoreloadMiddleware does not support positional or keyword arguments, received {args}, {kwargs}" + ) + # The starlette types and the asgiref types are compatible, but we'll use the + # latter internally. See the note in the __call__ method for more details. + self.app = cast(ASGI3Application, app) ws_url = autoreload_url() self.script = ( f""" @@ -103,10 +115,22 @@ def __init__(self, app: ASGI3Application): ) async def __call__( - self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable + self, + scope: starlette.types.Scope | Scope, + receive: starlette.types.Receive | ASGIReceiveCallable, + send: starlette.types.Send | ASGISendCallable, ) -> None: - if scope["type"] != "http" or scope["path"] != "/" or len(self.script) == 0: - return await self.app(scope, receive, send) + # The starlette types and the asgiref types are compatible, but the latter are + # more rigorous. In the call interface, we accept both types for compatibility + # with both. But internally we'll use the more rigorous types. + # See https://github.com/encode/starlette/blob/39dccd9/docs/middleware.md#type-annotations + scope = cast(Scope, scope) + receive_casted = cast(ASGIReceiveCallable, receive) + send_casted = cast(ASGISendCallable, send) + if scope["type"] != "http": + return await self.app(scope, receive_casted, send_casted) + if scope["path"] != "/" or len(self.script) == 0: + return await self.app(scope, receive_casted, send_casted) def mangle_callback(body: bytes) -> tuple[bytes, bool]: if b"" in body: @@ -114,8 +138,8 @@ def mangle_callback(body: bytes) -> tuple[bytes, bool]: else: return (body, False) - mangler = ResponseMangler(send, mangle_callback) - await self.app(scope, receive, mangler.send) + mangler = ResponseMangler(send_casted, mangle_callback) + await self.app(scope, receive_casted, mangler.send) # PARENT PROCESS ------------------------------------------------------------ From eaaf86efa6029c6cf085d4bd7959e40fe4bc766c Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Sat, 2 Mar 2024 00:22:21 -0600 Subject: [PATCH 14/35] Use more consistent types for `static_assets` (#1171) --- shiny/_app.py | 22 +++++++++++++++------- shiny/express/_run.py | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/shiny/_app.py b/shiny/_app.py index 8a23c5fca..c2c187d5d 100644 --- a/shiny/_app.py +++ b/shiny/_app.py @@ -113,7 +113,7 @@ def __init__( Callable[[Inputs], None] | Callable[[Inputs, Outputs, Session], None] | None ), *, - static_assets: Optional["str" | "os.PathLike[str]" | dict[str, Path]] = None, + static_assets: Optional[str | Path | dict[str, str | Path]] = None, debug: bool = False, ) -> None: # Used to store callbacks to be called when the app is shutting down (according @@ -142,20 +142,28 @@ def __init__( if static_assets is None: static_assets = {} - if isinstance(static_assets, (str, os.PathLike)): - if not os.path.isabs(static_assets): + + if isinstance(static_assets, dict): + static_assets_map = {k: Path(v) for k, v in static_assets.items()} + else: + static_assets_map = {"/": Path(static_assets)} + + for _, static_asset_path in static_assets_map.items(): + if not static_asset_path.is_absolute(): raise ValueError( - f"static_assets must be an absolute path: {static_assets}" + f'static_assets must be an absolute path: "{static_asset_path}".' + " Consider using one of the following instead:\n" + f' os.path.join(__file__, "{static_asset_path}") OR' + f' pathlib.Path(__file__).parent/"{static_asset_path}"' ) - static_assets = {"/": Path(static_assets)} # Sort the static assets keys by descending length, to ensure that the most # specific paths are mounted first. Suppose there are mounts "/foo" and "/". If # "/" is first in the dict, then requests to "/foo/file.html" will never reach # the second mount. We need to put "/foo" first and "/" second so that it will # actually look in the "/foo" mount. - static_assets = sort_keys_length(static_assets, descending=True) - self._static_assets: dict[str, Path] = static_assets + static_assets_map = sort_keys_length(static_assets_map, descending=True) + self._static_assets: dict[str, Path] = static_assets_map self._sessions: dict[str, Session] = {} diff --git a/shiny/express/_run.py b/shiny/express/_run.py index be8fa4314..cdfc9a9d0 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -85,7 +85,7 @@ def express_server(input: Inputs, output: Outputs, session: Session): app = App( app_ui, express_server, - **app_opts, + **app_opts, # pyright: ignore[reportArgumentType] ) return app From b458c35a48ad1aa5779896e8ac766435ec57aab7 Mon Sep 17 00:00:00 2001 From: Joe Schulte Date: Sun, 3 Mar 2024 00:03:46 -0500 Subject: [PATCH 15/35] test: Consolidate playwright nav component test (#1158) Co-authored-by: Barret Schloerke --- tests/playwright/shiny/components/nav/app.py | 2 +- .../shiny/components/nav/test_nav.py | 110 ++++++------------ 2 files changed, 39 insertions(+), 73 deletions(-) diff --git a/tests/playwright/shiny/components/nav/app.py b/tests/playwright/shiny/components/nav/app.py index 57279c44c..ffe36f163 100644 --- a/tests/playwright/shiny/components/nav/app.py +++ b/tests/playwright/shiny/components/nav/app.py @@ -91,7 +91,7 @@ def make_navset( app_ui = ui.page_navbar( - *nav_controls("page_navbar"), + *nav_controls("page_navbar()"), # bg="#0062cc", # inverse=True, id="page_navbar", diff --git a/tests/playwright/shiny/components/nav/test_nav.py b/tests/playwright/shiny/components/nav/test_nav.py index d7e9e442e..546f81090 100644 --- a/tests/playwright/shiny/components/nav/test_nav.py +++ b/tests/playwright/shiny/components/nav/test_nav.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from dataclasses import dataclass + import pytest from conftest import ShinyAppProc from controls import ( @@ -20,75 +24,37 @@ def test_nav(page: Page, local_app: ShinyAppProc) -> None: # Update the page size to be wider page.set_viewport_size({"width": 1500, "height": 800}) - # make it in a funcn and then loop through it - # navset_tab - navset_tab = LayoutNavsetTab(page, "navset_tab") - navset_tab.expect_nav_values(["a", "b", "c"]) - navset_tab.expect_value("a") - navset_tab.expect_content("navset_tab(): tab a content") - navset_tab.set("b") - navset_tab.expect_value("b") - navset_tab.expect_content("navset_tab(): tab b content") - - # # navset_pill - navset_pill = LayoutNavSetPill(page, "navset_pill") - navset_pill.expect_nav_values(["a", "b", "c"]) - navset_pill.expect_value("a") - navset_pill.expect_content("navset_pill(): tab a content") - navset_pill.set("b") - navset_pill.expect_value("b") - navset_pill.expect_content("navset_pill(): tab b content") - - # navset_underline - navset_underline = LayoutNavSetUnderline(page, "navset_underline") - navset_underline.expect_nav_values(["a", "b", "c"]) - navset_underline.expect_value("a") - navset_underline.expect_content("navset_underline(): tab a content") - navset_underline.set("b") - navset_underline.expect_value("b") - navset_underline.expect_content("navset_underline(): tab b content") - - # navset_card_tab - navset_card_tab = LayoutNavSetCardTab(page, "navset_card_tab") - navset_card_tab.expect_nav_values(["a", "b", "c"]) - navset_card_tab.expect_value("a") - navset_card_tab.expect_content("navset_card_tab(): tab a content") - navset_card_tab.set("b") - navset_card_tab.expect_value("b") - navset_card_tab.expect_content("navset_card_tab(): tab b content") - - # navset_card_pill - navset_card_pill = LayoutNavSetCardPill(page, "navset_card_pill") - navset_card_pill.expect_nav_values(["a", "b", "c"]) - navset_card_pill.expect_value("a") - navset_card_pill.expect_content("navset_card_pill(): tab a content") - navset_card_pill.set("b") - navset_card_pill.expect_value("b") - navset_card_pill.expect_content("navset_card_pill(): tab b content") - - # navset_card_underline - navset_card_underline = LayoutNavSetCardUnderline(page, "navset_card_underline") - navset_card_underline.expect_nav_values(["a", "b", "c"]) - navset_card_underline.expect_value("a") - navset_card_underline.expect_content("navset_card_underline(): tab a content") - navset_card_underline.set("b") - navset_card_underline.expect_value("b") - navset_card_underline.expect_content("navset_card_underline(): tab b content") - - # navset_pill_list - navset_card_pill = LayoutNavSetPillList(page, "navset_pill_list") - navset_card_pill.expect_nav_values(["a", "b", "c"]) - navset_card_pill.expect_value("a") - navset_card_pill.expect_content("navset_pill_list(): tab a content") - navset_card_pill.set("b") - navset_card_pill.expect_value("b") - navset_card_pill.expect_content("navset_pill_list(): tab b content") - - # Page_navbar - navset_bar = LayoutNavSetBar(page, "page_navbar") - navset_bar.expect_nav_values(["a", "b", "c"]) - navset_bar.expect_value("a") - navset_bar.expect_content("page_navbar: tab a content") - navset_bar.set("b") - navset_bar.expect_value("b") - navset_bar.expect_content("page_navbar: tab b content") + @dataclass + class LayoutInfo: + control: type[ + LayoutNavSetBar + | LayoutNavSetCardPill + | LayoutNavSetCardTab + | LayoutNavSetCardUnderline + | LayoutNavSetPill + | LayoutNavSetPillList + | LayoutNavsetTab + | LayoutNavSetUnderline + ] + verify: str + + nav_data: list[LayoutInfo] = [ + LayoutInfo(LayoutNavsetTab, "navset_tab()"), + LayoutInfo(LayoutNavSetPill, "navset_pill()"), + LayoutInfo(LayoutNavSetUnderline, "navset_underline()"), + LayoutInfo(LayoutNavSetCardTab, "navset_card_tab()"), + LayoutInfo(LayoutNavSetCardPill, "navset_card_pill()"), + LayoutInfo(LayoutNavSetCardUnderline, "navset_card_underline()"), + LayoutInfo(LayoutNavSetPillList, "navset_pill_list()"), + LayoutInfo(LayoutNavSetBar, "page_navbar()"), + ] + + for nav_info in nav_data: + el_name = nav_info.verify.replace("()", "") + element = nav_info.control(page, el_name) + element.expect_nav_values(["a", "b", "c"]) + element.expect_value("a") + element.expect_content(nav_info.verify + ": tab a content") + element.set("b") + element.expect_value("b") + element.expect_content(nav_info.verify + ": tab b content") From d612e1bf1ff8dd97a7f1b1f556a5725ba77707df Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Mon, 4 Mar 2024 08:06:17 -0800 Subject: [PATCH 16/35] Make autoreload work on codespaces (#1167) --- shiny/_autoreload.py | 5 ++++ shiny/_hostenv.py | 71 +++++++++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/shiny/_autoreload.py b/shiny/_autoreload.py index b4079d990..255ceb9ab 100644 --- a/shiny/_autoreload.py +++ b/shiny/_autoreload.py @@ -224,6 +224,11 @@ async def process_request( ) -> Optional[tuple[http.HTTPStatus, websockets.datastructures.HeadersLike, bytes]]: # If there's no Upgrade header, it's not a WebSocket request. if request_headers.get("Upgrade") is None: + # For some unknown reason, this fixes a tendency on GitHub Codespaces to + # correctly proxy through this request, but give a 404 when the redirect is + # followed and app_url is requested. With the sleep, both requests tend to + # succeed reliably. + await asyncio.sleep(1) return (http.HTTPStatus.MOVED_PERMANENTLY, [("Location", app_url)], b"") async with websockets.serve( diff --git a/shiny/_hostenv.py b/shiny/_hostenv.py index 53953e5c7..89f1e0273 100644 --- a/shiny/_hostenv.py +++ b/shiny/_hostenv.py @@ -7,24 +7,34 @@ from ipaddress import ip_address from subprocess import run from typing import Pattern -from urllib.parse import urlparse +from urllib.parse import ParseResult, urlparse def is_workbench() -> bool: return bool(os.getenv("RS_SERVER_URL") and os.getenv("RS_SESSION_URL")) +def is_codespaces() -> bool: + # See https://docs.github.com/en/codespaces/developing-in-a-codespace/default-environment-variables-for-your-codespace + return bool( + os.getenv("CODESPACES") + and os.getenv("CODESPACE_NAME") + and os.getenv("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN") + ) + + def is_proxy_env() -> bool: - return is_workbench() + return is_workbench() or is_codespaces() port_cache: dict[int, str] = {} def get_proxy_url(url: str) -> str: - if not is_workbench(): + if not is_proxy_env(): return url + # Regardless of proxying strategy, we don't want to proxy URLs that are not loopback parts = urlparse(url) is_loopback = parts.hostname == "localhost" if not is_loopback: @@ -35,6 +45,45 @@ def get_proxy_url(url: str) -> str: if not is_loopback: return url + # Regardless of proxying strategy, we need to know the port, whether explicit or + # implicit (from the scheme) + if parts.port is not None: + if parts.port == 0: + # Not sure if this is even legal but we're definitely not going to succeed + # in proxying it + return url + port = parts.port + elif parts.scheme.lower() in ["ws", "http"]: + port = 80 + elif parts.scheme.lower() in ["wss", "https"]: + port = 443 + else: + # No idea what kind of URL this is + return url + + if is_workbench(): + return _get_proxy_url_workbench(parts, port) or url + if is_codespaces(): + return _get_proxy_url_codespaces(parts, port) or url + return url + + +def _get_proxy_url_codespaces(parts: ParseResult, port: int) -> str | None: + # See https://docs.github.com/en/codespaces/developing-in-a-codespace/default-environment-variables-for-your-codespace + codespace_name = os.getenv("CODESPACE_NAME") + port_forwarding_domain = os.getenv("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN") + netloc = f"{codespace_name}-{port}.{port_forwarding_domain}" + if parts.scheme.lower() in ["ws", "wss"]: + scheme = "wss" + elif parts.scheme.lower() in ["http", "https"]: + scheme = "https" + else: + return None + + return parts._replace(scheme=scheme, netloc=netloc).geturl() + + +def _get_proxy_url_workbench(parts: ParseResult, port: int) -> str | None: path = parts.path or "/" server_url = os.getenv("RS_SERVER_URL", "") @@ -45,18 +94,6 @@ def get_proxy_url(url: str) -> str: server_url = re.sub("/$", "", server_url) session_url = re.sub("^/", "", session_url) - port = ( - parts.port - if parts.port - else ( - 80 - if parts.scheme in ["ws", "http"] - else 443 if parts.scheme in ["wss", "https"] else 0 - ) - ) - if port == 0: - return url - if port in port_cache: ptoken = port_cache[port] else: @@ -67,9 +104,9 @@ def get_proxy_url(url: str) -> str: encoding="ascii", ) except FileNotFoundError: - return url + return None if res.returncode != 0: - return url + return None ptoken = res.stdout port_cache[port] = ptoken From 67290e959782596af970fafba4d60257a9220c88 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 4 Mar 2024 11:18:51 -0500 Subject: [PATCH 17/35] Revert API changes made in `Selected rows method` #1121 (#1174) --- examples/dataframe/app.py | 4 +-- shiny/api-examples/data_frame/app-core.py | 6 ++-- shiny/api-examples/data_frame/app-express.py | 8 ++--- shiny/render/_dataframe.py | 31 +++++++------------ tests/playwright/deploys/plotly/app.py | 4 +-- .../shiny/bugs/0676-row-selection/app.py | 6 +++- 6 files changed, 28 insertions(+), 31 deletions(-) diff --git a/examples/dataframe/app.py b/examples/dataframe/app.py index e1d527534..99374627e 100644 --- a/examples/dataframe/app.py +++ b/examples/dataframe/app.py @@ -92,10 +92,10 @@ def handle_edit(): @render.text def detail(): - selected_rows = grid.input_selected_rows() or () + selected_rows = input.grid_selected_rows() or () if len(selected_rows) > 0: # "split", "records", "index", "columns", "values", "table" - return df().iloc[list(grid.input_selected_rows())] + return df().iloc[list(input.grid_selected_rows())] app = App(app_ui, server) diff --git a/shiny/api-examples/data_frame/app-core.py b/shiny/api-examples/data_frame/app-core.py index 83843ed57..a592ef238 100644 --- a/shiny/api-examples/data_frame/app-core.py +++ b/shiny/api-examples/data_frame/app-core.py @@ -44,11 +44,11 @@ def summary_data(): @reactive.calc def filtered_df(): - req(summary_data.input_selected_rows()) + req(input.summary_data_selected_rows()) - # summary_data.selected_rows() is a tuple, so we must convert it to list, + # input.summary_data_selected_rows() is a tuple, so we must convert it to list, # as that's what Pandas requires for indexing. - selected_idx = list(summary_data.input_selected_rows()) + selected_idx = list(input.summary_data_selected_rows()) countries = summary_df.iloc[selected_idx]["country"] # Filter data for selected countries return df[df["country"].isin(countries)] diff --git a/shiny/api-examples/data_frame/app-express.py b/shiny/api-examples/data_frame/app-express.py index 18064e7ee..24a5b47ba 100644 --- a/shiny/api-examples/data_frame/app-express.py +++ b/shiny/api-examples/data_frame/app-express.py @@ -3,7 +3,7 @@ from shinywidgets import render_widget from shiny import reactive, req -from shiny.express import render, ui +from shiny.express import input, render, ui # Load the Gapminder dataset df = px.data.gapminder() @@ -66,12 +66,12 @@ def country_detail_percap(): @reactive.calc def filtered_df(): - req(summary_data.input_selected_rows()) + req(input.summary_data_selected_rows()) - # summary_data.input_selected_rows() is a tuple, so we must convert it to list, + # input.summary_data_selected_rows() is a tuple, so we must convert it to list, # as that's what Pandas requires for indexing. - selected_idx = list(summary_data.input_selected_rows()) + selected_idx = list(input.summary_data_selected_rows()) countries = summary_df.iloc[selected_idx]["country"] # Filter data for selected countries return df[df["country"].isin(countries)] diff --git a/shiny/render/_dataframe.py b/shiny/render/_dataframe.py index 0b70381a3..16c0509f3 100644 --- a/shiny/render/_dataframe.py +++ b/shiny/render/_dataframe.py @@ -2,22 +2,12 @@ import abc import json -from typing import ( - TYPE_CHECKING, - Any, - Literal, - Optional, - Protocol, - Union, - cast, - 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, no_example -from ..session._utils import require_active_session from ._dataframe_unsafe import serialize_numpy_dtypes from .renderer import Jsonifiable, Renderer @@ -248,7 +238,8 @@ class data_frame(Renderer[DataFrameResult]): Row selection ------------- When using the row selection feature, you can access the selected rows by using the - `.input_selected_rows()` method, where `` is the render function name that corresponds with the `id=` used in :func:`~shiny.ui.outout_data_frame`. Internally, this method retrieves the selected row value from session's `input._selected_rows()` value. The value returned will be `None` if no rows + `input._selected_rows()` function, where `` is the `id` of the + :func:`~shiny.ui.output_data_frame`. The value returned will be `None` if no rows are selected, or a tuple of integers representing the indices of the selected rows. To filter a pandas data frame down to the selected rows, use `df.iloc[list(input._selected_rows())]`. @@ -267,6 +258,8 @@ class data_frame(Renderer[DataFrameResult]): objects you can return from the rendering function to specify options. """ + # `.input_selected_rows()` method, where `` is the render function name that corresponds with the `id=` used in :func:`~shiny.ui.outout_data_frame`. Internally, this method retrieves the selected row value from session's `input._selected_rows()` value. The value returned will be `None` if no rows + def auto_output_ui(self) -> Tag: return ui.output_data_frame(id=self.output_id) @@ -280,14 +273,14 @@ async def transform(self, value: DataFrameResult) -> Jsonifiable: ) return value.to_payload() - def input_selected_rows(self) -> Optional[tuple[int]]: - """ - When `row_selection_mode` is set to "single" or "multiple" this will return - a tuple of integers representing the rows selected by a user. - """ + # def input_selected_rows(self) -> Optional[tuple[int]]: + # """ + # When `row_selection_mode` is set to "single" or "multiple" this will return + # a tuple of integers representing the rows selected by a user. + # """ - active_session = require_active_session(None) - return active_session.input[self.output_id + "_selected_rows"]() + # active_session = require_active_session(None) + # return active_session.input[self.output_id + "_selected_rows"]() @runtime_checkable diff --git a/tests/playwright/deploys/plotly/app.py b/tests/playwright/deploys/plotly/app.py index 05b4885fd..bf90aa3f7 100644 --- a/tests/playwright/deploys/plotly/app.py +++ b/tests/playwright/deploys/plotly/app.py @@ -61,11 +61,11 @@ def summary_data(): @reactive.calc def filtered_df(): - req(summary_data.input_selected_rows()) + req(input.summary_data_selected_rows()) # input.summary_data_selected_rows() is a tuple, so we must convert it to list, # as that's what Pandas requires for indexing. - selected_idx = list(summary_data.input_selected_rows()) + selected_idx = list(input.summary_data_selected_rows()) countries = summary_df.iloc[selected_idx]["country"] # Filter data for selected countries return df[df["country"].isin(countries)] diff --git a/tests/playwright/shiny/bugs/0676-row-selection/app.py b/tests/playwright/shiny/bugs/0676-row-selection/app.py index 91fbebc97..1c8fee0fd 100644 --- a/tests/playwright/shiny/bugs/0676-row-selection/app.py +++ b/tests/playwright/shiny/bugs/0676-row-selection/app.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import cast + import pandas as pd from shiny import App, Inputs, Outputs, Session, render, ui @@ -33,7 +37,7 @@ def grid(): @render.table def detail(): - selected_rows = grid.input_selected_rows() or () + selected_rows = cast("tuple[int]", input.grid_selected_rows() or ()) if len(selected_rows) > 0: return df.iloc[list(selected_rows)] From de9b6d0af5bddcfd7db6113a7c0a2f6db4f688a1 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 4 Mar 2024 10:39:51 -0600 Subject: [PATCH 18/35] Bump version to 0.8.0 --- CHANGELOG.md | 2 +- shiny/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e32db89..26c88e249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ 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). -## [UNRELEASED] - YYYY-MM-DD +## [0.8.0] - 2024-03-04 ### Breaking Changes diff --git a/shiny/__init__.py b/shiny/__init__.py index dd4fa0367..831045706 100644 --- a/shiny/__init__.py +++ b/shiny/__init__.py @@ -1,6 +1,6 @@ """A package for building reactive web applications.""" -__version__ = "0.7.1.9000" +__version__ = "0.8.0" from ._shinyenv import is_pyodide as _is_pyodide From 8e1e22f46c85c8e5905f7ee20eed112aba1b343f Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 4 Mar 2024 13:32:52 -0500 Subject: [PATCH 19/35] Setup dev version 0.8.0.9000 (#1181) --- CHANGELOG.md | 2 ++ shiny/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26c88e249..440b72697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to Shiny for Python will be documented in this file. 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). +## [UNRELEASED] - YYYY-MM-DD + ## [0.8.0] - 2024-03-04 diff --git a/shiny/__init__.py b/shiny/__init__.py index 831045706..3c681c22e 100644 --- a/shiny/__init__.py +++ b/shiny/__init__.py @@ -1,6 +1,6 @@ """A package for building reactive web applications.""" -__version__ = "0.8.0" +__version__ = "0.8.0.9000" from ._shinyenv import is_pyodide as _is_pyodide From a6c759f551184439c8e71d49fd3d33c98bf5bea4 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 4 Mar 2024 13:59:35 -0500 Subject: [PATCH 20/35] tests(resolve-id): Update window size to ensure tooltip is visible (#1179) --- .../shiny/bugs/0696-resolve-id/test_0696_resolve_id.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py b/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py index d04618976..aed2ba65d 100644 --- a/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py +++ b/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py @@ -109,6 +109,7 @@ def expect_default_outputs(page: Page, module_id: str): # Sidebars do not seem to work on webkit. Skipping test on webkit @pytest.mark.skip_browser("webkit") def test_module_support(page: Page, local_app: ShinyAppProc) -> None: + page.set_viewport_size({"width": 3000, "height": 6000}) page.goto(local_app.url) # Verify reset state From c110202cefd0bc08a8a991286f7f4d1cd84dbe87 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 4 Mar 2024 13:27:28 -0600 Subject: [PATCH 21/35] Don't run playwright-deploys tests on release --- .github/workflows/pytest.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index f7c9085d2..486ee6678 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -107,6 +107,7 @@ jobs: make playwright-examples SUB_FILE=". -vv" playwright-deploys: + if: github.event_name != 'release' # Only allow one `playwright-deploys` job to run at a time. (Independent of branch / PR) concurrency: playwright-deploys runs-on: ${{ matrix.os }} From b96097e4ac45f66892817ef598c14494b61873d7 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 4 Mar 2024 13:31:12 -0600 Subject: [PATCH 22/35] Use consistent formatting for items in changelog --- CHANGELOG.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 440b72697..48cf1fbe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,14 +45,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug fixes * Fixed `input_task_button` not working in a Shiny module. (#1108) + * Fixed several issues with `page_navbar()` styling. (#1124) + * Fixed `Renderer.output_id` to not contain the module namespace prefix, only the output id. (#1130) + * Fixed gap-driven spacing between children in fillable `nav_panel()` containers. (#1152) + * Fixed #1138: An empty value in a date or date range input would cause an error; now it is treated as `None`. (#1139) ### Other changes * Replaced use of `sys.stderr.write()` with `print(file=sys.stderr)`, because on some platforms `sys.stderr` can be `None`. (#1131) + * Replaced soon-to-be deprecated `datetime` method calls when handling `shiny.datetime` inputs. (#1146) @@ -130,9 +135,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features * `shiny create` now allows you to select from a list of template apps. + * `shiny create` provides templates which help you build your own custom JavaScript components. + * Closed #814: The functions `reactive.Calc` and `reactive.Effect` have been changed to have lowercase names: `reactive.calc`, and `reactive.effect`. The old capitalized names are now aliases to the new lowercase names, so existing code will continue to work. Similarly, the class `reactive.Value` has a new alias, `reactive.value`, but in this case, since the original was a class, it keeps the original capitalized name as the primary name. The examples have not been changed yet, but will be changed in a future release. (#822) + * 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) ### Bug fixes @@ -142,27 +151,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Other changes * Closed #492: `shiny.ui.nav()` is now deprecated in favor of the more aptly named `shiny.ui.nav_panel()` (#876). + * Update penguins example to credit Allison Horst and drop usage of `shiny.experimental` (#798). + * `as_fillable_container()` and `as_fill_item()` no longer mutate the `Tag` object that was passed in. Instead, it returns a new `Tag` object. Also closed #856: these functions now put the `html-fill-container` and `html-fill-item` CSS classes last, instead of first. (#862) + * `App()` now accepts a server function with a single `input` parameter, or a server function with parameters `input`, `output` and `session`. Server functions with two or more than three parameters now raise an exception. (#920) ## [0.6.0] - 2023-10-30 ### Breaking Changes + * `shiny.run` only allows positional arguments for `app`, `host`, and `port`, all other arguments must be specified with keywords. ### New features + * `shiny run` now takes `reload-includes` and `reload-excludes` to allow you to define which files trigger a reload (#780). + * `shiny.run` now passes keyword arguments to `uvicorn.run` (#780). + * The `@output` decorator is no longer required for rendering functions; `@render.xxx` decorators now register themselves automatically. You can still use `@output` explicitly if you need to set specific output options (#747, #790). + * Added support for integration with Quarto (#746). + * Added `shiny.render.renderer_components` decorator to help create new output renderers (#621). + * Added `shiny.experimental.ui.popover()`, `update_popover()`, and `toggle_popover()` for easy creation (and server-side updating) of [Bootstrap popovers](https://getbootstrap.com/docs/5.3/components/popovers/). Popovers are similar to tooltips, but are more persistent, and should primarily be used with button-like UI elements (e.g. `input_action_button()` or icons) (#680). + * Added CSS classes to UI input methods (#680) . + * `Session` objects can now accept an asynchronous (or synchronous) function for `.on_flush(fn=)`, `.on_flushed(fn=)`, and `.on_ended(fn=)` (#686). + * `App()` now allows `static_assets` to represent multiple paths. To do this, pass in a dictionary instead of a string (#763). + * The `showcase_layout` argument of `value_box()` now accepts one of three character values: `"left center"`, `"top right"`, `"bottom"`. (#772) + * `value_box()` now supports many new themes and styles, or fully customizable themes using the new `value_box_theme()` function. To reflect the new capabilities, we've replaced `theme_color` with a new `theme` argument. The previous argument will continue work as expected, but with a deprecation warning. (#772) In addition to the Bootstrap theme names (`primary` ,`secondary`, etc.), you can now use the main Boostrap colors (`purple`, `blue`, `red`, etc.). You can also choose to apply the color to the background or foreground by prepending a `bg-` or `text-` prefix to the theme or color name. Finally, we've also added new gradient themes allowing you to pair any two color names as `bg-gradient-{from}-{to}` (e.g., `bg-gradient-purple-blue`). @@ -174,25 +198,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug fixes * `shiny run` now respects the user provided `reload-dir` argument (#765). + * Fixed #646: Wrap bare value box value in `

` tags. (#668) + * Fixed #676: The `render.data_frame` selection feature was underdocumented and buggy (sometimes returning `None` as a row identifier if the pandas data frame's index had gaps in it). With this release, the selection is consistently a tuple of the 0-based row numbers of the selected rows--or `None` if no rows are selected. (#677) + * Added tests to verify that ui input methods, ui labels, ui update (value) methods, and ui output methods work within modules (#696). + * Adjusted the `@render.plot` input type to be `object` to allow for any object (if any) to be returned (#712). + * In `layout_column_wrap()`, when `width` is a CSS unit -- e.g. `width = "400px"` or `width = "25%"` -- and `fixed_width = FALSE`, `layout_column_wrap()` will ensure that the columns are at least `width` wide, unless the parent container is narrower than `width`. (#772) ### Other changes * `input_action_button()` now defaults to having whitespace around it. (#758) + * `layout_sidebar()` now uses an `