mirror of https://github.com/astral-sh/uv
Add derivation chains for dependency errors (#14824)
## Summary
This PR adds derivation chain for another class of resolver failures.
For example, if we encounter a transitive URL dependency, we now tell
the user which package included it, and the full derivation chain:
```
× Failed to resolve dependencies for `foo` (v0.1.0)
╰─▶ Package `flask` was included as a URL dependency. URL dependencies must be
expressed as direct requirements or constraints. Consider adding `flask @
9d4508e893f34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl`
to your dependencies or constraints file.
help: `foo` (v0.1.0) was included because `baz` (v0.1.0) depends on `foo`
```
Closes #14795.
This commit is contained in:
parent
076677da20
commit
3d1fec2732
|
|
@ -37,6 +37,14 @@ use crate::{InMemoryIndex, Options};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ResolveError {
|
pub enum ResolveError {
|
||||||
|
#[error("Failed to resolve dependencies for package `{1}=={2}`")]
|
||||||
|
Dependencies(
|
||||||
|
#[source] Box<ResolveError>,
|
||||||
|
PackageName,
|
||||||
|
Version,
|
||||||
|
DerivationChain,
|
||||||
|
),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Client(#[from] uv_client::Error),
|
Client(#[from] uv_client::Error),
|
||||||
|
|
||||||
|
|
@ -92,9 +100,11 @@ pub enum ResolveError {
|
||||||
ConflictingIndexes(PackageName, String, String),
|
ConflictingIndexes(PackageName, String, String),
|
||||||
|
|
||||||
#[error(
|
#[error(
|
||||||
"Package `{0}` attempted to resolve via URL: {1}. URL dependencies must be expressed as direct requirements or constraints. Consider adding `{0} @ {1}` to your dependencies or constraints file."
|
"Package `{name}` was included as a URL dependency. URL dependencies must be expressed as direct requirements or constraints. Consider adding `{requirement}` to your dependencies or constraints file.",
|
||||||
|
name = name.cyan(),
|
||||||
|
requirement = format!("{name} @ {url}").cyan(),
|
||||||
)]
|
)]
|
||||||
DisallowedUrl(PackageName, String),
|
DisallowedUrl { name: PackageName, url: String },
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
DistributionType(#[from] uv_distribution_types::Error),
|
DistributionType(#[from] uv_distribution_types::Error),
|
||||||
|
|
|
||||||
|
|
@ -635,19 +635,26 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
}
|
}
|
||||||
ForkedDependencies::Unforked(dependencies) => {
|
ForkedDependencies::Unforked(dependencies) => {
|
||||||
// Enrich the state with any URLs, etc.
|
// Enrich the state with any URLs, etc.
|
||||||
state.visit_package_version_dependencies(
|
state
|
||||||
next_id,
|
.visit_package_version_dependencies(
|
||||||
&version,
|
next_id,
|
||||||
&self.urls,
|
&version,
|
||||||
&self.indexes,
|
&self.urls,
|
||||||
&dependencies,
|
&self.indexes,
|
||||||
&self.git,
|
&dependencies,
|
||||||
&self.workspace_members,
|
&self.git,
|
||||||
self.selector.resolution_strategy(),
|
&self.workspace_members,
|
||||||
)?;
|
self.selector.resolution_strategy(),
|
||||||
|
)
|
||||||
|
.map_err(|err| {
|
||||||
|
enrich_dependency_error(err, next_id, &version, &state.pubgrub)
|
||||||
|
})?;
|
||||||
|
|
||||||
// Emit a request to fetch the metadata for each registry package.
|
// Emit a request to fetch the metadata for each registry package.
|
||||||
self.visit_dependencies(&dependencies, &state, &request_sink)?;
|
self.visit_dependencies(&dependencies, &state, &request_sink)
|
||||||
|
.map_err(|err| {
|
||||||
|
enrich_dependency_error(err, next_id, &version, &state.pubgrub)
|
||||||
|
})?;
|
||||||
|
|
||||||
// Add the dependencies to the state.
|
// Add the dependencies to the state.
|
||||||
state.add_package_version_dependencies(next_id, &version, dependencies);
|
state.add_package_version_dependencies(next_id, &version, dependencies);
|
||||||
|
|
@ -870,19 +877,26 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
})
|
})
|
||||||
.map(move |(fork, mut forked_state)| {
|
.map(move |(fork, mut forked_state)| {
|
||||||
// Enrich the state with any URLs, etc.
|
// Enrich the state with any URLs, etc.
|
||||||
forked_state.visit_package_version_dependencies(
|
forked_state
|
||||||
package,
|
.visit_package_version_dependencies(
|
||||||
version,
|
package,
|
||||||
&self.urls,
|
version,
|
||||||
&self.indexes,
|
&self.urls,
|
||||||
&fork.dependencies,
|
&self.indexes,
|
||||||
&self.git,
|
&fork.dependencies,
|
||||||
&self.workspace_members,
|
&self.git,
|
||||||
self.selector.resolution_strategy(),
|
&self.workspace_members,
|
||||||
)?;
|
self.selector.resolution_strategy(),
|
||||||
|
)
|
||||||
|
.map_err(|err| {
|
||||||
|
enrich_dependency_error(err, package, version, &forked_state.pubgrub)
|
||||||
|
})?;
|
||||||
|
|
||||||
// Emit a request to fetch the metadata for each registry package.
|
// Emit a request to fetch the metadata for each registry package.
|
||||||
self.visit_dependencies(&fork.dependencies, &forked_state, request_sink)?;
|
self.visit_dependencies(&fork.dependencies, &forked_state, request_sink)
|
||||||
|
.map_err(|err| {
|
||||||
|
enrich_dependency_error(err, package, version, &forked_state.pubgrub)
|
||||||
|
})?;
|
||||||
|
|
||||||
// Add the dependencies to the state.
|
// Add the dependencies to the state.
|
||||||
forked_state.add_package_version_dependencies(package, version, fork.dependencies);
|
forked_state.add_package_version_dependencies(package, version, fork.dependencies);
|
||||||
|
|
@ -3836,6 +3850,20 @@ pub(crate) struct VersionFork {
|
||||||
version: Option<Version>,
|
version: Option<Version>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enrich a [`ResolveError`] with additional information about why a given package was included.
|
||||||
|
fn enrich_dependency_error(
|
||||||
|
error: ResolveError,
|
||||||
|
id: Id<PubGrubPackage>,
|
||||||
|
version: &Version,
|
||||||
|
pubgrub: &State<UvDependencyProvider>,
|
||||||
|
) -> ResolveError {
|
||||||
|
let Some(name) = pubgrub.package_store[id].name_no_root() else {
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
let chain = DerivationChainBuilder::from_state(id, version, pubgrub).unwrap_or_default();
|
||||||
|
ResolveError::Dependencies(Box::new(error), name.clone(), version.clone(), chain)
|
||||||
|
}
|
||||||
|
|
||||||
/// Compute the set of markers for which a package is known to be relevant.
|
/// Compute the set of markers for which a package is known to be relevant.
|
||||||
fn find_environments(id: Id<PubGrubPackage>, state: &State<UvDependencyProvider>) -> MarkerTree {
|
fn find_environments(id: Id<PubGrubPackage>, state: &State<UvDependencyProvider>) -> MarkerTree {
|
||||||
let package = &state.package_store[id];
|
let package = &state.package_store[id];
|
||||||
|
|
|
||||||
|
|
@ -155,10 +155,10 @@ impl Urls {
|
||||||
parsed_url: &'a ParsedUrl,
|
parsed_url: &'a ParsedUrl,
|
||||||
) -> Result<&'a VerbatimParsedUrl, ResolveError> {
|
) -> Result<&'a VerbatimParsedUrl, ResolveError> {
|
||||||
let Some(expected) = self.get_regular(package_name) else {
|
let Some(expected) = self.get_regular(package_name) else {
|
||||||
return Err(ResolveError::DisallowedUrl(
|
return Err(ResolveError::DisallowedUrl {
|
||||||
package_name.clone(),
|
name: package_name.clone(),
|
||||||
verbatim_url.to_string(),
|
url: verbatim_url.to_string(),
|
||||||
));
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let matching_urls: Vec<_> = expected
|
let matching_urls: Vec<_> = expected
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,15 @@ impl OperationDiagnostic {
|
||||||
requested_dist_error(kind, dist, &chain, err, self.hint);
|
requested_dist_error(kind, dist, &chain, err, self.hint);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
pip::operations::Error::Resolve(uv_resolver::ResolveError::Dependencies(
|
||||||
|
error,
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
chain,
|
||||||
|
)) => {
|
||||||
|
dependencies_error(error, &name, &version, &chain, self.hint.clone());
|
||||||
|
None
|
||||||
|
}
|
||||||
pip::operations::Error::Requirements(uv_requirements::Error::Dist(kind, dist, err)) => {
|
pip::operations::Error::Requirements(uv_requirements::Error::Dist(kind, dist, err)) => {
|
||||||
dist_error(
|
dist_error(
|
||||||
kind,
|
kind,
|
||||||
|
|
@ -232,6 +241,54 @@ pub(crate) fn requested_dist_error(
|
||||||
anstream::eprint!("{report:?}");
|
anstream::eprint!("{report:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render an error in fetching a package's dependencies.
|
||||||
|
pub(crate) fn dependencies_error(
|
||||||
|
error: Box<uv_resolver::ResolveError>,
|
||||||
|
name: &PackageName,
|
||||||
|
version: &Version,
|
||||||
|
chain: &DerivationChain,
|
||||||
|
help: Option<String>,
|
||||||
|
) {
|
||||||
|
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||||
|
#[error("Failed to resolve dependencies for `{}` ({})", name.cyan(), format!("v{version}").cyan())]
|
||||||
|
#[diagnostic()]
|
||||||
|
struct Diagnostic {
|
||||||
|
name: PackageName,
|
||||||
|
version: Version,
|
||||||
|
#[source]
|
||||||
|
cause: Box<uv_resolver::ResolveError>,
|
||||||
|
#[help]
|
||||||
|
help: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let help = help.or_else(|| {
|
||||||
|
SUGGESTIONS
|
||||||
|
.get(name)
|
||||||
|
.map(|suggestion| {
|
||||||
|
format!(
|
||||||
|
"`{}` is often confused for `{}` Did you mean to install `{}` instead?",
|
||||||
|
name.cyan(),
|
||||||
|
suggestion.cyan(),
|
||||||
|
suggestion.cyan(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
if chain.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(format_chain(name, Some(version), chain))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let report = miette::Report::new(Diagnostic {
|
||||||
|
name: name.clone(),
|
||||||
|
version: version.clone(),
|
||||||
|
cause: error,
|
||||||
|
help,
|
||||||
|
});
|
||||||
|
anstream::eprint!("{report:?}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Render a [`uv_resolver::NoSolutionError`].
|
/// Render a [`uv_resolver::NoSolutionError`].
|
||||||
pub(crate) fn no_solution(err: &uv_resolver::NoSolutionError) {
|
pub(crate) fn no_solution(err: &uv_resolver::NoSolutionError) {
|
||||||
let report = miette::Report::msg(format!("{err}")).context(err.header());
|
let report = miette::Report::msg(format!("{err}")).context(err.header());
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,17 @@ fn branching_urls_overlapping() -> Result<()> {
|
||||||
"# };
|
"# };
|
||||||
make_project(context.temp_dir.path(), "a", deps)?;
|
make_project(context.temp_dir.path(), "a", deps)?;
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.lock().current_dir(&context.temp_dir), @r###"
|
uv_snapshot!(context.filters(), context.lock().current_dir(&context.temp_dir), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Requirements contain conflicting URLs for package `iniconfig` in split `python_full_version == '3.11.*'`:
|
× Failed to resolve dependencies for `a` (v0.1.0)
|
||||||
- https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl
|
╰─▶ Requirements contain conflicting URLs for package `iniconfig` in split `python_full_version == '3.11.*'`:
|
||||||
- https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl
|
- https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl
|
||||||
"###
|
- https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl
|
||||||
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -128,16 +129,18 @@ fn root_package_splits_but_transitive_conflict() -> Result<()> {
|
||||||
"# };
|
"# };
|
||||||
make_project(&context.temp_dir.path().join("b2"), "b2", deps)?;
|
make_project(&context.temp_dir.path().join("b2"), "b2", deps)?;
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.lock().current_dir(&context.temp_dir), @r###"
|
uv_snapshot!(context.filters(), context.lock().current_dir(&context.temp_dir), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Requirements contain conflicting URLs for package `iniconfig` in split `python_full_version >= '3.12'`:
|
× Failed to resolve dependencies for `b2` (v0.1.0)
|
||||||
- https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl
|
╰─▶ Requirements contain conflicting URLs for package `iniconfig` in split `python_full_version >= '3.12'`:
|
||||||
- https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl
|
- https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl
|
||||||
"###
|
- https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl
|
||||||
|
help: `b2` (v0.1.0) was included because `a` (v0.1.0) depends on `b` (v0.1.0) which depends on `b2`
|
||||||
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -727,16 +730,17 @@ fn branching_urls_of_different_sources_conflict() -> Result<()> {
|
||||||
"# };
|
"# };
|
||||||
make_project(context.temp_dir.path(), "a", deps)?;
|
make_project(context.temp_dir.path(), "a", deps)?;
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.lock().current_dir(&context.temp_dir), @r###"
|
uv_snapshot!(context.filters(), context.lock().current_dir(&context.temp_dir), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Requirements contain conflicting URLs for package `iniconfig` in split `python_full_version == '3.11.*'`:
|
× Failed to resolve dependencies for `a` (v0.1.0)
|
||||||
- git+https://github.com/pytest-dev/iniconfig@93f5930e668c0d1ddf4597e38dd0dea4e2665e7a
|
╰─▶ Requirements contain conflicting URLs for package `iniconfig` in split `python_full_version == '3.11.*'`:
|
||||||
- https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl
|
- git+https://github.com/pytest-dev/iniconfig@93f5930e668c0d1ddf4597e38dd0dea4e2665e7a
|
||||||
"###
|
- https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl
|
||||||
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -11088,13 +11088,14 @@ fn lock_editable() -> Result<()> {
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Requirements contain conflicting URLs for package `library` in all marker environments:
|
× Failed to resolve dependencies for `workspace` (v0.1.0)
|
||||||
- file://[TEMP_DIR]/library
|
╰─▶ Requirements contain conflicting URLs for package `library` in all marker environments:
|
||||||
- file://[TEMP_DIR]/library (editable)
|
- file://[TEMP_DIR]/library
|
||||||
|
- file://[TEMP_DIR]/library (editable)
|
||||||
");
|
");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -20728,16 +20729,17 @@ fn lock_multiple_sources_index_overlapping_extras() -> Result<()> {
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Requirements contain conflicting indexes for package `jinja2` in all marker environments:
|
× Failed to resolve dependencies for `project` (v0.1.0)
|
||||||
- https://astral-sh.github.io/pytorch-mirror/whl/cu118
|
╰─▶ Requirements contain conflicting indexes for package `jinja2` in all marker environments:
|
||||||
- https://astral-sh.github.io/pytorch-mirror/whl/cu124
|
- https://astral-sh.github.io/pytorch-mirror/whl/cu118
|
||||||
"###);
|
- https://astral-sh.github.io/pytorch-mirror/whl/cu124
|
||||||
|
");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14860,16 +14860,17 @@ fn universal_conflicting_override_urls() -> Result<()> {
|
||||||
.arg("requirements.in")
|
.arg("requirements.in")
|
||||||
.arg("--overrides")
|
.arg("--overrides")
|
||||||
.arg("overrides.txt")
|
.arg("overrides.txt")
|
||||||
.arg("--universal"), @r###"
|
.arg("--universal"), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Requirements contain conflicting URLs for package `sniffio` in split `sys_platform == 'win32'`:
|
× Failed to resolve dependencies for `anyio` (v4.3.0)
|
||||||
- https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl
|
╰─▶ Requirements contain conflicting URLs for package `sniffio` in split `sys_platform == 'win32'`:
|
||||||
- https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl
|
- https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl
|
||||||
"###
|
- https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl
|
||||||
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue