From 17c5af96c7ebb882b40bcf6d20c366724e230e43 Mon Sep 17 00:00:00 2001 From: Neal Richardson Date: Tue, 13 Oct 2026 18:14:17 -0400 Subject: [PATCH] fix: quickstart builds the venv on the running Python Without --python, uv venv picked the newest Python satisfying requires-python. In CI that was a uv-managed 3.15, which has no pyarrow wheels, so the streamlit quickstart tests failed building from source. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/CHANGELOG.md | 4 +++ rsconnect/quickstart/quickstart.py | 14 +++++++-- tests/test_quickstart.py | 50 +++++++++++++++++++++++++++--- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f2fbf32a2..2077475b2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the manifest checksum. On Windows, the file had CRLF line endings and the checksum did not match. An existing requirements file with CRLF line endings also has the correct checksum now. +- `rsconnect quickstart` without `--python` now creates the project's virtual environment + with the Python that runs `rsconnect`. Before, uv picked the newest installed Python + that satisfied `requires-python`. Dependencies could then fail to install on a + Python version so new that they had no wheels for it yet. ## [1.31.1] - 2026-09-10 diff --git a/rsconnect/quickstart/quickstart.py b/rsconnect/quickstart/quickstart.py index b672d66d4..4f9ee9dcf 100644 --- a/rsconnect/quickstart/quickstart.py +++ b/rsconnect/quickstart/quickstart.py @@ -109,7 +109,10 @@ def run_quickstart( target.mkdir() try: _scaffold(target, name=name, spec=spec, requires_python=requires_python) - _install_venv(target) + # Without ``--python``, build the venv on the running interpreter. + # Otherwise uv picks the newest Python matching ``requires-python``, + # which may be too new for some dependencies to have wheels yet. + _install_venv(target, python=sys.executable if python_version is None else None) except BaseException: shutil.rmtree(target, ignore_errors=True) raise @@ -440,9 +443,13 @@ def _format_local_run(spec: TemplateSpec, *, name: str) -> str: # --------------------------------------------------------------------------- -def _install_venv(target: pathlib.Path) -> None: +def _install_venv(target: pathlib.Path, python: typing.Optional[str] = None) -> None: """Populate ``.venv/`` via ``uv venv`` + ``uv sync``. + ``python`` is passed to ``uv venv --python``; when omitted, uv chooses an + interpreter from ``requires-python``. ``uv sync`` keeps the venv's + interpreter as long as it satisfies ``requires-python``. + stdout/stderr are inherited from the parent process so users see uv's own progress output in real time ("Creating environment...", "Resolving dependencies..."). A non-zero exit raises ``RSConnectException``, which @@ -456,7 +463,8 @@ def _install_venv(target: pathlib.Path) -> None: env.pop("VIRTUAL_ENV", None) # ``uv venv`` first so ``uv sync`` reads the freshly-created ``.venv``; # if the first step fails there is no point continuing. - for command in (("uv", "venv"), ("uv", "sync")): + venv_command = ("uv", "venv") if python is None else ("uv", "venv", "--python", python) + for command in (venv_command, ("uv", "sync")): result = subprocess.run(list(command), cwd=target, env=env) if result.returncode != 0: joined = " ".join(command) diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 504285a61..b66ef2611 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -317,7 +317,7 @@ def test_quickstart_python_option_sets_requires_python( runner: CliRunner, in_tmp_cwd: pathlib.Path, monkeypatch: pytest.MonkeyPatch ): """``--python`` overrides the detected interpreter version in ``requires-python``.""" - monkeypatch.setattr("rsconnect.quickstart.quickstart._install_venv", lambda target: None) + monkeypatch.setattr("rsconnect.quickstart.quickstart._install_venv", lambda target, python=None: None) result = _invoke_quickstart(runner, "streamlit", "--python", ">=3.10", "hello_app") assert result.exit_code == 0, result.output data = _read_pyproject(in_tmp_cwd / "hello_app") @@ -328,7 +328,7 @@ def test_quickstart_python_option_used_verbatim( runner: CliRunner, in_tmp_cwd: pathlib.Path, monkeypatch: pytest.MonkeyPatch ): """A value starting with an operator (incl. a range) passes through unchanged.""" - monkeypatch.setattr("rsconnect.quickstart.quickstart._install_venv", lambda target: None) + monkeypatch.setattr("rsconnect.quickstart.quickstart._install_venv", lambda target, python=None: None) result = _invoke_quickstart(runner, "streamlit", "--python", ">=3.11,<3.14", "hello_app") assert result.exit_code == 0, result.output data = _read_pyproject(in_tmp_cwd / "hello_app") @@ -339,7 +339,7 @@ def test_quickstart_python_option_bare_version_means_any_patch( runner: CliRunner, in_tmp_cwd: pathlib.Path, monkeypatch: pytest.MonkeyPatch ): """A bare ``major.minor`` becomes ``==3.10.*`` so any 3.10.x satisfies it.""" - monkeypatch.setattr("rsconnect.quickstart.quickstart._install_venv", lambda target: None) + monkeypatch.setattr("rsconnect.quickstart.quickstart._install_venv", lambda target, python=None: None) result = _invoke_quickstart(runner, "streamlit", "--python", "3.10", "hello_app") assert result.exit_code == 0, result.output data = _read_pyproject(in_tmp_cwd / "hello_app") @@ -350,7 +350,7 @@ def test_quickstart_python_option_full_version_is_exact( runner: CliRunner, in_tmp_cwd: pathlib.Path, monkeypatch: pytest.MonkeyPatch ): """A full ``major.minor.patch`` becomes ``==3.11.14`` (exact, no trailing ``.*``).""" - monkeypatch.setattr("rsconnect.quickstart.quickstart._install_venv", lambda target: None) + monkeypatch.setattr("rsconnect.quickstart.quickstart._install_venv", lambda target, python=None: None) result = _invoke_quickstart(runner, "streamlit", "--python", "3.11.14", "hello_app") assert result.exit_code == 0, result.output data = _read_pyproject(in_tmp_cwd / "hello_app") @@ -570,6 +570,48 @@ def fake_run(cmd, cwd, env): assert "VIRTUAL_ENV" not in env, env +@pytest.mark.parametrize( + "extra_args, expected_python", + [ + # Without ``--python``, the venv uses the interpreter running quickstart, + # not the newest one uv can find. + ([], sys.executable), + # With ``--python``, uv picks an interpreter from ``requires-python``. + (["--python", ">=3.11"], None), + ], +) +def test_quickstart_venv_interpreter( + runner: CliRunner, + in_tmp_cwd: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, + extra_args: typing.List[str], + expected_python: typing.Optional[str], +): + calls: typing.List[typing.Optional[str]] = [] + monkeypatch.setattr( + "rsconnect.quickstart.quickstart._install_venv", + lambda target, python=None: calls.append(python), + ) + result = _invoke_quickstart(runner, "streamlit", *extra_args, "hello_app") + assert result.exit_code == 0, result.output + assert calls == [expected_python] + + +def test_install_venv_passes_python(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path): + commands: typing.List[typing.List[str]] = [] + + def fake_run(cmd, cwd, env): + commands.append(cmd) + return mock.Mock(returncode=0) + + monkeypatch.setattr("rsconnect.quickstart.quickstart.subprocess.run", fake_run) + from rsconnect.quickstart.quickstart import _install_venv + + _install_venv(tmp_path, python="/path/to/python") + + assert commands == [["uv", "venv", "--python", "/path/to/python"], ["uv", "sync"]] + + def test_quickstart_creates_populated_venv(runner: CliRunner, in_tmp_cwd: pathlib.Path): result = _invoke_quickstart(runner, "streamlit", "hello_app") assert result.exit_code == 0, result.output