Add hint when named index is found in a parent config file (#18087)

See https://github.com/astral-sh/uv/issues/18053

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Zanie Blue
2026-02-23 15:25:40 -06:00
committed by GitHub
parent 15e673f1e0
commit d720f2fb33
7 changed files with 233 additions and 26 deletions
@@ -5,6 +5,8 @@ pub enum Origin {
Cli,
/// The setting was provided via a user-level configuration file.
User,
/// The setting was provided via a system-level configuration file.
System,
/// The setting was provided via a project-level configuration file.
Project,
/// The setting was provided via a `requirements.txt` file.
@@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize};
use std::borrow::Cow;
use std::path::Path;
use crate::{Index, IndexUrl};
use crate::{Index, IndexUrl, Origin};
macro_rules! impl_index {
($name:ident, $from:expr) => {
@@ -18,6 +18,11 @@ macro_rules! impl_index {
pub fn relative_to(self, root_dir: &Path) -> Result<Self, crate::IndexUrlError> {
Ok(Self(self.0.relative_to(root_dir)?))
}
/// Set the [`Origin`] if not already set.
pub fn try_set_origin(&mut self, origin: Origin) {
self.0.origin.get_or_insert(origin);
}
}
impl From<$name> for Index {
@@ -3,6 +3,7 @@ use std::io;
use std::path::{Path, PathBuf};
use either::Either;
use owo_colors::OwoColorize;
use thiserror::Error;
use uv_auth::CredentialsCache;
use uv_distribution_filename::DistExtension;
@@ -226,10 +227,12 @@ impl LoweredRequirement {
name.as_ref().is_some_and(|name| *name == index)
})
else {
return Err(LoweringError::MissingIndex(
requirement.name.clone(),
let hint = missing_index_hint(locations, &index);
return Err(LoweringError::MissingIndex {
package: requirement.name.clone(),
index,
));
hint,
});
};
if let Some(credentials) = index.credentials() {
credentials_cache.store_credentials(index.raw_url(), credentials);
@@ -462,10 +465,12 @@ impl LoweredRequirement {
name.as_ref().is_some_and(|name| *name == index)
})
else {
return Err(LoweringError::MissingIndex(
requirement.name.clone(),
let hint = missing_index_hint(locations, &index);
return Err(LoweringError::MissingIndex {
package: requirement.name.clone(),
index,
));
hint,
});
};
if let Some(credentials) = index.credentials() {
credentials_cache.store_credentials(index.raw_url(), credentials);
@@ -528,8 +533,12 @@ pub enum LoweringError {
MoreThanOneGitRef,
#[error(transparent)]
GitUrlParse(#[from] GitUrlParseError),
#[error("Package `{0}` references an undeclared index: `{1}`")]
MissingIndex(PackageName, IndexName),
#[error("Package `{package}` references an undeclared index: `{index}`{}", if let Some(hint) = hint { format!("\n\n{}{} {hint}", "hint".bold().cyan(), ":".bold()) } else { String::new() })]
MissingIndex {
package: PackageName,
index: IndexName,
hint: Option<String>,
},
#[error("Workspace members are not allowed in non-workspace contexts")]
WorkspaceMember,
#[error(transparent)]
@@ -579,6 +588,29 @@ impl std::fmt::Display for SourceKind {
}
}
/// Generate a hint for a missing index if the index name is found in a configuration file
/// (e.g., `uv.toml`) rather than in the project's `pyproject.toml`.
fn missing_index_hint(locations: &IndexLocations, index: &IndexName) -> Option<String> {
let config_index = locations
.simple_indexes()
.filter(|idx| !matches!(idx.origin, Some(Origin::Cli)))
.find(|idx| idx.name.as_ref().is_some_and(|name| *name == *index));
config_index.and_then(|idx| {
let source = match idx.origin {
Some(Origin::User) => "a user-level `uv.toml`",
Some(Origin::System) => "a system-level `uv.toml`",
Some(Origin::Project) => "a project-level `uv.toml`",
Some(Origin::Cli | Origin::RequirementsTxt) | None => return None,
};
Some(format!(
"Index `{index}` was found in {source}, but indexes \
referenced via `tool.uv.sources` must be defined in the project's \
`pyproject.toml`"
))
})
}
/// Convert a Git source into a [`RequirementSource`].
fn git_source(
git: &DisplaySafeUrl,
+10 -3
View File
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use std::time::Duration;
use uv_client::{DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, DEFAULT_READ_TIMEOUT_UPLOAD};
use uv_dirs::{system_config_file, user_config_dir};
use uv_distribution_types::Origin;
use uv_flags::EnvironmentFlags;
use uv_fs::Simplified;
use uv_static::{EnvVars, InvalidEnvironmentVariable, parse_boolish_environment_variable};
@@ -24,6 +25,12 @@ impl FilesystemOptions {
pub fn into_options(self) -> Options {
self.0
}
/// Set the [`Origin`] on all indexes without an existing origin.
#[must_use]
pub fn with_origin(self, origin: Origin) -> Self {
Self(self.0.with_origin(origin))
}
}
impl Deref for FilesystemOptions {
@@ -48,7 +55,7 @@ impl FilesystemOptions {
Ok(options) => {
tracing::debug!("Found user configuration in: `{}`", file.display());
validate_uv_toml(&file, &options)?;
Ok(Some(Self(options)))
Ok(Some(Self(options.with_origin(Origin::User))))
}
Err(Error::Io(err))
if matches!(
@@ -72,7 +79,7 @@ impl FilesystemOptions {
tracing::debug!("Found system configuration in: `{}`", file.display());
let options = read_file(&file)?;
validate_uv_toml(&file, &options)?;
Ok(Some(Self(options)))
Ok(Some(Self(options.with_origin(Origin::System))))
}
/// Find the [`FilesystemOptions`] for the given path.
@@ -131,7 +138,7 @@ impl FilesystemOptions {
tracing::debug!("Found workspace configuration at `{}`", path.display());
validate_uv_toml(&path, &options)?;
return Ok(Some(Self(options)));
return Ok(Some(Self(options.with_origin(Origin::Project))));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
+36 -2
View File
@@ -8,8 +8,8 @@ use uv_configuration::{
RequiredVersion, TargetTriple, TrustedHost, TrustedPublishing, Upgrade,
};
use uv_distribution_types::{
ConfigSettings, ExtraBuildVariables, Index, IndexUrl, IndexUrlError, PackageConfigSettings,
PipExtraIndex, PipFindLinks, PipIndex, StaticMetadata,
ConfigSettings, ExtraBuildVariables, Index, IndexUrl, IndexUrlError, Origin,
PackageConfigSettings, PipExtraIndex, PipFindLinks, PipIndex, StaticMetadata,
};
use uv_install_wheel::LinkMode;
use uv_macros::{CombineOptions, OptionsMetadata};
@@ -171,6 +171,40 @@ impl Options {
}
}
/// Set the [`Origin`] on all indexes without an existing origin.
#[must_use]
pub fn with_origin(mut self, origin: Origin) -> Self {
if let Some(indexes) = &mut self.top_level.index {
for index in indexes {
index.origin.get_or_insert(origin);
}
}
if let Some(index_url) = &mut self.top_level.index_url {
index_url.try_set_origin(origin);
}
if let Some(extra_index_urls) = &mut self.top_level.extra_index_url {
for index_url in extra_index_urls {
index_url.try_set_origin(origin);
}
}
if let Some(pip) = &mut self.pip {
if let Some(indexes) = &mut pip.index {
for index in indexes {
index.origin.get_or_insert(origin);
}
}
if let Some(index_url) = &mut pip.index_url {
index_url.try_set_origin(origin);
}
if let Some(extra_index_urls) = &mut pip.extra_index_url {
for index_url in extra_index_urls {
index_url.try_set_origin(origin);
}
}
}
self
}
/// Resolve the [`Options`] relative to the given root directory.
pub fn relative_to(self, root_dir: &Path) -> Result<Self, IndexUrlError> {
Ok(Self {
+103
View File
@@ -19293,6 +19293,109 @@ fn lock_named_index_cli() -> Result<()> {
Ok(())
}
/// If a named index is referenced in `tool.uv.sources` but only defined in `uv.toml`, we should
/// provide a hint that the index was found in a configuration file.
#[test]
fn lock_named_index_config_file_hint() -> Result<()> {
let context = uv_test::test_context!("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 = ["jinja2==3.1.2"]
[tool.uv.sources]
jinja2 = { index = "pytorch" }
"#,
)?;
let uv_toml = context.temp_dir.child("uv.toml");
uv_toml.write_str(
r#"
[[index]]
name = "pytorch"
url = "https://astral-sh.github.io/pytorch-mirror/whl/cu121"
explicit = true
"#,
)?;
// The index is defined in `uv.toml`, which should produce a hint.
uv_snapshot!(context.filters(), context.lock(), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Failed to build `project @ file://[TEMP_DIR]/`
Failed to parse entry: `jinja2`
Package `jinja2` references an undeclared index: `pytorch`
hint: Index `pytorch` was found in a project-level `uv.toml`, but indexes referenced via `tool.uv.sources` must be defined in the project's `pyproject.toml`
");
Ok(())
}
/// If a named index is referenced in `tool.uv.sources` but only defined in a user-level `uv.toml`
/// (e.g., `~/.config/uv/uv.toml`), we should provide a hint that the index was found in a
/// configuration file.
#[test]
#[cfg_attr(
windows,
ignore = "Configuration tests are not yet supported on Windows"
)]
fn lock_named_index_user_config_file_hint() -> Result<()> {
let context = uv_test::test_context!("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 = ["jinja2==3.1.2"]
[tool.uv.sources]
jinja2 = { index = "pytorch" }
"#,
)?;
// Define the index in a user-level configuration file.
let xdg = assert_fs::TempDir::new().expect("Failed to create temp dir");
let uv_config = xdg.child("uv");
let uv_toml = uv_config.child("uv.toml");
uv_toml.write_str(
r#"
[[index]]
name = "pytorch"
url = "https://astral-sh.github.io/pytorch-mirror/whl/cu121"
explicit = true
"#,
)?;
// The index is defined in a user-level `uv.toml`, which should produce a hint.
uv_snapshot!(context.filters(), context.lock()
.env(EnvVars::XDG_CONFIG_HOME, xdg.path()), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Failed to build `project @ file://[TEMP_DIR]/`
Failed to parse entry: `jinja2`
Package `jinja2` references an undeclared index: `pytorch`
hint: Index `pytorch` was found in a user-level `uv.toml`, but indexes referenced via `tool.uv.sources` must be defined in the project's `pyproject.toml`
");
Ok(())
}
/// If a name is reused, within a single file, we should raise an error.
#[test]
fn lock_repeat_named_index() -> Result<()> {
+36 -12
View File
@@ -143,7 +143,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -349,7 +351,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -556,7 +560,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -795,7 +801,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -4027,7 +4035,9 @@ fn resolve_both() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -4279,7 +4289,9 @@ fn resolve_both_special_fields() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -5527,7 +5539,9 @@ fn index_priority() -> anyhow::Result<()> {
),
explicit: false,
default: false,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -5768,7 +5782,9 @@ fn index_priority() -> anyhow::Result<()> {
),
explicit: false,
default: false,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -6015,7 +6031,9 @@ fn index_priority() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -6257,7 +6275,9 @@ fn index_priority() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -6506,7 +6526,9 @@ fn index_priority() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,
@@ -6748,7 +6770,9 @@ fn index_priority() -> anyhow::Result<()> {
),
explicit: false,
default: true,
origin: None,
origin: Some(
Project,
),
format: Simple,
publish_url: None,
authenticate: Auto,