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
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 11 additions & 3 deletions rsconnect/quickstart/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
50 changes: 46 additions & 4 deletions tests/test_quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down
Loading