Skip to content

pip requirements in one file/group can raise or update the wrong platform #16476

Description

@SProst

Is there an existing issue for this?

  • I have searched the existing issues

Package ecosystem

pip / Python's RequirementFileUpdater; PEP 621 requirements in pyproject.toml.

Package manager version

No package manager or resolver is invoked by this reproduction. Frozen dependabot-core revision: d980166. The harness verifies eight upstream source files against their Git blob hashes before running them. Sorbet runtime is pinned to 0.6.13449, matching that revision’s Gemfile.lock.

Language version

The isolated harness ran on Ruby 3.4.11 in a digest-pinned Linux container. The manifest contains Python requirement strings; no Python package is installed. This is a focused file-updater reproduction, not a full updater-image run.

Manifest location and content before the Dependabot update

Synthetic pyproject.toml:

[project]
name = "updater-repro"
version = "0.0.0"
dependencies = [
  "example-alpha==1.0.0; platform_system == 'Darwin'",
  "example-alpha>=0.5.0; platform_system != 'Darwin'",
]

All package names and versions are synthetic; no application repository or logs are included.

dependabot.yml content

A representative configuration is below. The local harness calls the file updater directly and does not schedule a job or use this configuration.

version: 2
updates:
  - package-ecosystem: pip
    directory: /
    schedule:
      interval: weekly

Updated dependency

Synthetic example-alpha: change ==1.0.0 to ==1.1.0 for Darwin. Leave the already-satisfied non-Darwin requirement >=0.5.0 unchanged. The synthetic package does not need to exist in a registry.

The harness supplies old and new requirement arrays in the same order. Both records have file: "pyproject.toml", groups: ["dependencies"], and source: nil. Old specifiers are ["==1.0.0", ">=0.5.0"]; new specifiers are ["==1.1.0", ">=0.5.0"].

What you expected to see, versus what you actually saw

Expected: only the Darwin pin changes, regardless of declaration order.

Actual with the manifest above:

Declaration not found for example-alpha!

If both the manifest declarations and their requirement arrays are reversed, the updater completes but produces this incorrect output:

[project]
dependencies = [
  "example-alpha==1.1.0; platform_system != 'Darwin'",
  "example-alpha==1.0.0; platform_system == 'Darwin'",
]

The non-Darwin lower bound becomes the new exact pin; the Darwin pin remains old. Checking only whether an exception occurs would miss this failure.

Native package manager behavior

Not tested. The example uses synthetic names to isolate text rewriting from registry availability, dependency resolution and package installation. The harness constructs Dependabot::Dependency and DependencyFile objects; it does not claim to exercise parsing, resolution or hosted PR creation.

Images of the diff or a link to the PR, issue, or logs

Related issues

No exact duplicate was identified in the reviewed results. Closely related: PR #14578 introduced the file/group lookup while fixing duplicate extras (issue #14573). This candidate retains that change’s deduplication. Issue #4066 has a similar exception but a different manifest-selection problem. PR #15875 concerns marker parsing, not old/new requirement pairing.

Reproduction and observed results

Use the companion acquire.py, sources.json, reproduce.rb, and run.sh following README.md. Tool acquisition is a separate network-enabled phase. Tests run without networking, as a non-root user, with read-only mounts and dropped capabilities. Raw sanitized results are baseline.json and patched.json.

Focused cases Frozen upstream Candidate
Four synthetic constraint sets × two orders × same/separate files 4 exceptions, 4 incorrect edits, 8 correct 16 correct
Duplicate extras in either order; unchanged requirement; distinct groups 4 correct 4 correct
Total 4 exceptions, 4 incorrect edits, 12 correct 20 correct

The baseline exits 1; the patched run exits 0. Exact resulting file contents are checked. The four constraint sets comprise three exact-pin/lower-bound pairs and one upper-bounded/lower-bound pair. No application manifests are needed.

Root cause and proposed fix

In unique_requirement_changes, every new requirement is paired with the first old record matching only file and groups. Multiple platform declarations share that key. The second record can therefore be paired with a different declaration, even when unchanged.

RequirementsUpdater maps the existing array. The proposed one-line replacement restores positional correspondence while retaining deduplication and accumulated per-file edits:

-          changes = dependency.requirements.filter_map do |new_req|
-            old_req = previous_reqs.find { |r| r.file == new_req.file && r.groups == new_req.groups }
+          changes = dependency.requirements.zip(previous_reqs).filter_map do |new_req, old_req|

The complete applicable patch is requirement-pairing.patch. The target file also has the same Git blob hash on upstream main 0db0342964b1627e76be5d5449e7fbbf48b88b70, inspected on 2026-10-04.

Scope and remaining validation

The public updated_dependency_files method, real dependency/file classes, requirement regexes, name normalizer and replacer run from the frozen source. Unused collaborators are stubbed; native-helper calls are forbidden. The candidate changes source in memory only. No registry, network, resolver, application runtime or hosted updater is exercised.

The full Python/uv suites, upstream RuboCop and Sorbet have not been run. The array-order invariant should be reviewed across all callers before adoption. uv aliases this Python updater; shared code suggests impact there, but this is not evidence of a completed uv integration test. The no-specifier guard from PR #16323 is untouched.

Before an upstream PR, port the behavioral cases into upstream RSpec, retain duplicate-extras and no-specifier coverage, and run the Python/uv suites, RuboCop and Sorbet in the prescribed Docker environment. This patch is a review candidate, not a claim that the hosted service is fixed.

Smallest manifest that reproduces the issue

The two dependency declarations above, plus their corresponding old/new Dependency requirements, are sufficient for the focused file-updater test. Removing either declaration removes the ambiguous pairing.

Dependabot-upstream-review-package.zip

Metadata

Metadata

Assignees

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions