Warn when duplicate index names found in single file (#11824)

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->
## Summary

This pull request introduces validation for unique index names in the
`tool.uv.index` field and adds corresponding tests to ensure the
functionality. The most important changes include adding a custom
deserializer function, updating the `ToolUv` struct to use the new
deserializer, and adding tests to verify the behavior.

Validation and deserialization:

*
[`crates/uv-workspace/src/pyproject.rs`](diffhunk://#diff-e12cd255985adfd45ab06f398cb420d2f543841ccbeea4175ccf827aa9215b9dR283-R311):
Added a custom deserializer function `deserialize_index_vec` to validate
that index names in the `tool.uv.index` field are unique.
*
[`crates/uv-workspace/src/pyproject.rs`](diffhunk://#diff-e12cd255985adfd45ab06f398cb420d2f543841ccbeea4175ccf827aa9215b9dR374):
Updated the `ToolUv` struct to use the `deserialize_index_vec` function
for the `index` field.

Testing:

*
[`crates/uv/tests/it/lock.rs`](diffhunk://#diff-82edd36151736f44055f699a34c8b19a63ffc4cf3c86bf5fb34d69f8ac88a957R15336):
Added a test `lock_repeat_named_index` to verify that duplicate index
names result in an error.
[[1]](diffhunk://#diff-82edd36151736f44055f699a34c8b19a63ffc4cf3c86bf5fb34d69f8ac88a957R15336)
[[2]](diffhunk://#diff-82edd36151736f44055f699a34c8b19a63ffc4cf3c86bf5fb34d69f8ac88a957R15360-R15402)
*
[`crates/uv/tests/it/lock.rs`](diffhunk://#diff-82edd36151736f44055f699a34c8b19a63ffc4cf3c86bf5fb34d69f8ac88a957R15360-R15402):
Added a test `lock_unique_named_index` to verify that unique index names
result in successful lock file generation.

Schema update:

*
[`uv.schema.json`](diffhunk://#diff-c669473b258a19ba6d3557d0369126773b68b27171989f265333a77bc5cb935bR205):
Updated the schema to set the default value of the `index` field to
`null`.

Fixes #11804

## Test Plan
### Steps to reproduce and verify the fix:

1. Clone the repository and checkout the feature branch
   ```bash
   git clone https://github.com/astral-sh/uv.git
   cd uv
   git checkout feature/warn-duplicate-index-names
   ```

2. Build the modified binary
   ```bash
   cargo build
   ```

3. Create a test project using the system installed uv
   ```bash
   uv init uv-test
   cd uv-test
   ```

4. Manually edit pyproject.toml to add duplicate index names
   ```toml
   [[tool.uv.index]]
   name = "alpha_b"
   url = "<omitted>"

   [[tool.uv.index]]
   name = "alpha_b"
   url = "<omitted>"
   ```

5. Try to add a package using the modified binary
   ```bash
   ../target/debug/uv add numpy
   ```

### Results
Before: use release binary
![スクリーンショット 2025-02-27 15 52
28](https://github.com/user-attachments/assets/2823a4b4-b3ba-40aa-aa41-e593d35c3f3b)

After: use self build binary
![スクリーンショット 2025-02-27 15 51
58](https://github.com/user-attachments/assets/9ac773a9-58cd-4d4b-8685-148bf6dc85fb)

Now when attempting to use a pyproject.toml with duplicate index names,
the modified binary correctly detects the issue and produces an error
message:
```
error: Failed to parse: `pyproject.toml`
  Caused by: TOML parse error at line 9, column 1
  |
9 | [[tool.uv.index]]
  | ^^^^^^^^^^^^^^^^^
duplicate index name `alpha_b`
```

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
KATO So 2025-02-28 06:33:57 +09:00 committed by GitHub
parent 4d9c861506
commit ad86005e9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 68 additions and 5 deletions

View file

@ -14,6 +14,7 @@ use std::str::FromStr;
use glob::Pattern;
use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashSet};
use serde::{de::IntoDeserializer, de::SeqAccess, Deserialize, Deserializer, Serialize};
use thiserror::Error;
use url::Url;
@ -280,6 +281,30 @@ pub struct Tool {
pub uv: Option<ToolUv>,
}
/// Validates that index names in the `tool.uv.index` field are unique.
///
/// This custom deserializer function checks for duplicate index names
/// and returns an error if any duplicates are found.
fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
where
D: Deserializer<'de>,
{
let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
if let Some(indexes) = indexes.as_ref() {
let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
for index in indexes {
if let Some(name) = index.name.as_ref() {
if !seen_names.insert(name) {
return Err(serde::de::Error::custom(format!(
"duplicate index name `{name}`"
)));
}
}
}
}
Ok(indexes)
}
// NOTE(charlie): When adding fields to this struct, mark them as ignored on `Options` in
// `crates/uv-settings/src/settings.rs`.
#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
@ -342,6 +367,7 @@ pub struct ToolUv {
url = "https://download.pytorch.org/whl/cu121"
"#
)]
#[serde(deserialize_with = "deserialize_index_vec", default)]
pub index: Option<Vec<Index>>,
/// The workspace definition for the project, if any.

View file

@ -15328,11 +15328,7 @@ fn lock_named_index_cli() -> Result<()> {
Ok(())
}
/// If a name is reused, the higher-priority index should "overwrite" the lower-priority index.
/// In other words, the lower-priority index should be ignored entirely during implicit resolution.
///
/// In this test, we should use PyPI (the default index) and ignore `https://example.com` entirely.
/// (Querying `https://example.com` would fail with a 500.)
/// If a name is reused, within a single file, we should raise an error.
#[test]
fn lock_repeat_named_index() -> Result<()> {
let context = TestContext::new("3.12");
@ -15356,6 +15352,46 @@ fn lock_repeat_named_index() -> Result<()> {
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse: `pyproject.toml`
Caused by: TOML parse error at line 8, column 9
|
8 | [[tool.uv.index]]
| ^^^^^^^^^^^^^^^^^
duplicate index name `pytorch`
"###);
Ok(())
}
#[test]
fn lock_unique_named_index() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
[[tool.uv.index]]
name = "pytorch"
url = "https://astral-sh.github.io/pytorch-mirror/whl/cu121"
[[tool.uv.index]]
name = "example"
url = "https://example.com"
"#,
)?;
// Fall back to PyPI, since `iniconfig` doesn't exist on the PyTorch index.
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true