Skip to content

Commit 765febb

Browse files
committed
temporarily remove quarto-shiny skeleton, not yet supported
1 parent fecb0b0 commit 765febb

9 files changed

Lines changed: 153 additions & 74 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- `rsconnect quickstart` command for scaffolding a new Connect-ready project.
1313
Supported types: `streamlit`, `shiny`, `fastapi`, `api`, `flask`, `notebook`,
14-
`voila`, `quarto`, `quarto-shiny`. Creates a uv-managed virtualenv and prints
14+
`voila`, `quarto`. Creates a uv-managed virtualenv and prints
1515
the local-run and deploy commands.
1616
- `rsconnect deploy pyproject` command for deploying a project described by
1717
`pyproject.toml` with a `[tool.rsconnect]` table containing `app_mode` and

rsconnect/bundle.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from .exception import RSConnectException
5858
from .log import VERBOSE, logger
5959
from .models import AppMode, AppModes, GlobSet
60+
from .shiny_express import escape_to_var_name, is_express_app
6061

6162
if TYPE_CHECKING:
6263
from .actions import QuartoInspectResult
@@ -1182,8 +1183,12 @@ def infer_entrypoint_candidates(path: str, mimetype: str) -> list[str]:
11821183
def guess_deploy_dir(path: str | Path, entrypoint: Optional[str]) -> str:
11831184
if path and not exists(path):
11841185
raise RSConnectException(f"Path {path} does not exist.")
1186+
# The entrypoint is a bare basename meant to be resolved relative to ``path``
1187+
# (the later logic does ``join(abs_path, basename(entrypoint))``). Accept it
1188+
# when it exists relative to the CWD or inside ``path``; only reject when neither.
11851189
if entrypoint and not exists(entrypoint):
1186-
raise RSConnectException(f"Entrypoint {entrypoint} does not exist.")
1190+
if not (path and isfile(os.path.join(abspath(path), basename(entrypoint)))):
1191+
raise RSConnectException(f"Entrypoint {entrypoint} does not exist.")
11871192
abs_path = abspath(path)
11881193
abs_entrypoint = abspath(entrypoint) if entrypoint else None
11891194
if not path and not entrypoint:
@@ -1224,6 +1229,26 @@ def guess_deploy_dir(path: str | Path, entrypoint: Optional[str]) -> str:
12241229
return deploy_dir
12251230

12261231

1232+
def resolve_shiny_express_entrypoint(entrypoint: str, directory: str) -> str:
1233+
"""Rewrite a Shiny entrypoint to its Shiny Express module form when needed.
1234+
1235+
Connect runs Shiny Express apps through the ``shiny.express.app:<var>``
1236+
module rather than a plain file, so both ``deploy shiny`` and
1237+
``deploy pyproject`` must apply this rewrite to deploy a working app.
1238+
1239+
Accepts a bare module name (``"app"``) or a filename (``"app.py"``).
1240+
Returns the ``shiny.express.app:<escaped>`` form when the app file is a
1241+
Shiny Express app, otherwise returns ``entrypoint`` unchanged.
1242+
1243+
:param str entrypoint: the configured entrypoint, with or without ``.py``.
1244+
:param str directory: directory containing the app file.
1245+
"""
1246+
app_file = entrypoint if entrypoint.lower().endswith(".py") else entrypoint + ".py"
1247+
if is_express_app(app_file, directory):
1248+
return "shiny.express.app:" + escape_to_var_name(app_file)
1249+
return entrypoint
1250+
1251+
12271252
def abs_entrypoint(path: str | Path, entrypoint: str) -> str | None:
12281253
if isfile(entrypoint):
12291254
return abspath(entrypoint)

rsconnect/main.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
make_tensorflow_bundle,
8484
make_voila_bundle,
8585
read_manifest_app_mode,
86+
resolve_shiny_express_entrypoint,
8687
validate_entry_point,
8788
validate_extra_files,
8889
validate_file_is_notebook,
@@ -120,7 +121,6 @@
120121
)
121122
from .pyproject import InvalidPyprojectConfigError, TOMLDecodeError, read_tool_rsconnect
122123
from .environment import PackageInstaller
123-
from .shiny_express import escape_to_var_name, is_express_app
124124
from .utils_package import fix_starlette_requirements
125125

126126
T = TypeVar("T")
@@ -1028,7 +1028,7 @@ def info(file: str):
10281028
help=(
10291029
"Create a new Posit Connect project of the given TYPE in ./<name>/. "
10301030
"Supported TYPE values: streamlit, shiny, fastapi, api, flask, "
1031-
"notebook, voila, quarto, quarto-shiny. Writes a pyproject.toml "
1031+
"notebook, voila, quarto. Writes a pyproject.toml "
10321032
"with a [tool.rsconnect] section, creates a uv-managed virtualenv, "
10331033
"and prints the local-run and deploy commands."
10341034
),
@@ -1604,12 +1604,14 @@ def quickstart_hint() -> str:
16041604

16051605
# Requirements source precedence: ``-r`` flag > ``[tool.rsconnect].requirements_file``
16061606
# > built-in default ``pyproject.toml`` (top-level deps; Connect resolves transitive).
1607-
# Without an explicit source the inspector would silently ``pip freeze`` the caller's
1608-
# interpreter — the bug ``deploy pyproject`` exists to avoid. Malformed TOML values
1609-
# (wrong type, missing file) are surfaced by the inspector / file existence check.
1607+
# An explicit default keeps the inspector from falling back to a ``pip freeze`` of the
1608+
# caller's interpreter. Malformed TOML values (wrong type, missing file) are surfaced
1609+
# by the inspector / file existence check.
16101610
requirements_file = requirements_file or config.get("requirements_file") or "pyproject.toml"
16111611

