Skip to content

Vendored requirements.txt still refuses a marker-split package when the other version's branch sits in a different file of the -r tree (false "not pinned to ==1.16.0", exit 1), so a fresh install stays unpatched #1104

Description

[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).

Summary

#928 (fixed by #929) taught vendored requirements.txt that a pin to another version on its own environment-marker branch isn't ambiguous: six==1.16.0 ; python_version < '3.12' next to six==1.17.0 ; python_version >= '3.12' now vendors only the 1.16.0 branch. That fix works only when both branches are in the same file. When the branches are split across the root requirements.txt and a -r include, which is common in layered requirements (base.txt plus a root file), vendored mode still refuses the whole package:

pypi_requirement_not_pinned: base.txt: six is not pinned to ==1.16.0; pin it exactly or use agent mode (`scan --mode agent` + `socket-patch apply`) instead

The message is false. The tree pins six to ==1.16.0 exactly, and pip installs exactly one branch. Hosted mode handles the same layout, and so does vendored mode once both lines are moved into one file.

The uv routine raised this case in a comment on #928 (#928 (comment)) before #929 merged. #929's tests and scan_pins change cover only the single-file case, and #928 is now closed, so nothing tracks this gap.

Impact

  • A project that keeps a version-specific pin in a separate requirements file can't vendor a patch for that package. The run exits 1 (partial_failure), and pip install -r requirements.txt on a fresh checkout installs the unpatched release.
  • The remedy the CLI suggests ("pin it exactly") can't be followed, because the pin is already exact.
  • Nothing is written, so there's no corruption, and other packages in the same run still vendor.

Repro (Linux, main d7f8679, pip 26.2.1 / CPython 3.11)

I ran this against a local mock patch API serving a patched real six-1.16.0 wheel (the same routes as tests/vex_pypi_real_common).

printf 'idna==3.7\nsix==1.17.0 ; python_version >= "3.12"\n' > base.txt
printf -- '-r base.txt\nsix==1.16.0 ; python_version < "3.12"\n' > requirements.txt
python3.11 -m venv .venv && .venv/bin/pip install -r requirements.txt   # six 1.16.0
socket-patch scan --mode vendored --yes --json
# exit 1, vendor.events[0] = {action: failed, errorCode: pypi_requirement_not_pinned,
#   error: "base.txt: six is not pinned to ==1.16.0; ..."}; requirements.txt unchanged, no .socket/vendor
python3.11 -m venv fresh && fresh/bin/pip install -r requirements.txt  # six 1.16.0 from PyPI, unpatched

When the branches are swapped (1.17.0 in the root, 1.16.0 in base.txt), the error names requirements.txt instead and the outcome is the same.

Expected vs actual

Matrix (Linux; the refusal is pure planning code, so it doesn't depend on the OS or the pip version)

Layout scan --mode vendored Fresh pip install -r (pip 26.2.1 / py3.11)
root six==1.16.0 ; <3.12, base.txt six==1.17.0 ; >=3.12 fail: exit 1, pypi_requirement_not_pinned naming base.txt (reproduced twice) six 1.16.0 unpatched
root six==1.17.0 ; >=3.12, base.txt six==1.16.0 ; <3.12 fail: exit 1, same code naming requirements.txt (reproduced twice) six 1.16.0 unpatched
both branches in the root, include holds only idna (#928 control) pass: exit 0, only the 1.16.0 branch rewritten six 1.16.0 patched
first layout, scan --mode hosted pass: exit 0, redirected: 1, root branch rewritten with its marker six 1.16.0 patched

Not bisected: cross-file splits have never been accepted. Before #929 the same-file split was refused too.

(The vendored --dry-run previews would_vendor here. I'm not reporting that separately, because CLI_CONTRACT.md defines that preview as a ledger classification that predicts only the npm-family preflights.)

Suspect code

  • crates/socket-patch-core/src/vendor/pypi_requirements.rs:107: if other_branches && (exact.is_empty() || …) { found_range = true; }. This is evaluated per file, so a file that holds only the other branch is classified Range.
  • crates/socket-patch-core/src/vendor/pypi_requirements.rs:544-565: plan_requirements calls find_pin file by file and returns the first Range as pypi_requirement_not_pinned. The "every target pin carries a marker / the other branch is marked" decision would need to be made over the whole collected -r tree (collect_requirements_files).

No probe runs were needed: this is OS-independent planning code.


Backlog review — 2026-10-08

Priority: P1 → P2. A marker split across requirements includes causes an explicit vendored refusal; this fails closed.

Activity

  1. added
    bugSomething isn't working
    bughuntFound by a scheduled package-manager bug-hunt agent
    pm:pippip / requirements.txt
    on Oct 8, 2026
  2. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (PyPI / requirements.txt). Confirmed on main 77f305d by reading the code: find_pin makes the marker-branch decision per file (pypi_requirements.rs ~L107 sets found_range when a file holds only the other version's marker branch), and plan_requirements returns the first per-file Range as pypi_requirement_not_pinned before looking at the rest of the -r tree. Not a duplicate: #928/#929 fixed only the single-file split. Related area but a separate code path from #1086 (open PR #1091, lock contest across an include tree), so not clustered with it. Eligible for a fix in a later run.


    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

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:pippip / requirements.txtpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions