This commit is contained in:
InSync 2025-07-07 00:32:34 +00:00 committed by GitHub
commit 7e818aae7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 266 additions and 8 deletions

View file

@ -58,6 +58,15 @@ pub enum ListFormat {
Json, Json,
} }
#[derive(Debug, Default, Clone, Copy, clap::ValueEnum)]
pub enum ToolListFormat {
/// Display the list of tools as plain text.
#[default]
Text,
/// Display the list of tools as JSON.
Json,
}
fn extra_name_with_clap_error(arg: &str) -> Result<ExtraName> { fn extra_name_with_clap_error(arg: &str) -> Result<ExtraName> {
ExtraName::from_str(arg).map_err(|_err| { ExtraName::from_str(arg).map_err(|_err| {
anyhow!( anyhow!(
@ -4419,6 +4428,18 @@ pub struct ToolListArgs {
#[arg(long, hide = true)] #[arg(long, hide = true)]
pub no_python_downloads: bool, pub no_python_downloads: bool,
/// The format in which the list of tools would be displayed.
#[arg(
long,
value_enum,
default_value_t = ToolListFormat::default(),
conflicts_with = "show_paths",
conflicts_with = "show_version_specifiers",
conflicts_with = "show_with",
conflicts_with = "show_extras"
)]
pub output_format: ToolListFormat,
} }
#[derive(Args)] #[derive(Args)]

View file

@ -4,8 +4,11 @@ use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use serde::Serialize;
use uv_cache::Cache; use uv_cache::Cache;
use uv_cli::ToolListFormat;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_pep440::Version;
use uv_tool::InstalledTools; use uv_tool::InstalledTools;
use uv_warnings::warn_user; use uv_warnings::warn_user;
@ -15,6 +18,32 @@ use crate::printer::Printer;
/// List installed tools. /// List installed tools.
#[allow(clippy::fn_params_excessive_bools)] #[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn list( pub(crate) async fn list(
show_paths: bool,
show_version_specifiers: bool,
show_with: bool,
show_extras: bool,
output_format: ToolListFormat,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
match output_format {
ToolListFormat::Text => {
list_text(
show_paths,
show_version_specifiers,
show_with,
show_extras,
cache,
printer,
)
.await
}
ToolListFormat::Json => list_json(cache, printer).await,
}
}
#[allow(clippy::fn_params_excessive_bools)]
async fn list_text(
show_paths: bool, show_paths: bool,
show_version_specifiers: bool, show_version_specifiers: bool,
show_with: bool, show_with: bool,
@ -146,3 +175,148 @@ pub(crate) async fn list(
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
#[derive(Serialize)]
#[serde(untagged)]
enum ToolListEntry {
Tool {
name: String,
version: Version,
version_specifiers: Vec<String>,
extra_requirements: Vec<String>,
with_requirements: Vec<String>,
directory: String,
environment: EnvironmentInfo,
entrypoints: Vec<Entrypoint>,
},
MalformedTool {
name: String,
},
Error {
name: String,
error: String,
},
}
#[derive(Serialize)]
#[serde(untagged)]
enum EnvironmentInfo {
Environment { python: String, version: Version },
NoEnvironment,
Error { error: String },
}
#[derive(Serialize)]
struct Entrypoint {
name: String,
path: String,
}
async fn list_json(cache: &Cache, printer: Printer) -> Result<ExitStatus> {
let installed_tools = InstalledTools::from_settings()?;
match installed_tools.lock().await {
Ok(_lock) => (),
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
writeln!(printer.stdout(), "[]")?;
return Ok(ExitStatus::Success);
}
Err(err) => return Err(err.into()),
}
let tools = installed_tools.tools()?;
if tools.is_empty() {
writeln!(printer.stdout(), "[]")?;
return Ok(ExitStatus::Success);
}
let tool_list = tools
.into_iter()
.sorted_by_cached_key(|(name, _)| name.clone())
.map(|(name, tool)| match tool {
Err(_) => ToolListEntry::MalformedTool {
name: name.to_string(),
},
Ok(tool) => {
let version = match installed_tools.version(&name, cache) {
Ok(version) => version,
Err(error) => {
return ToolListEntry::Error {
name: name.to_string(),
error: error.to_string(),
};
}
};
let mut version_specifiers = vec![];
let mut extra_requirements = vec![];
let mut with_requirements = vec![];
tool.requirements().iter().for_each(|req| {
if req.name == name {
let specifier = req.source.to_string();
if !specifier.is_empty() {
version_specifiers.push(specifier);
}
for extra in &req.extras {
extra_requirements.push(extra.to_string());
}
} else {
with_requirements.push(format!("{}{}", req.name, req.source));
}
});
let directory = installed_tools.tool_dir(&name).display().to_string();
let environment = match installed_tools.get_environment(&name, cache) {
Ok(None) => EnvironmentInfo::NoEnvironment,
Err(error) => EnvironmentInfo::Error {
error: error.to_string(),
},
Ok(Some(environment)) => {
let python_executable = environment.python_executable();
let interpreter = environment.interpreter();
EnvironmentInfo::Environment {
python: python_executable.display().to_string(),
version: interpreter.python_version().clone(),
}
}
};
let entrypoints = tool
.entrypoints()
.iter()
.map(|entrypoint| {
let name = entrypoint.name.to_string();
let path = entrypoint.install_path.display().to_string();
let path = if cfg!(windows) {
path.replace('/', "\\")
} else {
path
};
Entrypoint { name, path }
})
.collect::<Vec<_>>();
ToolListEntry::Tool {
name: name.to_string(),
version,
version_specifiers,
extra_requirements,
with_requirements,
directory,
environment,
entrypoints,
}
}
})
.collect::<Vec<_>>();
writeln!(printer.stdout(), "{}", serde_json::to_string(&tool_list)?)?;
Ok(ExitStatus::Success)
}

View file

@ -1300,6 +1300,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.show_version_specifiers, args.show_version_specifiers,
args.show_with, args.show_with,
args.show_extras, args.show_extras,
args.output_format,
&cache, &cache,
printer, printer,
) )

View file

@ -11,8 +11,8 @@ use uv_cli::{
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs, PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs, PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs,
SyncArgs, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, SyncArgs, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolListFormat, ToolRunArgs,
VenvArgs, VersionArgs, VersionBump, VersionFormat, ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs, VersionBump, VersionFormat,
}; };
use uv_cli::{ use uv_cli::{
AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs, AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs,
@ -783,6 +783,7 @@ pub(crate) struct ToolListSettings {
pub(crate) show_version_specifiers: bool, pub(crate) show_version_specifiers: bool,
pub(crate) show_with: bool, pub(crate) show_with: bool,
pub(crate) show_extras: bool, pub(crate) show_extras: bool,
pub(crate) output_format: ToolListFormat,
} }
impl ToolListSettings { impl ToolListSettings {
@ -796,6 +797,7 @@ impl ToolListSettings {
show_extras, show_extras,
python_preference: _, python_preference: _,
no_python_downloads: _, no_python_downloads: _,
output_format,
} = args; } = args;
Self { Self {
@ -803,6 +805,7 @@ impl ToolListSettings {
show_version_specifiers, show_version_specifiers,
show_with, show_with,
show_extras, show_extras,
output_format,
} }
} }
} }

View file

@ -62,7 +62,7 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[
(r"tv_sec: \d+", "tv_sec: [TIME]"), (r"tv_sec: \d+", "tv_sec: [TIME]"),
(r"tv_nsec: \d+", "tv_nsec: [TIME]"), (r"tv_nsec: \d+", "tv_nsec: [TIME]"),
// Rewrite Windows output to Unix output // Rewrite Windows output to Unix output
(r"\\([\w\d]|\.)", "/$1"), (r"\\{1,2}([\w\d]|\.)", "/$1"),
(r"uv\.exe", "uv"), (r"uv\.exe", "uv"),
// uv version display // uv version display
( (
@ -219,7 +219,10 @@ impl TestContext {
#[must_use] #[must_use]
pub fn with_filtered_virtualenv_bin(mut self) -> Self { pub fn with_filtered_virtualenv_bin(mut self) -> Self {
self.filters.push(( self.filters.push((
format!(r"[\\/]{}", venv_bin_path(PathBuf::new()).to_string_lossy()), format!(
r"(?:\\{{1,2}}|/){}",
venv_bin_path(PathBuf::new()).to_string_lossy()
),
"/[BIN]".to_string(), "/[BIN]".to_string(),
)); ));
self self
@ -1166,11 +1169,12 @@ impl TestContext {
fn path_pattern(path: impl AsRef<Path>) -> String { fn path_pattern(path: impl AsRef<Path>) -> String {
format!( format!(
// Trim the trailing separator for cross-platform directories filters // Trim the trailing separator for cross-platform directories filters
r"{}\\?/?", r"{}\\{{0,2}}/?",
regex::escape(&path.as_ref().simplified_display().to_string()) regex::escape(&path.as_ref().simplified_display().to_string())
// Make separators platform agnostic because on Windows we will display // Make separators platform agnostic because on Windows we will display
// paths with Unix-style separators sometimes // paths with Unix-style separators sometimes
.replace(r"\\", r"(\\|\/)") // (Double-backslashes is for JSON-ified Windows paths.)
.replace(r"\\", r"(\\{1,2}|\/)")
) )
} }

View file

@ -483,7 +483,6 @@ fn list_exclude() {
#[test] #[test]
#[cfg(feature = "pypi")] #[cfg(feature = "pypi")]
#[cfg(not(windows))]
fn list_format_json() { fn list_format_json() {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");

View file

@ -563,3 +563,54 @@ fn tool_list_show_extras() {
----- stderr ----- ----- stderr -----
"###); "###);
} }
#[test]
fn tool_list_output_format_json() {
let context = TestContext::new("3.12").with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
// Install `black` without extras
context
.tool_install()
.arg("black==24.2.0")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.assert()
.success();
// Install `flask` with extras and additional requirements
context
.tool_install()
.arg("flask[async,dotenv]")
.arg("--with")
.arg("requests")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.assert()
.success();
if cfg!(windows) {
uv_snapshot!(context.filters(), context.tool_list().arg("--output-format=json")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
[{"name":"black","version":"24.2.0","version_specifiers":["==24.2.0"],"extra_requirements":[],"with_requirements":[],"directory":"[TEMP_DIR]/tools/black","environment":{"python":"[TEMP_DIR]/tools/black/Scripts/python","version":"3.12.[X]"},"entrypoints":[{"name":"black","path":"[TEMP_DIR]/bin/black"},{"name":"blackd","path":"[TEMP_DIR]/bin/blackd"}]},{"name":"flask","version":"3.0.2","version_specifiers":[],"extra_requirements":["async","dotenv"],"with_requirements":["requests"],"directory":"[TEMP_DIR]/tools/flask","environment":{"python":"[TEMP_DIR]/tools/flask/Scripts/python","version":"3.12.[X]"},"entrypoints":[{"name":"flask","path":"[TEMP_DIR]/bin/flask"}]}]
----- stderr -----
"###);
} else {
uv_snapshot!(context.filters(), context.tool_list().arg("--output-format=json")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
[{"name":"black","version":"24.2.0","version_specifiers":["==24.2.0"],"extra_requirements":[],"with_requirements":[],"directory":"[TEMP_DIR]/tools/black","environment":{"python":"[TEMP_DIR]/tools/black/bin/python3","version":"3.12.[X]"},"entrypoints":[{"name":"black","path":"[TEMP_DIR]/bin/black"},{"name":"blackd","path":"[TEMP_DIR]/bin/blackd"}]},{"name":"flask","version":"3.0.2","version_specifiers":[],"extra_requirements":["async","dotenv"],"with_requirements":["requests"],"directory":"[TEMP_DIR]/tools/flask","environment":{"python":"[TEMP_DIR]/tools/flask/bin/python3","version":"3.12.[X]"},"entrypoints":[{"name":"flask","path":"[TEMP_DIR]/bin/flask"}]}]
----- stderr -----
"###);
}
}

View file

@ -2298,7 +2298,12 @@ uv tool list [OPTIONS]
<p>For example, spinners or progress bars.</p> <p>For example, spinners or progress bars.</p>
<p>May also be set with the <code>UV_NO_PROGRESS</code> environment variable.</p></dd><dt id="uv-tool-list--offline"><a href="#uv-tool-list--offline"><code>--offline</code></a></dt><dd><p>Disable network access.</p> <p>May also be set with the <code>UV_NO_PROGRESS</code> environment variable.</p></dd><dt id="uv-tool-list--offline"><a href="#uv-tool-list--offline"><code>--offline</code></a></dt><dd><p>Disable network access.</p>
<p>When disabled, uv will only use locally cached data and locally available files.</p> <p>When disabled, uv will only use locally cached data and locally available files.</p>
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-tool-list--project"><a href="#uv-tool-list--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p> <p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-tool-list--output-format"><a href="#uv-tool-list--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format in which the list of tools would be displayed</p>
<p>[default: text]</p><p>Possible values:</p>
<ul>
<li><code>text</code>: Display the list of tools as plain text</li>
<li><code>json</code>: Display the list of tools as JSON</li>
</ul></dd><dt id="uv-tool-list--project"><a href="#uv-tool-list--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code>, <code>uv.toml</code>, and <code>.python-version</code> files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (<code>.venv</code>).</p> <p>All <code>pyproject.toml</code>, <code>uv.toml</code>, and <code>.python-version</code> files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (<code>.venv</code>).</p>
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p> <p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
<p>See <code>--directory</code> to change the working directory entirely.</p> <p>See <code>--directory</code> to change the working directory entirely.</p>