mirror of https://github.com/astral-sh/uv
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  After: use self build binary  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:
parent
4d9c861506
commit
ad86005e9a
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@
|
|||
},
|
||||
"index": {
|
||||
"description": "The indexes to use when resolving dependencies.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nIndexes are considered in the order in which they're defined, such that the first-defined index has the highest priority. Further, the indexes provided by this setting are given higher priority than any indexes specified via [`index_url`](#index-url) or [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains a given package, unless an alternative [index strategy](#index-strategy) is specified.\n\nIf an index is marked as `explicit = true`, it will be used exclusively for the dependencies that select it explicitly via `[tool.uv.sources]`, as in:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\" explicit = true\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```\n\nIf an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is given the lowest priority when resolving packages. Additionally, marking an index as default will disable the PyPI default index.",
|
||||
"default": null,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
|
|
|
|||
Loading…
Reference in New Issue