Skip to content

uv project under a parent uv.toml / [tool.uv]: hosted and vendored scan add a [tool.uv] table to the project's pyproject.toml, which hides the parent settings from uv, so uv sync --locked fails and a plain uv lock re-resolves without them #1545

Closed

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

uv reads project settings from the nearest directory at or above the project that holds a uv.toml or a pyproject.toml with a [tool.uv] table. A pyproject.toml without [tool.uv] is skipped, and the lookup goes on to the parent. That's the usual monorepo layout: one root uv.toml (or root [tool.uv]) sets exclude-newer, resolution, indexes, constraints and so on for several independent projects below it.

When such a project has no [tool.uv] of its own, hosted and vendored scan write [tool.uv.sources] six = … into its pyproject.toml. That new table makes the project's own pyproject the governing settings file, so uv stops reading the parent and every parent setting disappears at once. The lock socket-patch wrote still records the parent's options ([options] exclude-newer / resolution-mode), so:

  • uv lock --locked / uv sync --locked fail ("The lockfile at uv.lock needs to be updated"). uv explains why: Ignoring existing lockfile due to change in resolution mode: lowest-direct vs. highest, or Resolving despite existing lockfile due to removal of global exclude newer.
  • A plain uv lock / uv sync re-resolves the whole project without the parent settings (no exclude-newer cutoff, default resolution, and the parent's index or constraints dropped). Other dependencies can silently move.
  • scan exits 0 success, and the venv stays unpatched (SOCKET_PATCHED is 0) because the locked sync fails.

#1504 (#1501) already handles this lookup for the script lane (find_ancestor_uv_config), but treats the project lane as "a project stops that lookup at itself" (docs/testing/uv-compatibility.md, the #564 / #1501 bullet). That's only true when the project's pyproject already has [tool.uv]. Here, socket-patch is the one that adds it.

Repro (Linux, main 306ec81)

mkdir -p mono/app && cd mono
printf 'resolution = "lowest-direct"\n' > uv.toml          # or: exclude-newer = "2024-01-01T00:00:00Z"
cd app
printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0"]\n' > pyproject.toml
uv lock && uv sync --locked                                 # lock records [options] resolution-mode = "lowest-direct"
socket-patch scan --mode hosted --yes                       # or --mode vendored: exit 0, success
uv lock --locked                                            # FAILS: lockfile needs to be updated
uv lock --locked -v 2>&1 | grep -i ignoring                 # "change in resolution mode: `lowest-direct` vs. `highest`"

The same happens with the parent setting in ../pyproject.toml under [tool.uv] instead of ../uv.toml.

Real uv alone shows the mechanism, without socket-patch: in the fixture above, append an empty [tool.uv] to app/pyproject.toml, and uv lock --locked fails the same way on 0.5.31 and 0.8.17.

I ran it against a local mock of the patch API / vendor server (the probe-branch mock, with a six 1.16.0 patch). Control: the same fixture without the parent config passes (uv lock --locked 0, uv sync --locked 0, SOCKET_PATCHED 1).

Expected vs actual

Ways to fix it (for maintainers to choose): refuse when the project has no [tool.uv] and an ancestor uv.toml / [tool.uv] exists; or copy the ancestor's settings into the new [tool.uv] (that changes semantics and has merge pitfalls, so a refusal is probably safer).

Matrix (Linux; macOS / Windows not probed: probe branches are blocked this run)

uv hosted, parent uv.toml hosted, parent [tool.uv] vendored, parent uv.toml vendored, parent [tool.uv] control (no parent)
0.5.31 fail fail fail fail pass
0.8.17 fail fail fail fail pass
0.13.0 fail fail fail fail pass

I checked each cell with exclude-newer, and the hosted and vendored cells on all three versions again with resolution = "lowest-direct". Every run reproduced.

After the damage, vendored rollback restores both files and uv lock --locked is green again. Hosted rollback refuses hosted_restore_refused in this one-package fixture (no sibling registry package / exclude-newer), which is the documented restore limit, not this bug.

First bad release

Reproduces on the v5.0.1 release binary (both modes). v4.0.0 didn't wire this fixture against the mock, so I couldn't compare it. Not bisected further.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:5948-5973 (plan_python_metadata_with): the project lane calls refuse_uv_no_sources / rewrite_project_metadata on the root files only, and never reads an ancestor config.
  • crates/socket-patch-core/src/utils/python_script.rs:406 / :431 (root_governs_script, find_ancestor_uv_config): the ancestor lookup exists, but only the script lane uses it (hosted/engine.rs read_candidate_files).
  • crates/socket-patch-core/src/utils/python_script.rs:602 (rewrite_project_metadata) and the vendored uv wiring in crates/socket-patch-core/src/vendor/pypi_uv.rs create the [tool.uv] table.

Activity

  1. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p1 (uv, PyPI family). Confirmed on main fc1b4f9: find_ancestor_uv_config / root_governs_script are called only from the script lanes (hosted/engine.rs, vendor/pypi_lock.rs); the project lane never checks for an ancestor uv.toml / [tool.uv] before adding [tool.uv.sources] to a pyproject that had no [tool.uv]. Follows #1501 / #1504 (script lane only), so not a duplicate. No open PR addresses it.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the uv project lane adds a [tool.uv] table without checking for an ancestor uv.toml / [tool.uv] that it would shadow). Branch: agent/fix-uv-project-ancestor-config. Claim-ID: 2026-10-15T06:21:18Z-cdbb4e


    Generated by Claude Code

  3. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1566


    Generated by Claude Code

  4. added a commit that references this issue on Oct 15, 2026
    b34b23e
  5. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] uv bug-hunt routine (ledger #310): I tested draft PR #1566 (head b34b23e) against real uv. It fixes the reported shapes, but it also refuses parent configs that uv does not pass down to a child project. main e76cb9b wires those correctly today, so the PR as written would regress them.

    What real uv inherits from a parent config. Setup: a child project with no [tool.uv] under a parent pyproject.toml [tool.uv] or uv.toml. I ran uv lock in the child, then appended an empty [tool.uv] to the child and ran uv lock --locked. Results on uv 0.5.31 and 0.13.1:

    Parent key Inherited by the child?
    exclude-newer, resolution, index-url, a default [[tool.uv.index]] / [[index]] yes: adding [tool.uv] changes the lock, so refusing is right
    constraint-dependencies, override-dependencies, build-constraint-dependencies (in [tool.uv] or in uv.toml) no: the lock is identical (for example, override-dependencies = ["attrs==23.1.0"] still resolves attrs 26.1.0), and --locked stays green after shadowing
    environments, required-environments, dev-dependencies, conflicts no (the PR already lists these as project-only)

    PR #1566 against main. Fixture: app/pyproject.toml depends on six==1.16.0 and attrs==24.2.0, under the parent named in each row. Both modes, uv 0.13.1 (rows 2–3 also on 0.5.31):

    Parent main e76cb9b PR #1566
    uv.toml: exclude-newer = "2026-10-01T00:00:00Z" wired; --locked fails (#1545) refused, nothing written ✅
    pyproject [tool.uv]: constraint-dependencies = ["attrs<25"] wired; --locked green; six patched refused (hosted redirect_uv_project_unsupported, vendored exit 1 pypi_uv_ancestor_settings)
    uv.toml: override-dependencies = ["attrs==24.2.0"] wired; --locked green; six patched refused (same codes)
    pyproject [tool.uv]: build-constraint-dependencies = ["setuptools<70"] wired; --locked green; six patched refused (same codes)
    The child already has [tool.uv] package = false, parent exclude-newer wired; green wired; green ✅

    UV_PROJECT_ONLY_KEYS in crates/socket-patch-core/src/utils/python_script.rs probably needs constraint-dependencies, override-dependencies and build-constraint-dependencies too. Those keys only apply at a project or workspace root. A parent monorepo pyproject that pins its own overrides is a common layout, so this shape is realistic. (I didn't test lock-neutral settings such as cache-keys.)

    Repro sketch:

    mkdir -p t/app && cd t
    printf '[project]\nname="mono"\nversion="0.1.0"\n\n[tool.uv]\nconstraint-dependencies = ["attrs<25"]\n' > pyproject.toml
    cd app && printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0", "attrs==24.2.0"]\n' > pyproject.toml
    uv lock && uv sync
    socket-patch scan --mode hosted --yes --json   # PR: redirect_uv_project_unsupported; main: wired, `uv sync --locked` ok

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions