Skip to content

fix: normalize self-ref extras on both sides of compare (#6049)#6052

Merged
ruben-arts merged 1 commit into
prefix-dev:mainfrom
baszalmstra:claude/investigate-issue-6049-GbeTr
May 11, 2026
Merged

fix: normalize self-ref extras on both sides of compare (#6049)#6052
ruben-arts merged 1 commit into
prefix-dev:mainfrom
baszalmstra:claude/investigate-issue-6049-GbeTr

Conversation

@baszalmstra
Copy link
Copy Markdown
Contributor

Description

pixi install --locked immediately rejected a freshly-written lockfile when pyproject.toml contained a self-referential extra such as dev = ["foo[test]"]. The lockfile-write path stored foo[test]; extra == 'dev' verbatim, while the satisfiability path expanded it to pytest; extra == 'dev', producing a phantom "added: [pytest], removed: [foo]" diff.

This PR makes compare_metadata normalize both sides through expand_self_extras before diffing. Each side scans only its own requires_dist (uv's static parse already flattens [project.optional-dependencies] into it with ; extra == "X" markers), so no parallel optional-deps map is needed and edits to a group still surface as a real diff. Self-extras expansion runs on an explicit work stack, not recursion, so deep optional-deps graphs can't stack-overflow.

Fixes #6049

How Has This Been Tested?

  • Unit tests in pypi_metadata.rs:
    • Issue reproducer at the unit level (lock with foo[test]; extra == 'dev', current static parse, asserts no mismatch).
    • Companion case where the lock came from build-backend wheel METADATA (already expanded).
    • Stale [project.optional-dependencies] still surfaces as a diff.
    • Direct self-loop and a -> b -> a cycle terminate.
    • python_version + extra markers preserved through expansion.
    • A test that calls uv_pypi_types::RequiresDist::from_pyproject_toml directly to pin the assumption that uv flattens optional-deps with ; extra == "X" markers — if uv ever changes that, the snapshot catches it.
  • Integration test self_referential_extras_lockfile_roundtrip in pypi_tests.rs: builds the issue's exact pyproject.toml, runs update_lock_file() then re-runs with LockFileUsage::Locked to exercise the --locked satisfiability path without spinning up a conda prefix or building a wheel.

AI Disclosure

  • This PR contains AI-generated content.
    • I have tested any AI-generated content in my PR.
    • I take responsibility for any AI-generated content in my PR.

Tools: Claude Code

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added sufficient tests to cover my changes.

@baszalmstra baszalmstra added the test:extra_slow Run the extra slow tests label May 8, 2026
…-dev#6049)

`pixi install --locked` rejected freshly-written lockfiles when
pyproject.toml contained a self-referential extra such as
`dev = ["foo[test]"]`: the lockfile-write path stored
`foo[test]; extra == 'dev'` verbatim while the satisfiability path
expanded it to `pytest; extra == 'dev'`, producing a phantom
"added: [pytest], removed: [foo]" diff.

`compare_metadata` now normalizes both sides via `expand_self_extras`
before diffing. Each side scans only its own `requires_dist` (uv's
static parse already flattens `[project.optional-dependencies]` into
it with `; extra == "X"` markers), so no parallel optional-deps map
is needed and edits to a group still surface as a real diff.
Self-extras expansion runs on an explicit work stack rather than
recursion, so deep optional-deps graphs can't blow the call stack.

Tests:
- Issue reproducer at the unit level + the build-backend-expanded
  companion case.
- Stale `[project.optional-dependencies]` still surfaces a diff.
- Direct self-loop and `a -> b -> a` cycle terminate.
- Non-extra marker constraints (e.g. `python_version`) preserved
  through expansion.
- A test that calls `uv_pypi_types::RequiresDist::from_pyproject_toml`
  directly to pin the assumption that uv flattens optional-deps with
  `; extra == "X"` markers.
- Integration test `self_referential_extras_lockfile_roundtrip`:
  builds the issue's exact pyproject.toml, runs `update_lock_file()`
  then re-runs with `LockFileUsage::Locked` to exercise the
  `--locked` satisfiability path without spinning up a conda prefix
  or building a wheel.
@baszalmstra baszalmstra force-pushed the claude/investigate-issue-6049-GbeTr branch from d1b7a4e to a4f8d3c Compare May 8, 2026 14:39
@baszalmstra baszalmstra requested review from ruben-arts and tdejager May 8, 2026 14:50
@baszalmstra
Copy link
Copy Markdown
Contributor Author

@ruben-arts THis is ready too

Copy link
Copy Markdown
Contributor

@ruben-arts ruben-arts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I didn't test it manually but the test cases look solid!

@ruben-arts ruben-arts merged commit 87e54eb into prefix-dev:main May 11, 2026
69 of 70 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:extra_slow Run the extra slow tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Self-referential extras in pyproject.toml cause pixi.lock to always appear out of date with --locked

3 participants