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:
konsti 2025-07-28 18:23:39 +02:00 committed by GitHub
parent 90885fd0d9
commit ac135278c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 99 additions and 64 deletions

View File

@ -1,3 +1,5 @@
use std::error::Error;
use std::iter;
use std::sync::atomic::AtomicBool;
use std::sync::{LazyLock, Mutex};
@ -6,6 +8,7 @@ use std::sync::{LazyLock, Mutex};
pub use anstream;
#[doc(hidden)]
pub use owo_colors;
use owo_colors::{DynColor, OwoColorize};
use rustc_hash::FxHashSet;
/// 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(())
}

View File

@ -1,11 +1,10 @@
use std::fmt::Write;
use std::iter;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result, bail};
use console::Term;
use owo_colors::OwoColorize;
use owo_colors::{AnsiColors, OwoColorize};
use tokio::sync::Semaphore;
use tracing::{debug, info};
use uv_auth::Credentials;
@ -17,7 +16,7 @@ use uv_publish::{
CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload,
};
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::{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 \
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(),
"{}: {err}",
"Trusted publishing error".red().bold()
"error",
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()
)?;
}
}
}

View File

@ -10,7 +10,7 @@ use futures::StreamExt;
use futures::stream::FuturesUnordered;
use indexmap::IndexSet;
use itertools::{Either, Itertools};
use owo_colors::OwoColorize;
use owo_colors::{AnsiColors, OwoColorize};
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{debug, trace};
@ -30,7 +30,7 @@ use uv_python::{
};
use uv_shell::Shell;
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::reporters::PythonDownloadReporter;
@ -139,7 +139,7 @@ impl Changelog {
enum InstallErrorKind {
DownloadUnpack,
Bin,
#[cfg(windows)]
#[cfg_attr(not(windows), allow(dead_code))]
Registry,
}
@ -667,7 +667,6 @@ pub(crate) async fn install(
// to warn
let fatal = !errors.iter().all(|(kind, _, _)| match kind {
InstallErrorKind::Bin => bin.is_none(),
#[cfg(windows)]
InstallErrorKind::Registry => registry.is_none(),
InstallErrorKind::DownloadUnpack => false,
});
@ -676,40 +675,45 @@ pub(crate) async fn install(
.into_iter()
.sorted_unstable_by(|(_, key_a, _), (_, key_b, _)| key_a.cmp(key_b))
{
let (level, verb) = match kind {
InstallErrorKind::DownloadUnpack => ("error".red().bold().to_string(), "install"),
match kind {
InstallErrorKind::DownloadUnpack => {
write_error_chain(
err.context(format!("Failed to install {key}")).as_ref(),
printer.stderr(),
"error",
AnsiColors::Red,
)?;
}
InstallErrorKind::Bin => {
let level = match bin {
None => "warning".yellow().bold().to_string(),
let (level, color) = match bin {
None => ("warning", AnsiColors::Yellow),
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!(
printer.stderr(),
"{level}{} Failed to {verb} {}",
":".bold(),
key.green()
)?;
for err in err.chain() {
writeln!(
printer.stderr(),
" {}: {}",
"Caused by".red().bold(),
err.to_string().trim()
)?;
write_error_chain(
err.context(format!("Failed to install executable for {key}"))
.as_ref(),
printer.stderr(),
level,
color,
)?;
}
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(),
level,
color,
)?;
}
}
}

View File

@ -1,6 +1,6 @@
use anyhow::Result;
use itertools::Itertools;
use owo_colors::OwoColorize;
use owo_colors::{AnsiColors, OwoColorize};
use std::collections::BTreeMap;
use std::fmt::Write;
use tracing::debug;
@ -18,6 +18,7 @@ use uv_python::{
use uv_requirements::RequirementsSpecification;
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools;
use uv_warnings::write_error_chain;
use uv_workspace::WorkspaceCache;
use crate::commands::pip::loggers::{
@ -155,20 +156,13 @@ pub(crate) async fn upgrade(
.into_iter()
.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(),
"{}: Failed to upgrade {}",
"error".red().bold(),
name.green()
"error",
AnsiColors::Red,
)?;
for err in err.chain() {
writeln!(
printer.stderr(),
" {}: {}",
"Caused by".red().bold(),
err.to_string().trim()
)?;
}
}
return Ok(ExitStatus::Failure);
}

View File

@ -126,7 +126,7 @@ fn no_credentials() {
// Emulate CI
.env(EnvVars::GITHUB_ACTIONS, "true")
// Just to make sure
.env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r###"
.env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r"
success: false
exit_code: 2
----- stdout -----
@ -134,12 +134,13 @@ fn no_credentials() {
----- stderr -----
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.
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])
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: Missing credentials for https://test.pypi.org/legacy/
"###
"
);
}