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:
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user