diff --git a/packages/reflex-hosting-cli/news/+config-missing-pyyaml.bugfix.md b/packages/reflex-hosting-cli/news/+config-missing-pyyaml.bugfix.md new file mode 100644 index 00000000000..1455199b66c --- /dev/null +++ b/packages/reflex-hosting-cli/news/+config-missing-pyyaml.bugfix.md @@ -0,0 +1 @@ +`reflex cloud config` now exits with status 1 when PyYAML is not installed, and with `--json` writes no document, instead of exiting 0 and reporting `{"generated": false, "path": null}` as if a `cloud.yml` already existed. diff --git a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py index 9225aacc5b1..09f8097ac48 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py @@ -1928,16 +1928,17 @@ def generate_config(interactive: bool = True, token: str | None = None) -> Path token: An existing authentication token to use instead of interactive auth. Returns: - The path of the config file written, or None if none was. + The path of the config file written, or None if a cloud.yml already exists. Raises: - click.exceptions.Exit: If authentication fails or user cancels operation. + Exit: If PyYAML is not installed, authentication fails or user cancels operation. """ try: import yaml except ImportError: logger.error("Please install PyYAML to use this command: pip install pyyaml") - return None + # A failure, not "nothing to write": None would read as success to a caller. + raise click.exceptions.Exit(1) from None config_path = Path("cloud.yml") if config_path.exists(): diff --git a/tests/units/reflex_cli/utils/test_hosting.py b/tests/units/reflex_cli/utils/test_hosting.py index 5e909d942a5..938ce9ddd93 100644 --- a/tests/units/reflex_cli/utils/test_hosting.py +++ b/tests/units/reflex_cli/utils/test_hosting.py @@ -1834,3 +1834,38 @@ def test_watch_hands_back_a_control_plane_that_stays_unreachable( warnings = _log_messages(caplog, logging.WARNING) assert "lost contact" in warnings[0] assert f"apps status {deployment} --watch" in warnings[-1] + + +def test_generate_config_without_pyyaml_raises(mocker: MockFixture): + """A missing PyYAML stops before authenticating, and is not reported as None. + + Args: + mocker: Pytest mocker fixture. + """ + mocker.patch.dict("sys.modules", {"yaml": None}) + get_client = mocker.patch("reflex_cli.utils.hosting.get_authenticated_client") + + with pytest.raises(click.exceptions.Exit) as exc_info: + hosting.generate_config(interactive=False) + + assert exc_info.value.exit_code == 1 + get_client.assert_not_called() + + +def test_generate_config_with_existing_file_returns_none( + mocker: MockFixture, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + """An existing cloud.yml is left alone and reported as nothing written. + + Args: + mocker: Pytest mocker fixture. + tmp_path: A temporary directory standing in for the app root. + monkeypatch: Pytest monkeypatch fixture. + """ + monkeypatch.chdir(tmp_path) + (tmp_path / "cloud.yml").write_text("name: existing\n") + get_client = mocker.patch("reflex_cli.utils.hosting.get_authenticated_client") + + assert hosting.generate_config(interactive=False) is None + assert (tmp_path / "cloud.yml").read_text() == "name: existing\n" + get_client.assert_not_called() diff --git a/tests/units/reflex_cli/v2/test_vmtypes_regions.py b/tests/units/reflex_cli/v2/test_vmtypes_regions.py index 926c7f1a66a..a5bf054b87a 100644 --- a/tests/units/reflex_cli/v2/test_vmtypes_regions.py +++ b/tests/units/reflex_cli/v2/test_vmtypes_regions.py @@ -327,3 +327,42 @@ def test_generate_cloud_config_json_output_when_nothing_written(mocker: MockFixt assert result.exit_code == 0, result.output assert json.loads(result.stdout) == {"generated": False, "path": None} + + +def test_generate_cloud_config_json_without_pyyaml_writes_no_document( + mocker: MockFixture, +): + """Missing PyYAML is a failure, not a config that was already there. + + Args: + mocker: The pytest-mock fixture. + """ + # A None entry makes `import yaml` raise ImportError. + mocker.patch.dict("sys.modules", {"yaml": None}) + get_client = mocker.patch("reflex_cli.utils.hosting.get_authenticated_client") + + result = runner.invoke(hosting_cli, ["config", "--json"]) + + assert result.exit_code == 1 + assert result.stdout == "" + get_client.assert_not_called() + + +def test_generate_cloud_config_without_pyyaml_exits_nonzero( + mocker: MockFixture, caplog: pytest.LogCaptureFixture +): + """Without --json, a missing PyYAML still fails the command. + + Args: + mocker: The pytest-mock fixture. + caplog: Pytest log capture fixture. + """ + mocker.patch.dict("sys.modules", {"yaml": None}) + mock_console_print = mocker.patch("reflex_cli.utils.console.print") + + result = runner.invoke(hosting_cli, ["config"]) + + assert result.exit_code == 1 + errors = [r.getMessage() for r in caplog.records if r.levelno == logging.ERROR] + assert errors == ["Please install PyYAML to use this command: pip install pyyaml"] + mock_console_print.assert_not_called()