mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25: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,
|
||||
}
|
||||
|
||||
#[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> {
|
||||
ExtraName::from_str(arg).map_err(|_err| {
|
||||
anyhow!(
|
||||
|
@ -4419,6 +4428,18 @@ pub struct ToolListArgs {
|
|||
|
||||
#[arg(long, hide = true)]
|
||||
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)]
|
||||
|
|
|
@ -4,8 +4,11 @@ use anyhow::Result;
|
|||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
use serde::Serialize;
|
||||
use uv_cache::Cache;
|
||||
use uv_cli::ToolListFormat;
|
||||
use uv_fs::Simplified;
|
||||
use uv_pep440::Version;
|
||||
use uv_tool::InstalledTools;
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
|
@ -15,6 +18,32 @@ use crate::printer::Printer;
|
|||
/// List installed tools.
|
||||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
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_version_specifiers: bool,
|
||||
show_with: bool,
|
||||
|
@ -146,3 +175,148 @@ pub(crate) async fn list(
|
|||
|
||||
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_with,
|
||||
args.show_extras,
|
||||
args.output_format,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
@ -11,8 +11,8 @@ use uv_cli::{
|
|||
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
||||
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
|
||||
PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs,
|
||||
SyncArgs, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs,
|
||||
VenvArgs, VersionArgs, VersionBump, VersionFormat,
|
||||
SyncArgs, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolListFormat, ToolRunArgs,
|
||||
ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs, VersionBump, VersionFormat,
|
||||
};
|
||||
use uv_cli::{
|
||||
AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs,
|
||||
|
@ -783,6 +783,7 @@ pub(crate) struct ToolListSettings {
|
|||
pub(crate) show_version_specifiers: bool,
|
||||
pub(crate) show_with: bool,
|
||||
pub(crate) show_extras: bool,
|
||||
pub(crate) output_format: ToolListFormat,
|
||||
}
|
||||
|
||||
impl ToolListSettings {
|
||||
|
@ -796,6 +797,7 @@ impl ToolListSettings {
|
|||
show_extras,
|
||||
python_preference: _,
|
||||
no_python_downloads: _,
|
||||
output_format,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
|
@ -803,6 +805,7 @@ impl ToolListSettings {
|
|||
show_version_specifiers,
|
||||
show_with,
|
||||
show_extras,
|
||||
output_format,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[
|
|||
(r"tv_sec: \d+", "tv_sec: [TIME]"),
|
||||
(r"tv_nsec: \d+", "tv_nsec: [TIME]"),
|
||||
// Rewrite Windows output to Unix output
|
||||
(r"\\([\w\d]|\.)", "/$1"),
|
||||
(r"\\{1,2}([\w\d]|\.)", "/$1"),
|
||||
(r"uv\.exe", "uv"),
|
||||
// uv version display
|
||||
(
|
||||
|
@ -219,7 +219,10 @@ impl TestContext {
|
|||
#[must_use]
|
||||
pub fn with_filtered_virtualenv_bin(mut self) -> Self {
|
||||
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(),
|
||||
));
|
||||
self
|
||||
|
@ -1166,11 +1169,12 @@ impl TestContext {
|
|||
fn path_pattern(path: impl AsRef<Path>) -> String {
|
||||
format!(
|
||||
// Trim the trailing separator for cross-platform directories filters
|
||||
r"{}\\?/?",
|
||||
r"{}\\{{0,2}}/?",
|
||||
regex::escape(&path.as_ref().simplified_display().to_string())
|
||||
// Make separators platform agnostic because on Windows we will display
|
||||
// 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]
|
||||
#[cfg(feature = "pypi")]
|
||||
#[cfg(not(windows))]
|
||||
fn list_format_json() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
|
|
|
@ -563,3 +563,54 @@ fn tool_list_show_extras() {
|
|||
----- 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>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>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>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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue