mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Merge 0c732da527
into f609e1ddaf
This commit is contained in:
commit
7e818aae7b
8 changed files with 266 additions and 8 deletions
|
@ -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)]
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}|\/)")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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 -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue