diff --git a/Cargo.lock b/Cargo.lock index 455a600b3..dccf26845 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4440,6 +4440,7 @@ dependencies = [ "byteorder", "cache-key", "clap", + "console", "ctrlc", "distribution-filename", "distribution-types", @@ -4488,6 +4489,7 @@ dependencies = [ "uv-cli", "uv-client", "uv-configuration", + "uv-console", "uv-dispatch", "uv-distribution", "uv-extract", @@ -4707,6 +4709,14 @@ dependencies = [ "which", ] +[[package]] +name = "uv-console" +version = "0.0.1" +dependencies = [ + "console", + "ctrlc", +] + [[package]] name = "uv-dev" version = "0.0.1" @@ -5078,7 +5088,6 @@ dependencies = [ "cache-key", "configparser", "console", - "ctrlc", "distribution-filename", "distribution-types", "fs-err", @@ -5094,6 +5103,7 @@ dependencies = [ "url", "uv-client", "uv-configuration", + "uv-console", "uv-distribution", "uv-fs", "uv-git", diff --git a/Cargo.toml b/Cargo.toml index 7d47f24cf..bc942220f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ uv-cache-info = { path = "crates/uv-cache-info" } uv-cli = { path = "crates/uv-cli" } uv-client = { path = "crates/uv-client" } uv-configuration = { path = "crates/uv-configuration" } +uv-console = { path = "crates/uv-console" } uv-dispatch = { path = "crates/uv-dispatch" } uv-distribution = { path = "crates/uv-distribution" } uv-extract = { path = "crates/uv-extract" } diff --git a/crates/uv-console/Cargo.toml b/crates/uv-console/Cargo.toml new file mode 100644 index 000000000..ef0303f71 --- /dev/null +++ b/crates/uv-console/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "uv-console" +version = "0.0.1" +edition = "2021" +description = "Utilities for interacting with the terminal" + +[lints] +workspace = true + +[dependencies] +ctrlc = { workspace = true } +console = { workspace = true } diff --git a/crates/uv-requirements/src/confirm.rs b/crates/uv-console/src/lib.rs similarity index 92% rename from crates/uv-requirements/src/confirm.rs rename to crates/uv-console/src/lib.rs index 7eb7d8b59..83d50f0cd 100644 --- a/crates/uv-requirements/src/confirm.rs +++ b/crates/uv-console/src/lib.rs @@ -1,11 +1,10 @@ -use anyhow::Result; use console::{style, Key, Term}; /// Prompt the user for confirmation in the given [`Term`]. /// /// This is a slimmed-down version of `dialoguer::Confirm`, with the post-confirmation report /// enabled. -pub(crate) fn confirm(message: &str, term: &Term, default: bool) -> Result { +pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result { // Set the Ctrl-C handler to exit the process. let result = ctrlc::set_handler(move || { let term = Term::stderr(); @@ -26,7 +25,7 @@ pub(crate) fn confirm(message: &str, term: &Term, default: bool) -> Result // If multiple handlers were set, we assume that the existing handler is our // confirmation handler, and continue. } - Err(e) => return Err(e.into()), + Err(err) => return Err(std::io::Error::new(std::io::ErrorKind::Other, err)), } let prompt = format!( diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index 5d6ea6905..ffb928c96 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -21,6 +21,7 @@ pypi-types = { workspace = true } requirements-txt = { workspace = true, features = ["http"] } uv-client = { workspace = true } uv-configuration = { workspace = true } +uv-console = { workspace = true } uv-distribution = { workspace = true } uv-fs = { workspace = true } uv-git = { workspace = true } @@ -33,7 +34,6 @@ uv-workspace = { workspace = true } anyhow = { workspace = true } configparser = { workspace = true } console = { workspace = true } -ctrlc = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/uv-requirements/src/lib.rs b/crates/uv-requirements/src/lib.rs index 2a582bc06..d11d152f8 100644 --- a/crates/uv-requirements/src/lib.rs +++ b/crates/uv-requirements/src/lib.rs @@ -4,7 +4,6 @@ pub use crate::sources::*; pub use crate::specification::*; pub use crate::unnamed::*; -mod confirm; mod lookahead; mod source_tree; mod sources; diff --git a/crates/uv-requirements/src/sources.rs b/crates/uv-requirements/src/sources.rs index fca23c051..e59377c18 100644 --- a/crates/uv-requirements/src/sources.rs +++ b/crates/uv-requirements/src/sources.rs @@ -5,8 +5,6 @@ use console::Term; use uv_fs::Simplified; use uv_warnings::warn_user; -use crate::confirm; - #[derive(Debug, Clone)] pub enum RequirementsSource { /// A package was provided on the command line (e.g., `pip install flask`). @@ -96,7 +94,7 @@ 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 = confirm::confirm(&prompt, &term, true).unwrap(); + let confirmation = uv_console::confirm(&prompt, &term, true).unwrap(); if confirmation { return Self::from_requirements_file(name.into()); } @@ -113,7 +111,7 @@ 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 = confirm::confirm(&prompt, &term, true).unwrap(); + let confirmation = uv_console::confirm(&prompt, &term, true).unwrap(); if confirmation { return Self::from_requirements_file(name.into()); } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 2efd2b045..df3fe198a 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -28,6 +28,7 @@ uv-cache-info = { workspace = true } uv-cli = { workspace = true } uv-client = { workspace = true } uv-configuration = { workspace = true } +uv-console = { workspace = true } uv-dispatch = { workspace = true } uv-distribution = { workspace = true } uv-extract = { workspace = true } @@ -57,6 +58,7 @@ axoupdater = { workspace = true, features = [ "tokio", ], optional = true } clap = { workspace = true, features = ["derive", "string", "wrap_help"] } +console = { workspace = true } ctrlc = { workspace = true } flate2 = { workspace = true, default-features = false } fs-err = { workspace = true, features = ["tokio"] } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 1727765a2..c451f99c4 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -1,11 +1,13 @@ -use std::collections::hash_map::Entry; -use std::fmt::Write; -use std::path::{Path, PathBuf}; - use anyhow::{bail, Context, Result}; +use console::Term; use itertools::Itertools; use owo_colors::OwoColorize; use rustc_hash::{FxBuildHasher, FxHashMap}; +use std::collections::hash_map::Entry; +use std::fmt::Write; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::LazyLock; use tracing::debug; use cache_key::RepositoryUrl; @@ -48,6 +50,18 @@ use crate::commands::{pip, project, ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::{ResolverInstallerSettings, ResolverInstallerSettingsRef}; +static CORRECTIONS: LazyLock> = LazyLock::new(|| { + [("dotenv", "python-dotenv"), ("sklearn", "scikit-learn")] + .iter() + .map(|(k, v)| { + ( + PackageName::from_str(k).unwrap(), + PackageName::from_str(v).unwrap(), + ) + }) + .collect() +}); + /// Add one or more packages to the project requirements. #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn add( @@ -371,6 +385,23 @@ pub(crate) async fn add( }?; let mut edits = Vec::::with_capacity(requirements.len()); for mut requirement in requirements { + // If the user requested a package that is often confused for another package, prompt them. + if let Some(correction) = CORRECTIONS.get(&requirement.name) { + let term = Term::stderr(); + if term.is_term() { + let prompt = format!( + "`{}` is often confused for `{}`. Did you mean `{}`?", + requirement.name.cyan(), + correction.cyan(), + format!("uv add {correction}").green() + ); + let confirmation = uv_console::confirm(&prompt, &term, true)?; + if confirmation { + requirement.name = correction.clone(); + } + } + } + // Add the specified extras. requirement.extras.extend(extras.iter().cloned()); requirement.extras.sort_unstable();