Allow conflicting extras in explicit index assignments (#9160)

## Summary

This PR enables something like the "final boss" of PyTorch setups --
explicit support for CPU vs. GPU-enabled variants via extras:

```toml
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.13.0"
dependencies = []

[project.optional-dependencies]
cpu = [
    "torch==2.5.1+cpu",
]
gpu = [
    "torch==2.5.1",
]

[tool.uv.sources]
torch = [
    { index = "torch-cpu", extra = "cpu" },
    { index = "torch-gpu", extra = "gpu" },
]

[[tool.uv.index]]
name = "torch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

[[tool.uv.index]]
name = "torch-gpu"
url = "https://download.pytorch.org/whl/cu124"
explicit = true

[tool.uv]
conflicts = [
    [
        { extra = "cpu" },
        { extra = "gpu" },
    ],
]
```

It builds atop the conflicting extras work to allow sources to be marked
as specific to a dedicated extra being enabled or disabled.

As part of this work, sources now have an `extra` field. If a source has
an `extra`, it means that the source is only applied to the requirement
when defined within that optional group. For example, `{ index =
"torch-cpu", extra = "cpu" }` above only applies to
`"torch==2.5.1+cpu"`.

The `extra` field does _not_ mean that the source is "enabled" when the
extra is activated. For example, this wouldn't work:

```toml
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.13.0"
dependencies = ["torch"]

[tool.uv.sources]
torch = [
    { index = "torch-cpu", extra = "cpu" },
    { index = "torch-gpu", extra = "gpu" },
]

[[tool.uv.index]]
name = "torch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

[[tool.uv.index]]
name = "torch-gpu"
url = "https://download.pytorch.org/whl/cu124"
explicit = true
```

In this case, the sources would effectively be ignored. Extras are
really confusing... but I think this is correct? We don't want enabling
or disabling extras to affect resolution information that's _outside_ of
the relevant optional group.
This commit is contained in:
Charlie Marsh 2024-11-18 20:06:25 -05:00 committed by GitHub
parent a88a3e5eba
commit e4fc875afa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1607 additions and 227 deletions

View file

@ -335,18 +335,17 @@ dependencies = ["torch"]
[tool.uv.sources]
torch = [
{ index = "torch-cu118", marker = "sys_platform == 'darwin'"},
{ index = "torch-cu124", marker = "sys_platform != 'darwin'"},
{ index = "torch-cpu", marker = "platform_system == 'Darwin'"},
{ index = "torch-gpu", marker = "platform_system == 'Linux'"},
]
[[tool.uv.index]]
name = "torch-cu118"
url = "https://download.pytorch.org/whl/cu118"
name = "torch-cpu"
url = "https://download.pytorch.org/whl/cpu"
[[tool.uv.index]]
name = "torch-cu124"
name = "torch-gpu"
url = "https://download.pytorch.org/whl/cu124"
```
## Optional dependencies
@ -394,6 +393,36 @@ $ uv add httpx --optional network
If you have optional dependencies that conflict with one another, resolution will fail
unless you explicitly [declare them as conflicting](./projects.md#optional-dependencies).
Sources can also be declared as applying only to a specific optional dependency. For example, to
pull `torch` from different PyTorch indexes based on an optional `cpu` or `gpu` extra:
```toml title="pyproject.toml"
[project]
dependencies = []
[project.optional-dependencies]
cpu = [
"torch",
]
gpu = [
"torch",
]
[tool.uv.sources]
torch = [
{ index = "torch-cpu", extra = "cpu" },
{ index = "torch-gpu", extra = "gpu" },
]
[[tool.uv.index]]
name = "torch-cpu"
url = "https://download.pytorch.org/whl/cpu"
[[tool.uv.index]]
name = "torch-gpu"
url = "https://download.pytorch.org/whl/cu124"
```
## Development dependencies
Unlike optional dependencies, development dependencies are local-only and will _not_ be included in