fix(pm): match recorded extras to declarations by normalized name

uv treats `foo_bar` and `foo-bar` as the same extra (PEP 685), so an
exact comparison would drop a recorded extra whose spelling differs from
its declaration. Compare normalized names; the recorded spelling is still
what reaches uv.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Matt Healey
2026-09-25 00:30:52 -07:00
committed by ethernet
parent 24e8c341c1
commit ac3ebbfedc
2 changed files with 22 additions and 2 deletions

View File

@@ -548,14 +548,20 @@ def _still_declared(package, recorded: list[str]) -> list[str]:
An extra the source removed (``hindsight``) would otherwise ride the ledger
into every later ``uv sync`` and fail it with "Extra is not defined". Only
recorded extras are pruned; an explicitly requested unknown extra still fails.
Membership uses PEP 685 names (uv matches ``foo_bar`` to ``foo-bar``); the
recorded spelling is what reaches uv.
"""
import re
from pm.features import declared_extras
def normalized(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower()
root = package.project_root()
if not (root / "pyproject.toml").is_file():
return list(recorded)
declared = set(declared_extras(root))
return [extra for extra in recorded if extra in declared]
declared = {normalized(extra) for extra in declared_extras(root)}
return [extra for extra in recorded if normalized(extra) in declared]
def venv_is_current(*, extras: list[str] | None = None, plugins: Members | Candidates | None = None,

View File

@@ -347,6 +347,20 @@ def test_source_update_that_removes_a_recorded_extra_still_syncs(source_launch):
assert pm.venv_is_current(project_root=root)
@pytest.mark.platforms("posix")
def test_recorded_extra_spelled_differently_from_its_declaration_survives(source_launch):
"""uv matches extras by PEP 685 name; `launch_extra` is the declared `launch-extra`."""
root, store_python, _ = source_launch
pm.sync_venv(["all", "launch_extra"], explicit=True, project_root=root)
assert _fact(root)["extras"] == ["all", "launch_extra"]
lock = root / "uv.lock"
lock.write_bytes(lock.read_bytes() + b"\n# source update changes the committed lock\n")
assert venv_sync.prepare_launch(root, []) == store_python
assert _fact(root)["extras"] == ["all", "launch_extra"]
assert pm.venv_is_current(project_root=root)
@pytest.mark.platforms("posix")
@pytest.mark.parametrize("mode", ["script", "module", "command"])
def test_real_bootstrap_reexecs_before_app_imports(source_launch, tmp_path, isolated_python, mode):