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)] #[arg(long)]
pub show_extras: bool, 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. // Hide unused global Python options.
#[arg(long, hide = true)] #[arg(long, hide = true)]
pub python_preference: Option<PythonPreference>, pub python_preference: Option<PythonPreference>,

View file

@ -26,6 +26,41 @@ pub use tool::{Tool, ToolEntrypoint};
mod receipt; mod receipt;
mod tool; 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)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
@ -50,6 +85,8 @@ pub enum Error {
EnvironmentRead(PathBuf, String), EnvironmentRead(PathBuf, String),
#[error("Failed find package `{0}` in tool environment")] #[error("Failed find package `{0}` in tool environment")]
MissingToolPackage(PackageName), MissingToolPackage(PackageName),
#[error("Tool `{0}` environment not found at `{1}`")]
ToolEnvironmentNotFound(PackageName, PathBuf),
} }
/// A collection of uv-managed tools installed on the current system. /// A collection of uv-managed tools installed on the current system.
@ -201,7 +238,7 @@ impl InstalledTools {
&self, &self,
name: &PackageName, name: &PackageName,
cache: &Cache, cache: &Cache,
) -> Result<Option<PythonEnvironment>, Error> { ) -> Result<Option<ToolEnvironment>, Error> {
let environment_path = self.tool_dir(name); let environment_path = self.tool_dir(name);
match PythonEnvironment::from_root(&environment_path, cache) { match PythonEnvironment::from_root(&environment_path, cache) {
@ -210,7 +247,7 @@ impl InstalledTools {
"Found existing environment for tool `{name}`: {}", "Found existing environment for tool `{name}`: {}",
environment_path.user_display() 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::MissingEnvironment(_)) => Ok(None),
Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound( 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. /// Initialize the tools directory.
/// ///
/// Ensures the directory is created. /// Ensures the directory is created.

View file

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

View file

@ -6,6 +6,7 @@ use owo_colors::OwoColorize;
use uv_cache::Cache; use uv_cache::Cache;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_python::LenientImplementationName;
use uv_tool::InstalledTools; use uv_tool::InstalledTools;
use uv_warnings::warn_user; use uv_warnings::warn_user;
@ -19,6 +20,7 @@ pub(crate) async fn list(
show_version_specifiers: bool, show_version_specifiers: bool,
show_with: bool, show_with: bool,
show_extras: bool, show_extras: bool,
show_python: bool,
cache: &Cache, cache: &Cache,
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
@ -50,8 +52,27 @@ pub(crate) async fn list(
continue; continue;
}; };
// Output tool name and version // Get the tool environment
let version = match installed_tools.version(&name, cache) { 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, Ok(version) => version,
Err(e) => { Err(e) => {
if let uv_tool::Error::EnvironmentError(e) = e { if let uv_tool::Error::EnvironmentError(e) = e {
@ -97,6 +118,18 @@ pub(crate) async fn list(
}) })
.unwrap_or_default(); .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 let with_requirements = show_with
.then(|| { .then(|| {
tool.requirements() tool.requirements()
@ -118,7 +151,7 @@ pub(crate) async fn list(
printer.stdout(), printer.stdout(),
"{} ({})", "{} ({})",
format!( format!(
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}" "{name} v{version}{version_specifier}{extra_requirements}{with_requirements}{python_version}"
) )
.bold(), .bold(),
installed_tools.tool_dir(&name).simplified_display().cyan(), installed_tools.tool_dir(&name).simplified_display().cyan(),
@ -128,7 +161,7 @@ pub(crate) async fn list(
printer.stdout(), printer.stdout(),
"{}", "{}",
format!( format!(
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}" "{name} v{version}{version_specifier}{extra_requirements}{with_requirements}{python_version}"
) )
.bold() .bold()
)?; )?;

View file

@ -460,8 +460,10 @@ async fn show_help(
.filter_map(|(name, tool)| { .filter_map(|(name, tool)| {
tool.ok().and_then(|_| { tool.ok().and_then(|_| {
installed_tools installed_tools
.version(&name, cache) .get_environment(&name, cache)
.ok() .ok()
.flatten()
.and_then(|tool_env| tool_env.version().ok())
.map(|version| (name, version)) .map(|version| (name, version))
}) })
}) })
@ -931,7 +933,7 @@ async fn get_or_create_environment(
.get_environment(&requirement.name, cache)? .get_environment(&requirement.name, cache)?
.filter(|environment| { .filter(|environment| {
python_request.as_ref().is_none_or(|python_request| { 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)?; let tags = pip::resolution_tags(None, python_platform.as_ref(), &interpreter)?;
// Check if the installed packages meet the requirements. // 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!( if matches!(
site_packages.satisfies_requirements( site_packages.satisfies_requirements(
requirements.iter(), requirements.iter(),
@ -984,7 +986,7 @@ async fn get_or_create_environment(
Ok(SatisfiesResult::Fresh { .. }) Ok(SatisfiesResult::Fresh { .. })
) { ) {
debug!("Using existing tool `{}`", requirement.name); 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 // Check if we need to create a new environment — if so, resolve it first, then
// install the requested tool // install the requested tool
let (environment, outcome) = if let Some(interpreter) = 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. // If we're using a new interpreter, re-create the environment for each tool.
let resolution = resolve_environment( let resolution = resolve_environment(
@ -388,7 +388,7 @@ async fn upgrade_tool(
environment, environment,
changelog, changelog,
} = update_environment( } = update_environment(
environment, environment.into_environment(),
spec, spec,
Modifications::Exact, Modifications::Exact,
python_platform, python_platform,

View file

@ -1421,6 +1421,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.show_python,
&cache, &cache,
printer, printer,
) )

View file

@ -859,6 +859,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) show_python: bool,
} }
impl ToolListSettings { impl ToolListSettings {
@ -870,6 +871,7 @@ impl ToolListSettings {
show_version_specifiers, show_version_specifiers,
show_with, show_with,
show_extras, show_extras,
show_python,
python_preference: _, python_preference: _,
no_python_downloads: _, no_python_downloads: _,
} = args; } = args;
@ -879,6 +881,7 @@ impl ToolListSettings {
show_version_specifiers, show_version_specifiers,
show_with, show_with,
show_extras, show_extras,
show_python,
} }
} }
} }

View file

@ -563,3 +563,81 @@ fn tool_list_show_extras() {
----- stderr ----- ----- 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 -----
"###);
}

View file

@ -3047,6 +3047,7 @@ uv tool list [OPTIONS]
<p>Repeating this option, e.g., <code>-qq</code>, will enable a silent mode in which uv will write no output to stdout.</p> <p>Repeating this option, e.g., <code>-qq</code>, will enable a silent mode in which uv will write no output to stdout.</p>
</dd><dt id="uv-tool-list--show-extras"><a href="#uv-tool-list--show-extras"><code>--show-extras</code></a></dt><dd><p>Whether to display the extra requirements installed with each tool</p> </dd><dt id="uv-tool-list--show-extras"><a href="#uv-tool-list--show-extras"><code>--show-extras</code></a></dt><dd><p>Whether to display the extra requirements installed with each tool</p>
</dd><dt id="uv-tool-list--show-paths"><a href="#uv-tool-list--show-paths"><code>--show-paths</code></a></dt><dd><p>Whether to display the path to each tool environment and installed executable</p> </dd><dt id="uv-tool-list--show-paths"><a href="#uv-tool-list--show-paths"><code>--show-paths</code></a></dt><dd><p>Whether to display the path to each tool environment and installed executable</p>
</dd><dt id="uv-tool-list--show-python"><a href="#uv-tool-list--show-python"><code>--show-python</code></a></dt><dd><p>Whether to display the Python version associated with run each tool</p>
</dd><dt id="uv-tool-list--show-version-specifiers"><a href="#uv-tool-list--show-version-specifiers"><code>--show-version-specifiers</code></a></dt><dd><p>Whether to display the version specifier(s) used to install each tool</p> </dd><dt id="uv-tool-list--show-version-specifiers"><a href="#uv-tool-list--show-version-specifiers"><code>--show-version-specifiers</code></a></dt><dd><p>Whether to display the version specifier(s) used to install each tool</p>
</dd><dt id="uv-tool-list--show-with"><a href="#uv-tool-list--show-with"><code>--show-with</code></a></dt><dd><p>Whether to display the additional requirements installed with each tool</p> </dd><dt id="uv-tool-list--show-with"><a href="#uv-tool-list--show-with"><code>--show-with</code></a></dt><dd><p>Whether to display the additional requirements installed with each tool</p>
</dd><dt id="uv-tool-list--verbose"><a href="#uv-tool-list--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p> </dd><dt id="uv-tool-list--verbose"><a href="#uv-tool-list--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>