mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-20 03:49:54 +00:00
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:
parent
6fb00a9936
commit
b4168e665e
10 changed files with 175 additions and 29 deletions
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)?;
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -----
|
||||
"###);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</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-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-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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue