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.
This commit is contained in:
Zanie Blue 2024-07-09 13:43:13 -04:00 committed by GitHub
parent 2e307d9081
commit 5f20bdb2ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 294 additions and 43 deletions

View file

@ -52,7 +52,12 @@ fn extra_name_with_clap_error(arg: &str) -> Result<ExtraName> {
#[command(name = "uv", author, version = uv_version::version(), long_version = crate::version::version())] #[command(name = "uv", author, version = uv_version::version(), long_version = crate::version::version())]
#[command(about = "An extremely fast Python package manager.")] #[command(about = "An extremely fast Python package manager.")]
#[command(propagate_version = true)] #[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)] #[allow(clippy::struct_excessive_bools)]
pub struct Cli { pub struct Cli {
#[command(subcommand)] #[command(subcommand)]
@ -175,18 +180,39 @@ impl From<ColorChoice> for anstream::ColorChoice {
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum Commands { pub enum Commands {
/// Resolve and install Python packages. /// Resolve and install Python packages.
#[command(
after_help = "Use `uv help pip`` for more details.",
after_long_help = ""
)]
Pip(PipNamespace), Pip(PipNamespace),
/// Run and manage executable Python packages. /// Run and manage executable Python packages.
#[command(
after_help = "Use `uv help tool` for more details.",
after_long_help = ""
)]
Tool(ToolNamespace), Tool(ToolNamespace),
/// Manage Python installations. /// Manage Python installations.
#[command(
after_help = "Use `uv help python` for more details.",
after_long_help = ""
)]
Python(PythonNamespace), Python(PythonNamespace),
/// Manage Python projects. /// Manage Python projects.
#[command(flatten)] #[command(flatten)]
Project(Box<ProjectCommand>), Project(Box<ProjectCommand>),
/// Create a virtual environment. /// 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), Venv(VenvArgs),
/// Manage the cache. /// Manage the cache.
#[command(
after_help = "Use `uv help cache` for more details.",
after_long_help = ""
)]
Cache(CacheNamespace), Cache(CacheNamespace),
/// Manage the `uv` executable. /// Manage the `uv` executable.
#[command(name = "self")] #[command(name = "self")]
@ -203,6 +229,17 @@ pub enum Commands {
/// Generate shell completion /// Generate shell completion
#[command(alias = "--generate-shell-completion", hide = true)] #[command(alias = "--generate-shell-completion", hide = true)]
GenerateShellCompletion { shell: clap_complete_command::Shell }, 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<Vec<String>>,
} }
#[derive(Args)] #[derive(Args)]
@ -253,22 +290,58 @@ pub struct PipNamespace {
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum PipCommand { pub enum PipCommand {
/// Compile a `requirements.in` file to a `requirements.txt` file. /// 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), Compile(PipCompileArgs),
/// Sync an environment with a `requirements.txt` file. /// Sync an environment with a `requirements.txt` file.
#[command(
after_help = "Use `uv help pip sync` for more details.",
after_long_help = ""
)]
Sync(PipSyncArgs), Sync(PipSyncArgs),
/// Install packages into an environment. /// Install packages into an environment.
#[command(
after_help = "Use `uv help pip install` for more details.",
after_long_help = ""
)]
Install(PipInstallArgs), Install(PipInstallArgs),
/// Uninstall packages from an environment. /// Uninstall packages from an environment.
#[command(
after_help = "Use `uv help pip uninstall` for more details.",
after_long_help = ""
)]
Uninstall(PipUninstallArgs), Uninstall(PipUninstallArgs),
/// List, in requirements format, packages installed in an environment. /// 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), Freeze(PipFreezeArgs),
/// List, in tabular format, packages installed in an environment. /// 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), List(PipListArgs),
/// Show information about one or more installed packages. /// Show information about one or more installed packages.
#[command(
after_help = "Use `uv help pip show` for more details.",
after_long_help = ""
)]
Show(PipShowArgs), Show(PipShowArgs),
/// Display the dependency tree for an environment. /// Display the dependency tree for an environment.
#[command(
after_help = "Use `uv help pip tree` for more details.",
after_long_help = ""
)]
Tree(PipTreeArgs), Tree(PipTreeArgs),
/// Verify installed packages have compatible dependencies. /// Verify installed packages have compatible dependencies.
#[command(
after_help = "Use `uv help pip check` for more details.",
after_long_help = ""
)]
Check(PipCheckArgs), Check(PipCheckArgs),
} }
@ -276,18 +349,38 @@ pub enum PipCommand {
pub enum ProjectCommand { pub enum ProjectCommand {
/// Run a command in the project environment. /// Run a command in the project environment.
#[clap(hide = true)] #[clap(hide = true)]
#[command(
after_help = "Use `uv help run` for more details.",
after_long_help = ""
)]
Run(RunArgs), Run(RunArgs),
/// Sync the project's dependencies with the environment. /// Sync the project's dependencies with the environment.
#[clap(hide = true)] #[clap(hide = true)]
#[command(
after_help = "Use `uv help sync` for more details.",
after_long_help = ""
)]
Sync(SyncArgs), Sync(SyncArgs),
/// Resolve the project requirements into a lockfile. /// Resolve the project requirements into a lockfile.
#[clap(hide = true)] #[clap(hide = true)]
#[command(
after_help = "Use `uv help lock` for more details.",
after_long_help = ""
)]
Lock(LockArgs), Lock(LockArgs),
/// Add one or more packages to the project requirements. /// Add one or more packages to the project requirements.
#[clap(hide = true)] #[clap(hide = true)]
#[command(
after_help = "Use `uv help add` for more details.",
after_long_help = ""
)]
Add(AddArgs), Add(AddArgs),
/// Remove one or more packages from the project requirements. /// Remove one or more packages from the project requirements.
#[clap(hide = true)] #[clap(hide = true)]
#[command(
after_help = "Use `uv help remove` for more details.",
after_long_help = ""
)]
Remove(RemoveArgs), Remove(RemoveArgs),
/// Display the dependency tree for the project. /// Display the dependency tree for the project.
#[clap(hide = true)] #[clap(hide = true)]

View file

@ -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<ExitStatus> {
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)
}

View file

@ -8,6 +8,7 @@ pub(crate) use cache_clean::cache_clean;
pub(crate) use cache_dir::cache_dir; pub(crate) use cache_dir::cache_dir;
pub(crate) use cache_prune::cache_prune; pub(crate) use cache_prune::cache_prune;
use distribution_types::InstalledMetadata; use distribution_types::InstalledMetadata;
pub(crate) use help::help;
pub(crate) use pip::check::pip_check; pub(crate) use pip::check::pip_check;
pub(crate) use pip::compile::pip_compile; pub(crate) use pip::compile::pip_compile;
pub(crate) use pip::freeze::pip_freeze; pub(crate) use pip::freeze::pip_freeze;
@ -51,6 +52,7 @@ use crate::printer::Printer;
mod cache_clean; mod cache_clean;
mod cache_dir; mod cache_dir;
mod cache_prune; mod cache_prune;
mod help;
pub(crate) mod pip; pub(crate) mod pip;
mod project; mod project;
mod python; mod python;

View file

@ -217,6 +217,9 @@ async fn run() -> Result<ExitStatus> {
let cache = Cache::from_settings(cache_settings.no_cache, cache_settings.cache_dir)?; let cache = Cache::from_settings(cache_settings.no_cache, cache_settings.cache_dir)?;
match *cli.command { match *cli.command {
Commands::Help(args) => {
commands::help(args.command.unwrap_or_default().as_slice(), printer)
}
Commands::Pip(PipNamespace { Commands::Pip(PipNamespace {
command: PipCommand::Compile(args), command: PipCommand::Compile(args),
}) => { }) => {

View file

@ -11,6 +11,8 @@ fn help() {
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr -----
An extremely fast Python package manager. An extremely fast Python package manager.
Usage: uv [OPTIONS] <COMMAND> Usage: uv [OPTIONS] <COMMAND>
@ -22,7 +24,7 @@ fn help() {
venv Create a virtual environment venv Create a virtual environment
cache Manage the cache cache Manage the cache
version Display uv's version version Display uv's version
help Print this message or the help of the given subcommand(s) help Display documentation for a command
Options: Options:
-q, --quiet -q, --quiet
@ -107,7 +109,6 @@ fn help() {
-V, --version -V, --version
Print version Print version
----- stderr -----
"###); "###);
} }
@ -129,7 +130,7 @@ fn help_flag() {
venv Create a virtual environment venv Create a virtual environment
cache Manage the cache cache Manage the cache
version Display uv's version version Display uv's version
help Print this message or the help of the given subcommand(s) help Display documentation for a command
Options: Options:
-q, --quiet -q, --quiet
@ -160,6 +161,8 @@ fn help_flag() {
-V, --version -V, --version
Print version Print version
Use `uv help` for more details.
----- stderr ----- ----- stderr -----
"###); "###);
} }
@ -182,7 +185,7 @@ fn help_short_flag() {
venv Create a virtual environment venv Create a virtual environment
cache Manage the cache cache Manage the cache
version Display uv's version version Display uv's version
help Print this message or the help of the given subcommand(s) help Display documentation for a command
Options: Options:
-q, --quiet -q, --quiet
@ -213,6 +216,8 @@ fn help_short_flag() {
-V, --version -V, --version
Print version Print version
Use `uv help` for more details.
----- stderr ----- ----- stderr -----
"###); "###);
} }
@ -225,6 +230,8 @@ fn help_subcommand() {
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr -----
Manage Python installations Manage Python installations
Usage: uv python [OPTIONS] <COMMAND> Usage: uv python [OPTIONS] <COMMAND>
@ -235,7 +242,6 @@ fn help_subcommand() {
find Search for a Python installation find Search for a Python installation
dir Show the uv Python installation directory dir Show the uv Python installation directory
uninstall Uninstall Python versions uninstall Uninstall Python versions
help Print this message or the help of the given subcommand(s)
Options: Options:
-q, --quiet -q, --quiet
@ -320,7 +326,6 @@ fn help_subcommand() {
-V, --version -V, --version
Print version Print version
----- stderr -----
"###); "###);
} }
@ -332,6 +337,8 @@ fn help_subsubcommand() {
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr -----
Download and install Python versions Download and install Python versions
Usage: uv python install [OPTIONS] [TARGETS]... Usage: uv python install [OPTIONS] [TARGETS]...
@ -430,7 +437,6 @@ fn help_subsubcommand() {
-V, --version -V, --version
Print version Print version
----- stderr -----
"###); "###);
} }
@ -452,7 +458,6 @@ fn help_flag_subcommand() {
find Search for a Python installation find Search for a Python installation
dir Show the uv Python installation directory dir Show the uv Python installation directory
uninstall Uninstall Python versions uninstall Uninstall Python versions
help Print this message or the help of the given subcommand(s)
Options: Options:
-q, --quiet -q, --quiet
@ -483,6 +488,8 @@ fn help_flag_subcommand() {
-V, --version -V, --version
Print version Print version
Use `uv help python` for more details.
----- stderr ----- ----- stderr -----
"###); "###);
} }
@ -547,11 +554,13 @@ fn help_unknown_subcommand() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: unrecognized subcommand 'foobar' error: There is no command `foobar` for `uv`. Did you mean one of:
pip
Usage: uv [OPTIONS] <COMMAND> tool
python
For more information, try '--help'. venv
cache
version
"###); "###);
uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r###" uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r###"
@ -560,11 +569,13 @@ fn help_unknown_subcommand() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: unrecognized subcommand 'foo' error: There is no command `foo bar` for `uv`. Did you mean one of:
pip
Usage: uv [OPTIONS] <COMMAND> tool
python
For more information, try '--help'. venv
cache
version
"###); "###);
} }
@ -578,11 +589,12 @@ fn help_unknown_subsubcommand() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: unrecognized subcommand 'foobar' error: There is no command `foobar` for `uv python`. Did you mean one of:
list
Usage: uv python [OPTIONS] <COMMAND> install
find
For more information, try '--help'. dir
uninstall
"###); "###);
} }
@ -591,16 +603,107 @@ fn help_with_global_option() {
let context = TestContext::new_with_versions(&[]); let context = TestContext::new_with_versions(&[]);
uv_snapshot!(context.filters(), context.help().arg("--cache-dir").arg("/dev/null"), @r###" uv_snapshot!(context.filters(), context.help().arg("--cache-dir").arg("/dev/null"), @r###"
success: false success: true
exit_code: 2 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: unrecognized subcommand '--cache-dir' An extremely fast Python package manager.
Usage: uv [OPTIONS] <COMMAND> Usage: uv [OPTIONS] <COMMAND>
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.
(<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)
--color <COLOR_CHOICE>
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 <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 <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 <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(&[]); let context = TestContext::new_with_versions(&[]);
uv_snapshot!(context.filters(), context.help().arg("--help"), @r###" uv_snapshot!(context.filters(), context.help().arg("--help"), @r###"
success: false success: true
exit_code: 2 exit_code: 0
----- stdout ----- ----- stdout -----
Display documentation for a command
Usage: uv help [OPTIONS] [COMMAND]...
----- stderr ----- ----- stderr -----
error: unrecognized subcommand '--help'
Usage: uv [OPTIONS] <COMMAND>
For more information, try '--help'.
"###); "###);
} }
@ -627,15 +728,11 @@ fn help_with_version() {
let context = TestContext::new_with_versions(&[]); let context = TestContext::new_with_versions(&[]);
uv_snapshot!(context.filters(), context.help().arg("--version"), @r###" uv_snapshot!(context.filters(), context.help().arg("--version"), @r###"
success: false success: true
exit_code: 2 exit_code: 0
----- stdout ----- ----- stdout -----
uv [VERSION] ([COMMIT] DATE)
----- stderr ----- ----- stderr -----
error: unrecognized subcommand '--version'
Usage: uv [OPTIONS] <COMMAND>
For more information, try '--help'.
"###); "###);
} }