Skip to content

Commit b4168e6

Browse files
harshithvhpythonweb2Wade Roberts
authored
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 #16237 --------- Co-authored-by: pythonweb2 <32141163+pythonweb2@users.noreply.github.com> Co-authored-by: Wade Roberts <wade.roberts@centralsquare.com>
1 parent 6fb00a9 commit b4168e6

10 files changed

Lines changed: 175 additions & 29 deletions

File tree

crates/uv-cli/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4899,6 +4899,10 @@ pub struct ToolListArgs {
48994899
#[arg(long)]
49004900
pub show_extras: bool,
49014901

4902+
/// Whether to display the Python version associated with run each tool.
4903+
#[arg(long)]
4904+
pub show_python: bool,
4905+
49024906
// Hide unused global Python options.
49034907
#[arg(long, hide = true)]
49044908
pub python_preference: Option<PythonPreference>,

crates/uv-tool/src/lib.rs

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,41 @@ pub use tool::{Tool, ToolEntrypoint};
2626
mod receipt;
2727
mod tool;
2828

29+
/// A wrapper around [`PythonEnvironment`] for tools that provides additional functionality.
30+
#[derive(Debug, Clone)]
31+
pub struct ToolEnvironment {
32+
environment: PythonEnvironment,
33+
name: PackageName,
34+
}
35+
36+
impl ToolEnvironment {
37+
pub fn new(environment: PythonEnvironment, name: PackageName) -> Self {
38+
Self { environment, name }
39+
}
40+
41+
/// Return the [`Version`] of the tool package in this environment.
42+
pub fn version(&self) -> Result<Version, Error> {
43+
let site_packages = SitePackages::from_environment(&self.environment).map_err(|err| {
44+
Error::EnvironmentRead(self.environment.root().to_path_buf(), err.to_string())
45+
})?;
46+
let packages = site_packages.get_packages(&self.name);
47+
let package = packages
48+
.first()
49+
.ok_or_else(|| Error::MissingToolPackage(self.name.clone()))?;
50+
Ok(package.version().clone())
51+
}
52+
53+
/// Get the underlying [`PythonEnvironment`].
54+
pub fn into_environment(self) -> PythonEnvironment {
55+
self.environment
56+
}
57+
58+
/// Get a reference to the underlying [`PythonEnvironment`].
59+
pub fn environment(&self) -> &PythonEnvironment {
60+
&self.environment
61+
}
62+
}
63+
2964
#[derive(Error, Debug)]
3065
pub enum Error {
3166
#[error(transparent)]
@@ -50,6 +85,8 @@ pub enum Error {
5085
EnvironmentRead(PathBuf, String),
5186
#[error("Failed find package `{0}` in tool environment")]
5287
MissingToolPackage(PackageName),
88+
#[error("Tool `{0}` environment not found at `{1}`")]
89+
ToolEnvironmentNotFound(PackageName, PathBuf),
5390
}
5491

5592
/// A collection of uv-managed tools installed on the current system.
@@ -201,7 +238,7 @@ impl InstalledTools {
201238
&self,
202239
name: &PackageName,
203240
cache: &Cache,
204-
) -> Result<Option<PythonEnvironment>, Error> {
241+
) -> Result<Option<ToolEnvironment>, Error> {
205242
let environment_path = self.tool_dir(name);
206243

207244
match PythonEnvironment::from_root(&environment_path, cache) {
@@ -210,7 +247,7 @@ impl InstalledTools {
210247
"Found existing environment for tool `{name}`: {}",
211248
environment_path.user_display()
212249
);
213-
Ok(Some(venv))
250+
Ok(Some(ToolEnvironment::new(venv, name.clone())))
214251
}
215252
Err(uv_python::Error::MissingEnvironment(_)) => Ok(None),
216253
Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(
@@ -290,19 +327,6 @@ impl InstalledTools {
290327
))
291328
}
292329

293-
/// Return the [`Version`] of an installed tool.
294-
pub fn version(&self, name: &PackageName, cache: &Cache) -> Result<Version, Error> {
295-
let environment_path = self.tool_dir(name);
296-
let environment = PythonEnvironment::from_root(&environment_path, cache)?;
297-
let site_packages = SitePackages::from_environment(&environment)
298-
.map_err(|err| Error::EnvironmentRead(environment_path.clone(), err.to_string()))?;
299-
let packages = site_packages.get_packages(name);
300-
let package = packages
301-
.first()
302-
.ok_or_else(|| Error::MissingToolPackage(name.clone()))?;
303-
Ok(package.version().clone())
304-
}
305-
306330
/// Initialize the tools directory.
307331
///
308332
/// Ensures the directory is created.

crates/uv/src/commands/tool/install.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,11 +348,11 @@ pub(crate) async fn install(
348348
installed_tools
349349
.get_environment(package_name, &cache)?
350350
.filter(|environment| {
351-
if environment.uses(&interpreter) {
351+
if environment.environment().uses(&interpreter) {
352352
trace!(
353353
"Existing interpreter matches the requested interpreter for `{}`: {}",
354354
package_name,
355-
environment.interpreter().sys_executable().display()
355+
environment.environment().interpreter().sys_executable().display()
356356
);
357357
true
358358
} else {
@@ -399,7 +399,7 @@ pub(crate) async fn install(
399399
let tags = resolution_tags(None, python_platform.as_ref(), &interpreter)?;
400400

401401
// Check if the installed packages meet the requirements.
402-
let site_packages = SitePackages::from_environment(environment)?;
402+
let site_packages = SitePackages::from_environment(environment.environment())?;
403403
if matches!(
404404
site_packages.satisfies_requirements(
405405
requirements.iter(),
@@ -461,7 +461,7 @@ pub(crate) async fn install(
461461
// be invalidated by moving the environment.
462462
let environment = if let Some(environment) = existing_environment {
463463
let environment = match update_environment(
464-
environment,
464+
environment.into_environment(),
465465
spec,
466466
Modifications::Exact,
467467
python_platform.as_ref(),

crates/uv/src/commands/tool/list.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use owo_colors::OwoColorize;
66

77
use uv_cache::Cache;
88
use uv_fs::Simplified;
9+
use uv_python::LenientImplementationName;
910
use uv_tool::InstalledTools;
1011
use uv_warnings::warn_user;
1112

@@ -19,6 +20,7 @@ pub(crate) async fn list(
1920
show_version_specifiers: bool,
2021
show_with: bool,
2122
show_extras: bool,
23+
show_python: bool,
2224
cache: &Cache,
2325
printer: Printer,
2426
) -> Result<ExitStatus> {
@@ -50,8 +52,27 @@ pub(crate) async fn list(
5052
continue;
5153
};
5254

53-
// Output tool name and version
54-
let version = match installed_tools.version(&name, cache) {
55+
// Get the tool environment
56+
let tool_env = match installed_tools.get_environment(&name, cache) {
57+
Ok(Some(env)) => env,
58+
Ok(None) => {
59+
warn_user!(
60+
"Tool `{name}` environment not found (run `{}` to reinstall)",
61+
format!("uv tool install {name} --reinstall").green()
62+
);
63+
continue;
64+
}
65+
Err(e) => {
66+
warn_user!(
67+
"{e} (run `{}` to reinstall)",
68+
format!("uv tool install {name} --reinstall").green()
69+
);
70+
continue;
71+
}
72+
};
73+
74+
// Get the tool version
75+
let version = match tool_env.version() {
5576
Ok(version) => version,
5677
Err(e) => {
5778
if let uv_tool::Error::EnvironmentError(e) = e {
@@ -97,6 +118,18 @@ pub(crate) async fn list(
97118
})
98119
.unwrap_or_default();
99120

121+
let python_version = if show_python {
122+
let interpreter = tool_env.environment().interpreter();
123+
let implementation = LenientImplementationName::from(interpreter.implementation_name());
124+
format!(
125+
" [{} {}]",
126+
implementation.pretty(),
127+
interpreter.python_full_version()
128+
)
129+
} else {
130+
String::new()
131+
};
132+
100133
let with_requirements = show_with
101134
.then(|| {
102135
tool.requirements()
@@ -118,7 +151,7 @@ pub(crate) async fn list(
118151
printer.stdout(),
119152
"{} ({})",
120153
format!(
121-
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}"
154+
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}{python_version}"
122155
)
123156
.bold(),
124157
installed_tools.tool_dir(&name).simplified_display().cyan(),
@@ -128,7 +161,7 @@ pub(crate) async fn list(
128161
printer.stdout(),
129162
"{}",
130163
format!(
131-
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}"
164+
"{name} v{version}{version_specifier}{extra_requirements}{with_requirements}{python_version}"
132165
)
133166
.bold()
134167
)?;

crates/uv/src/commands/tool/run.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,10 @@ async fn show_help(
460460
.filter_map(|(name, tool)| {
461461
tool.ok().and_then(|_| {
462462
installed_tools
463-
.version(&name, cache)
463+
.get_environment(&name, cache)
464464
.ok()
465+
.flatten()
466+
.and_then(|tool_env| tool_env.version().ok())
465467
.map(|version| (name, version))
466468
})
467469
})
@@ -931,7 +933,7 @@ async fn get_or_create_environment(
931933
.get_environment(&requirement.name, cache)?
932934
.filter(|environment| {
933935
python_request.as_ref().is_none_or(|python_request| {
934-
python_request.satisfied(environment.interpreter(), cache)
936+
python_request.satisfied(environment.environment().interpreter(), cache)
935937
})
936938
});
937939

@@ -967,7 +969,7 @@ async fn get_or_create_environment(
967969
let tags = pip::resolution_tags(None, python_platform.as_ref(), &interpreter)?;
968970

969971
// Check if the installed packages meet the requirements.
970-
let site_packages = SitePackages::from_environment(&environment)?;
972+
let site_packages = SitePackages::from_environment(environment.environment())?;
971973
if matches!(
972974
site_packages.satisfies_requirements(
973975
requirements.iter(),
@@ -984,7 +986,7 @@ async fn get_or_create_environment(
984986
Ok(SatisfiesResult::Fresh { .. })
985987
) {
986988
debug!("Using existing tool `{}`", requirement.name);
987-
return Ok((from, environment));
989+
return Ok((from, environment.into_environment()));
988990
}
989991
}
990992
}

crates/uv/src/commands/tool/upgrade.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ async fn upgrade_tool(
341341
// Check if we need to create a new environment — if so, resolve it first, then
342342
// install the requested tool
343343
let (environment, outcome) = if let Some(interpreter) =
344-
interpreter.filter(|interpreter| !environment.uses(interpreter))
344+
interpreter.filter(|interpreter| !environment.environment().uses(interpreter))
345345
{
346346
// If we're using a new interpreter, re-create the environment for each tool.
347347
let resolution = resolve_environment(
@@ -388,7 +388,7 @@ async fn upgrade_tool(
388388
environment,
389389
changelog,
390390
} = update_environment(
391-
environment,
391+
environment.into_environment(),
392392
spec,
393393
Modifications::Exact,
394394
python_platform,

crates/uv/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
14211421
args.show_version_specifiers,
14221422
args.show_with,
14231423
args.show_extras,
1424+
args.show_python,
14241425
&cache,
14251426
printer,
14261427
)

crates/uv/src/settings.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,7 @@ pub(crate) struct ToolListSettings {
859859
pub(crate) show_version_specifiers: bool,
860860
pub(crate) show_with: bool,
861861
pub(crate) show_extras: bool,
862+
pub(crate) show_python: bool,
862863
}
863864

864865
impl ToolListSettings {
@@ -870,6 +871,7 @@ impl ToolListSettings {
870871
show_version_specifiers,
871872
show_with,
872873
show_extras,
874+
show_python,
873875
python_preference: _,
874876
no_python_downloads: _,
875877
} = args;
@@ -879,6 +881,7 @@ impl ToolListSettings {
879881
show_version_specifiers,
880882
show_with,
881883
show_extras,
884+
show_python,
882885
}
883886
}
884887
}

crates/uv/tests/it/tool_list.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,3 +563,81 @@ fn tool_list_show_extras() {
563563
----- stderr -----
564564
"###);
565565
}
566+
567+
#[test]
568+
fn tool_list_show_python() {
569+
let context = TestContext::new("3.12").with_filtered_exe_suffix();
570+
let tool_dir = context.temp_dir.child("tools");
571+
let bin_dir = context.temp_dir.child("bin");
572+
573+
// Install `black` with python 3.12
574+
context
575+
.tool_install()
576+
.arg("black==24.2.0")
577+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
578+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
579+
.assert()
580+
.success();
581+
582+
// Test with --show-python
583+
uv_snapshot!(context.filters(), context.tool_list().arg("--show-python")
584+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
585+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
586+
success: true
587+
exit_code: 0
588+
----- stdout -----
589+
black v24.2.0 [CPython 3.12.[X]]
590+
- black
591+
- blackd
592+
593+
----- stderr -----
594+
"###);
595+
}
596+
597+
#[test]
598+
fn tool_list_show_all() {
599+
let context = TestContext::new("3.12").with_filtered_exe_suffix();
600+
let tool_dir = context.temp_dir.child("tools");
601+
let bin_dir = context.temp_dir.child("bin");
602+
603+
// Install `black` without extras
604+
context
605+
.tool_install()
606+
.arg("black==24.2.0")
607+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
608+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
609+
.assert()
610+
.success();
611+
612+
// Install `flask` with extras and additional requirements
613+
context
614+
.tool_install()
615+
.arg("flask[async,dotenv]")
616+
.arg("--with")
617+
.arg("requests")
618+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
619+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
620+
.assert()
621+
.success();
622+
623+
// Test with all flags
624+
uv_snapshot!(context.filters(), context.tool_list()
625+
.arg("--show-extras")
626+
.arg("--show-with")
627+
.arg("--show-version-specifiers")
628+
.arg("--show-paths")
629+
.arg("--show-python")
630+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
631+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
632+
success: true
633+
exit_code: 0
634+
----- stdout -----
635+
black v24.2.0 [required: ==24.2.0] [CPython 3.12.[X]] ([TEMP_DIR]/tools/black)
636+
- black ([TEMP_DIR]/bin/black)
637+
- blackd ([TEMP_DIR]/bin/blackd)
638+
flask v3.0.2 [extras: async, dotenv] [with: requests] [CPython 3.12.[X]] ([TEMP_DIR]/tools/flask)
639+
- flask ([TEMP_DIR]/bin/flask)
640+
641+
----- stderr -----
642+
"###);
643+
}

docs/reference/cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3047,6 +3047,7 @@ uv tool list [OPTIONS]
30473047
<p>Repeating this option, e.g., <code>-qq</code>, will enable a silent mode in which uv will write no output to stdout.</p>
30483048
</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>
30493049
</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>
3050+
</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>
30503051
</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>
30513052
</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>
30523053
</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>

0 commit comments

Comments
 (0)