Add uv tool list --show-python (#15814)

<!--
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 #15312 
Closes https://github.com/astral-sh/uv/issues/16237

---------

Co-authored-by: pythonweb2 <32141163+pythonweb2@users.noreply.github.com>
Co-authored-by: Wade Roberts <wade.roberts@centralsquare.com>
This commit is contained in:
Harshith VH 2025-10-10 23:03:26 +05:30 committed by GitHub
parent 6fb00a9936
commit b4168e665e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 175 additions and 29 deletions

View file

@ -4899,6 +4899,10 @@ pub struct ToolListArgs {
#[arg(long)]
pub show_extras: bool,
/// Whether to display the Python version associated with run each tool.
#[arg(long)]
pub show_python: bool,
// Hide unused global Python options.
#[arg(long, hide = true)]
pub python_preference: Option<PythonPreference>,

View file

@ -26,6 +26,41 @@ pub use tool::{Tool, ToolEntrypoint};
mod receipt;
mod tool;
/// A wrapper around [`PythonEnvironment`] for tools that provides additional functionality.
#[derive(Debug, Clone)]
pub struct ToolEnvironment {
environment: PythonEnvironment,
name: PackageName,
}
impl ToolEnvironment {
pub fn new(environment: PythonEnvironment, name: PackageName) -> Self {
Self { environment, name }
}
/// Return the [`Version`] of the tool package in this environment.
pub fn version(&self) -> Result<Version, Error> {
let site_packages = SitePackages::from_environment(&self.environment).map_err(|err| {
Error::EnvironmentRead(self.environment.root().to_path_buf(), err.to_string())
})?;
let packages = site_packages.get_packages(&self.name);
let package = packages
.first()
.ok_or_else(|| Error::MissingToolPackage(self.name.clone()))?;
Ok(package.version().clone())
}
/// Get the underlying [`PythonEnvironment`].
pub fn into_environment(self) -> PythonEnvironment {
self.environment
}
/// Get a reference to the underlying [`PythonEnvironment`].
pub fn environment(&self) -> &PythonEnvironment {
&self.environment
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
@ -50,6 +85,8 @@ pub enum Error {
EnvironmentRead(PathBuf, String),
#[error("Failed find package `{0}` in tool environment")]
MissingToolPackage(PackageName),
#[error("Tool `{0}` environment not found at `{1}`")]
ToolEnvironmentNotFound(PackageName, PathBuf),
}
/// A collection of uv-managed tools installed on the current system.
@ -201,7 +238,7 @@ impl InstalledTools {
&self,
name: &PackageName,
cache: &Cache,
) -> Result<Option<PythonEnvironment>, Error> {
) -> Result<Option<ToolEnvironment>, Error> {
let environment_path = self.tool_dir(name);
match PythonEnvironment::from_root(&environment_path, cache) {
@ -210,7 +247,7 @@ impl InstalledTools {
"Found existing environment for tool `{name}`: {}",
environment_path.user_display()
);
Ok(Some(venv))
Ok(Some(ToolEnvironment::new(venv, name.clone())))
}
Err(uv_python::Error::MissingEnvironment(_)) => Ok(None),
Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(
@ -290,19 +327,6 @@ impl InstalledTools {
))
}
/// Return the [`Version`] of an installed tool.
pub fn version(&self, name: &PackageName, cache: &Cache) -> Result<Version, Error> {
let environment_path = self.tool_dir(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(name);
let package = packages
.first()
.ok_or_else(|| Error::MissingToolPackage(name.clone()))?;
Ok(package.version().clone())
}
/// Initialize the tools directory.
///
/// Ensures the directory is created.

View file

@ -348,11 +348,11 @@ pub(crate) async fn install(
installed_tools
.get_environment(package_name, &cache)?
.filter(|environment| {
if environment.uses(&interpreter) {
if environment.environment().uses(&interpreter) {
trace!(
"Existing interpreter matches the requested interpreter for `{}`: {}",
package_name,
environment.interpreter().sys_executable().display()
environment.environment().interpreter().sys_executable().display()
);
true
} else {
@ -399,7 +399,7 @@ pub(crate) async fn install(
let tags = resolution_tags(None, python_platform.as_ref(), &interpreter)?;
// Check if the installed packages meet the requirements.
let site_packages = SitePackages::from_environment(environment)?;
let site_packages = SitePackages::from_environment(environment.environment())?;
if matches!(
site_packages.satisfies_requirements(
requirements.iter(),
@ -461,7 +461,7 @@ pub(crate) async fn install(
// be invalidated by moving the environment.
let environment = if let Some(environment) = existing_environment {
let environment = match update_environment(
environment,
environment.into_environment(),
spec,
Modifications::Exact,
python_platform.as_ref(),

View file

@ -6,6 +6,7 @@ use owo_colors::OwoColorize;
use uv_cache::Cache;
use uv_fs::Simplified;
use uv_python::LenientImplementationName;
use uv_tool::InstalledTools;
use uv_warnings::warn_user;
@ -19,6 +20,7 @@ pub(crate) async fn list(
show_version_specifiers: bool,
show_with: bool,
show_extras: bool,
show_python: bool,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
@ -50,8 +52,27 @@ pub(crate) async fn list(
continue;
};
// Output tool name and version
let version = match installed_tools.version(&name, cache) {
// Get the tool environment
let tool_env = match installed_tools.get_environment(&name, cache) {
Ok(Some(env)) => env,
Ok(None) => {
warn_user!(
"Tool `{name}` environment not found (run `{}` to reinstall)",
format!("uv tool install {name} --reinstall").green()
);
continue;
}
Err(e) => {
warn_user!(
"{e} (run `{}` to reinstall)",
format!("uv tool install {name} --reinstall").green()
);
continue;
}
};
// Get the tool version
let version = match tool_env.version() {
Ok(version) => version,
Err(e) => {
if let uv_tool::Error::EnvironmentError(e) = e {
@ -97,6 +118,18 @@ pub(crate) async fn list(
})
.unwrap_or_default();
let python_version = if show_python {
let interpreter = tool_env.environment().interpreter();
let implementation = LenientImplementationName::from(interpreter.implementation_name());
format!(
" [{} {}]",
implementation.pretty(),
interpreter.python_full_version()
)
} else {
String::new()
};
let with_requirements = show_with
.then(|| {
tool.requirements()
@ -118,7 +151,7 @@ pub(crate) async fn list(
printer.stdout(),
"{} ({})",
format!(
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}"
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}{python_version}"
)
.bold(),
installed_tools.tool_dir(&name).simplified_display().cyan(),
@ -128,7 +161,7 @@ pub(crate) async fn list(
printer.stdout(),
"{}",
format!(
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}"
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}{python_version}"
)
.bold()
)?;

View file

@ -460,8 +460,10 @@ async fn show_help(
.filter_map(|(name, tool)| {
tool.ok().and_then(|_| {
installed_tools
.version(&name, cache)
.get_environment(&name, cache)
.ok()
.flatten()
.and_then(|tool_env| tool_env.version().ok())
.map(|version| (name, version))
})
})
@ -931,7 +933,7 @@ async fn get_or_create_environment(
.get_environment(&requirement.name, cache)?
.filter(|environment| {
python_request.as_ref().is_none_or(|python_request| {
python_request.satisfied(environment.interpreter(), cache)
python_request.satisfied(environment.environment().interpreter(), cache)
})
});
@ -967,7 +969,7 @@ async fn get_or_create_environment(
let tags = pip::resolution_tags(None, python_platform.as_ref(), &interpreter)?;
// Check if the installed packages meet the requirements.
let site_packages = SitePackages::from_environment(&environment)?;
let site_packages = SitePackages::from_environment(environment.environment())?;
if matches!(
site_packages.satisfies_requirements(
requirements.iter(),
@ -984,7 +986,7 @@ async fn get_or_create_environment(
Ok(SatisfiesResult::Fresh { .. })
) {
debug!("Using existing tool `{}`", requirement.name);
return Ok((from, environment));
return Ok((from, environment.into_environment()));
}
}
}

View file

@ -341,7 +341,7 @@ async fn upgrade_tool(
// Check if we need to create a new environment — if so, resolve it first, then
// install the requested tool
let (environment, outcome) = if let Some(interpreter) =
interpreter.filter(|interpreter| !environment.uses(interpreter))
interpreter.filter(|interpreter| !environment.environment().uses(interpreter))
{
// If we're using a new interpreter, re-create the environment for each tool.
let resolution = resolve_environment(
@ -388,7 +388,7 @@ async fn upgrade_tool(
environment,
changelog,
} = update_environment(
environment,
environment.into_environment(),
spec,
Modifications::Exact,
python_platform,

View file

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

View file

@ -859,6 +859,7 @@ pub(crate) struct ToolListSettings {
pub(crate) show_version_specifiers: bool,
pub(crate) show_with: bool,
pub(crate) show_extras: bool,
pub(crate) show_python: bool,
}
impl ToolListSettings {
@ -870,6 +871,7 @@ impl ToolListSettings {
show_version_specifiers,
show_with,
show_extras,
show_python,
python_preference: _,
no_python_downloads: _,
} = args;
@ -879,6 +881,7 @@ impl ToolListSettings {
show_version_specifiers,
show_with,
show_extras,
show_python,
}
}
}

View file

@ -563,3 +563,81 @@ fn tool_list_show_extras() {
----- stderr -----
"###);
}
#[test]
fn tool_list_show_python() {
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` with python 3.12
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();
// Test with --show-python
uv_snapshot!(context.filters(), context.tool_list().arg("--show-python")
.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 -----
black v24.2.0 [CPython 3.12.[X]]
- black
- blackd
----- stderr -----
"###);
}
#[test]
fn tool_list_show_all() {
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();
// Test with all flags
uv_snapshot!(context.filters(), context.tool_list()
.arg("--show-extras")
.arg("--show-with")
.arg("--show-version-specifiers")
.arg("--show-paths")
.arg("--show-python")
.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 -----
black v24.2.0 [required: ==24.2.0] [CPython 3.12.[X]] ([TEMP_DIR]/tools/black)
- black ([TEMP_DIR]/bin/black)
- blackd ([TEMP_DIR]/bin/blackd)
flask v3.0.2 [extras: async, dotenv] [with: requests] [CPython 3.12.[X]] ([TEMP_DIR]/tools/flask)
- flask ([TEMP_DIR]/bin/flask)
----- stderr -----
"###);
}