Use colors for lock errors (#10736)

## Summary

These now better match the errors we show when failing to resolve.
This commit is contained in:
Charlie Marsh 2025-01-18 13:50:20 -05:00 committed by GitHub
parent 3fe4e7168b
commit 35aec8863e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 45 additions and 91 deletions

View file

@ -8,6 +8,7 @@ use std::str::FromStr;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use itertools::Itertools; use itertools::Itertools;
use owo_colors::OwoColorize;
use petgraph::graph::NodeIndex; use petgraph::graph::NodeIndex;
use petgraph::visit::EdgeRef; use petgraph::visit::EdgeRef;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
@ -4156,14 +4157,14 @@ where
enum LockErrorKind { enum LockErrorKind {
/// An error that occurs when multiple packages with the same /// An error that occurs when multiple packages with the same
/// ID were found. /// ID were found.
#[error("Found duplicate package `{id}`")] #[error("Found duplicate package `{id}`", id = id.cyan())]
DuplicatePackage { DuplicatePackage {
/// The ID of the conflicting package. /// The ID of the conflicting package.
id: PackageId, id: PackageId,
}, },
/// An error that occurs when there are multiple dependencies for the /// An error that occurs when there are multiple dependencies for the
/// same package that have identical identifiers. /// same package that have identical identifiers.
#[error("For package `{id}`, found duplicate dependency `{dependency}`")] #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
DuplicateDependency { DuplicateDependency {
/// The ID of the package for which a duplicate dependency was /// The ID of the package for which a duplicate dependency was
/// found. /// found.
@ -4174,7 +4175,7 @@ enum LockErrorKind {
/// An error that occurs when there are multiple dependencies for the /// An error that occurs when there are multiple dependencies for the
/// same package that have identical identifiers, as part of the /// same package that have identical identifiers, as part of the
/// that package's optional dependencies. /// that package's optional dependencies.
#[error("For package `{id}[{extra}]`, found duplicate dependency `{dependency}`")] #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
DuplicateOptionalDependency { DuplicateOptionalDependency {
/// The ID of the package for which a duplicate dependency was /// The ID of the package for which a duplicate dependency was
/// found. /// found.
@ -4187,7 +4188,7 @@ enum LockErrorKind {
/// An error that occurs when there are multiple dependencies for the /// An error that occurs when there are multiple dependencies for the
/// same package that have identical identifiers, as part of the /// same package that have identical identifiers, as part of the
/// that package's development dependencies. /// that package's development dependencies.
#[error("For package `{id}:{group}`, found duplicate dependency `{dependency}`")] #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
DuplicateDevDependency { DuplicateDevDependency {
/// The ID of the package for which a duplicate dependency was /// The ID of the package for which a duplicate dependency was
/// found. /// found.
@ -4210,8 +4211,8 @@ enum LockErrorKind {
/// for a given wheel or source distribution. /// for a given wheel or source distribution.
#[error("Failed to parse file extension; expected one of: {0}")] #[error("Failed to parse file extension; expected one of: {0}")]
MissingExtension(#[from] ExtensionError), MissingExtension(#[from] ExtensionError),
/// Failed to parse a git source URL. /// Failed to parse a Git source URL.
#[error("Failed to parse source git URL")] #[error("Failed to parse Git URL")]
InvalidGitSourceUrl( InvalidGitSourceUrl(
/// The underlying error that occurred. This includes the /// The underlying error that occurred. This includes the
/// errant URL in the message. /// errant URL in the message.
@ -4221,7 +4222,7 @@ enum LockErrorKind {
/// An error that occurs when there's an unrecognized dependency. /// An error that occurs when there's an unrecognized dependency.
/// ///
/// That is, a dependency for a package that isn't in the lockfile. /// That is, a dependency for a package that isn't in the lockfile.
#[error("For package `{id}`, found dependency `{dependency}` with no locked package")] #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
UnrecognizedDependency { UnrecognizedDependency {
/// The ID of the package that has an unrecognized dependency. /// The ID of the package that has an unrecognized dependency.
id: PackageId, id: PackageId,
@ -4231,7 +4232,7 @@ enum LockErrorKind {
}, },
/// An error that occurs when a hash is expected (or not) for a particular /// An error that occurs when a hash is expected (or not) for a particular
/// artifact, but one was not found (or was). /// artifact, but one was not found (or was).
#[error("Since the package `{id}` comes from a {source} dependency, a hash was {expected} but one was not found for {artifact_type}", source = id.source.name(), expected = if *expected { "expected" } else { "not expected" })] #[error("Since the package `{id}` comes from a {source} dependency, a hash was {expected} but one was not found for {artifact_type}", id = id.cyan(), source = id.source.name(), expected = if *expected { "expected" } else { "not expected" })]
Hash { Hash {
/// The ID of the package that has a missing hash. /// The ID of the package that has a missing hash.
id: PackageId, id: PackageId,
@ -4243,7 +4244,7 @@ enum LockErrorKind {
}, },
/// An error that occurs when a package is included with an extra name, /// An error that occurs when a package is included with an extra name,
/// but no corresponding base package (i.e., without the extra) exists. /// but no corresponding base package (i.e., without the extra) exists.
#[error("Found package `{id}` with extra `{extra}` but no base package")] #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
MissingExtraBase { MissingExtraBase {
/// The ID of the package that has a missing base. /// The ID of the package that has a missing base.
id: PackageId, id: PackageId,
@ -4253,9 +4254,7 @@ enum LockErrorKind {
/// An error that occurs when a package is included with a development /// An error that occurs when a package is included with a development
/// dependency group, but no corresponding base package (i.e., without /// dependency group, but no corresponding base package (i.e., without
/// the group) exists. /// the group) exists.
#[error( #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
"found package `{id}` with development dependency group `{group}` but no base package"
)]
MissingDevBase { MissingDevBase {
/// The ID of the package that has a missing base. /// The ID of the package that has a missing base.
id: PackageId, id: PackageId,
@ -4273,7 +4272,7 @@ enum LockErrorKind {
}, },
/// An error that occurs when a distribution indicates that it is sourced from a remote /// An error that occurs when a distribution indicates that it is sourced from a remote
/// registry, but is missing a URL. /// registry, but is missing a URL.
#[error("Found registry distribution `{name}=={version}` without a valid URL")] #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
MissingUrl { MissingUrl {
/// The name of the distribution that is missing a URL. /// The name of the distribution that is missing a URL.
name: PackageName, name: PackageName,
@ -4282,7 +4281,7 @@ enum LockErrorKind {
}, },
/// An error that occurs when a distribution indicates that it is sourced from a local registry, /// An error that occurs when a distribution indicates that it is sourced from a local registry,
/// but is missing a path. /// but is missing a path.
#[error("Found registry distribution `{name}=={version}` without a valid path")] #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
MissingPath { MissingPath {
/// The name of the distribution that is missing a path. /// The name of the distribution that is missing a path.
name: PackageName, name: PackageName,
@ -4291,55 +4290,53 @@ enum LockErrorKind {
}, },
/// An error that occurs when a distribution indicates that it is sourced from a registry, but /// An error that occurs when a distribution indicates that it is sourced from a registry, but
/// is missing a filename. /// is missing a filename.
#[error("Found registry distribution `{id}` without a valid filename")] #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
MissingFilename { MissingFilename {
/// The ID of the distribution that is missing a filename. /// The ID of the distribution that is missing a filename.
id: PackageId, id: PackageId,
}, },
/// An error that occurs when a distribution is included with neither wheels nor a source /// An error that occurs when a distribution is included with neither wheels nor a source
/// distribution. /// distribution.
#[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform")] #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
NeitherSourceDistNorWheel { NeitherSourceDistNorWheel {
/// The ID of the distribution. /// The ID of the distribution.
id: PackageId, id: PackageId,
}, },
/// An error that occurs when a distribution is marked as both `--no-binary` and `--no-build`. /// An error that occurs when a distribution is marked as both `--no-binary` and `--no-build`.
#[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`")] #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
NoBinaryNoBuild { NoBinaryNoBuild {
/// The ID of the distribution. /// The ID of the distribution.
id: PackageId, id: PackageId,
}, },
/// An error that occurs when a distribution is marked as `--no-binary`, but no source /// An error that occurs when a distribution is marked as `--no-binary`, but no source
/// distribution is available. /// distribution is available.
#[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution")] #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
NoBinary { NoBinary {
/// The ID of the distribution. /// The ID of the distribution.
id: PackageId, id: PackageId,
}, },
/// An error that occurs when a distribution is marked as `--no-build`, but no binary /// An error that occurs when a distribution is marked as `--no-build`, but no binary
/// distribution is available. /// distribution is available.
#[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution")] #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
NoBuild { NoBuild {
/// The ID of the distribution. /// The ID of the distribution.
id: PackageId, id: PackageId,
}, },
/// An error that occurs when a wheel-only distribution is incompatible with the current /// An error that occurs when a wheel-only distribution is incompatible with the current
/// platform. /// platform.
#[error( #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
"distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform"
)]
IncompatibleWheelOnly { IncompatibleWheelOnly {
/// The ID of the distribution. /// The ID of the distribution.
id: PackageId, id: PackageId,
}, },
/// An error that occurs when a wheel-only source is marked as `--no-binary`. /// An error that occurs when a wheel-only source is marked as `--no-binary`.
#[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution")] #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
NoBinaryWheelOnly { NoBinaryWheelOnly {
/// The ID of the distribution. /// The ID of the distribution.
id: PackageId, id: PackageId,
}, },
/// An error that occurs when converting between URLs and paths. /// An error that occurs when converting between URLs and paths.
#[error("Found dependency `{id}` with no locked distribution")] #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
VerbatimUrl { VerbatimUrl {
/// The ID of the distribution that has a missing base. /// The ID of the distribution that has a missing base.
id: PackageId, id: PackageId,
@ -4370,20 +4367,14 @@ enum LockErrorKind {
), ),
/// An error that occurs when an ambiguous `package.dependency` is /// An error that occurs when an ambiguous `package.dependency` is
/// missing a `version` field. /// missing a `version` field.
#[error( #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
"Dependency `{name}` has missing `version` \
field but has more than one matching package"
)]
MissingDependencyVersion { MissingDependencyVersion {
/// The name of the dependency that is missing a `version` field. /// The name of the dependency that is missing a `version` field.
name: PackageName, name: PackageName,
}, },
/// An error that occurs when an ambiguous `package.dependency` is /// An error that occurs when an ambiguous `package.dependency` is
/// missing a `source` field. /// missing a `source` field.
#[error( #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
"Dependency `{name}` has missing `source` \
field but has more than one matching package"
)]
MissingDependencySource { MissingDependencySource {
/// The name of the dependency that is missing a `source` field. /// The name of the dependency that is missing a `source` field.
name: PackageName, name: PackageName,
@ -4417,19 +4408,19 @@ enum LockErrorKind {
UrlToPath, UrlToPath,
/// An error that occurs when multiple packages with the same /// An error that occurs when multiple packages with the same
/// name were found when identifying the root packages. /// name were found when identifying the root packages.
#[error("Found multiple packages matching `{name}`")] #[error("Found multiple packages matching `{name}`", name = name.cyan())]
MultipleRootPackages { MultipleRootPackages {
/// The ID of the package. /// The ID of the package.
name: PackageName, name: PackageName,
}, },
/// An error that occurs when a root package can't be found. /// An error that occurs when a root package can't be found.
#[error("Could not find root package `{name}`")] #[error("Could not find root package `{name}`", name = name.cyan())]
MissingRootPackage { MissingRootPackage {
/// The ID of the package. /// The ID of the package.
name: PackageName, name: PackageName,
}, },
/// An error that occurs when resolving metadata for a package. /// An error that occurs when resolving metadata for a package.
#[error("Failed to generate package metadata for `{id}`")] #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
Resolution { Resolution {
/// The ID of the distribution that failed to resolve. /// The ID of the distribution that failed to resolve.
id: PackageId, id: PackageId,
@ -4546,8 +4537,19 @@ fn deduplicated_simplified_pep508_markers(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use uv_warnings::anstream;
use super::*; use super::*;
/// Assert a given display snapshot, stripping ANSI color codes.
macro_rules! assert_stripped_snapshot {
($expr:expr, @$snapshot:literal) => {{
let expr = format!("{}", $expr);
let expr = format!("{}", anstream::adapter::strip_str(&expr));
insta::assert_snapshot!(expr, @$snapshot);
}};
}
#[test] #[test]
fn missing_dependency_source_unambiguous() { fn missing_dependency_source_unambiguous() {
let data = r#" let data = r#"
@ -4653,8 +4655,8 @@ sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d
name = "a" name = "a"
version = "0.1.0" version = "0.1.0"
"#; "#;
let result: Result<Lock, _> = toml::from_str(data); let result = toml::from_str::<Lock>(data).unwrap_err();
insta::assert_debug_snapshot!(result); assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
} }
#[test] #[test]
@ -4685,8 +4687,8 @@ sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d
name = "a" name = "a"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
"#; "#;
let result: Result<Lock, _> = toml::from_str(data); let result = toml::from_str::<Lock>(data).unwrap_err();
insta::assert_debug_snapshot!(result); assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
} }
#[test] #[test]
@ -4716,8 +4718,8 @@ sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d
[[package.dependencies]] [[package.dependencies]]
name = "a" name = "a"
"#; "#;
let result: Result<Lock, _> = toml::from_str(data); let result = toml::from_str::<Lock>(data).unwrap_err();
insta::assert_debug_snapshot!(result); assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
} }
#[test] #[test]

View file

@ -1,16 +0,0 @@
---
source: crates/uv-resolver/src/lock/mod.rs
expression: result
---
Err(
Error {
inner: Error {
inner: TomlError {
message: "Dependency `a` has missing `source` field but has more than one matching package",
raw: None,
keys: [],
span: None,
},
},
},
)

View file

@ -1,16 +0,0 @@
---
source: crates/uv-resolver/src/lock/mod.rs
expression: result
---
Err(
Error {
inner: Error {
inner: TomlError {
message: "Dependency `a` has missing `version` field but has more than one matching package",
raw: None,
keys: [],
span: None,
},
},
},
)

View file

@ -1,16 +0,0 @@
---
source: crates/uv-resolver/src/lock/mod.rs
expression: result
---
Err(
Error {
inner: Error {
inner: TomlError {
message: "Dependency `a` has missing `version` field but has more than one matching package",
raw: None,
keys: [],
span: None,
},
},
},
)

View file

@ -3640,7 +3640,7 @@ fn sync_wheel_url_source_error() -> Result<()> {
----- stderr ----- ----- stderr -----
Resolved 3 packages in [TIME] Resolved 3 packages in [TIME]
error: distribution `cffi==1.17.1 @ direct+https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl` can't be installed because the binary distribution is incompatible with the current platform error: Distribution `cffi==1.17.1 @ direct+https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl` can't be installed because the binary distribution is incompatible with the current platform
"###); "###);
Ok(()) Ok(())
@ -3689,7 +3689,7 @@ fn sync_wheel_path_source_error() -> Result<()> {
----- stderr ----- ----- stderr -----
Resolved 3 packages in [TIME] Resolved 3 packages in [TIME]
error: distribution `cffi==1.17.1 @ path+cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl` can't be installed because the binary distribution is incompatible with the current platform error: Distribution `cffi==1.17.1 @ path+cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl` can't be installed because the binary distribution is incompatible with the current platform
"###); "###);
Ok(()) Ok(())