From c17761904ebe79e6085938c1a6ffcea95045f9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=C3=ADque=20Porfirio?= <56317416+caiquejjx@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:24:37 -0300 Subject: [PATCH] feat: add tool version to list command (#4674) 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` --------- Co-authored-by: Zanie Blue --- Cargo.lock | 1 + crates/uv-tool/Cargo.toml | 1 + crates/uv-tool/src/lib.rs | 33 ++++++++++++++-- crates/uv/src/commands/tool/list.rs | 14 ++++++- crates/uv/tests/tool_list.rs | 59 ++++++++++++++++++++++++++++- 5 files changed, 100 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec9628471..a5e99e3c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5094,6 +5094,7 @@ dependencies = [ "tracing", "uv-cache", "uv-fs", + "uv-installer", "uv-python", "uv-state", "uv-virtualenv", diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index 26b276b89..db7e5fc3c 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -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 } diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index b07c79d3e..7b5bc5426 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -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 { + 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. diff --git a/crates/uv/src/commands/tool/list.rs b/crates/uv/src/commands/tool/list.rs index 523bd298e..ec2ba87fa 100644 --- a/crates/uv/src/commands/tool/list.rs +++ b/crates/uv/src/commands/tool/list.rs @@ -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 version, + Err(e) => { + writeln!(printer.stderr(), "{e}")?; + continue; + } + }; + + writeln!(printer.stdout(), "{name} v{version}")?; // Output tool entrypoints for entrypoint in tool.entrypoints() { diff --git a/crates/uv/tests/tool_list.rs b/crates/uv/tests/tool_list.rs index 80806b669..b4f8cdf52 100644 --- a/crates/uv/tests/tool_list.rs +++ b/crates/uv/tests/tool_list.rs @@ -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(()) +}