diff --git a/pm/install.py b/pm/install.py index fbea7b9d7d..a9575adb23 100644 --- a/pm/install.py +++ b/pm/install.py @@ -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, diff --git a/tests/pm/test_source_update_launch.py b/tests/pm/test_source_update_launch.py index 0bd0a36a02..377032c315 100644 --- a/tests/pm/test_source_update_launch.py +++ b/tests/pm/test_source_update_launch.py @@ -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):