Add a custom suggestion for `uv add dotenv` (#7799)

## Summary

This was brought up on Twitter recently. `dotenv` hasn't been updated in
years and doesn't build successfully anymore. Users almost always mean
to install `python-dotenv`. I think we can add helpful hints here to
point users in the right direction.

## Test Plan

![Screenshot 2024-09-29 at 9 27
27 PM](https://github.com/user-attachments/assets/72585860-9d98-4478-9eac-2c17ac06178b)
This commit is contained in:
Charlie Marsh 2024-09-30 13:00:31 -04:00 committed by GitHub
parent da9e85cc6a
commit 71d5661bd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 66 additions and 14 deletions

12
Cargo.lock generated
View File

@ -4440,6 +4440,7 @@ dependencies = [
"byteorder", "byteorder",
"cache-key", "cache-key",
"clap", "clap",
"console",
"ctrlc", "ctrlc",
"distribution-filename", "distribution-filename",
"distribution-types", "distribution-types",
@ -4488,6 +4489,7 @@ dependencies = [
"uv-cli", "uv-cli",
"uv-client", "uv-client",
"uv-configuration", "uv-configuration",
"uv-console",
"uv-dispatch", "uv-dispatch",
"uv-distribution", "uv-distribution",
"uv-extract", "uv-extract",
@ -4707,6 +4709,14 @@ dependencies = [
"which", "which",
] ]
[[package]]
name = "uv-console"
version = "0.0.1"
dependencies = [
"console",
"ctrlc",
]
[[package]] [[package]]
name = "uv-dev" name = "uv-dev"
version = "0.0.1" version = "0.0.1"
@ -5078,7 +5088,6 @@ dependencies = [
"cache-key", "cache-key",
"configparser", "configparser",
"console", "console",
"ctrlc",
"distribution-filename", "distribution-filename",
"distribution-types", "distribution-types",
"fs-err", "fs-err",
@ -5094,6 +5103,7 @@ dependencies = [
"url", "url",
"uv-client", "uv-client",
"uv-configuration", "uv-configuration",
"uv-console",
"uv-distribution", "uv-distribution",
"uv-fs", "uv-fs",
"uv-git", "uv-git",

View File

@ -38,6 +38,7 @@ uv-cache-info = { path = "crates/uv-cache-info" }
uv-cli = { path = "crates/uv-cli" } uv-cli = { path = "crates/uv-cli" }
uv-client = { path = "crates/uv-client" } uv-client = { path = "crates/uv-client" }
uv-configuration = { path = "crates/uv-configuration" } uv-configuration = { path = "crates/uv-configuration" }
uv-console = { path = "crates/uv-console" }
uv-dispatch = { path = "crates/uv-dispatch" } uv-dispatch = { path = "crates/uv-dispatch" }
uv-distribution = { path = "crates/uv-distribution" } uv-distribution = { path = "crates/uv-distribution" }
uv-extract = { path = "crates/uv-extract" } uv-extract = { path = "crates/uv-extract" }

View File

@ -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 }

View File

@ -1,11 +1,10 @@
use anyhow::Result;
use console::{style, Key, Term}; use console::{style, Key, Term};
/// Prompt the user for confirmation in the given [`Term`]. /// Prompt the user for confirmation in the given [`Term`].
/// ///
/// This is a slimmed-down version of `dialoguer::Confirm`, with the post-confirmation report /// This is a slimmed-down version of `dialoguer::Confirm`, with the post-confirmation report
/// enabled. /// enabled.
pub(crate) fn confirm(message: &str, term: &Term, default: bool) -> Result<bool> { pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result<bool> {
// Set the Ctrl-C handler to exit the process. // Set the Ctrl-C handler to exit the process.
let result = ctrlc::set_handler(move || { let result = ctrlc::set_handler(move || {
let term = Term::stderr(); let term = Term::stderr();
@ -26,7 +25,7 @@ pub(crate) fn confirm(message: &str, term: &Term, default: bool) -> Result<bool>
// If multiple handlers were set, we assume that the existing handler is our // If multiple handlers were set, we assume that the existing handler is our
// confirmation handler, and continue. // 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!( let prompt = format!(

View File

@ -21,6 +21,7 @@ pypi-types = { workspace = true }
requirements-txt = { workspace = true, features = ["http"] } requirements-txt = { workspace = true, features = ["http"] }
uv-client = { workspace = true } uv-client = { workspace = true }
uv-configuration = { workspace = true } uv-configuration = { workspace = true }
uv-console = { workspace = true }
uv-distribution = { workspace = true } uv-distribution = { workspace = true }
uv-fs = { workspace = true } uv-fs = { workspace = true }
uv-git = { workspace = true } uv-git = { workspace = true }
@ -33,7 +34,6 @@ uv-workspace = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
configparser = { workspace = true } configparser = { workspace = true }
console = { workspace = true } console = { workspace = true }
ctrlc = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] } fs-err = { workspace = true, features = ["tokio"] }
futures = { workspace = true } futures = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }

View File

@ -4,7 +4,6 @@ pub use crate::sources::*;
pub use crate::specification::*; pub use crate::specification::*;
pub use crate::unnamed::*; pub use crate::unnamed::*;
mod confirm;
mod lookahead; mod lookahead;
mod source_tree; mod source_tree;
mod sources; mod sources;

View File

@ -5,8 +5,6 @@ use console::Term;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_warnings::warn_user; use uv_warnings::warn_user;
use crate::confirm;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum RequirementsSource { pub enum RequirementsSource {
/// A package was provided on the command line (e.g., `pip install flask`). /// A package was provided on the command line (e.g., `pip install flask`).
@ -96,7 +94,7 @@ impl RequirementsSource {
let prompt = format!( let prompt = format!(
"`{name}` looks like a local requirements file but was passed as a package name. Did you mean `-r {name}`?" "`{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 { if confirmation {
return Self::from_requirements_file(name.into()); return Self::from_requirements_file(name.into());
} }
@ -113,7 +111,7 @@ impl RequirementsSource {
let prompt = format!( let prompt = format!(
"`{name}` looks like a local metadata file but was passed as a package name. Did you mean `-r {name}`?" "`{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 { if confirmation {
return Self::from_requirements_file(name.into()); return Self::from_requirements_file(name.into());
} }

View File

@ -28,6 +28,7 @@ uv-cache-info = { workspace = true }
uv-cli = { workspace = true } uv-cli = { workspace = true }
uv-client = { workspace = true } uv-client = { workspace = true }
uv-configuration = { workspace = true } uv-configuration = { workspace = true }
uv-console = { workspace = true }
uv-dispatch = { workspace = true } uv-dispatch = { workspace = true }
uv-distribution = { workspace = true } uv-distribution = { workspace = true }
uv-extract = { workspace = true } uv-extract = { workspace = true }
@ -57,6 +58,7 @@ axoupdater = { workspace = true, features = [
"tokio", "tokio",
], optional = true } ], optional = true }
clap = { workspace = true, features = ["derive", "string", "wrap_help"] } clap = { workspace = true, features = ["derive", "string", "wrap_help"] }
console = { workspace = true }
ctrlc = { workspace = true } ctrlc = { workspace = true }
flate2 = { workspace = true, default-features = false } flate2 = { workspace = true, default-features = false }
fs-err = { workspace = true, features = ["tokio"] } fs-err = { workspace = true, features = ["tokio"] }

View File

@ -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 anyhow::{bail, Context, Result};
use console::Term;
use itertools::Itertools; use itertools::Itertools;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashMap}; 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 tracing::debug;
use cache_key::RepositoryUrl; use cache_key::RepositoryUrl;
@ -48,6 +50,18 @@ use crate::commands::{pip, project, ExitStatus, SharedState};
use crate::printer::Printer; use crate::printer::Printer;
use crate::settings::{ResolverInstallerSettings, ResolverInstallerSettingsRef}; use crate::settings::{ResolverInstallerSettings, ResolverInstallerSettingsRef};
static CORRECTIONS: LazyLock<FxHashMap<PackageName, PackageName>> = 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. /// Add one or more packages to the project requirements.
#[allow(clippy::fn_params_excessive_bools)] #[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn add( pub(crate) async fn add(
@ -371,6 +385,23 @@ pub(crate) async fn add(
}?; }?;
let mut edits = Vec::<DependencyEdit>::with_capacity(requirements.len()); let mut edits = Vec::<DependencyEdit>::with_capacity(requirements.len());
for mut requirement in requirements { 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. // Add the specified extras.
requirement.extras.extend(extras.iter().cloned()); requirement.extras.extend(extras.iter().cloned());
requirement.extras.sort_unstable(); requirement.extras.sort_unstable();