Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

* `ui.card()` and `ui.value_box()` now take an `id` argument that, when provided, is used to report the full screen state of the card or value box to the server. For example, when using `ui.card(id = "my_card", full_screen = TRUE)` you can determine if the card is currently in full screen mode by reading the boolean value of `input.my_card()["full_screen"]`. (#1215)

* Added support for using `shiny.express` in Quarto Dashboards. (#1217)

### Bug fixes

### Other changes
Expand Down
38 changes: 37 additions & 1 deletion shiny/express/_is_express.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
from __future__ import annotations

import ast
import re
import sys
from pathlib import Path
from typing import Literal, cast

from .._docstring import no_example

Expand Down Expand Up @@ -42,8 +45,16 @@ def is_express_app(app: str, app_dir: str | None) -> bool:

try:
# Read the file, parse it, and look for any imports of shiny.express.
with open(app_path) as f:
with open(app_path, encoding="utf-8") as f:
content = f.read()

# Check for magic comment in the first 1000 characters
forced_mode = find_magic_comment_mode(content[:1000])
if forced_mode == "express":
return True
elif forced_mode == "core":
return False

tree = ast.parse(content, app_path)
detector = DetectShinyExpressVisitor()
detector.visit(tree)
Expand Down Expand Up @@ -78,3 +89,28 @@ def visit_Module(self, node: ast.Module) -> None:
# Don't recurse into any nodes, so the we'll only ever look at top-level nodes.
def generic_visit(self, node: ast.AST) -> None:
pass


def find_magic_comment_mode(content: str) -> Literal["core", "express"] | None:
"""
Look for a magic comment of the form "# shiny_mode: express" or "# shiny_mode:
core".

If a line of the form "# shiny_mode: x" is found, where "x" is not "express" or
"core", then a message will be printed to stderr.

Returns
-------
:
`True` if Shiny Express comment is found, `False` if Shiny Core comment is
found, and `None` if no magic comment is found.
"""
m = re.search(r"^#[ \t]*shiny_mode:[ \t]*(\S*)[ \t]*$", content, re.MULTILINE)
if m is not None:
shiny_mode = cast(str, m.group(1))
if shiny_mode in ("express", "core"):
return shiny_mode
else:
print(f'Invalid shiny_mode: "{shiny_mode}"', file=sys.stderr)

return None
26 changes: 22 additions & 4 deletions shiny/express/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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 ._is_express import find_magic_comment_mode
from ._mock_session import ExpressMockSession
from ._recall_context import RecallContextManager
from .expressify_decorator._func_displayhook import _expressify_decorator_function_def
Expand Down Expand Up @@ -144,8 +145,14 @@ def set_result(x: object):
get_top_level_recall_context_manager().__exit__(None, None, None)

# If we're running as an Express app but there's also a top-level item named app
# which is a shiny.App object, the user probably made a mistake.
if "app" in var_context and isinstance(var_context["app"], App):
# which is a shiny.App object, the user probably made a mistake. (But if there's
# a magic comment to force it into Express mode, don't raise, because that means
# the user should know what they're doing.)
if (
"app" in var_context
and isinstance(var_context["app"], App)
and find_magic_comment_mode(content[:1000]) is None
):
raise RuntimeError(
"This looks like a Shiny Express app because it imports shiny.express, "
"but it also looks like a Shiny Core app because it has a variable named "
Expand All @@ -165,7 +172,7 @@ def set_result(x: object):
sys.displayhook = prev_displayhook


_top_level_recall_context_manager: RecallContextManager[Tag]
_top_level_recall_context_manager: RecallContextManager[Tag] | None = None


def reset_top_level_recall_context_manager() -> None:
Expand All @@ -176,6 +183,9 @@ def reset_top_level_recall_context_manager() -> None:


def get_top_level_recall_context_manager() -> RecallContextManager[Tag]:
if _top_level_recall_context_manager is None:
raise RuntimeError("No top-level recall context manager has been set.")

return _top_level_recall_context_manager


Expand Down Expand Up @@ -221,8 +231,16 @@ def app_opts(
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 mock_session is None:
# We can get here if a Shiny Core app, or if we're in the UI rendering phase of
# a Quarto-Shiny dashboard.
raise RuntimeError(
"express.app_opts() can only be used in a standalone Shiny Express app."
)

# Store these options only if we're in the UI-rendering phase of Shiny Express.
if not isinstance(mock_session, ExpressMockSession):
return

Expand Down
9 changes: 8 additions & 1 deletion shiny/express/ui/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,14 @@ def page_opts(
one based on the arguments provided. If not ``None``, this will override all
heuristics for choosing page functions.
"""
cm = get_top_level_recall_context_manager()
try:
cm = get_top_level_recall_context_manager()
except RuntimeError:
# We can get here if a Shiny Core app, or if we're in the UI rendering phase of
# a Quarto-Shiny dashboard.
raise RuntimeError(
"express.ui.page_opts() can only be used inside of a standalone Shiny Express app."
)

if not isinstance(title, MISSING_TYPE):
cm.kwargs["title"] = title
Expand Down
1 change: 1 addition & 0 deletions shiny/quarto.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def convert_code_cells_to_app_py(json_file: str | Path, app_file: str | Path) ->
)

app_content = f"""# This file generated by Quarto; do not edit by hand.
# shiny_mode: core

from __future__ import annotations

Expand Down