use std::str::FromStr;
use std::sync::{Arc, LazyLock};
use owo_colors::OwoColorize;
use rustc_hash::FxHashMap;
use version_ranges::Ranges;
use uv_distribution_types::{
DerivationChain, DerivationStep, Dist, DistErrorKind, Name, RequestedDist,
};
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_resolver::SentinelRange;
use crate::commands::pip;
static SUGGESTIONS: LazyLock<FxHashMap<PackageName, PackageName>> = LazyLock::new(|| {
let suggestions: Vec<(String, String)> =
serde_json::from_str(include_str!("suggestions.json")).unwrap();
suggestions
.iter()
.map(|(k, v)| {
(
PackageName::from_str(k).unwrap(),
PackageName::from_str(v).unwrap(),
)
})
.collect()
});
#[derive(Debug, Default)]
pub(crate) struct OperationDiagnostic {
pub(crate) hint: Option<String>,
pub(crate) system_certs: bool,
pub(crate) context: Option<&'static str>,
}
impl OperationDiagnostic {
#[must_use]
pub(crate) fn with_system_certs(system_certs: bool) -> Self {
Self {
system_certs,
..Default::default()
}
}
#[must_use]
pub(crate) fn with_hint(self, hint: String) -> Self {
Self {
hint: Some(hint),
..self
}
}
#[must_use]
pub(crate) fn with_context(self, context: &'static str) -> Self {
Self {
context: Some(context),
..self
}
}
pub(crate) fn report(self, err: pip::operations::Error) -> Option<pip::operations::Error> {
match err {
pip::operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(err)) => {
if let Some(context) = self.context {
no_solution_context(&err, context);
} else if let Some(hint) = self.hint {
no_solution_hint(err, hint);
} else {
no_solution(&err);
}
None
}
pip::operations::Error::Resolve(uv_resolver::ResolveError::Dist(
kind,
dist,
chain,
err,
)) => {
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,
dist,
&DerivationChain::default(),
Arc::new(*err),
self.hint,
);
None
}
pip::operations::Error::Prepare(uv_installer::PrepareError::Dist(
kind,
dist,
chain,
err,
)) => {
dist_error(kind, dist, &chain, Arc::new(*err), self.hint);
None
}
pip::operations::Error::Requirements(err) => {
if let Some(context) = self.context {
let err = miette::Report::msg(format!("{err}"))
.context(format!("Failed to resolve {context} requirement"));
anstream::eprint!("{err:?}");
None
} else {
Some(pip::operations::Error::Requirements(err))
}
}
pip::operations::Error::Resolve(uv_resolver::ResolveError::Client(err))
if !self.system_certs && err.is_ssl() =>
{
system_certs_hint(err);
None
}
err @ pip::operations::Error::OutdatedEnvironment(..) => {
anstream::eprintln!("{}", err);
None
}
err => Some(err),
}
}
}
#[allow(unused_assignments)]
pub(crate) fn dist_error(
kind: DistErrorKind,
dist: Box<Dist>,
chain: &DerivationChain,
cause: Arc<uv_distribution::Error>,
help: Option<String>,
) {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("{kind} `{dist}`")]
#[diagnostic()]
struct Diagnostic {
kind: DistErrorKind,
dist: Box<Dist>,
#[source]
cause: Arc<uv_distribution::Error>,
#[help]
help: Option<String>,
}
let help = help.or_else(|| {
SUGGESTIONS
.get(dist.name())
.map(|suggestion| {
format!(
"`{}` is often confused for `{}` Did you mean to install `{}` instead?",
dist.name().cyan(),
suggestion.cyan(),
suggestion.cyan(),
)
})
.or_else(|| {
if chain.is_empty() {
None
} else {
Some(format_chain(dist.name(), dist.version(), chain))
}
})
});
let report = miette::Report::new(Diagnostic {
kind,
dist,
cause,
help,
});
anstream::eprint!("{report:?}");
}
#[allow(unused_assignments)]
pub(crate) fn requested_dist_error(
kind: DistErrorKind,
dist: Box<RequestedDist>,
chain: &DerivationChain,
cause: Arc<uv_distribution::Error>,
help: Option<String>,
) {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("{kind} `{dist}`")]
#[diagnostic()]
struct Diagnostic {
kind: DistErrorKind,
dist: Box<RequestedDist>,
#[source]
cause: Arc<uv_distribution::Error>,
#[help]
help: Option<String>,
}
let help = help.or_else(|| {
SUGGESTIONS
.get(dist.name())
.map(|suggestion| {
format!(
"`{}` is often confused for `{}` Did you mean to install `{}` instead?",
dist.name().cyan(),
suggestion.cyan(),
suggestion.cyan(),
)
})
.or_else(|| {
if chain.is_empty() {
None
} else {
Some(format_chain(dist.name(), dist.version(), chain))
}
})
});
let report = miette::Report::new(Diagnostic {
kind,
dist,
cause,
help,
});
anstream::eprint!("{report:?}");
}
#[allow(unused_assignments)]
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:?}");
}
pub(crate) fn no_solution(err: &uv_resolver::NoSolutionError) {
let report = miette::Report::msg(format!("{err}")).context(err.header());
anstream::eprint!("{report:?}");
}
pub(crate) fn no_solution_context(err: &uv_resolver::NoSolutionError, context: &'static str) {
let report = miette::Report::msg(format!("{err}")).context(err.header().with_context(context));
anstream::eprint!("{report:?}");
}
#[allow(unused_assignments)]
pub(crate) fn no_solution_hint(err: Box<uv_resolver::NoSolutionError>, help: String) {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("{header}")]
#[diagnostic()]
struct Error {
header: uv_resolver::NoSolutionHeader,
#[source]
err: Box<uv_resolver::NoSolutionError>,
#[help]
help: String,
}
let header = err.header();
let report = miette::Report::new(Error { header, err, help });
anstream::eprint!("{report:?}");
}
#[allow(unused_assignments)]
pub(crate) fn system_certs_hint(err: uv_client::Error) {
#[derive(Debug, miette::Diagnostic)]
#[diagnostic()]
struct Error {
err: uv_client::Error,
#[help]
help: String,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.err)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.err.source()
}
}
let report = miette::Report::new(Error {
err,
help: format!(
"Consider enabling use of system TLS certificates with the `{}` command-line flag",
"--system-certs".green()
),
});
anstream::eprint!("{report:?}");
}
fn format_chain(name: &PackageName, version: Option<&Version>, chain: &DerivationChain) -> String {
fn format_step(step: &DerivationStep, range: Option<Ranges<Version>>) -> String {
if let Some(range) =
range.filter(|range| *range != Ranges::empty() && *range != Ranges::full())
{
if let Some(extra) = &step.extra {
if let Some(version) = step.version.as_ref() {
format!(
"`{}{}` ({})",
format!("{}[{}]", step.name, extra).cyan(),
range.cyan(),
format!("v{version}").cyan(),
)
} else {
format!(
"`{}{}`",
format!("{}[{}]", step.name, extra).cyan(),
range.cyan(),
)
}
} else if let Some(group) = &step.group {
if let Some(version) = step.version.as_ref() {
format!(
"`{}{}` ({})",
format!("{}:{}", step.name, group).cyan(),
range.cyan(),
format!("v{version}").cyan(),
)
} else {
format!(
"`{}{}`",
format!("{}:{}", step.name, group).cyan(),
range.cyan(),
)
}
} else {
if let Some(version) = step.version.as_ref() {
format!(
"`{}{}` ({})",
step.name.cyan(),
range.cyan(),
format!("v{version}").cyan(),
)
} else {
format!("`{}{}`", step.name.cyan(), range.cyan())
}
}
} else {
if let Some(extra) = &step.extra {
if let Some(version) = step.version.as_ref() {
format!(
"`{}` ({})",
format!("{}[{}]", step.name, extra).cyan(),
format!("v{version}").cyan(),
)
} else {
format!("`{}`", format!("{}[{}]", step.name, extra).cyan())
}
} else if let Some(group) = &step.group {
if let Some(version) = step.version.as_ref() {
format!(
"`{}` ({})",
format!("{}:{}", step.name, group).cyan(),
format!("v{version}").cyan(),
)
} else {
format!("`{}`", format!("{}:{}", step.name, group).cyan())
}
} else {
if let Some(version) = step.version.as_ref() {
format!("`{}` ({})", step.name.cyan(), format!("v{version}").cyan())
} else {
format!("`{}`", step.name.cyan())
}
}
}
}
let mut message = if let Some(version) = version {
format!(
"`{}` ({}) was included because",
name.cyan(),
format!("v{version}").cyan()
)
} else {
format!("`{}` was included because", name.cyan())
};
let mut range: Option<Ranges<Version>> = None;
for (i, step) in chain.iter().enumerate() {
if i > 0 {
message = format!("{message} {} which depends on", format_step(step, range));
} else {
message = format!("{message} {} depends on", format_step(step, range));
}
range = Some(SentinelRange::from(&step.range).strip());
}
if let Some(range) = range.filter(|range| *range != Ranges::empty() && *range != Ranges::full())
{
message = format!("{message} `{}{}`", name.cyan(), range.cyan());
} else {
message = format!("{message} `{}`", name.cyan());
}
message
}