Skip to content

Honor project requires-python declarations for venv base interpreter - #446

Open
isaacbrodsky wants to merge 2 commits into
mainfrom
claude/fused-render-python-version-n332ks
Open

Honor project requires-python declarations for venv base interpreter#446
isaacbrodsky wants to merge 2 commits into
mainfrom
claude/fused-render-python-version-n332ks

Conversation

@isaacbrodsky

@isaacbrodsky isaacbrodsky commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

This change implements support for honoring a project's requires-python declaration when building virtual environments. Previously, all project venvs were built on Python 3.12 regardless of what the project declared. Now, if a project specifies a different Python version requirement, the app will build the venv on the nearest compatible interpreter.

Key Changes

  • Multi-version interpreter resolution: Changed from a single cached interpreter resolution to a per-version cache (_python_by_version dict) to support resolving different Python versions independently. This allows the app to be ready for 3.12 but not for a project's requested 3.11 simultaneously.

  • New project_python_version() function: Reads a project's requires-python declaration and determines the nearest compatible Python version to build on. Returns None if the project's requirement is already satisfied by the default 3.12, ensuring no unnecessary downloads.

  • Version-specific bootstrap keys: Changed PYTHON_BOOTSTRAP_KEY from a constant to a function python_bootstrap_key(version) that includes the version in the key. This prevents two different Python downloads from interfering with each other's progress tracking.

  • Minimum Python floor: Added _MIN_PROJECT_PYTHON_MINOR = 10 (Python 3.10) as the oldest version the app will build on, with a test verifying the app's own code parses on this version.

  • New UnsupportedPythonRequirement exception: Raised when a project's requires-python cannot be satisfied by any interpreter the app supports, with a clear error message quoting the declaration.

  • Project environment integration: Added requires_python_of(), python_pin(), and python_pin_allows() functions to projectenv.py to read and evaluate project Python requirements using PEP 440 specifiers.

  • Fast path gating: Added _app_interpreter_may_run() check in engine.py to prevent the fast path (skipping venv build) when a project pins a Python version that the app's interpreter doesn't satisfy.

  • Worker updates: Modified _env_install_worker.py to accept the base interpreter as a parameter, allowing projects to specify their own Python version for venv building.

Implementation Details

  • The resolution order for finding a suitable interpreter remains: explicit override → current interpreter (if matching version) → uv-managed interpreter → fallback to app's own.

  • Unreadable requires-python declarations are logged as warnings and ignored (fail-open), matching the behavior for invalid TOML and broken markers. This prevents typos from breaking all scripts in a folder.

  • The version string is folded into bootstrap progress keys so that two projects requesting different Pythons don't interfere with each other's download tracking.

  • All existing behavior is preserved for projects that don't declare requires-python — they continue to use the default 3.12 interpreter.

https://claude.ai/code/session_017yR6kUqBNPWnF8292nFs3o


Note

Medium Risk
Changes core env install and /api/run interpreter selection; wrong pin logic could break venv builds or wrongly allow/deny the app fast path, though behavior for unpinned projects is explicitly preserved and heavily tested.

Overview
Project requires-python now drives which Python builds each folder’s venv, instead of always using 3.12. Pins the default already satisfies behave as before; when they don’t, the app picks the nearest allowed minor (floor 3.10), downloads that interpreter under a version-specific bootstrap progress key, and runs uv sync --python with it.

projectenv reads and evaluates pins (PEP 440); envinstall adds project_python_version, per-version resolution cache, UnsupportedPythonRequirement, and project_base_python for the install worker. engine blocks the no-venv fast path when the app interpreter doesn’t satisfy the pin, fails unsatisfiable pins before install, and quotes the folder’s declaration in “needs Python” messages. Build failures now include the chosen base interpreter next to uv’s stderr.

Docs/spec (PY-19, D244), usage.md, and authoring skill are updated; tests cover pin selection, bootstrap keys, worker argv, and fast-path gating.

Reviewed by Cursor Bugbot for commit 5e1e641. Bugbot is set up for automated code reviews on this repo. Configure here.

…D244)

A project venv was always built on the pinned 3.12 (D214), and
`requires-python` was read by nobody: a folder declaring `==3.11.*` was
structurally unbuildable, and the failure was uv's raw requires-python
conflict naming a 3.12 that appears in none of the user's files — no hint
that the version came from the app, and no action that could fix it.

The pin stays the default and becomes the starting point of a search. A
declaration 3.12 satisfies changes nothing (same interpreter, same key, no
download); one it fails moves the base interpreter to the nearest release
the folder allows — `==3.11.*` → 3.11, `>=3.13` → 3.13, `<3.11` → 3.10 —
acquired first under that version's own bootstrap key, then handed to
`uv sync --python`.

- projectenv reads the pin and evaluates it (fail-open on an unreadable
  declaration, fail-closed on an unreadable version).
- envinstall resolves per version rather than per machine: one cache slot
  would let "ready for 3.12" stand in for "ready for the folder's 3.11".
  Candidate minors are derived from the specifier's own clauses, so there
  is no ceiling constant to go stale; the 3.10 floor is checked by a test
  that compiles our wrapper and upstream's child scripts at that grammar.
- The app-interpreter fast path is declined for a folder whose pin this
  app fails — skipping the venv claims equivalence, and a version pin
  denies it.
- A pin nothing can satisfy is refused before any install, with its own
  error type quoting the declaration instead of the generic "internal
  error (not your script)".
- The worker's build-failure message now names the base interpreter — the
  one fact uv's verbatim text cannot supply.

SPEC PY-19, D244; docs/usage.md and the authoring skill updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yR6kUqBNPWnF8292nFs3o

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a25b850. Configure here.

Comment thread fused_render/envinstall.py
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  fused_render
  _env_install_worker.py 310-315
  engine.py 603, 1049
  envinstall.py 349, 506, 576-577, 592-593, 644, 651, 675, 982, 1715
  projectenv.py 402, 405, 431-436, 473-474
  fused_render/server/routers
  env.py 84
Project Total  

This report was generated by python-coverage-comment-action

…on check

Bugbot flagged patch-level pins being judged against bare `3.12`. Checked:
`>=3.12.5`, `>3.12` and `==3.12.5` are NOT read as satisfied (they resolve to
the 3.12 line), but `<=3.12.9` and `!=3.12.3` are — and either direction can
land on a 3.12.x whose patch the folder forbade, because the resolution
matches on (major, minor).

In every one of those cases `uv sync` refuses in its own words, naming both
versions, with the base interpreter beside it — loud and terminal, never a
run on a Python the folder ruled out. The gap is that this module cannot
promise a patch: what it can obtain is `uv python install 3.X`, whose patch
is uv's choice.

So the boundary is real and stays, but it is now written down where the
reasoning lives (D177's corollary: a deliberate omission that is not
recorded is indistinguishable from a bug) and pinned by a test covering both
directions. What closing it properly would take — threading the specifier
through the resolution and its cache, preferring a uv-managed newer patch,
and a terminal answer for downloaded-and-still-unsatisfying — is recorded
too, rather than left to be rediscovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yR6kUqBNPWnF8292nFs3o
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.

2 participants