Don't panic on Ctrl-C in confirm prompt (#11706)

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
Resolves #11704 

Propagate errors from `uv_console::confirm` up instead of `unwrap`ping
them, causing panics.

## Test Plan

<!-- How was it tested? -->
Regression testing the bug is very difficult, as the behavior of
`confirm` changes based on whether `uv` is talking to a `tty`. We can
trick it using ptys, but the best rust pty crate I could find only
provides blocking reads of the spawned child, which is insufficient to
write the regression test.

---------

Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
Eric Mark Martin 2025-02-26 05:10:04 -05:00 committed by GitHub
parent b180fe99b4
commit 6e7ec3274a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 84 additions and 78 deletions

View file

@ -10,6 +10,7 @@ pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result<boo
let result = ctrlc::set_handler(move || {
let term = Term::stderr();
term.show_cursor().ok();
term.write_str("\n").ok();
term.flush().ok();
#[allow(clippy::exit, clippy::cast_possible_wrap)]

View file

@ -1,5 +1,6 @@
use std::path::{Path, PathBuf};
use anyhow::Result;
use console::Term;
use uv_fs::Simplified;
@ -85,7 +86,7 @@ impl RequirementsSource {
///
/// If the user provided a value that appears to be a `requirements.txt` file or a local
/// directory, prompt them to correct it (if the terminal is interactive).
pub fn from_package(name: String) -> Self {
pub fn from_package(name: String) -> Result<Self> {
// If the user provided a `requirements.txt` file without `-r` (as in
// `uv pip install requirements.txt`), prompt them to correct it.
#[allow(clippy::case_sensitive_file_extension_comparisons)]
@ -95,9 +96,9 @@ impl RequirementsSource {
let prompt = format!(
"`{name}` looks like a local requirements file but was passed as a package name. Did you mean `-r {name}`?"
);
let confirmation = uv_console::confirm(&prompt, &term, true).unwrap();
let confirmation = uv_console::confirm(&prompt, &term, true)?;
if confirmation {
return Self::from_requirements_file(name.into());
return Ok(Self::from_requirements_file(name.into()));
}
}
}
@ -112,14 +113,14 @@ impl RequirementsSource {
let prompt = format!(
"`{name}` looks like a local metadata file but was passed as a package name. Did you mean `-r {name}`?"
);
let confirmation = uv_console::confirm(&prompt, &term, true).unwrap();
let confirmation = uv_console::confirm(&prompt, &term, true)?;
if confirmation {
return Self::from_requirements_file(name.into());
return Ok(Self::from_requirements_file(name.into()));
}
}
}
Self::Package(name)
Ok(Self::Package(name))
}
/// Parse a [`RequirementsSource`] from a user-provided string, assumed to be a `--with`
@ -127,7 +128,7 @@ impl RequirementsSource {
///
/// If the user provided a value that appears to be a `requirements.txt` file or a local
/// directory, prompt them to correct it (if the terminal is interactive).
pub fn from_with_package(name: String) -> Self {
pub fn from_with_package(name: String) -> Result<Self> {
// If the user provided a `requirements.txt` file without `--with-requirements` (as in
// `uvx --with requirements.txt ruff`), prompt them to correct it.
#[allow(clippy::case_sensitive_file_extension_comparisons)]
@ -137,9 +138,9 @@ impl RequirementsSource {
let prompt = format!(
"`{name}` looks like a local requirements file but was passed as a package name. Did you mean `--with-requirements {name}`?"
);
let confirmation = uv_console::confirm(&prompt, &term, true).unwrap();
let confirmation = uv_console::confirm(&prompt, &term, true)?;
if confirmation {
return Self::from_requirements_file(name.into());
return Ok(Self::from_requirements_file(name.into()));
}
}
}
@ -154,14 +155,14 @@ impl RequirementsSource {
let prompt = format!(
"`{name}` looks like a local metadata file but was passed as a package name. Did you mean `--with-requirements {name}`?"
);
let confirmation = uv_console::confirm(&prompt, &term, true).unwrap();
let confirmation = uv_console::confirm(&prompt, &term, true)?;
if confirmation {
return Self::from_requirements_file(name.into());
return Ok(Self::from_requirements_file(name.into()));
}
}
}
Self::Package(name)
Ok(Self::Package(name))
}
/// Parse a [`RequirementsSource`] from a user-provided string, assumed to be a path to a source

View file

@ -537,17 +537,18 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.combine(Refresh::from(args.settings.upgrade.clone())),
);
let requirements = args
.package
.into_iter()
.map(RequirementsSource::from_package)
.chain(args.editables.into_iter().map(RequirementsSource::Editable))
.chain(
args.requirements
.into_iter()
.map(RequirementsSource::from_requirements_file),
)
.collect::<Vec<_>>();
let mut requirements = Vec::with_capacity(
args.package.len() + args.editables.len() + args.requirements.len(),
);
for package in args.package {
requirements.push(RequirementsSource::from_package(package)?);
}
requirements.extend(args.editables.into_iter().map(RequirementsSource::Editable));
requirements.extend(
args.requirements
.into_iter()
.map(RequirementsSource::from_requirements_file),
);
let constraints = args
.constraints
.into_iter()
@ -624,16 +625,15 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Initialize the cache.
let cache = cache.init()?;
let sources = args
.package
.into_iter()
.map(RequirementsSource::from_package)
.chain(
args.requirements
.into_iter()
.map(RequirementsSource::from_requirements_txt),
)
.collect::<Vec<_>>();
let mut sources = Vec::with_capacity(args.package.len() + args.requirements.len());
for package in args.package {
sources.push(RequirementsSource::from_package(package)?);
}
sources.extend(
args.requirements
.into_iter()
.map(RequirementsSource::from_requirements_file),
);
commands::pip_uninstall(
&sources,
args.settings.python,
@ -985,21 +985,22 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.combine(Refresh::from(args.settings.upgrade.clone())),
);
let requirements = args
.with
.into_iter()
.map(RequirementsSource::from_with_package)
.chain(
args.with_editable
.into_iter()
.map(RequirementsSource::Editable),
)
.chain(
args.with_requirements
.into_iter()
.map(RequirementsSource::from_requirements_file),
)
.collect::<Vec<_>>();
let mut requirements = Vec::with_capacity(
args.with.len() + args.with_editable.len() + args.with_requirements.len(),
);
for package in args.with {
requirements.push(RequirementsSource::from_with_package(package)?);
}
requirements.extend(
args.with_editable
.into_iter()
.map(RequirementsSource::Editable),
);
requirements.extend(
args.with_requirements
.into_iter()
.map(RequirementsSource::from_requirements_file),
);
Box::pin(commands::tool_run(
args.command,
@ -1038,21 +1039,23 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.combine(Refresh::from(args.settings.upgrade.clone())),
);
let requirements = args
.with
.into_iter()
.map(RequirementsSource::from_with_package)
.chain(
args.with_editable
.into_iter()
.map(RequirementsSource::Editable),
)
.chain(
args.with_requirements
.into_iter()
.map(RequirementsSource::from_requirements_file),
)
.collect::<Vec<_>>();
let mut requirements = Vec::with_capacity(
args.with.len() + args.with_editable.len() + args.with_requirements.len(),
);
for package in args.with {
requirements.push(RequirementsSource::from_with_package(package)?);
}
requirements.extend(
args.with_editable
.into_iter()
.map(RequirementsSource::Editable),
);
requirements.extend(
args.with_requirements
.into_iter()
.map(RequirementsSource::from_requirements_file),
);
let constraints = args
.constraints
.into_iter()
@ -1468,21 +1471,22 @@ async fn run_project(
.combine(Refresh::from(args.settings.upgrade.clone())),
);
let requirements = args
.with
.into_iter()
.map(RequirementsSource::from_with_package)
.chain(
args.with_editable
.into_iter()
.map(RequirementsSource::Editable),
)
.chain(
args.with_requirements
.into_iter()
.map(RequirementsSource::from_requirements_file),
)
.collect::<Vec<_>>();
let mut requirements = Vec::with_capacity(
args.with.len() + args.with_editable.len() + args.with_requirements.len(),
);
for package in args.with {
requirements.push(RequirementsSource::from_with_package(package)?);
}
requirements.extend(
args.with_editable
.into_iter()
.map(RequirementsSource::Editable),
);
requirements.extend(
args.with_requirements
.into_iter()
.map(RequirementsSource::from_requirements_file),
);
Box::pin(commands::run(
project_dir,