feat: add tool version to list command (#4674)

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->
Closes #4653

## Summary
Adds the tool version to the list command right beside the tool name

```
$ uv tool list
black v24.2.0
```

Following the proposed format discussed in #4653


## Test Plan
`cargo test tool_list`

<!-- How was it tested? -->

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
Caíque Porfirio 2024-07-03 15:24:37 -03:00 committed by GitHub
parent d24b075b2d
commit c17761904e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 100 additions and 8 deletions

1
Cargo.lock generated
View file

@ -5094,6 +5094,7 @@ dependencies = [
"tracing",
"uv-cache",
"uv-fs",
"uv-installer",
"uv-python",
"uv-state",
"uv-virtualenv",

View file

@ -23,6 +23,7 @@ uv-state = { workspace = true }
uv-python = { workspace = true }
uv-virtualenv = { workspace = true }
uv-warnings = { workspace = true }
uv-installer = { workspace = true }
dirs-sys = { workspace = true }
fs-err = { workspace = true }

View file

@ -1,19 +1,25 @@
use core::fmt;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use fs_err as fs;
use pep440_rs::Version;
use pep508_rs::{InvalidNameError, PackageName};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use fs_err::File;
use thiserror::Error;
use tracing::debug;
use install_wheel_rs::read_record_file;
use pep440_rs::Version;
use pep508_rs::PackageName;
pub use receipt::ToolReceipt;
pub use tool::{Tool, ToolEntrypoint};
use uv_cache::Cache;
use uv_fs::{LockedFile, Simplified};
use uv_installer::SitePackages;
use uv_python::{Interpreter, PythonEnvironment};
use uv_state::{StateBucket, StateStore};
use uv_warnings::warn_user_once;
@ -38,9 +44,15 @@ pub enum Error {
#[error("Failed to find a directory for executables")]
NoExecutableDirectory,
#[error(transparent)]
ToolName(#[from] InvalidNameError),
#[error(transparent)]
EnvironmentError(#[from] uv_python::Error),
#[error("Failed to find a receipt for tool `{0}` at {1}")]
MissingToolReceipt(String, PathBuf),
#[error("Failed to read tool environment packages at `{0}`: {1}")]
EnvironmentRead(PathBuf, String),
#[error("Failed find tool package `{0}` at `{1}`")]
MissingToolPackage(PackageName, PathBuf),
}
/// A collection of uv-managed tools installed on the current system.
@ -230,6 +242,19 @@ impl InstalledTools {
))
}
pub fn version(&self, name: &str, cache: &Cache) -> Result<Version, Error> {
let environment_path = self.root.join(name);
let package_name = PackageName::from_str(name)?;
let environment = PythonEnvironment::from_root(&environment_path, cache)?;
let site_packages = SitePackages::from_environment(&environment)
.map_err(|err| Error::EnvironmentRead(environment_path.clone(), err.to_string()))?;
let packages = site_packages.get_packages(&package_name);
let package = packages
.first()
.ok_or_else(|| Error::MissingToolPackage(package_name, environment_path))?;
Ok(package.version().clone())
}
/// Initialize the tools directory.
///
/// Ensures the directory is created.

View file

@ -2,6 +2,7 @@ use std::fmt::Write;
use anyhow::Result;
use uv_cache::Cache;
use uv_configuration::PreviewMode;
use uv_tool::InstalledTools;
use uv_warnings::warn_user_once;
@ -26,8 +27,17 @@ pub(crate) async fn list(preview: PreviewMode, printer: Printer) -> Result<ExitS
}
for (name, tool) in tools {
// Output tool name
writeln!(printer.stdout(), "{name}")?;
// Output tool name and version
let version =
match installed_tools.version(&name, &Cache::from_path(installed_tools.root())) {
Ok(version) => version,
Err(e) => {
writeln!(printer.stderr(), "{e}")?;
continue;
}
};
writeln!(printer.stdout(), "{name} v{version}")?;
// Output tool entrypoints
for entrypoint in tool.entrypoints() {

View file

@ -1,9 +1,11 @@
#![cfg(all(feature = "python", feature = "pypi"))]
use fs_err as fs;
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::PathChild;
use common::{uv_snapshot, TestContext};
mod common;
#[test]
@ -27,7 +29,7 @@ fn tool_list() {
success: true
exit_code: 0
----- stdout -----
black
black v24.2.0
black
blackd
@ -85,3 +87,56 @@ fn tool_list_missing_receipt() {
No tools installed
"###);
}
#[test]
fn tool_list_bad_environment() -> Result<()> {
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`
context
.tool_install()
.arg("black==24.2.0")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.assert()
.success();
// Install `ruff`
context
.tool_install()
.arg("ruff==0.3.4")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.assert()
.success();
let venv_path = common::venv_bin_path(tool_dir.path().join("black"));
// Remove the python interpreter for black
fs::remove_dir_all(venv_path.clone())?;
let mut filters = context.filters().clone();
filters.push((r"/black/.*", "/black/[VENV_PATH]`"));
uv_snapshot!(
filters,
context
.tool_list()
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str()),
@r###"
success: true
exit_code: 0
----- stdout -----
ruff v0.3.4
ruff
----- stderr -----
warning: `uv tool list` is experimental and may change without warning.
Python interpreter not found at `[TEMP_DIR]/tools/black/[VENV_PATH]`
"###
);
Ok(())
}