mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Add first-party line wrapping (#17535)
Required for https://github.com/astral-sh/uv/pull/17110 to avoid regressions where miette previously wrapped lines in error messages.
This commit is contained in:
Generated
+11
-3
@@ -6250,6 +6250,7 @@ dependencies = [
|
||||
"uv-configuration",
|
||||
"uv-distribution-filename",
|
||||
"uv-distribution-types",
|
||||
"uv-errors",
|
||||
"uv-extract",
|
||||
"uv-git",
|
||||
"uv-installer",
|
||||
@@ -6426,7 +6427,15 @@ dependencies = [
|
||||
name = "uv-errors"
|
||||
version = "0.0.49"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anyhow",
|
||||
"indoc",
|
||||
"insta",
|
||||
"owo-colors",
|
||||
"terminal_size",
|
||||
"textwrap",
|
||||
"thiserror",
|
||||
"uv-static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6848,6 +6857,7 @@ dependencies = [
|
||||
"glob",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
"owo-colors",
|
||||
"reqwest",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
@@ -6863,6 +6873,7 @@ dependencies = [
|
||||
"uv-configuration",
|
||||
"uv-distribution-filename",
|
||||
"uv-distribution-types",
|
||||
"uv-errors",
|
||||
"uv-extract",
|
||||
"uv-fs",
|
||||
"uv-metadata",
|
||||
@@ -7386,9 +7397,6 @@ name = "uv-warnings"
|
||||
version = "0.0.49"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anyhow",
|
||||
"indoc",
|
||||
"insta",
|
||||
"owo-colors",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
@@ -260,6 +260,7 @@ spdx = { version = "0.13.0" }
|
||||
syn = { version = "2.0.77" }
|
||||
target-lexicon = { version = "0.13.0" }
|
||||
tempfile = { version = "3.14.0" }
|
||||
terminal_size = { version = "0.4.2" }
|
||||
textwrap = { version = "0.16.1" }
|
||||
thiserror = { version = "2.0.0" }
|
||||
astral-tl = { version = "0.7.11" }
|
||||
|
||||
@@ -21,6 +21,7 @@ uv-client = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-distribution-filename = { workspace = true }
|
||||
uv-distribution-types = { workspace = true }
|
||||
uv-errors = { workspace = true }
|
||||
uv-extract = { workspace = true }
|
||||
uv-git = { workspace = true }
|
||||
uv-installer = { workspace = true }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::str::FromStr;
|
||||
use std::time::Instant;
|
||||
|
||||
use anstream::eprintln;
|
||||
use owo_colors::OwoColorize;
|
||||
use owo_colors::AnsiColors;
|
||||
use tracing::{debug, trace};
|
||||
use tracing_durations_export::DurationsLayerBuilder;
|
||||
use tracing_durations_export::plot::PlotConfig;
|
||||
@@ -17,6 +17,15 @@ use tracing_subscriber::{EnvFilter, Layer};
|
||||
use uv_dev::run;
|
||||
use uv_static::EnvVars;
|
||||
|
||||
struct Stderr;
|
||||
|
||||
impl fmt::Write for Stderr {
|
||||
fn write_str(&mut self, output: &str) -> fmt::Result {
|
||||
anstream::eprint!("{output}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> ExitCode {
|
||||
let (duration_layer, _guard) = if let Ok(location) = env::var(EnvVars::TRACING_DURATIONS_FILE) {
|
||||
@@ -69,10 +78,16 @@ async fn main() -> ExitCode {
|
||||
debug!("Took {}ms", start.elapsed().as_millis());
|
||||
if let Err(err) = result {
|
||||
trace!("Error trace: {err:?}");
|
||||
eprintln!("{}", "uv-dev failed".red().bold());
|
||||
for err in err.chain() {
|
||||
eprintln!(" {}: {}", "Caused by".red().bold(), err.to_string().trim());
|
||||
}
|
||||
let err = err.context("uv-dev failed");
|
||||
uv_errors::write_error_chain(
|
||||
err.as_ref(),
|
||||
Stderr,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
uv_errors::Hints::none(),
|
||||
None,
|
||||
)
|
||||
.expect("writing to stderr should not fail");
|
||||
ExitCode::FAILURE
|
||||
} else {
|
||||
ExitCode::SUCCESS
|
||||
|
||||
@@ -11,6 +11,16 @@ license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
owo-colors = { workspace = true }
|
||||
terminal_size = { workspace = true }
|
||||
textwrap = { workspace = true }
|
||||
uv-static = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anstream = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
indoc = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+357
-4
@@ -1,7 +1,13 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
mod line_wrap;
|
||||
|
||||
use owo_colors::OwoColorize;
|
||||
use std::borrow::Cow;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::iter;
|
||||
|
||||
use owo_colors::{DynColor, OwoColorize};
|
||||
|
||||
use line_wrap::{get_wrap_width, wrap_text};
|
||||
|
||||
/// An error that may carry user-facing hints.
|
||||
///
|
||||
@@ -129,9 +135,75 @@ impl fmt::Display for HintPrefix {
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an error or warning chain with hints and a custom level and color.
|
||||
///
|
||||
/// Each hint is rendered on its own line, prefixed with the styled `hint:` label.
|
||||
///
|
||||
/// `width_override` allows callers to override the terminal width for wrapping
|
||||
/// (primarily for testing). Pass `None` for automatic detection.
|
||||
pub fn write_error_chain(
|
||||
err: &dyn Error,
|
||||
mut stream: impl fmt::Write,
|
||||
level: impl AsRef<str>,
|
||||
color: impl DynColor + Copy,
|
||||
hints: Hints<'_>,
|
||||
width_override: Option<usize>,
|
||||
) -> fmt::Result {
|
||||
let width = get_wrap_width(width_override);
|
||||
|
||||
let main_msg = err.to_string();
|
||||
let main_padding = " ".repeat(level.as_ref().len() + 2);
|
||||
let wrapped_main = wrap_text(&main_msg, width, &main_padding, &main_padding);
|
||||
writeln!(
|
||||
&mut stream,
|
||||
"{}{} {}",
|
||||
level.as_ref().color(color).bold(),
|
||||
":".bold(),
|
||||
wrapped_main.trim()
|
||||
)?;
|
||||
|
||||
for source in iter::successors(err.source(), |&err| err.source()) {
|
||||
let msg = source.to_string();
|
||||
let padding = " ";
|
||||
let cause = "Caused by";
|
||||
let child_padding = " ".repeat(padding.len() + cause.len() + 2);
|
||||
|
||||
let wrapped = wrap_text(&msg, width, "", &child_padding);
|
||||
|
||||
let mut lines = wrapped.lines();
|
||||
if let Some(first) = lines.next() {
|
||||
writeln!(
|
||||
&mut stream,
|
||||
"{}{}: {}",
|
||||
padding,
|
||||
cause.color(color).bold(),
|
||||
first.trim()
|
||||
)?;
|
||||
for line in lines {
|
||||
if line.trim().is_empty() {
|
||||
writeln!(&mut stream)?;
|
||||
} else {
|
||||
writeln!(&mut stream, "{line}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for hint in hints {
|
||||
writeln!(&mut stream, "\n{HintPrefix} {hint}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ErrorWithHints, HintPrefix, Hints};
|
||||
use anyhow::anyhow;
|
||||
use indoc::indoc;
|
||||
use insta::assert_snapshot;
|
||||
use owo_colors::AnsiColors;
|
||||
|
||||
use super::{ErrorWithHints, HintPrefix, Hints, write_error_chain};
|
||||
|
||||
#[test]
|
||||
fn extend_deduplicates_matching_hints() {
|
||||
@@ -157,4 +229,285 @@ mod tests {
|
||||
"error"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_wrapping_with_columns() {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error(
|
||||
"Because fiasobfhuasbf was not found in the package registry and you require fiasobfhuasbf, we can conclude that your requirements are unsatisfiable."
|
||||
)]
|
||||
struct Inner;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("No solution found when resolving dependencies")]
|
||||
struct Outer {
|
||||
#[source]
|
||||
source: Inner,
|
||||
}
|
||||
|
||||
let error = Outer { source: Inner };
|
||||
let mut output = String::new();
|
||||
write_error_chain(
|
||||
&error,
|
||||
&mut output,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
Some(80),
|
||||
)
|
||||
.unwrap();
|
||||
let output = anstream::adapter::strip_str(&output);
|
||||
|
||||
assert_snapshot!(output, @r"
|
||||
error: No solution found when resolving dependencies
|
||||
Caused by: Because fiasobfhuasbf was not found in the package registry and you require
|
||||
fiasobfhuasbf, we can conclude that your requirements are
|
||||
unsatisfiable.
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_chain_with_cause() {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Permission denied")]
|
||||
struct Inner;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Failed to write file")]
|
||||
struct Outer {
|
||||
#[source]
|
||||
source: Inner,
|
||||
}
|
||||
|
||||
let error = Outer { source: Inner };
|
||||
let mut output = String::new();
|
||||
write_error_chain(
|
||||
&error,
|
||||
&mut output,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let output = anstream::adapter::strip_str(&output);
|
||||
|
||||
assert_snapshot!(output, @r"
|
||||
error: Failed to write file
|
||||
Caused by: Permission denied
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_hyphenation() {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error(
|
||||
"Failed to download package from https://files.pythonhosted.org/packages/verylongpackagename"
|
||||
)]
|
||||
struct LongWord;
|
||||
|
||||
let error = LongWord;
|
||||
let mut output = String::new();
|
||||
write_error_chain(
|
||||
&error,
|
||||
&mut output,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
Some(50),
|
||||
)
|
||||
.unwrap();
|
||||
let output = anstream::adapter::strip_str(&output);
|
||||
assert_snapshot!(output, @r"
|
||||
error: Failed to download package from
|
||||
https://files.pythonhosted.org/packages/verylongpackagename
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_long_words_not_broken() {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error(
|
||||
"The package supercalifragilisticexpialidocious-extraordinarily-long-name was not found"
|
||||
)]
|
||||
struct VeryLongWord;
|
||||
|
||||
let error = VeryLongWord;
|
||||
let mut output = String::new();
|
||||
write_error_chain(
|
||||
&error,
|
||||
&mut output,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
Some(40),
|
||||
)
|
||||
.unwrap();
|
||||
let output = anstream::adapter::strip_str(&output);
|
||||
assert_snapshot!(output, @r"
|
||||
error: The package
|
||||
supercalifragilisticexpialidocious-extraordinarily-long-name
|
||||
was not found
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_error_sources() {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Network connection timeout after multiple retry attempts")]
|
||||
struct DeepError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Failed to fetch package metadata from registry")]
|
||||
struct MiddleError {
|
||||
#[source]
|
||||
source: DeepError,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Unable to resolve package dependencies")]
|
||||
struct TopError {
|
||||
#[source]
|
||||
source: MiddleError,
|
||||
}
|
||||
|
||||
let error = TopError {
|
||||
source: MiddleError { source: DeepError },
|
||||
};
|
||||
let mut output = String::new();
|
||||
write_error_chain(
|
||||
&error,
|
||||
&mut output,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
Some(60),
|
||||
)
|
||||
.unwrap();
|
||||
let output = anstream::adapter::strip_str(&output);
|
||||
assert_snapshot!(output, @r"
|
||||
error: Unable to resolve package dependencies
|
||||
Caused by: Failed to fetch package metadata from registry
|
||||
Caused by: Network connection timeout after multiple retry attempts
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline_main_message_wraps_each_line() {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error(
|
||||
"There is no command `foobar` for `uv`. Did you mean one of:\n auth\n run\n init"
|
||||
)]
|
||||
struct Suggestions;
|
||||
|
||||
let error = Suggestions;
|
||||
let mut output = String::new();
|
||||
write_error_chain(
|
||||
&error,
|
||||
&mut output,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
Some(50),
|
||||
)
|
||||
.unwrap();
|
||||
let output = anstream::adapter::strip_str(&output);
|
||||
|
||||
assert_snapshot!(output, @r"
|
||||
error: There is no command `foobar` for `uv`. Did
|
||||
you mean one of:
|
||||
auth
|
||||
run
|
||||
init
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_only_on_ascii_space() {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Path /usr/local/lib/python3.12/site-packages not found in filesystem hierarchy")]
|
||||
struct SpecialChars;
|
||||
|
||||
let error = SpecialChars;
|
||||
let mut output = String::new();
|
||||
write_error_chain(
|
||||
&error,
|
||||
&mut output,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
Some(50),
|
||||
)
|
||||
.unwrap();
|
||||
let output = anstream::adapter::strip_str(&output);
|
||||
assert_snapshot!(output, @r"
|
||||
error: Path /usr/local/lib/python3.12/site-packages
|
||||
not found in filesystem hierarchy
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_with_hints() {
|
||||
let err = anyhow!("Permission denied").context("Failed to fetch package");
|
||||
|
||||
let hints = [
|
||||
"Try running with `--verbose` for more information.".to_string(),
|
||||
"Try running without --offline.".to_string(),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let mut rendered = String::new();
|
||||
write_error_chain(
|
||||
err.as_ref(),
|
||||
&mut rendered,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
hints,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let rendered = anstream::adapter::strip_str(&rendered);
|
||||
|
||||
assert_snapshot!(rendered, @r"
|
||||
error: Failed to fetch package
|
||||
Caused by: Permission denied
|
||||
|
||||
hint: Try running with `--verbose` for more information.
|
||||
|
||||
hint: Try running without --offline.
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_multiline_message() {
|
||||
let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
|
||||
Server says: This endpoint only support POST requests.
|
||||
|
||||
For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
|
||||
let err = anyhow!("Caused By: HTTP Error 400")
|
||||
.context(err_middle)
|
||||
.context("Failed to download Python 3.12");
|
||||
|
||||
let mut rendered = String::new();
|
||||
write_error_chain(
|
||||
err.as_ref(),
|
||||
&mut rendered,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let rendered = anstream::adapter::strip_str(&rendered);
|
||||
|
||||
assert_snapshot!(rendered, @r"
|
||||
error: Failed to download Python 3.12
|
||||
Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
|
||||
Server says: This endpoint only support POST requests.
|
||||
|
||||
For downloads, please refer to https://example.com/download/python3.13.tar.zst
|
||||
Caused by: Caused By: HTTP Error 400
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use uv_static::EnvVars;
|
||||
|
||||
/// Checks if line wrapping should be enabled.
|
||||
///
|
||||
/// Returns `false` if `UV_NO_WRAP` is set.
|
||||
fn should_wrap_lines() -> bool {
|
||||
std::env::var_os(EnvVars::UV_NO_WRAP).is_none()
|
||||
}
|
||||
|
||||
/// Gets the terminal width for wrapping.
|
||||
///
|
||||
/// Uses `width_override`, then the `COLUMNS` environment variable, and finally attempts to detect
|
||||
/// the width from the terminal. Returns `None` if no width is available.
|
||||
pub(crate) fn get_wrap_width(width_override: Option<usize>) -> Option<usize> {
|
||||
if !should_wrap_lines() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(width) = width_override {
|
||||
return Some(width);
|
||||
}
|
||||
|
||||
if let Ok(cols) = std::env::var(EnvVars::COLUMNS) {
|
||||
if let Ok(width) = cols.parse::<usize>() {
|
||||
return Some(width);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((terminal_size::Width(width), _)) = terminal_size::terminal_size() {
|
||||
return Some(width as usize);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Wraps text at word boundaries with proper indentation.
|
||||
///
|
||||
/// Based on miette's `wrap()` implementation from:
|
||||
/// <https://github.com/zkat/miette/blob/v7.2.0/src/handlers/graphical.rs#L876-L909>
|
||||
pub(crate) fn wrap_text(
|
||||
text: &str,
|
||||
width: Option<usize>,
|
||||
initial_indent: &str,
|
||||
subsequent_indent: &str,
|
||||
) -> String {
|
||||
let Some(width) = width else {
|
||||
return format!("{initial_indent}{text}");
|
||||
};
|
||||
|
||||
let options = textwrap::Options::new(width)
|
||||
.initial_indent(initial_indent)
|
||||
.subsequent_indent(subsequent_indent)
|
||||
.break_words(false)
|
||||
.word_separator(textwrap::WordSeparator::AsciiSpace)
|
||||
.word_splitter(textwrap::WordSplitter::NoHyphenation);
|
||||
|
||||
let mut wrapped = String::with_capacity(text.len());
|
||||
|
||||
for (index, line) in text.split_terminator('\n').enumerate() {
|
||||
if index > 0 {
|
||||
wrapped.push('\n');
|
||||
}
|
||||
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preserve authored line breaks and wrap each line independently. Hanging indentation is
|
||||
// only for continuation lines created by wrapping, not for lines already in the message.
|
||||
wrapped.push_str(&textwrap::fill(
|
||||
line,
|
||||
if index == 0 {
|
||||
options.clone()
|
||||
} else {
|
||||
options.clone().initial_indent("")
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
wrapped
|
||||
}
|
||||
@@ -16,6 +16,7 @@ doctest = false
|
||||
uv-auth = { workspace = true }
|
||||
uv-cache = { workspace = true }
|
||||
uv-client = { workspace = true }
|
||||
uv-errors = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-distribution-filename = { workspace = true }
|
||||
uv-distribution-types = { workspace = true }
|
||||
@@ -52,6 +53,7 @@ anstream = { workspace = true }
|
||||
fastrand = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
wiremock = { workspace = true }
|
||||
owo-colors = { workspace = true }
|
||||
|
||||
[features]
|
||||
# Test only feature to enable non-HTTPS URL handling
|
||||
|
||||
@@ -1518,9 +1518,9 @@ mod tests {
|
||||
FormMetadata, PublishError, Reporter, UploadDistribution, build_upload_request,
|
||||
group_files, upload,
|
||||
};
|
||||
use owo_colors::AnsiColors;
|
||||
use tokio::sync::Semaphore;
|
||||
use uv_warnings::owo_colors::AnsiColors;
|
||||
use uv_warnings::write_error_chain;
|
||||
use uv_errors::{Hints, write_error_chain};
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
@@ -2190,7 +2190,15 @@ mod tests {
|
||||
let err = mock_server_upload(&mock_server).await.unwrap_err();
|
||||
|
||||
let mut capture = String::new();
|
||||
write_error_chain(&err, &mut capture, "error", AnsiColors::Red).unwrap();
|
||||
write_error_chain(
|
||||
&err,
|
||||
&mut capture,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let capture = capture.replace(&mock_server.uri(), "[SERVER]");
|
||||
let capture = anstream::adapter::strip_str(&capture);
|
||||
@@ -2218,7 +2226,15 @@ mod tests {
|
||||
let err = mock_server_upload(&mock_server).await.unwrap_err();
|
||||
|
||||
let mut capture = String::new();
|
||||
write_error_chain(&err, &mut capture, "error", AnsiColors::Red).unwrap();
|
||||
write_error_chain(
|
||||
&err,
|
||||
&mut capture,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let capture = capture.replace(&mock_server.uri(), "[SERVER]");
|
||||
let capture = anstream::adapter::strip_str(&capture);
|
||||
@@ -2251,7 +2267,15 @@ mod tests {
|
||||
let err = mock_server_upload(&mock_server).await.unwrap_err();
|
||||
|
||||
let mut capture = String::new();
|
||||
write_error_chain(&err, &mut capture, "error", AnsiColors::Red).unwrap();
|
||||
write_error_chain(
|
||||
&err,
|
||||
&mut capture,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let capture = capture.replace(&mock_server.uri(), "[SERVER]");
|
||||
let capture = anstream::adapter::strip_str(&capture);
|
||||
@@ -2287,7 +2311,15 @@ mod tests {
|
||||
let err = mock_server_upload(&mock_server).await.unwrap_err();
|
||||
|
||||
let mut capture = String::new();
|
||||
write_error_chain(&err, &mut capture, "error", AnsiColors::Red).unwrap();
|
||||
write_error_chain(
|
||||
&err,
|
||||
&mut capture,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let capture = capture.replace(&mock_server.uri(), "[SERVER]");
|
||||
let capture = anstream::adapter::strip_str(&capture);
|
||||
|
||||
@@ -1519,11 +1519,13 @@ pub(crate) async fn find_best_python_installation(
|
||||
let error = anyhow::Error::from(error).context(format!(
|
||||
"A managed Python download is available for {request}, but an error occurred when attempting to download it."
|
||||
));
|
||||
uv_warnings::write_error_chain(
|
||||
uv_errors::write_error_chain(
|
||||
error.as_ref(),
|
||||
&mut error_chain,
|
||||
"warning",
|
||||
AnsiColors::Yellow,
|
||||
uv_errors::Hints::none(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
anstream::eprint!("{}", error_chain);
|
||||
|
||||
@@ -19,8 +19,3 @@ workspace = true
|
||||
anstream = { workspace = true }
|
||||
owo-colors = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
indoc = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::error::Error;
|
||||
use std::iter;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
@@ -8,7 +6,6 @@ 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.
|
||||
@@ -59,100 +56,3 @@ 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
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// error: Failed to download Python 3.12
|
||||
/// Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
|
||||
/// Server says: This endpoint only support POST requests.
|
||||
///
|
||||
/// For downloads, please refer to https://example.com/download/python3.13.tar.zst
|
||||
/// Caused by: Caused By: HTTP Error 400
|
||||
/// ```
|
||||
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().bold()
|
||||
)?;
|
||||
for source in iter::successors(err.source(), |&err| err.source()) {
|
||||
let msg = source.to_string();
|
||||
let mut lines = msg.lines();
|
||||
if let Some(first) = lines.next() {
|
||||
let padding = " ";
|
||||
let cause = "Caused by";
|
||||
let child_padding = " ".repeat(padding.len() + cause.len() + 2);
|
||||
writeln!(
|
||||
&mut stream,
|
||||
"{}{}: {}",
|
||||
padding,
|
||||
cause.color(color).bold(),
|
||||
first.trim()
|
||||
)?;
|
||||
for line in lines {
|
||||
let line = line.trim_end();
|
||||
if line.is_empty() {
|
||||
// Avoid showing indents on empty lines
|
||||
writeln!(&mut stream)?;
|
||||
} else {
|
||||
writeln!(&mut stream, "{}{}", child_padding, line.trim_end())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::write_error_chain;
|
||||
use anyhow::anyhow;
|
||||
use indoc::indoc;
|
||||
use insta::assert_snapshot;
|
||||
use owo_colors::AnsiColors;
|
||||
|
||||
#[test]
|
||||
fn format_multiline_message() {
|
||||
let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
|
||||
Server says: This endpoint only support POST requests.
|
||||
|
||||
For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
|
||||
let err = anyhow!("Caused By: HTTP Error 400")
|
||||
.context(err_middle)
|
||||
.context("Failed to download Python 3.12");
|
||||
|
||||
let mut rendered = String::new();
|
||||
write_error_chain(err.as_ref(), &mut rendered, "error", AnsiColors::Red).unwrap();
|
||||
let rendered = anstream::adapter::strip_str(&rendered);
|
||||
|
||||
assert_snapshot!(rendered, @"
|
||||
error: Failed to download Python 3.12
|
||||
Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
|
||||
Server says: This endpoint only support POST requests.
|
||||
|
||||
For downloads, please refer to https://example.com/download/python3.13.tar.zst
|
||||
Caused by: Caused By: HTTP Error 400
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use uv_client::{
|
||||
};
|
||||
use uv_configuration::{KeyringProviderType, TrustedPublishing};
|
||||
use uv_distribution_types::{IndexCapabilities, IndexLocations, IndexUrl};
|
||||
use uv_errors::{Hints, write_error_chain};
|
||||
use uv_preview::{Preview, PreviewFeature};
|
||||
use uv_publish::{
|
||||
CheckUrlClient, FormMetadata, PublishError, TrustedPublishResult, check_trusted_publishing,
|
||||
@@ -20,7 +21,7 @@ use uv_publish::{
|
||||
};
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
use uv_settings::EnvironmentOptions;
|
||||
use uv_warnings::{warn_user_once, write_error_chain};
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::commands::reporters::PublishReporter;
|
||||
use crate::commands::{ExitStatus, human_readable_bytes};
|
||||
@@ -240,7 +241,14 @@ pub(crate) async fn publish(
|
||||
Ok(false) => {}
|
||||
Err(err) => {
|
||||
if dry_run {
|
||||
write_error_chain(&err, printer.stderr(), "error", AnsiColors::Red)?;
|
||||
write_error_chain(
|
||||
&err,
|
||||
printer.stderr(),
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)?;
|
||||
error_count += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -278,7 +286,14 @@ pub(crate) async fn publish(
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => {
|
||||
if dry_run {
|
||||
write_error_chain(&err, printer.stderr(), "error", AnsiColors::Red)?;
|
||||
write_error_chain(
|
||||
&err,
|
||||
printer.stderr(),
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)?;
|
||||
error_count += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -324,6 +339,8 @@ pub(crate) async fn publish(
|
||||
printer.stderr(),
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)?;
|
||||
error_count += 1;
|
||||
}
|
||||
@@ -387,6 +404,8 @@ pub(crate) async fn publish(
|
||||
printer.stderr(),
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)?;
|
||||
error_count += 1;
|
||||
continue;
|
||||
@@ -551,6 +570,8 @@ async fn gather_credentials(
|
||||
printer.stderr(),
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use tracing::{debug, trace, warn};
|
||||
use uv_cache::Cache;
|
||||
use uv_client::BaseClientBuilder;
|
||||
use uv_configuration::Concurrency;
|
||||
use uv_errors::{Hints, write_error_chain};
|
||||
use uv_fs::Simplified;
|
||||
use uv_platform::{Arch, Libc};
|
||||
use uv_preview::{Preview, PreviewFeature};
|
||||
@@ -35,7 +36,7 @@ use uv_python::{
|
||||
};
|
||||
use uv_shell::Shell;
|
||||
use uv_trampoline_builder::{Launcher, LauncherKind};
|
||||
use uv_warnings::{warn_user, write_error_chain};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::python::{ChangeEvent, ChangeEventKind};
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
@@ -919,6 +920,8 @@ async fn perform_install(
|
||||
printer.stderr(),
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
InstallErrorKind::Bin => {
|
||||
@@ -934,6 +937,8 @@ async fn perform_install(
|
||||
printer.stderr(),
|
||||
level,
|
||||
color,
|
||||
Hints::none(),
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
InstallErrorKind::Registry => {
|
||||
@@ -950,6 +955,8 @@ async fn perform_install(
|
||||
printer.stderr(),
|
||||
level,
|
||||
color,
|
||||
Hints::none(),
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use uv_cache::Cache;
|
||||
use uv_client::BaseClientBuilder;
|
||||
use uv_configuration::{Concurrency, Constraints, DryRun, TargetTriple};
|
||||
use uv_distribution_types::{ExtraBuildRequires, Requirement, RequirementSource};
|
||||
use uv_errors::{Hints, write_error_chain};
|
||||
use uv_fs::CWD;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{Operator, Version};
|
||||
@@ -22,7 +23,6 @@ use uv_requirements::RequirementsSpecification;
|
||||
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
|
||||
use uv_tool::{InstalledTools, Tool};
|
||||
use uv_types::SourceTreeEditablePolicy;
|
||||
use uv_warnings::write_error_chain;
|
||||
use uv_workspace::WorkspaceCache;
|
||||
|
||||
use crate::commands::pip::loggers::{
|
||||
@@ -177,6 +177,8 @@ pub(crate) async fn upgrade(
|
||||
printer.stderr(),
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
Hints::none(),
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
return Ok(ExitStatus::Failure);
|
||||
|
||||
+12
-16
@@ -11,12 +11,11 @@ use std::process::ExitCode;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use anstream::eprintln;
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use clap::error::{ContextKind, ContextValue};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use futures::FutureExt;
|
||||
use owo_colors::OwoColorize;
|
||||
use owo_colors::{AnsiColors, OwoColorize};
|
||||
use settings::PipTreeSettings;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, instrument, trace};
|
||||
@@ -2839,21 +2838,18 @@ where
|
||||
Err(err) => {
|
||||
trace!("Error trace: {err:?}");
|
||||
|
||||
// Collect hints before rendering the error chain.
|
||||
let hints = commands::diagnostics::hints_for_error(&err);
|
||||
|
||||
let mut causes = err.chain();
|
||||
eprintln!(
|
||||
"{}: {}",
|
||||
"error".red().bold(),
|
||||
causes.next().unwrap().to_string().trim()
|
||||
);
|
||||
for err in causes {
|
||||
eprintln!(" {}: {}", "Caused by".red().bold(), err.to_string().trim());
|
||||
}
|
||||
|
||||
// Render hints after the error chain.
|
||||
anstream::eprint!("{hints}");
|
||||
let mut error_chain = String::new();
|
||||
uv_errors::write_error_chain(
|
||||
err.as_ref(),
|
||||
&mut error_chain,
|
||||
"error",
|
||||
AnsiColors::Red,
|
||||
hints,
|
||||
None,
|
||||
)
|
||||
.expect("writing to a string should not fail");
|
||||
anstream::eprint!("{error_chain}");
|
||||
|
||||
ExitStatus::Error.into()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user