diff --git a/rsconnect/actions.py b/rsconnect/actions.py index a16e7359..fb117a28 100644 --- a/rsconnect/actions.py +++ b/rsconnect/actions.py @@ -276,6 +276,59 @@ def validate_quarto_engines(inspect: QuartoInspectResult): return engines +def is_quarto_shiny(inspect: QuartoInspectResult) -> bool: + """ + Determines if the Quarto document uses Shiny by checking the quarto inspect output. + + Quarto documents with `server: shiny` in their YAML front matter will have + this reflected in the inspect output in multiple locations: + - formats..metadata.server.type == "shiny" + - fileInformation..metadata.server == "shiny" + + :param inspect: The parsed JSON from a 'quarto inspect' against the project. + :return: True if the document uses Shiny, False otherwise. + """ + # Cast to Any for accessing fields not defined in the TypedDict. + # The quarto inspect output contains many more fields than we've typed. + inspect_any = cast(typing.Any, inspect) + + # Check formats..metadata.server.type + formats = inspect_any.get("formats", {}) + if isinstance(formats, dict): + for format_data in formats.values(): + if isinstance(format_data, dict): + metadata = format_data.get("metadata", {}) + if isinstance(metadata, dict): + server = metadata.get("server", {}) + if isinstance(server, dict) and server.get("type") == "shiny": + return True + + # Check fileInformation..metadata.server + file_info = inspect_any.get("fileInformation", {}) + if isinstance(file_info, dict): + for file_data in file_info.values(): + if isinstance(file_data, dict): + metadata = file_data.get("metadata", {}) + if isinstance(metadata, dict): + server = metadata.get("server") + if server == "shiny": + return True + + return False + + +def infer_quarto_app_mode(inspect: QuartoInspectResult) -> AppMode: + """ + Infers the appropriate app mode for a Quarto document based on the inspect output. + + :param inspect: The parsed JSON from a 'quarto inspect' against the project. + :return: AppModes.SHINY_QUARTO if the document uses Shiny, AppModes.STATIC_QUARTO otherwise. + """ + if is_quarto_shiny(inspect): + return AppModes.SHINY_QUARTO + return AppModes.STATIC_QUARTO + + # =============================================================================== # START: The following deprecated functions are here only for the vetiver-python # package. diff --git a/rsconnect/main.py b/rsconnect/main.py index e939ab30..4a8df359 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -42,6 +42,7 @@ cli_feedback, create_quarto_deployment_bundle, describe_manifest, + infer_quarto_app_mode, quarto_inspect, set_verbosity, test_api_key, @@ -1598,6 +1599,7 @@ def deploy_quarto( logger.debug("Quarto: %s" % quarto) inspect = quarto_inspect(quarto, file_or_directory) engines = validate_quarto_engines(inspect) + app_mode = infer_quarto_app_mode(inspect) environment = None if "jupyter" in engines: @@ -1635,13 +1637,13 @@ def deploy_quarto( ( ce.validate_server() - .validate_app_mode(app_mode=AppModes.STATIC_QUARTO) + .validate_app_mode(app_mode=app_mode) .make_bundle( create_quarto_deployment_bundle, file_or_directory, extra_files, exclude, - AppModes.STATIC_QUARTO, + app_mode, inspect, environment, image=image, @@ -2542,11 +2544,12 @@ def write_manifest_quarto( with cli_feedback("Creating %s" % environment.filename): write_environment_file(environment, base_dir) + app_mode = infer_quarto_app_mode(inspect) with cli_feedback("Creating manifest.json"): write_quarto_manifest_json( file_or_directory, inspect, - AppModes.STATIC_QUARTO, + app_mode, environment, extra_files, exclude, diff --git a/tests/test_actions.py b/tests/test_actions.py index aca947cf..13ad59de 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -1,9 +1,118 @@ import os from unittest import TestCase -from rsconnect.actions import _verify_server +from rsconnect.actions import _verify_server, infer_quarto_app_mode, is_quarto_shiny from rsconnect.api import RSConnectServer from rsconnect.exception import RSConnectException +from rsconnect.models import AppModes + + +class TestQuartoShinyDetection(TestCase): + def test_is_quarto_shiny_via_formats_metadata(self): + """Test detection via formats..metadata.server.type""" + inspect = { + "quarto": {"version": "1.3.0"}, + "engines": ["jupyter"], + "formats": { + "html": { + "metadata": { + "server": {"type": "shiny"}, + }, + }, + }, + } + self.assertTrue(is_quarto_shiny(inspect)) + + def test_is_quarto_shiny_via_file_information(self): + """Test detection via fileInformation..metadata.server""" + inspect = { + "quarto": {"version": "1.3.0"}, + "engines": ["jupyter"], + "fileInformation": { + "/path/to/app.qmd": { + "metadata": { + "server": "shiny", + }, + }, + }, + } + self.assertTrue(is_quarto_shiny(inspect)) + + def test_is_quarto_shiny_static_document(self): + """Test that static documents are not detected as Shiny""" + inspect = { + "quarto": {"version": "1.3.0"}, + "engines": ["jupyter"], + "formats": { + "html": { + "metadata": { + "title": "My Document", + }, + }, + }, + } + self.assertFalse(is_quarto_shiny(inspect)) + + def test_is_quarto_shiny_empty_inspect(self): + """Test with minimal inspect output""" + inspect = { + "quarto": {"version": "1.3.0"}, + "engines": ["markdown"], + } + self.assertFalse(is_quarto_shiny(inspect)) + + def test_is_quarto_shiny_wrong_server_type(self): + """Test that documents with wrong server type are not detected as Shiny""" + inspect = { + "quarto": {"version": "1.3.0"}, + "engines": ["jupyter"], + "formats": { + "html": { + "metadata": { + "server": {"type": "other"}, + }, + }, + }, + } + self.assertFalse(is_quarto_shiny(inspect)) + + def test_is_quarto_shiny_wrong_server_value(self): + """Test that documents with wrong server value in fileInformation are not detected as Shiny""" + inspect = { + "quarto": {"version": "1.3.0"}, + "engines": ["jupyter"], + "fileInformation": { + "/path/to/app.qmd": { + "metadata": { + "server": "other", + }, + }, + }, + } + self.assertFalse(is_quarto_shiny(inspect)) + + def test_infer_quarto_app_mode_shiny(self): + """Test that Shiny documents get SHINY_QUARTO mode""" + inspect = { + "quarto": {"version": "1.3.0"}, + "engines": ["jupyter"], + "formats": { + "html": { + "metadata": { + "server": {"type": "shiny"}, + }, + }, + }, + } + self.assertEqual(infer_quarto_app_mode(inspect), AppModes.SHINY_QUARTO) + + def test_infer_quarto_app_mode_static(self): + """Test that static documents get STATIC_QUARTO mode""" + inspect = { + "quarto": {"version": "1.3.0"}, + "engines": ["markdown"], + } + self.assertEqual(infer_quarto_app_mode(inspect), AppModes.STATIC_QUARTO) class TestActions(TestCase):