Is there an existing issue for this?
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
Is there an existing issue for this?
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: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.
Updated dependency
Synthetic
example-alpha: change==1.0.0to==1.1.0for Darwin. Leave the already-satisfied non-Darwin requirement>=0.5.0unchanged. 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"], andsource: 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:
If both the manifest declarations and their requirement arrays are reversed, the updater completes but produces this incorrect output:
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::DependencyandDependencyFileobjects; 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, andrun.shfollowingREADME.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 arebaseline.jsonandpatched.json.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:
The complete applicable patch is
requirement-pairing.patch. The target file also has the same Git blob hash on upstream main0db0342964b1627e76be5d5449e7fbbf48b88b70, inspected on 2026-10-04.Scope and remaining validation
The public
updated_dependency_filesmethod, 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
Dependencyrequirements, are sufficient for the focused file-updater test. Removing either declaration removes the ambiguous pairing.Dependabot-upstream-review-package.zip