Skip to content

fix: send the Python version to Connect Cloud - #849

Merged
samperman merged 4 commits into
mainfrom
send-python-version-to-connect-cloud
Sep 10, 2026
Merged

samperman merged 4 commits into
mainfrom
send-python-version-to-connect-cloud

Conversation

@samperman

@samperman samperman commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Intent

Resolves #848.

Deploys to Posit Connect Cloud always built with Connect Cloud's fallback of Python 3.9, whatever the project asked for. rsconnect detects the Python version requirement correctly and writes it into manifest.json, but never sent it to the Connect Cloud API — and Connect Cloud does not read it out of the bundle. Content needing 3.10+ failed during dependency resolution.

Reported on the forum: https://forum.posit.co/t/set-python-version-on-connect-cloud-when-deploying-using-rsconnect-via-command-line/217824

Type of Change

  • Bug Fix
  • New Feature
  • Breaking Change

Approach

Connect Cloud takes the interpreter from the revision's python_version field — next_revision.python_version on POST /v1/contents, revision_overrides.python_version on PATCH /v1/contents/{id}. It is MAJOR.MINOR only, from a fixed set (RequestPythonVersion in https://api.connect.posit.cloud/openapi.json, currently 3.9 through 3.14). Posit Publisher already sends it; neither rsconnect client did.

resolve_python_version() derives the value from the manifest the bundle already carries — environment.python.requires and python.version. Reading the built bundle rather than the environment means deploy manifest is covered too, which does no environment inspection of its own, and no per-command plumbing was needed.

Which version wins:

  • The interpreter the bundle was built against, when Connect Cloud offers that MAJOR.MINOR and the constraint allows it, so a deploy reproduces the development environment.
  • Otherwise the lowest offered line satisfying the constraint, so >=3.10 stays on 3.10 as Connect Cloud adds newer ones rather than drifting upward.
  • A constraint no offered line satisfies fails before the bundle is uploaded, instead of surfacing as a server-side dependency resolution error.

Two trade-offs worth flagging:

Patch-level constraints can only be honored as far as their minor line. The API cannot express a patch, so ==3.11.14 deploys onto whatever 3.11.x Connect Cloud runs. Rejecting such constraints would refuse content Connect Cloud can run, and there is no endpoint exposing the patch behind each line. This matches Connect's own default of MAJOR.MINOR matching.

Judging a constraint against a minor line needs care. Testing 3.11 alone treats it as 3.11.0, which would wrongly reject >=3.11.3,<3.12. _line_satisfies() instead tests the patches the constraint names, one above each, and the bottom of the line — every edge of the region a PEP 440 constraint admits falls on one of those. This avoids an arbitrary patch cap, so ==3.11.99 and ==3.11.100 behave identically.

Both payloads omit the field when there is no version to send, so R content deploys unchanged and a redeploy does not clear a version picked in the Connect Cloud UI. python_version sits at the end of the optional parameters on create_content, update_content, and prepare_deploy so it cannot capture a positional argument.

r_version and quarto_version are accepted on the same payloads and are still never sent — likely the same bug for R and Quarto content, left for a separate change since confirming it needs a real deploy.

Automated Tests

25 new tests in tests/test_connect_cloud.py:

  • TestResolvePythonVersion — constraint shapes (~=, >=, ==X.Y.*, exact patch, !=), local-interpreter preference, versions outside the offered range, unparsable constraints, and the high-patch cases.
  • TestConnectCloudPythonVersionFromBundle — reads the version out of real gzipped bundles, including a manifest with no Python section.
  • Client payload tests — the field present and absent on both create_content and update_content.
  • prepare_deploy passthrough on the create and update paths.

_line_satisfies() was additionally checked against exhaustive enumeration over patches 0–400, across 9,996 line/constraint pairs generated from 1,666 constraints, with no disagreements. That check is not committed; it is a one-off verification of the boundary-point argument.

Full suite: 1240 passed, 12 skipped. ruff clean. Pyright unchanged from baseline.

Directions for Reviewers

The Python version rsconnect runs under is the one it inspects, so install the branch into the project's venv rather than running it from this repo:

cd ~/some-shiny-app          # with .python-version containing 3.12
VIRTUAL_ENV=$PWD/.venv uv pip install -e /path/to/rsconnect-python
.venv/bin/rsconnect deploy shiny -n <cloud-target> -vv .

-vv logs request bodies. Look for "python_version": "3.12" in next_revision on a first deploy, or revision_overrides on a redeploy, and confirm the build log shows 3.12 rather than 3.9.17.

Worth exercising:

  • A redeploy, which takes the PATCH path with a different payload.
  • .python-version set to 3.8, which should fail before the upload with "Posit Connect Cloud does not offer".
  • R content, where the field should be absent rather than null, so a version set in the UI survives.
  • deploy manifest, which is the path from the forum report.

Checklist

  • I have updated CHANGELOG.md to cover notable changes.
  • I have updated all related GitHub issues to reflect their current state.
  • I have run the rsconnect-python-tests-at-night workflow in Connect against this feature branch.

Connect Cloud picks the interpreter from the revision's python_version field
on POST /v1/contents and PATCH /v1/contents/{id}. rsconnect never sent it, and
Connect Cloud does not read the version out of the bundle's manifest, so every
deploy built with its fallback of Python 3.9 no matter what the project asked
for. Content needing 3.10+ failed during dependency resolution.

Resolve a MAJOR.MINOR version from the manifest the bundle already carries and
send it on both requests. Reading the bundle rather than the environment covers
`deploy manifest` too, which does no environment inspection of its own.

The interpreter the bundle was built against wins when Connect Cloud offers
that MAJOR.MINOR and the constraint allows it. Otherwise the lowest offered
version satisfying the constraint is used, so ">=3.10" stays on 3.10 as Connect
Cloud adds newer ones. A constraint no offered version satisfies now fails
before the bundle is uploaded.

Both payloads omit the field when there is no Python version to send, so R
content deploys unchanged and a redeploy does not clear a version picked in the
Connect Cloud UI.

r_version and quarto_version are accepted on the same payloads and are still
never sent; that is left for a separate change.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-10 11:37 UTC

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
8956 7708 86% 0% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
rsconnect/api.py 88% 🟢
rsconnect/connect_cloud.py 98% 🟢
TOTAL 93% 🟢

updated for commit: c5158fd by action🐍

