Skip to content

Fix uv project shadowing parent uv settings (#1545) - #1566

Merged
Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-uv-project-ancestor-config
Oct 15, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-uv-project-ancestor-config

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1545

Summary

A uv project whose own pyproject.toml has no [tool.uv] table (and no uv.toml beside it) takes uv's settings from a parent uv.toml or [tool.uv]. Hosted and vendored scan added [tool.uv.sources] to such a project, which made uv read the project's pyproject instead and drop every parent setting. uv sync --locked then failed, a plain uv lock re-resolved without the settings, and the scan still reported success.

Both modes now refuse before writing anything and name the parent file:

  • hosted: redirect_uv_project_unsupported, the code it already uses for uv project shapes it can't wire
  • vendored: a new pypi_uv_ancestor_settings code

This takes the issue's safer option (refuse) over copying the parent's settings into the project, which would change semantics.

Root cause

uv looks up a project's settings the same way it looks up a PEP 723 script's: the nearest directory at or above it with a uv.toml or a pyproject.toml holding [tool.uv]. #1501 / #1504 added that lookup (find_ancestor_uv_config) but used it only in the script lane, on the assumption that "a project stops that lookup at itself". That only holds once the project has [tool.uv], and here socket-patch is what adds it.

Changes

  • utils/python_script.rs: new refuse_uv_project_shadowing(pyproject, uv_toml, ancestor). It refuses when the root sets no uv settings and the nearest ancestor settings file sets something the project inherits.
    • Keys that uv documents as project metadata (workspace, sources, managed, dev-dependencies, override-/constraint-dependencies and so on) don't count. A monorepo root that is only a workspace root still allows patching.
    • An ancestor file that doesn't parse sets nothing (uv reports it itself).
  • hosted/engine.rs: the ancestor settings file is now read for the uv.lock lane too, not only for script locks. patch/redirect/mod.rs runs the new check on the project lane.
  • vendor/pypi_uv.rs: load_uv_project records the ancestor and check_target_guards refuses with pypi_uv_ancestor_settings. These guards run before the wheel is built, so a refusal writes nothing.
  • docs/testing/uv-compatibility.md: documents the project lane.

No wrapper changes (npm/, pypi/, gem/ only dispatch to the binary).

Test evidence

Red to green (each new test failed against a no-op refuse_uv_project_shadowing and passes with the fix):

Issue Test Without fix With fix
#1545 core rule utils::python_script::ancestor_config_tests::project_refuses_to_shadow_an_ancestors_settings FAILED ok
#1545 hosted planner patch::redirect::python_metadata_pairing_tests::ancestor_settings_refuse_a_project_that_would_shadow_them FAILED ok
#1545 vendored guard vendor::pypi_uv::tests::guards_refuse_shadowing_a_parents_uv_settings FAILED ok
#1545 CLI, both modes, parent uv.toml and parent [tool.uv] mode_migration_pypi::uv_project_under_parent_settings_is_refused_in_both_modes FAILED (hosted pinned 1) ok

The CLI test also checks that a parent holding only [tool.uv.workspace] is still wired in hosted and vendored mode.

With real uv 0.11.32 I confirmed the mechanism and the exemption:

  • with a parent uv.toml (resolution), adding [tool.uv] to the child makes uv lock --locked fail
  • a parent with only [tool.uv.workspace] stays green
  • a parent's constraint-dependencies / override-dependencies never reach the child's lock (in a child-only control they do)

Commands run locally:

  • cargo fmt --all -- --check: ok
  • cargo clippy --workspace --all-features -- -D warnings: ok
  • cargo test --workspace --all-features --no-fail-fast: everything passes except 10 tests that fail only in this sandbox, none in uv code:
    • 9 inject write failures with chmod 0o555, which root ignores here: in_process_redirect x3, repair x2, and core copy_tree / vlt_heal / pypi_poetry / pypi_requirements
    • 1 needs the live patch API: e2e_bun_lockb::binary_shared_bundled_record_hosted_pin_is_managed
  • e2e_redirect_uv_build and e2e_vendor_pypi_build with real uv 0.11.32 (--include-ignored): 23/23 and 55/55

/code-review high findings

Fixed:

  • added override-/constraint-/build-constraint-/exclude-dependencies to the project-only keys (verified with real uv)
  • reworded the refusal so it no longer claims every dropped setting breaks --locked
  • updated the stale docs that said only scripts use the lookup
  • de-duplicated the engine predicate
  • the script and project lanes now share one label/table helper (UvConfig::settings_location)
  • the CLI test now runs vendored mode for the non-refused case too

Not fixed, and why:

🤖 Generated with Claude Code


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A uv project whose pyproject.toml has no [tool.uv] table takes its
settings (exclude-newer, resolution, indexes) from a parent uv.toml or
[tool.uv]. Hosted and vendored scans added [tool.uv.sources] to such a
project, which made uv read the project's pyproject instead and drop
every parent setting: `uv sync --locked` then failed, and a plain
`uv lock` re-resolved without them, while the scan reported success.

Both modes now look up the parent settings file the way uv does and
refuse before writing anything, naming that file: hosted with
redirect_uv_project_unsupported, vendored with the new
pypi_uv_ancestor_settings. A parent that holds only another project's
metadata (a workspace root's workspace or sources table) sets nothing
the project inherits and is still wired.

Fixes #1545

Assisted-by: Claude Code:claude-opus-5-5
A parent's override-dependencies and constraint-dependencies never
reach a child project's lock (checked with uv 0.11), so a monorepo root
that sets only those no longer blocks patching the projects below it.
The refusal no longer claims every dropped setting breaks
`uv sync --locked`, the docs no longer say only scripts read a parent's
settings, the script and project lanes share one way to name the parent
file, and the CLI test now runs vendored mode under a parent that must
not be refused.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 15, 2026 07:21
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a754663. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 15, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at a754663.

  • CI: 36/36 run checks green, 50 skipped by path filters (86 total); no main-wide failures.
  • Bugbot: reviewed a754663, no findings.
  • Mergeable against main, no conflicts; CHANGELOG.md untouched.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief: #1566 at a754663

What it does: A uv project with no [tool.uv] and no uv.toml of its own inherits settings from the nearest parent uv.toml or [tool.uv]. Wiring a patch adds [tool.uv.sources] to the project, and that silently hides the parent's settings. Both modes now detect such a parent and refuse before writing anything: vendored mode with pypi_uv_ancestor_settings, hosted mode with redirect_uv_project_unsupported. The refusal names the parent file. A parent that holds only metadata (workspace, sources, dev-deps and so on) is still allowed.

Risk: low. The change only adds a refusal, the guard runs before any write in both modes, and it reuses the ancestor lookup already used for PEP 723 scripts. One deliberate behaviour change: some projects that wired fine before (under a parent that sets uv options) are now refused.

Look here:

Verified:

Changes I made: none.

Open questions (non-blocking):

  • The refusal fires on any inherited setting, including ones that don't affect resolution (cache-dir, native-tls). The error text always says it "fails uv sync --locked", which is misleading for those keys. Is this strictness what you want?
  • Projects wired by earlier versions already have [tool.uv], so they aren't flagged retroactively.
  • A minor wasted read: hosted lock-only checkouts read the parent file without using it.

Auto-merge (squash) is armed, so approving sends this straight to the merge queue.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] On the first open question: since a754663, the refusal no longer says every dropped setting breaks --locked. It reads "...drop them (a lock written with them then fails uv sync --locked)" (python_script.rs, refuse_uv_project_shadowing). The strictness is deliberate: dropping settings such as keyring-provider, allow-insecure-host or required-version breaks installs even though --locked passes. The PR description covers this under the review findings that were not fixed. If you want cache-only keys such as cache-dir and native-tls exempted, adding them to UV_PROJECT_ONLY_KEYS is a one-line change.


Generated by Claude Code

Merged via the queue into main with commit cc07333 Oct 15, 2026
86 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-uv-project-ancestor-config branch October 15, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants