mirror of https://github.com/astral-sh/uv
Better warning chain styling (#14934)
Improve the styling of warning chains for Python installation errors. Apply the same logic to other internal warning and error formatting locations. **Before** <img width="1232" height="364" alt="Screenshot from 2025-07-28 10-06-41" src="https://github.com/user-attachments/assets/e3befe14-ad4c-44ed-8b0a-57d9c9a3b815" /> **After** <img width="1232" height="364" alt="Screenshot from 2025-07-28 10-23-49" src="https://github.com/user-attachments/assets/1bd890c1-5dbb-4662-93bd-14430c060a69" />
This commit is contained in:
parent
90885fd0d9
commit
ac135278c3
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::iter;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::{LazyLock, Mutex};
|
use std::sync::{LazyLock, Mutex};
|
||||||
|
|
||||||
|
|
@ -6,6 +8,7 @@ use std::sync::{LazyLock, Mutex};
|
||||||
pub use anstream;
|
pub use anstream;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use owo_colors;
|
pub use owo_colors;
|
||||||
|
use owo_colors::{DynColor, OwoColorize};
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
/// Whether user-facing warnings are enabled.
|
/// Whether user-facing warnings are enabled.
|
||||||
|
|
@ -56,3 +59,41 @@ macro_rules! warn_user_once {
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format an error or warning chain.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// error: Failed to install app
|
||||||
|
/// Caused By: Failed to install dependency
|
||||||
|
/// Caused By: Error writing failed `/home/ferris/deps/foo`: Permission denied
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// warning: Failed to create registry entry for Python 3.12
|
||||||
|
/// Caused By: Security policy forbids chaining registry entries
|
||||||
|
/// ```
|
||||||
|
pub fn write_error_chain(
|
||||||
|
err: &dyn Error,
|
||||||
|
mut stream: impl std::fmt::Write,
|
||||||
|
level: impl AsRef<str>,
|
||||||
|
color: impl DynColor + Copy,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
writeln!(
|
||||||
|
&mut stream,
|
||||||
|
"{}{} {}",
|
||||||
|
level.as_ref().color(color).bold(),
|
||||||
|
":".bold(),
|
||||||
|
err.to_string().trim()
|
||||||
|
)?;
|
||||||
|
for source in iter::successors(err.source(), |&err| err.source()) {
|
||||||
|
writeln!(
|
||||||
|
&mut stream,
|
||||||
|
" {}: {}",
|
||||||
|
"Caused by".color(color).bold(),
|
||||||
|
source.to_string().trim()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::iter;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use console::Term;
|
use console::Term;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::{AnsiColors, OwoColorize};
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
use uv_auth::Credentials;
|
use uv_auth::Credentials;
|
||||||
|
|
@ -17,7 +16,7 @@ use uv_publish::{
|
||||||
CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload,
|
CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload,
|
||||||
};
|
};
|
||||||
use uv_redacted::DisplaySafeUrl;
|
use uv_redacted::DisplaySafeUrl;
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::{warn_user_once, write_error_chain};
|
||||||
|
|
||||||
use crate::commands::reporters::PublishReporter;
|
use crate::commands::reporters::PublishReporter;
|
||||||
use crate::commands::{ExitStatus, human_readable_bytes};
|
use crate::commands::{ExitStatus, human_readable_bytes};
|
||||||
|
|
@ -274,19 +273,15 @@ async fn gather_credentials(
|
||||||
fetching the trusted publishing token. If you don't want to use trusted \
|
fetching the trusted publishing token. If you don't want to use trusted \
|
||||||
publishing, you can ignore this error, but you need to provide credentials."
|
publishing, you can ignore this error, but you need to provide credentials."
|
||||||
)?;
|
)?;
|
||||||
writeln!(
|
|
||||||
|
write_error_chain(
|
||||||
|
anyhow::Error::from(err)
|
||||||
|
.context("Trusted publishing failed")
|
||||||
|
.as_ref(),
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"{}: {err}",
|
"error",
|
||||||
"Trusted publishing error".red().bold()
|
AnsiColors::Red,
|
||||||
)?;
|
)?;
|
||||||
for source in iter::successors(std::error::Error::source(&err), |&err| err.source()) {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
" {}: {}",
|
|
||||||
"Caused by".red().bold(),
|
|
||||||
source.to_string().trim()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use futures::StreamExt;
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use itertools::{Either, Itertools};
|
use itertools::{Either, Itertools};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::{AnsiColors, OwoColorize};
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ use uv_python::{
|
||||||
};
|
};
|
||||||
use uv_shell::Shell;
|
use uv_shell::Shell;
|
||||||
use uv_trampoline_builder::{Launcher, LauncherKind};
|
use uv_trampoline_builder::{Launcher, LauncherKind};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::{warn_user, write_error_chain};
|
||||||
|
|
||||||
use crate::commands::python::{ChangeEvent, ChangeEventKind};
|
use crate::commands::python::{ChangeEvent, ChangeEventKind};
|
||||||
use crate::commands::reporters::PythonDownloadReporter;
|
use crate::commands::reporters::PythonDownloadReporter;
|
||||||
|
|
@ -139,7 +139,7 @@ impl Changelog {
|
||||||
enum InstallErrorKind {
|
enum InstallErrorKind {
|
||||||
DownloadUnpack,
|
DownloadUnpack,
|
||||||
Bin,
|
Bin,
|
||||||
#[cfg(windows)]
|
#[cfg_attr(not(windows), allow(dead_code))]
|
||||||
Registry,
|
Registry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -667,7 +667,6 @@ pub(crate) async fn install(
|
||||||
// to warn
|
// to warn
|
||||||
let fatal = !errors.iter().all(|(kind, _, _)| match kind {
|
let fatal = !errors.iter().all(|(kind, _, _)| match kind {
|
||||||
InstallErrorKind::Bin => bin.is_none(),
|
InstallErrorKind::Bin => bin.is_none(),
|
||||||
#[cfg(windows)]
|
|
||||||
InstallErrorKind::Registry => registry.is_none(),
|
InstallErrorKind::Registry => registry.is_none(),
|
||||||
InstallErrorKind::DownloadUnpack => false,
|
InstallErrorKind::DownloadUnpack => false,
|
||||||
});
|
});
|
||||||
|
|
@ -676,42 +675,47 @@ pub(crate) async fn install(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_unstable_by(|(_, key_a, _), (_, key_b, _)| key_a.cmp(key_b))
|
.sorted_unstable_by(|(_, key_a, _), (_, key_b, _)| key_a.cmp(key_b))
|
||||||
{
|
{
|
||||||
let (level, verb) = match kind {
|
match kind {
|
||||||
InstallErrorKind::DownloadUnpack => ("error".red().bold().to_string(), "install"),
|
InstallErrorKind::DownloadUnpack => {
|
||||||
|
write_error_chain(
|
||||||
|
err.context(format!("Failed to install {key}")).as_ref(),
|
||||||
|
printer.stderr(),
|
||||||
|
"error",
|
||||||
|
AnsiColors::Red,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
InstallErrorKind::Bin => {
|
InstallErrorKind::Bin => {
|
||||||
let level = match bin {
|
let (level, color) = match bin {
|
||||||
None => "warning".yellow().bold().to_string(),
|
None => ("warning", AnsiColors::Yellow),
|
||||||
Some(false) => continue,
|
Some(false) => continue,
|
||||||
Some(true) => "error".red().bold().to_string(),
|
Some(true) => ("error", AnsiColors::Red),
|
||||||
};
|
|
||||||
(level, "install executable for")
|
|
||||||
}
|
|
||||||
#[cfg(windows)]
|
|
||||||
InstallErrorKind::Registry => {
|
|
||||||
let level = match registry {
|
|
||||||
None => "warning".yellow().bold().to_string(),
|
|
||||||
Some(false) => continue,
|
|
||||||
Some(true) => "error".red().bold().to_string(),
|
|
||||||
};
|
|
||||||
(level, "install registry entry for")
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
writeln!(
|
write_error_chain(
|
||||||
|
err.context(format!("Failed to install executable for {key}"))
|
||||||
|
.as_ref(),
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"{level}{} Failed to {verb} {}",
|
level,
|
||||||
":".bold(),
|
color,
|
||||||
key.green()
|
|
||||||
)?;
|
)?;
|
||||||
for err in err.chain() {
|
}
|
||||||
writeln!(
|
InstallErrorKind::Registry => {
|
||||||
|
let (level, color) = match registry {
|
||||||
|
None => ("warning", AnsiColors::Yellow),
|
||||||
|
Some(false) => continue,
|
||||||
|
Some(true) => ("error", AnsiColors::Red),
|
||||||
|
};
|
||||||
|
|
||||||
|
write_error_chain(
|
||||||
|
err.context(format!("Failed to create registry entry for {key}"))
|
||||||
|
.as_ref(),
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
" {}: {}",
|
level,
|
||||||
"Caused by".red().bold(),
|
color,
|
||||||
err.to_string().trim()
|
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if fatal {
|
if fatal {
|
||||||
return Ok(ExitStatus::Failure);
|
return Ok(ExitStatus::Failure);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::{AnsiColors, OwoColorize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
@ -18,6 +18,7 @@ use uv_python::{
|
||||||
use uv_requirements::RequirementsSpecification;
|
use uv_requirements::RequirementsSpecification;
|
||||||
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
|
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
|
||||||
use uv_tool::InstalledTools;
|
use uv_tool::InstalledTools;
|
||||||
|
use uv_warnings::write_error_chain;
|
||||||
use uv_workspace::WorkspaceCache;
|
use uv_workspace::WorkspaceCache;
|
||||||
|
|
||||||
use crate::commands::pip::loggers::{
|
use crate::commands::pip::loggers::{
|
||||||
|
|
@ -155,20 +156,13 @@ pub(crate) async fn upgrade(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b))
|
.sorted_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b))
|
||||||
{
|
{
|
||||||
writeln!(
|
write_error_chain(
|
||||||
|
err.context(format!("Failed to upgrade {}", name.green()))
|
||||||
|
.as_ref(),
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"{}: Failed to upgrade {}",
|
"error",
|
||||||
"error".red().bold(),
|
AnsiColors::Red,
|
||||||
name.green()
|
|
||||||
)?;
|
)?;
|
||||||
for err in err.chain() {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
" {}: {}",
|
|
||||||
"Caused by".red().bold(),
|
|
||||||
err.to_string().trim()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Ok(ExitStatus::Failure);
|
return Ok(ExitStatus::Failure);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ fn no_credentials() {
|
||||||
// Emulate CI
|
// Emulate CI
|
||||||
.env(EnvVars::GITHUB_ACTIONS, "true")
|
.env(EnvVars::GITHUB_ACTIONS, "true")
|
||||||
// Just to make sure
|
// Just to make sure
|
||||||
.env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r###"
|
.env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -134,12 +134,13 @@ fn no_credentials() {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Publishing 1 file to https://test.pypi.org/legacy/
|
Publishing 1 file to https://test.pypi.org/legacy/
|
||||||
Note: Neither credentials nor keyring are configured, and there was an error fetching the trusted publishing token. If you don't want to use trusted publishing, you can ignore this error, but you need to provide credentials.
|
Note: Neither credentials nor keyring are configured, and there was an error fetching the trusted publishing token. If you don't want to use trusted publishing, you can ignore this error, but you need to provide credentials.
|
||||||
Trusted publishing error: Environment variable ACTIONS_ID_TOKEN_REQUEST_TOKEN not set, is the `id-token: write` permission missing?
|
error: Trusted publishing failed
|
||||||
|
Caused by: Environment variable ACTIONS_ID_TOKEN_REQUEST_TOKEN not set, is the `id-token: write` permission missing?
|
||||||
Uploading ok-1.0.0-py3-none-any.whl ([SIZE])
|
Uploading ok-1.0.0-py3-none-any.whl ([SIZE])
|
||||||
error: Failed to publish `../../scripts/links/ok-1.0.0-py3-none-any.whl` to https://test.pypi.org/legacy/
|
error: Failed to publish `../../scripts/links/ok-1.0.0-py3-none-any.whl` to https://test.pypi.org/legacy/
|
||||||
Caused by: Failed to send POST request
|
Caused by: Failed to send POST request
|
||||||
Caused by: Missing credentials for https://test.pypi.org/legacy/
|
Caused by: Missing credentials for https://test.pypi.org/legacy/
|
||||||
"###
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue