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(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<ColorChoice> 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<ProjectCommand>),
/// 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<Vec<String>>,
}
#[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)]

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_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;

View file

@ -217,6 +217,9 @@ async fn run() -> Result<ExitStatus> {
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),
}) => {

View file

@ -11,6 +11,8 @@ fn help() {
success: true
exit_code: 0
----- stdout -----
----- stderr -----
An extremely fast Python package manager.
Usage: uv [OPTIONS] <COMMAND>
@ -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] <COMMAND>
@ -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] <COMMAND>
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] <COMMAND>
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] <COMMAND>
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] <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(&[]);
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
Usage: uv help [OPTIONS] [COMMAND]...
----- 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(&[]);
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] <COMMAND>
For more information, try '--help'.
"###);
}