diff --git a/crates/uv/src/commands/diagnostics.rs b/crates/uv/src/commands/diagnostics.rs index d0f19aaedff10..a3f4956a7ba4c 100644 --- a/crates/uv/src/commands/diagnostics.rs +++ b/crates/uv/src/commands/diagnostics.rs @@ -22,6 +22,7 @@ use crate::commands::project::run::RecursionLimitError; use crate::commands::project::version::MissingProjectVersionError; use crate::commands::tool::common::NoExecutablesError; use crate::commands::tool::run::ToolRunScriptError; +use crate::printer::Printer; static SUGGESTIONS: LazyLock> = LazyLock::new(|| { let suggestions: Vec<(String, String)> = @@ -223,6 +224,15 @@ fn no_solution(err: &uv_resolver::NoSolutionError, context: Option<&'static str> anstream::eprint!("{hints}"); } +/// Format an error chain with the default user-facing hints and output settings. +pub(crate) fn write_error_chain(err: &anyhow::Error, printer: Printer) -> std::fmt::Result { + uv_errors::write_error_chain_with_options( + err.as_ref(), + hints_for_error(err), + uv_errors::ErrorOptions::default().with_stream(printer.stderr_important()), + ) +} + /// Walk an error chain and collect hint strings from all known error types. /// /// This is the central "hint for error" function. It walks the full error chain diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index b62220931aa67..618380804650e 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -530,17 +530,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } // Configure the `Printer`, which controls user-facing output in the CLI. - let printer = if globals.quiet == 1 { - Printer::Quiet - } else if globals.quiet > 1 { - Printer::Silent - } else if globals.verbose > 0 { - Printer::Verbose - } else if globals.no_progress { - Printer::NoProgress - } else { - Printer::Default - }; + let printer = Printer::new(globals.quiet, globals.verbose, globals.no_progress); // Configure the `warn!` macros, which control user-facing warnings in the CLI. if globals.quiet > 0 { @@ -3025,6 +3015,14 @@ where } }; + // Configure a printer for failures that escape command execution. The resolved `no_progress` + // setting can differ due to environment variables, but it does not affect important stderr. + let printer = Printer::new( + cli.top_level.global_args.quiet, + cli.top_level.global_args.verbose, + cli.top_level.global_args.no_progress, + ); + // See `min_stack_size` doc comment about `main2` let min_stack_size = min_stack_size(); let main2 = move || { @@ -3061,13 +3059,13 @@ where match error { UvError::User(err) => { trace!("Error trace: {err:?}"); - anstream::eprintln!("{}", err.to_string().bold()); + writeln!(printer.stderr_important(), "{}", err.to_string().bold()) + .expect("writing to stderr should not fail"); ExitStatus::Failure.into() } UvError::Unexpected(err) => { trace!("Error trace: {err:?}"); - let hints = commands::diagnostics::hints_for_error(&err); - uv_errors::write_error_chain(err.as_ref(), hints) + commands::diagnostics::write_error_chain(&err, printer) .expect("writing to stderr should not fail"); ExitStatus::Error.into() } diff --git a/crates/uv/src/printer.rs b/crates/uv/src/printer.rs index 3a9bf31fe0d7a..14e3b38b45f78 100644 --- a/crates/uv/src/printer.rs +++ b/crates/uv/src/printer.rs @@ -16,6 +16,21 @@ pub(crate) enum Printer { } impl Printer { + /// Create a printer from the global output settings. + pub(crate) fn new(quiet: u8, verbose: u8, no_progress: bool) -> Self { + if quiet == 1 { + Self::Quiet + } else if quiet > 1 { + Self::Silent + } else if verbose > 0 { + Self::Verbose + } else if no_progress { + Self::NoProgress + } else { + Self::Default + } + } + /// Return the [`ProgressDrawTarget`] for this printer. pub(crate) fn target(self) -> ProgressDrawTarget { match self { diff --git a/crates/uv/tests/project/run.rs b/crates/uv/tests/project/run.rs index 5b99d70e750f6..01f9d2bdd9382 100644 --- a/crates/uv/tests/project/run.rs +++ b/crates/uv/tests/project/run.rs @@ -5281,6 +5281,23 @@ fn run_with_not_existing_env_file() -> Result<()> { error: No environment file found at: `.env.development` "); + uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env.development").arg("--quiet").arg("test.py"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No environment file found at: `.env.development` + "); + + uv_snapshot!(context.filters(), context.run().arg("--env-file").arg(".env.development").arg("--quiet").arg("--quiet").arg("test.py"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + "); + Ok(()) } diff --git a/crates/uv/tests/sync/sync.rs b/crates/uv/tests/sync/sync.rs index 0e8f2449ea8b1..01545e44c3318 100644 --- a/crates/uv/tests/sync/sync.rs +++ b/crates/uv/tests/sync/sync.rs @@ -255,6 +255,25 @@ fn locked() -> Result<()> { The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. "); + // Quiet mode suppresses the resolution summary, but preserves the user-facing failure. + uv_snapshot!(context.filters(), context.sync().arg("--locked").arg("--quiet"), @" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. + "); + + // Silent mode suppresses the final error too. + uv_snapshot!(context.filters(), context.sync().arg("--locked").arg("--quiet").arg("--quiet"), @" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + "); + let updated = context.read("uv.lock"); // And the lockfile should be unchanged.