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:
Zanie Blue
2026-05-27 13:59:07 -05:00
committed by GitHub
parent a48af7fa8a
commit b1e02c5aef
16 changed files with 572 additions and 146 deletions
Generated
+11 -3
View File
@@ -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",
]
+1
View File
@@ -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" }
+1
View File
@@ -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 }
+21 -6
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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
");
}
}
+81
View File
@@ -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
}
+2
View File
@@ -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
+38 -6
View File
@@ -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);
+3 -1
View File
@@ -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);
-5
View File
@@ -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 }
-100
View File
@@ -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
");
}
}
+24 -3
View File
@@ -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,
)?;
}
}
+8 -1
View File
@@ -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,
)?;
}
}
+3 -1
View File
@@ -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
View File
@@ -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()
}