Skip to content

Commit 552c5a8

Browse files
committed
Allow Shiny Express syntax in Quarto Dashboards
1 parent 9fcdb9a commit 552c5a8

File tree

4 files changed

+41
-3
lines changed

4 files changed

+41
-3
lines changed

shiny/express/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@
3737
session: _Session
3838

3939

40+
allow_express_in_core = False
41+
"""
42+
Control whether Shiny Express imports are allowed in Shiny Core apps.
43+
44+
Normally Shiny Express imports are not allowed in Shiny Core apps because the mixing of
45+
the two could lead to confusion. However, there are special cases where we do allow
46+
this, like when using Shiny Express syntax in a Quarto Dashboard.
47+
"""
48+
49+
4050
# Note that users should use `from shiny.express import input` instead of `from shiny
4151
# import express` and acces via `express.input`. The former provides a static value for
4252
# `input`, but the latter is dynamic -- every time `express.input` is accessed, it

shiny/express/_is_express.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,17 @@ def is_express_app(app: str, app_dir: str | None) -> bool:
5151
except Exception:
5252
return False
5353

54-
return detector.found_shiny_express_import
54+
return detector.is_express_app()
5555

5656

5757
class DetectShinyExpressVisitor(ast.NodeVisitor):
5858
def __init__(self):
5959
super().__init__()
6060
self.found_shiny_express_import = False
61+
self.found_shiny_express_in_core = False
62+
63+
def is_express_app(self) -> bool:
64+
return self.found_shiny_express_import and not self.found_shiny_express_in_core
6165

6266
def visit_Import(self, node: ast.Import) -> None:
6367
if any(alias.name == "shiny.express" for alias in node.names):
@@ -71,6 +75,20 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
7175
):
7276
self.found_shiny_express_import = True
7377

78+
def visit_Assign(self, node: ast.Assign) -> None:
79+
for target in node.targets:
80+
# Look for the statement `shiny.express.allow_express_in_core = True` at the
81+
# top level.
82+
if (
83+
isinstance(target, ast.Attribute)
84+
and isinstance(target.value, ast.Attribute)
85+
and target.value.attr == "express"
86+
and isinstance(target.value.value, ast.Name)
87+
and target.value.value.id == "shiny"
88+
and target.attr == "allow_express_in_core"
89+
):
90+
self.found_shiny_express_in_core = True
91+
7492
# Visit top-level nodes.
7593
def visit_Module(self, node: ast.Module) -> None:
7694
super().generic_visit(node)

shiny/express/_run.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,15 @@ def set_result(x: object):
144144
get_top_level_recall_context_manager().__exit__(None, None, None)
145145

146146
# If we're running as an Express app but there's also a top-level item named app
147-
# which is a shiny.App object, the user probably made a mistake.
148-
if "app" in var_context and isinstance(var_context["app"], App):
147+
# which is a shiny.App object, the user probably made a mistake. The exception
148+
# is if allow_express_in_core is True, which happens with Quarto Dashboards.
149+
from . import allow_express_in_core
150+
151+
if (
152+
not allow_express_in_core
153+
and "app" in var_context
154+
and isinstance(var_context["app"], App)
155+
):
149156
raise RuntimeError(
150157
"This looks like a Shiny Express app because it imports shiny.express, "
151158
"but it also looks like a Shiny Core app because it has a variable named "

shiny/quarto.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ def convert_code_cells_to_app_py(json_file: str | Path, app_file: str | Path) ->
7373
7474
from pathlib import Path
7575
from shiny import App, Inputs, Outputs, Session, ui
76+
import shiny.express
77+
78+
shiny.express.allow_express_in_core = True
7679
7780
{"".join(global_code_cell_texts)}
7881

0 commit comments

Comments
 (0)