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)]
|
||||
pub enum ResolveError {
|
||||
#[error("Failed to resolve dependencies for package `{1}=={2}`")]
|
||||
Dependencies(
|
||||
#[source] Box<ResolveError>,
|
||||
PackageName,
|
||||
Version,
|
||||
DerivationChain,
|
||||
),
|
||||
|
||||
#[error(transparent)]
|
||||
Client(#[from] uv_client::Error),
|
||||
|
||||
|
|
@ -92,9 +100,11 @@ pub enum ResolveError {
|
|||
ConflictingIndexes(PackageName, String, String),
|
||||
|
||||
#[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)]
|
||||
DistributionType(#[from] uv_distribution_types::Error),
|
||||
|
|
|
|||
|
|
@ -635,7 +635,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
}
|
||||
ForkedDependencies::Unforked(dependencies) => {
|
||||
// Enrich the state with any URLs, etc.
|
||||
state.visit_package_version_dependencies(
|
||||
state
|
||||
.visit_package_version_dependencies(
|
||||
next_id,
|
||||
&version,
|
||||
&self.urls,
|
||||
|
|
@ -644,10 +645,16 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
&self.git,
|
||||
&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.
|
||||
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.
|
||||
state.add_package_version_dependencies(next_id, &version, dependencies);
|
||||
|
|
@ -870,7 +877,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
})
|
||||
.map(move |(fork, mut forked_state)| {
|
||||
// Enrich the state with any URLs, etc.
|
||||
forked_state.visit_package_version_dependencies(
|
||||
forked_state
|
||||
.visit_package_version_dependencies(
|
||||
package,
|
||||
version,
|
||||
&self.urls,
|
||||
|
|
@ -879,10 +887,16 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
&self.git,
|
||||
&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.
|
||||
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.
|
||||
forked_state.add_package_version_dependencies(package, version, fork.dependencies);
|
||||
|
|
@ -3836,6 +3850,20 @@ pub(crate) struct VersionFork {
|
|||
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.
|
||||
fn find_environments(id: Id<PubGrubPackage>, state: &State<UvDependencyProvider>) -> MarkerTree {
|
||||
let package = &state.package_store[id];
|
||||
|
|
|
|||
|
|
@ -155,10 +155,10 @@ impl Urls {
|
|||
parsed_url: &'a ParsedUrl,
|
||||
) -> Result<&'a VerbatimParsedUrl, ResolveError> {
|
||||
let Some(expected) = self.get_regular(package_name) else {
|
||||
return Err(ResolveError::DisallowedUrl(
|
||||
package_name.clone(),
|
||||
verbatim_url.to_string(),
|
||||
));
|
||||
return Err(ResolveError::DisallowedUrl {
|
||||
name: package_name.clone(),
|
||||
url: verbatim_url.to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let matching_urls: Vec<_> = expected
|
||||
|
|
|
|||
|
|
@ -92,6 +92,15 @@ impl OperationDiagnostic {
|
|||
requested_dist_error(kind, dist, &chain, err, self.hint);
|
||||
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)) => {
|
||||
dist_error(
|
||||
kind,
|
||||
|
|
@ -232,6 +241,54 @@ pub(crate) fn requested_dist_error(
|
|||
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`].
|
||||
pub(crate) fn no_solution(err: &uv_resolver::NoSolutionError) {
|
||||
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)?;
|
||||
|
||||
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
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- 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)
|
||||
╰─▶ 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
|
||||
- https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl
|
||||
"###
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
|
@ -128,16 +129,18 @@ fn root_package_splits_but_transitive_conflict() -> Result<()> {
|
|||
"# };
|
||||
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
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- 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)
|
||||
╰─▶ Requirements contain conflicting URLs for package `iniconfig` in split `python_full_version >= '3.12'`:
|
||||
- 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(())
|
||||
|
|
@ -727,16 +730,17 @@ fn branching_urls_of_different_sources_conflict() -> Result<()> {
|
|||
"# };
|
||||
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
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- 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)
|
||||
╰─▶ Requirements contain conflicting URLs for package `iniconfig` in split `python_full_version == '3.11.*'`:
|
||||
- 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(())
|
||||
|
|
|
|||
|
|
@ -11088,11 +11088,12 @@ fn lock_editable() -> Result<()> {
|
|||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Requirements contain conflicting URLs for package `library` in all marker environments:
|
||||
× Failed to resolve dependencies for `workspace` (v0.1.0)
|
||||
╰─▶ Requirements contain conflicting URLs for package `library` in all marker environments:
|
||||
- file://[TEMP_DIR]/library
|
||||
- file://[TEMP_DIR]/library (editable)
|
||||
");
|
||||
|
|
@ -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
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Requirements contain conflicting indexes for package `jinja2` in all marker environments:
|
||||
× Failed to resolve dependencies for `project` (v0.1.0)
|
||||
╰─▶ Requirements contain conflicting indexes for package `jinja2` in all marker environments:
|
||||
- https://astral-sh.github.io/pytorch-mirror/whl/cu118
|
||||
- https://astral-sh.github.io/pytorch-mirror/whl/cu124
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14860,16 +14860,17 @@ fn universal_conflicting_override_urls() -> Result<()> {
|
|||
.arg("requirements.in")
|
||||
.arg("--overrides")
|
||||
.arg("overrides.txt")
|
||||
.arg("--universal"), @r###"
|
||||
.arg("--universal"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Requirements contain conflicting URLs for package `sniffio` in split `sys_platform == 'win32'`:
|
||||
× Failed to resolve dependencies for `anyio` (v4.3.0)
|
||||
╰─▶ Requirements contain conflicting URLs for package `sniffio` in split `sys_platform == 'win32'`:
|
||||
- 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(())
|
||||
|
|
|
|||
Loading…
Reference in New Issue