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