@samperman
samperman requested review from karawoo, nealrichardson and vrsarah and removed request for nealrichardson September 8, 2026 22:04
Comment thread rsconnect/connect_cloud.py Outdated
Comment on lines +416 to +419
raise RSConnectException(
"This content requires Python %s, which Posit Connect Cloud does not offer. "
"Supported versions are %s. Change the requirement in .python-version, "
"pyproject.toml, or setup.cfg." % (requires, ", ".join(PYTHON_VERSIONS))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or, the user might need to upgrade their rsconnect-python version (if we add a new python version to the PYTHON_VERSIONS list)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. i'm going to get rid of the list of versions

The resolver held the whole RequestPythonVersion enum and refused anything
outside it. That list goes stale in the direction that hurts: the day Connect
Cloud adds 3.15, a project requiring it would be told to change a requirement
that is in fact fine, and the only real remedy would be upgrading rsconnect.

Take the version from the constraint's lower bound rather than by enumerating a
candidate set, and send it without judging whether Connect Cloud offers it. A
version added after this release now deploys as it is.

Keep only the floor. Requests below it are rejected outright rather than
normalized -- the fallback to the lowest supported version applies to versions
already stored, not to ones arriving over the API -- and rsconnect still runs on
Pythons older than Connect Cloud offers, so a 3.8 user takes the platform
default with a warning instead of a failed deploy.

This drops the patch enumeration along with the list, since the lower bound
carries the same information without a bound to pick.
@samperman
samperman requested a review from vrsarah September 9, 2026 01:09

@karawoo karawoo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are some cases here where someone could get a Python version they've explicitly forbidden which will be confusing. If they've set .python-version to >3.11.5, we request 3.11, and if Connect Cloud only has 3.11.2 they'll be running on a version <3.11.5. Another scenario is if the version constraint isn't satisfiable by Connect Cloud, e.g. >=3.8,<3.9. 3.8 is the only version that satisfies this but it's lower than Connect Cloud's floor so it resolves to None and then Connect Cloud will pick a higher version that's expliclty not allowed according to the constraint.

Comment on lines +439 to +440
Connect Cloud picks the patch itself, so a constraint that names one ("==3.11.14")
can only be honored as far as its minor line.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no way to respect the patch version? If not, can we include some sort of warning or info logging if the .python-version specifies a patch version so users know it won't be respected?

@samperman samperman Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, Connect Cloud doesn't have all the patch versions available. we can provide the detailed patch but connect cloud will basically ignore it and choose the latest patch available. We could simplify this and just send the full patch version, then let connect cloud decide what to do. that might be less complicated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turns out this doesn't work (Connect Cloud doesn't accept it). i'll add some logging

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we're now logging Requesting Python <major>.<minor> but I don't think that is enough to convey that the patch version is being ignored.

Comment thread rsconnect/connect_cloud.py Outdated
Comment on lines +434 to +437
Whether Connect Cloud offers the result is left to Connect Cloud, so a version
it adds after this release still deploys. The one version rule kept here is the
floor: below it the request would be rejected outright, and omitting the field
to take the platform default is more useful than failing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like context for decisions within the PR more than external facing documentation

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed... will clean this up

Say which version each deploy asks for, and warn when nothing Connect Cloud
offers can satisfy the requirement. A .python-version of 3.11.5 is widened to
the 3.11 line before the manifest is written, so the request was previously
silent about dropping the patch.

Also resolve from the higher of the requirement's lowest version and Connect
Cloud's, so a requirement it can meet no longer falls back to the platform
default: ">=3.8" resolves to 3.9 instead of nothing.
@samperman
samperman requested a review from karawoo September 9, 2026 13:32
@samperman

Copy link
Copy Markdown
Collaborator Author

@karawoo i think i've addressed all the issues you brought up. thanks!

Comment on lines +439 to +440
Connect Cloud picks the patch itself, so a constraint that names one ("==3.11.14")
can only be honored as far as its minor line.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we're now logging Requesting Python <major>.<minor> but I don't think that is enough to convey that the patch version is being ignored.

Comment thread rsconnect/connect_cloud.py Outdated
Comment on lines +456 to +457
nothing usable to send, which leaves the field off the request so Connect
Cloud keeps whatever the content already has.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes a redeploy, can we also note what happens on first deploy?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i updated all of these... the log about patch version being ignored doesn't actually say what patch version is being ignored, since at the time that log happens, the info isn't readily available, so it require a little more surgery than i thought was needed. let me know if you think that is still not strong enough. (but if thats the only thing left, maybe we could do that in a future PR)

Comment thread rsconnect/connect_cloud.py Outdated
# with one, the search below may still find a version it does offer.
if specifier is None:
logger.warning(
"Posit Connect Cloud does not offer Python %s; it will choose a version for this content." % minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning message seems specific to first deploys; redeploys behave differently right (reuse the current python version for the content)?

Comment thread rsconnect/connect_cloud.py Outdated
Comment on lines +505 to +508
"No Python version Posit Connect Cloud offers satisfies the requirement %s. "
"It will choose a version, and the content will run on one the requirement "
"does not allow." % requires
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also seems inaccurate in the case of redeploys.

Comment thread rsconnect/api.py Outdated
Comment on lines +2197 to +2198
None means the request omits the field, leaving the version Connect Cloud
already has on the content. Content with no Python at all lands here too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this is a first deploy?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Comment on lines +482 to +483
if minor is not None and Version(minor) >= MINIMUM_PYTHON_VERSION:
return _log_requested(minor)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the local version we send is higher than the highest one Connect Cloud has?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the build will fail with an error message that the requested python version is unsupported. I thought this was better than maintaining a list of supported versions in the client and requiring a patch when new versions are added. Do you think we should maintain a list here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I don't think we should maintain a list, I think it makes sense to fail if the requested version isn't supported, but it seems odd that the behavior is different depending on if the user's python version is too low or too high.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true... i considering dropping the min too 🤷 there is no chance we will ever support python 3.8 or 3.7 (or anything lower) so its possible to short circuit in that case

The warnings and docstrings described only a redeploy, where leaving
python_version off the request keeps the version already on the content. On a
first deploy there is no stored version and Connect Cloud uses its own default,
which the messages did not mention.

Both warnings now end with the same sentence naming the two outcomes.
resolve_python_version runs before prepare_deploy has looked the content up, so
it cannot tell which case a deploy is in.

Also say that Connect Cloud chooses the patch release when logging the request.
"Requesting Python 3.11" alone did not convey that a patch the user asked for
was dropped.
@samperman
samperman requested a review from karawoo September 9, 2026 21:15
@samperman
samperman merged commit 438c894 into main Sep 10, 2026
25 checks passed
@samperman
samperman deleted the send-python-version-to-connect-cloud branch September 10, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Connect Cloud deploys always use default python version

3 participants