16121612
if app_mode in (AppModes.STREAMLIT_APP, AppModes.PYTHON_SHINY, AppModes.PYTHON_FASTAPI, AppModes.PYTHON_API):
1613+
if app_mode == AppModes.PYTHON_SHINY:
1614+
entrypoint = resolve_shiny_express_entrypoint(entrypoint, directory)
16131615
environment = Environment.create_python_environment(
16141616
directory,
16151617
requirements_file=requirements_file,
@@ -2275,8 +2277,7 @@ def deploy_app(
22752277
)
22762278

22772279
if app_mode == AppModes.PYTHON_SHINY:
2278-
if is_express_app(entrypoint + ".py", directory):
2279-
entrypoint = "shiny.express.app:" + escape_to_var_name(entrypoint + ".py")
2280+
entrypoint = resolve_shiny_express_entrypoint(entrypoint, directory)
22802281

22812282
# Get server version for metadata support check
22822283
server_version = None
@@ -3290,8 +3291,7 @@ def _write_framework_manifest(
32903291

32913292
if app_mode == AppModes.PYTHON_SHINY:
32923293
with cli_feedback("Inspecting Shiny for Python app"):
3293-
if is_express_app(entrypoint + ".py", directory):
3294-
entrypoint = "shiny.express.app:" + escape_to_var_name(entrypoint + ".py")
3294+
entrypoint = resolve_shiny_express_entrypoint(entrypoint, directory)
32953295

32963296
with cli_feedback("Creating manifest.json"):
32973297
environment_file_exists = write_api_manifest_json(

rsconnect/quickstart/quickstart.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def run_quickstart(
6868

6969
# Pre-flight checks. Each helper raises ``RSConnectException``
7070
# with an actionable message; nothing on disk is mutated until every
71-
# check has passed. Type validation now lives in Click's argument
71+
# check has passed. Type validation lives in Click's argument
7272
# parser (see ``rsconnect/main.py``), so it has already passed before
7373
# we get here.
7474
_require_uv_on_path()
@@ -288,15 +288,6 @@ class TemplateSpec:
288288
source_files=(FileSpec(name="report.qmd", template="quarto/report.qmd.tmpl"),),
289289
notes=(_QUARTO_INSTALL_NOTE,),
290290
),
291-
# quarto-shiny shares the README with quarto-static: same local-run
292-
# command, same deploy command, same quarto-install note.
293-
AppModes.SHINY_QUARTO: TemplateSpec(
294-
pyproject_template="quarto/pyproject_shiny.toml.tmpl",
295-
readme_template="quarto/README.md.tmpl",
296-
local_run_command=("uv", "run", "quarto", "preview", "report.qmd"),
297-
source_files=(FileSpec(name="report.qmd", template="quarto/report_shiny.qmd.tmpl"),),
298-
notes=(_QUARTO_INSTALL_NOTE,),
299-
),
300291
}
301292

302293

rsconnect/quickstart/templates/quarto/pyproject_shiny.toml.tmpl

Lines changed: 0 additions & 13 deletions
This file was deleted.

rsconnect/quickstart/templates/quarto/report_shiny.qmd.tmpl

Lines changed: 0 additions & 18 deletions
This file was deleted.

tests/test_bundle.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
make_tensorflow_bundle,
3636
make_tensorflow_manifest,
3737
make_voila_bundle,
38+
resolve_shiny_express_entrypoint,
3839
to_bytes,
3940
validate_entry_point,
4041
validate_extra_files,
4142
validate_node_entry_point,
4243
)
44+
from rsconnect.shiny_express import escape_to_var_name
4345
from rsconnect.environment_node import NodeEnvironment
4446
from rsconnect.environment import Environment, PackageInstaller
4547
from rsconnect.exception import RSConnectException
@@ -1300,6 +1302,17 @@ def test_guess_deploy_dir(self):
13001302
self.assertEqual(abspath(bqplot_dir), guess_deploy_dir(bqplot_ipynb, bqplot_ipynb))
13011303
self.assertEqual(abspath(bqplot_dir), guess_deploy_dir(bqplot_dir, bqplot_ipynb))
13021304

1305+
def test_guess_deploy_dir_bare_entrypoint_relative_to_path(self):
1306+
# A bare entrypoint basename that exists inside ``path`` but not in the
1307+
# CWD must be accepted (deploy pyproject/voila run from outside the
1308+
# project pass ``notebook.ipynb`` resolved relative to the directory).
1309+
bare_entrypoint = basename(bqplot_ipynb)
1310+
assert not os.path.exists(bare_entrypoint)
1311+
self.assertEqual(abspath(bqplot_dir), guess_deploy_dir(bqplot_dir, bare_entrypoint))
1312+
# A bare entrypoint that exists in neither location is still rejected.
1313+
with self.assertRaises(RSConnectException):
1314+
guess_deploy_dir(bqplot_dir, "does_not_exist.ipynb")
1315+
13031316

13041317
@pytest.mark.parametrize(
13051318
(
@@ -3214,3 +3227,21 @@ def test_ts_bundle_contents(self):
32143227
def test_ts_entrypoint_detection(self):
32153228
ep = get_default_node_entrypoint(_NODE_TS_EXPRESS_DIR)
32163229
assert ep == "app.ts"
3230+
3231+
3232+
def test_resolve_shiny_express_entrypoint_bare_name(tmp_path):
3233+
(tmp_path / "app.py").write_text("from shiny.express import ui\n")
3234+
resolved = resolve_shiny_express_entrypoint("app", str(tmp_path))
3235+
assert resolved == "shiny.express.app:" + escape_to_var_name("app.py")
3236+
3237+
3238+
def test_resolve_shiny_express_entrypoint_normalizes_py_extension(tmp_path):
3239+
(tmp_path / "app.py").write_text("from shiny.express import ui\n")
3240+
assert resolve_shiny_express_entrypoint("app.py", str(tmp_path)) == resolve_shiny_express_entrypoint(
3241+
"app", str(tmp_path)
3242+
)
3243+
3244+
3245+
def test_resolve_shiny_express_entrypoint_non_express_unchanged(tmp_path):
3246+
(tmp_path / "app.py").write_text("from shiny import App\n")
3247+
assert resolve_shiny_express_entrypoint("app.py", str(tmp_path)) == "app.py"

tests/test_deploy_pyproject.py

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -280,16 +280,16 @@ def test_deploy_pyproject_error_message_mentions_quickstart(runner: CliRunner, p
280280
]
281281

282282

283-
@pytest.mark.parametrize("app_mode,entrypoint,expected_builder_name", DISPATCH_MATRIX)
284-
def test_deploy_pyproject_dispatches_by_app_mode(
285-
runner: CliRunner,
286-
project_dir: pathlib.Path,
287-
app_mode: str,
288-
entrypoint: str,
289-
expected_builder_name: str,
290-
monkeypatch: pytest.MonkeyPatch,
291-
):
292-
"""Each ``[tool.rsconnect].app_mode`` routes to its matching bundle builder."""
283+
def _spy_make_bundle(monkeypatch: pytest.MonkeyPatch) -> dict[str, typing.Any]:
284+
"""Short-circuit a deploy at ``make_bundle`` and capture the dispatched call.
285+
286+
Records the builder name plus the positional/keyword args passed to
287+
``make_bundle`` (the bundle builder is ``args[0]`` to ``make_bundle`` and the
288+
entrypoint is ``args[1]`` of the captured ``args``), then raises a sentinel so
289+
no network call happens.
290+
291+
:param monkeypatch: pytest fixture used to stub out the deploy collaborators.
292+
"""
293293
captured: dict[str, typing.Any] = {}
294294

295295
class _StopDispatch(Exception):
@@ -319,6 +319,20 @@ def spy_make_bundle(
319319
monkeypatch.setattr(api_mod.RSConnectExecutor, "validate_server", lambda self: self)
320320
monkeypatch.setattr(api_mod.RSConnectExecutor, "validate_app_mode", lambda self, app_mode: self)
321321
monkeypatch.setattr(api_mod.RSConnectExecutor, "make_bundle", spy_make_bundle)
322+
return captured
323+
324+
325+
@pytest.mark.parametrize("app_mode,entrypoint,expected_builder_name", DISPATCH_MATRIX)
326+
def test_deploy_pyproject_dispatches_by_app_mode(
327+
runner: CliRunner,
328+
project_dir: pathlib.Path,
329+
app_mode: str,
330+
entrypoint: str,
331+
expected_builder_name: str,
332+
monkeypatch: pytest.MonkeyPatch,
333+
):
334+
"""Each ``[tool.rsconnect].app_mode`` routes to its matching bundle builder."""
335+
captured = _spy_make_bundle(monkeypatch)
322336

323337
_write_pyproject(
324338
project_dir,
@@ -347,6 +361,47 @@ def spy_make_bundle(
347361
pass
348362

349363

364+
_EXPRESS_APP = "from shiny.express import ui\n\nui.h1('hi')\n"
365+
366+
367+
def test_deploy_pyproject_shiny_express_entrypoint_matches_deploy_shiny(
368+
runner: CliRunner, project_dir: pathlib.Path, monkeypatch: pytest.MonkeyPatch
369+
):
370+
"""A Shiny Express app deployed via ``deploy pyproject`` gets the same
371+
``shiny.express.app:`` entrypoint as the dedicated ``deploy shiny`` path.
372+
373+
Without the rewrite Connect cannot find an application object and the app
374+
crashes on boot, so both deploy paths must agree on the express form.
375+
"""
376+
(project_dir / "app.py").write_text(_EXPRESS_APP)
377+
_write_pyproject(
378+
project_dir,
379+
"""
380+
[project]
381+
name = "hello_app"
382+
version = "0.0.1"
383+
dependencies = ["shiny"]
384+
385+
[tool.rsconnect]
386+
app_mode = "python-shiny"
387+
entrypoint = "app.py"
388+
title = "Express App"
389+
""",
390+
)
391+
server = ["-s", "http://example.invalid", "-k", "fake-key"]
392+
393+
captured = _spy_make_bundle(monkeypatch)
394+
runner.invoke(cli, ["deploy", "shiny", str(project_dir), *server])
395+
shiny_entrypoint = captured["args"][1]
396+
397+
captured = _spy_make_bundle(monkeypatch)
398+
runner.invoke(cli, ["deploy", "pyproject", str(project_dir), *server])
399+
pyproject_entrypoint = captured["args"][1]
400+
401+
assert shiny_entrypoint.startswith("shiny.express.app:")
402+
assert pyproject_entrypoint == shiny_entrypoint
403+
404+
350405
# ---------------------------------------------------------------------------
351406
# Title / entrypoint override
352407
# ---------------------------------------------------------------------------

tests/test_quickstart.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,31 @@ def test_quickstart_requires_type_and_name(runner: CliRunner, in_tmp_cwd: pathli
9595
assert result.exit_code != 0
9696

9797

98-
def test_quickstart_help_lists_quarto_shiny(runner: CliRunner):
99-
"""``quarto-shiny`` is a first-class TYPE, not a flag on ``quarto``."""
98+
def test_quickstart_help_lists_quarto(runner: CliRunner):
99+
"""``quarto`` is a supported TYPE; the broken ``quarto-shiny`` scaffold was removed."""
100100
result = runner.invoke(cli, ["quickstart", "--help"])
101101
assert result.exit_code == 0, result.output
102-
assert "quarto-shiny" in result.output
102+
assert "quarto" in result.output
103+
assert "quarto-shiny" not in result.output
103104
# The legacy ``--shiny`` flag was removed in favor of the explicit type.
104105
assert "--shiny" not in result.output
105106

106107

108+
def test_quickstart_quarto_shiny_not_supported(runner: CliRunner, in_tmp_cwd: pathlib.Path):
109+
"""``quarto-shiny`` stays a known alias but no longer scaffolds.
110+
111+
The template produced an invalid doc that Connect rejected, so the
112+
scaffold was removed while the alias still routes to the shared
113+
"does not yet support" error rather than a hard "unknown type".
114+
"""
115+
result = _invoke_quickstart(runner, "quarto-shiny", "hello_app")
116+
assert result.exit_code != 0
117+
combined = result.output + (result.stderr if result.stderr_bytes else "")
118+
assert "does not yet support" in combined
119+
assert "quarto-shiny" in combined
120+
assert not (in_tmp_cwd / "hello_app").exists()
121+
122+
107123
@pytest.mark.parametrize(
108124
"args,expected",
109125
[
@@ -167,7 +183,7 @@ def test_quickstart_unknown_type_lists_supported(runner: CliRunner, in_tmp_cwd:
167183
result = _invoke_quickstart(runner, "nonesuch", "hello_app")
168184
assert result.exit_code != 0
169185
combined = result.output + (result.stderr if result.stderr_bytes else "")
170-
for expected in ("streamlit", "shiny", "fastapi", "api", "flask", "notebook", "voila", "quarto", "quarto-shiny"):
186+
for expected in ("streamlit", "shiny", "fastapi", "api", "flask", "notebook", "voila", "quarto"):
171187
assert expected in combined, f"{expected!r} missing from error output: {combined!r}"
172188
assert not (in_tmp_cwd / "hello_app").exists()
173189

@@ -307,7 +323,6 @@ def test_quickstart_does_not_duplicate_deps_in_tool_rsconnect(runner: CliRunner,
307323
pytest.param(("notebook",), "jupyter-static", id="notebook-default"),
308324
pytest.param(("voila",), "jupyter-voila", id="voila"),
309325
pytest.param(("quarto",), "quarto-static", id="quarto-default"),
310-
pytest.param(("quarto-shiny",), "quarto-shiny", id="quarto-shiny"),
311326
]
312327

313328

@@ -591,13 +606,6 @@ def test_quickstart_rolls_back_on_keyboard_interrupt(
591606
("Note: Quarto must be installed separately: https://quarto.org",),
592607
id="quarto-default",
593608
),
594-
pytest.param(
595-
"quarto-shiny",
596-
(),
597-
"uv run quarto preview report.qmd",
598-
("Note: Quarto must be installed separately: https://quarto.org",),
599-
id="quarto-shiny",
600-
),
601609
]
602610

603611

0 commit comments

Comments
 (0)