From 5f20bdb2ee1207fb7617360335fbfdd23aba6dd1 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 9 Jul 2024 13:43:13 -0400 Subject: [PATCH] Implement `uv help` manually instead of using Clap default (#4906) Extends #4772 Implements `uv help` ourselves so we can do things like #4909 Adds hints to use `uv help` for more details during short help display. --- crates/uv-cli/src/lib.rs | 97 +++++++++++++++++- crates/uv/src/commands/help.rs | 56 +++++++++++ crates/uv/src/commands/mod.rs | 2 + crates/uv/src/main.rs | 3 + crates/uv/tests/help.rs | 179 +++++++++++++++++++++++++-------- 5 files changed, 294 insertions(+), 43 deletions(-) create mode 100644 crates/uv/src/commands/help.rs diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 2ad27927e30b..4126f68bc3e8 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -52,7 +52,12 @@ fn extra_name_with_clap_error(arg: &str) -> Result { #[command(name = "uv", author, version = uv_version::version(), long_version = crate::version::version())] #[command(about = "An extremely fast Python package manager.")] #[command(propagate_version = true)] -#[command(disable_help_flag = true)] +#[command( + after_help = "Use `uv help` for more details.", + after_long_help = "", + disable_help_flag = true, + disable_help_subcommand = true +)] #[allow(clippy::struct_excessive_bools)] pub struct Cli { #[command(subcommand)] @@ -175,18 +180,39 @@ impl From for anstream::ColorChoice { #[allow(clippy::large_enum_variant)] pub enum Commands { /// Resolve and install Python packages. + #[command( + after_help = "Use `uv help pip`` for more details.", + after_long_help = "" + )] Pip(PipNamespace), /// Run and manage executable Python packages. + #[command( + after_help = "Use `uv help tool` for more details.", + after_long_help = "" + )] Tool(ToolNamespace), /// Manage Python installations. + #[command( + after_help = "Use `uv help python` for more details.", + after_long_help = "" + )] Python(PythonNamespace), /// Manage Python projects. #[command(flatten)] Project(Box), /// Create a virtual environment. - #[command(alias = "virtualenv", alias = "v")] + #[command( + alias = "virtualenv", + alias = "v", + after_help = "Use `uv help venv` for more details.", + after_long_help = "" + )] Venv(VenvArgs), /// Manage the cache. + #[command( + after_help = "Use `uv help cache` for more details.", + after_long_help = "" + )] Cache(CacheNamespace), /// Manage the `uv` executable. #[command(name = "self")] @@ -203,6 +229,17 @@ pub enum Commands { /// Generate shell completion #[command(alias = "--generate-shell-completion", hide = true)] GenerateShellCompletion { shell: clap_complete_command::Shell }, + /// Display documentation for a command. + #[command(help_template = "\ +{about-with-newline} +{usage-heading} {usage} +")] + Help(HelpArgs), +} + +#[derive(Args, Debug)] +pub struct HelpArgs { + pub command: Option>, } #[derive(Args)] @@ -253,22 +290,58 @@ pub struct PipNamespace { #[derive(Subcommand)] pub enum PipCommand { /// Compile a `requirements.in` file to a `requirements.txt` file. + #[command( + after_help = "Use `uv help pip compile` for more details.", + after_long_help = "" + )] Compile(PipCompileArgs), /// Sync an environment with a `requirements.txt` file. + #[command( + after_help = "Use `uv help pip sync` for more details.", + after_long_help = "" + )] Sync(PipSyncArgs), /// Install packages into an environment. + #[command( + after_help = "Use `uv help pip install` for more details.", + after_long_help = "" + )] Install(PipInstallArgs), /// Uninstall packages from an environment. + #[command( + after_help = "Use `uv help pip uninstall` for more details.", + after_long_help = "" + )] Uninstall(PipUninstallArgs), /// List, in requirements format, packages installed in an environment. + #[command( + after_help = "Use `uv help pip freeze` for more details.", + after_long_help = "" + )] Freeze(PipFreezeArgs), /// List, in tabular format, packages installed in an environment. + #[command( + after_help = "Use `uv help pip list` for more details.", + after_long_help = "" + )] List(PipListArgs), /// Show information about one or more installed packages. + #[command( + after_help = "Use `uv help pip show` for more details.", + after_long_help = "" + )] Show(PipShowArgs), /// Display the dependency tree for an environment. + #[command( + after_help = "Use `uv help pip tree` for more details.", + after_long_help = "" + )] Tree(PipTreeArgs), /// Verify installed packages have compatible dependencies. + #[command( + after_help = "Use `uv help pip check` for more details.", + after_long_help = "" + )] Check(PipCheckArgs), } @@ -276,18 +349,38 @@ pub enum PipCommand { pub enum ProjectCommand { /// Run a command in the project environment. #[clap(hide = true)] + #[command( + after_help = "Use `uv help run` for more details.", + after_long_help = "" + )] Run(RunArgs), /// Sync the project's dependencies with the environment. #[clap(hide = true)] + #[command( + after_help = "Use `uv help sync` for more details.", + after_long_help = "" + )] Sync(SyncArgs), /// Resolve the project requirements into a lockfile. #[clap(hide = true)] + #[command( + after_help = "Use `uv help lock` for more details.", + after_long_help = "" + )] Lock(LockArgs), /// Add one or more packages to the project requirements. #[clap(hide = true)] + #[command( + after_help = "Use `uv help add` for more details.", + after_long_help = "" + )] Add(AddArgs), /// Remove one or more packages from the project requirements. #[clap(hide = true)] + #[command( + after_help = "Use `uv help remove` for more details.", + after_long_help = "" + )] Remove(RemoveArgs), /// Display the dependency tree for the project. #[clap(hide = true)] diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs new file mode 100644 index 000000000000..431513750f3a --- /dev/null +++ b/crates/uv/src/commands/help.rs @@ -0,0 +1,56 @@ +use std::fmt::Write; + +use anyhow::{anyhow, Result}; +use clap::CommandFactory; +use itertools::Itertools; + +use super::ExitStatus; +use crate::printer::Printer; +use uv_cli::Cli; + +pub(crate) fn help(query: &[String], printer: Printer) -> Result { + let mut uv = Cli::command(); + + // It is very important to build the command before beginning inspection or subcommands + // will be missing all of the propagated options. + uv.build(); + + let command = find_command(query, &uv).map_err(|(unmatched, nearest)| { + let missing = if unmatched.len() == query.len() { + format!("`{}` for `uv`", query.join(" ")) + } else { + format!("`{}` for `uv {}`", unmatched.join(" "), nearest.get_name()) + }; + anyhow!( + "There is no command {}. Did you mean one of:\n {}", + missing, + nearest + .get_subcommands() + .filter(|cmd| !cmd.is_hide_set()) + .map(clap::Command::get_name) + .filter(|name| *name != "help") + .join("\n "), + ) + })?; + + let mut command = command.clone(); + let help = command.render_long_help(); + writeln!(printer.stderr(), "{}", help.ansi())?; + + Ok(ExitStatus::Success) +} + +/// Find the command corresponding to a set of arguments, e.g., `["uv", "pip", "install"]`. +/// +/// If the command cannot be found, the nearest command is returned. +fn find_command<'a>( + query: &'a [String], + cmd: &'a clap::Command, +) -> Result<&'a clap::Command, (&'a [String], &'a clap::Command)> { + let Some(next) = query.first() else { + return Ok(cmd); + }; + + let subcommand = cmd.find_subcommand(next).ok_or((query, cmd))?; + find_command(&query[1..], subcommand) +} diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index ba000c02fb80..9d73b8b7148a 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -8,6 +8,7 @@ pub(crate) use cache_clean::cache_clean; pub(crate) use cache_dir::cache_dir; pub(crate) use cache_prune::cache_prune; use distribution_types::InstalledMetadata; +pub(crate) use help::help; pub(crate) use pip::check::pip_check; pub(crate) use pip::compile::pip_compile; pub(crate) use pip::freeze::pip_freeze; @@ -51,6 +52,7 @@ use crate::printer::Printer; mod cache_clean; mod cache_dir; mod cache_prune; +mod help; pub(crate) mod pip; mod project; mod python; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 4385b9b04c8f..e9d0163a793b 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -217,6 +217,9 @@ async fn run() -> Result { let cache = Cache::from_settings(cache_settings.no_cache, cache_settings.cache_dir)?; match *cli.command { + Commands::Help(args) => { + commands::help(args.command.unwrap_or_default().as_slice(), printer) + } Commands::Pip(PipNamespace { command: PipCommand::Compile(args), }) => { diff --git a/crates/uv/tests/help.rs b/crates/uv/tests/help.rs index ebacce1b3a49..6c9d838d8746 100644 --- a/crates/uv/tests/help.rs +++ b/crates/uv/tests/help.rs @@ -11,6 +11,8 @@ fn help() { success: true exit_code: 0 ----- stdout ----- + + ----- stderr ----- An extremely fast Python package manager. Usage: uv [OPTIONS] @@ -22,7 +24,7 @@ fn help() { venv Create a virtual environment cache Manage the cache version Display uv's version - help Print this message or the help of the given subcommand(s) + help Display documentation for a command Options: -q, --quiet @@ -107,7 +109,6 @@ fn help() { -V, --version Print version - ----- stderr ----- "###); } @@ -129,7 +130,7 @@ fn help_flag() { venv Create a virtual environment cache Manage the cache version Display uv's version - help Print this message or the help of the given subcommand(s) + help Display documentation for a command Options: -q, --quiet @@ -160,6 +161,8 @@ fn help_flag() { -V, --version Print version + Use `uv help` for more details. + ----- stderr ----- "###); } @@ -182,7 +185,7 @@ fn help_short_flag() { venv Create a virtual environment cache Manage the cache version Display uv's version - help Print this message or the help of the given subcommand(s) + help Display documentation for a command Options: -q, --quiet @@ -213,6 +216,8 @@ fn help_short_flag() { -V, --version Print version + Use `uv help` for more details. + ----- stderr ----- "###); } @@ -225,6 +230,8 @@ fn help_subcommand() { success: true exit_code: 0 ----- stdout ----- + + ----- stderr ----- Manage Python installations Usage: uv python [OPTIONS] @@ -235,7 +242,6 @@ fn help_subcommand() { find Search for a Python installation dir Show the uv Python installation directory uninstall Uninstall Python versions - help Print this message or the help of the given subcommand(s) Options: -q, --quiet @@ -320,7 +326,6 @@ fn help_subcommand() { -V, --version Print version - ----- stderr ----- "###); } @@ -332,6 +337,8 @@ fn help_subsubcommand() { success: true exit_code: 0 ----- stdout ----- + + ----- stderr ----- Download and install Python versions Usage: uv python install [OPTIONS] [TARGETS]... @@ -430,7 +437,6 @@ fn help_subsubcommand() { -V, --version Print version - ----- stderr ----- "###); } @@ -452,7 +458,6 @@ fn help_flag_subcommand() { find Search for a Python installation dir Show the uv Python installation directory uninstall Uninstall Python versions - help Print this message or the help of the given subcommand(s) Options: -q, --quiet @@ -483,6 +488,8 @@ fn help_flag_subcommand() { -V, --version Print version + Use `uv help python` for more details. + ----- stderr ----- "###); } @@ -547,11 +554,13 @@ fn help_unknown_subcommand() { ----- stdout ----- ----- stderr ----- - error: unrecognized subcommand 'foobar' - - Usage: uv [OPTIONS] - - For more information, try '--help'. + error: There is no command `foobar` for `uv`. Did you mean one of: + pip + tool + python + venv + cache + version "###); uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r###" @@ -560,11 +569,13 @@ fn help_unknown_subcommand() { ----- stdout ----- ----- stderr ----- - error: unrecognized subcommand 'foo' - - Usage: uv [OPTIONS] - - For more information, try '--help'. + error: There is no command `foo bar` for `uv`. Did you mean one of: + pip + tool + python + venv + cache + version "###); } @@ -578,11 +589,12 @@ fn help_unknown_subsubcommand() { ----- stdout ----- ----- stderr ----- - error: unrecognized subcommand 'foobar' - - Usage: uv python [OPTIONS] - - For more information, try '--help'. + error: There is no command `foobar` for `uv python`. Did you mean one of: + list + install + find + dir + uninstall "###); } @@ -591,16 +603,107 @@ fn help_with_global_option() { let context = TestContext::new_with_versions(&[]); uv_snapshot!(context.filters(), context.help().arg("--cache-dir").arg("/dev/null"), @r###" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - error: unrecognized subcommand '--cache-dir' + An extremely fast Python package manager. Usage: uv [OPTIONS] - For more information, try '--help'. + Commands: + pip Resolve and install Python packages + tool Run and manage executable Python packages + python Manage Python installations + venv Create a virtual environment + cache Manage the cache + version Display uv's version + help Display documentation for a command + + Options: + -q, --quiet + Do not print any output + + -v, --verbose... + Use verbose output. + + You can configure fine-grained logging using the `RUST_LOG` environment variable. + () + + --color + Control colors in output + + [default: auto] + + Possible values: + - auto: Enables colored output only when the output is going to a terminal or TTY with + support + - always: Enables colored output regardless of the detected environment + - never: Disables colored output + + --native-tls + Whether to load TLS certificates from the platform's native certificate store. + + By default, `uv` loads certificates from the bundled `webpki-roots` crate. The + `webpki-roots` are a reliable set of trust roots from Mozilla, and including them in `uv` + improves portability and performance (especially on macOS). + + However, in some cases, you may want to use the platform's native certificate store, + especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) + that's included in your system's certificate store. + + [env: UV_NATIVE_TLS=] + + --offline + Disable network access, relying only on locally cached data and locally available files + + --python-preference + Whether to prefer using Python from uv or on the system + + Possible values: + - only-managed: Only use managed Python installations; never use system Python + installations + - installed: Prefer installed Python installations, only download managed Python + installations if no system Python installation is found + - managed: Prefer managed Python installations over system Python installations, even + if fetching is required + - system: Prefer system Python installations over managed Python installations + - only-system: Only use system Python installations; never use managed Python + installations + + --python-fetch + Whether to automatically download Python when required + + Possible values: + - automatic: Automatically fetch managed Python installations when needed + - manual: Do not automatically fetch managed Python installations; require explicit + installation + + -n, --no-cache + Avoid reading from or writing to the cache + + [env: UV_NO_CACHE=] + + --cache-dir [CACHE_DIR] + Path to the cache directory. + + Defaults to `$HOME/Library/Caches/uv` on macOS, `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` + on Linux, and `{FOLDERID_LocalAppData}/uv/cache` on Windows. + + [env: UV_CACHE_DIR=] + + --config-file + The path to a `uv.toml` file to use for configuration + + [env: UV_CONFIG_FILE=] + + -h, --help + Print help + + -V, --version + Print version + "###); } @@ -609,16 +712,14 @@ fn help_with_help() { let context = TestContext::new_with_versions(&[]); uv_snapshot!(context.filters(), context.help().arg("--help"), @r###" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- + Display documentation for a command - ----- stderr ----- - error: unrecognized subcommand '--help' - - Usage: uv [OPTIONS] + Usage: uv help [OPTIONS] [COMMAND]... - For more information, try '--help'. + ----- stderr ----- "###); } @@ -627,15 +728,11 @@ fn help_with_version() { let context = TestContext::new_with_versions(&[]); uv_snapshot!(context.filters(), context.help().arg("--version"), @r###" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- + uv [VERSION] ([COMMIT] DATE) ----- stderr ----- - error: unrecognized subcommand '--version' - - Usage: uv [OPTIONS] - - For more information, try '--help'. "###); }