Skip to content

Commit

Permalink
Add uv tool list --outdated to show outdated tools
Browse files Browse the repository at this point in the history
  • Loading branch information
j178 committed Dec 4, 2024
1 parent d283fff commit 154f3fe
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 8 deletions.
4 changes: 4 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3857,6 +3857,10 @@ pub struct ToolListArgs {
#[arg(long)]
pub show_version_specifiers: bool,

/// List only tools that are outdated.
#[arg(long)]
pub outdated: bool,

// Hide unused global Python options.
#[arg(long, hide = true)]
pub python_preference: Option<PythonPreference>,
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/pip/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl<'env> LatestClient<'env> {
&self,
package: &PackageName,
index: Option<&IndexUrl>,
) -> anyhow::Result<Option<DistFilename>, uv_client::Error> {
) -> Result<Option<DistFilename>, uv_client::Error> {
debug!("Fetching latest version of: `{package}`");

let archives = match self.client.simple(package, index, self.capabilities).await {
Expand Down
246 changes: 239 additions & 7 deletions crates/uv/src/commands/tool/list.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
use std::fmt::Write;

use anyhow::Result;
use futures::StreamExt;
use itertools::Itertools;
use owo_colors::OwoColorize;
use rustc_hash::FxHashMap;
use tracing::warn;

use uv_cache::Cache;
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_client::{Connectivity, RegistryClientBuilder};
use uv_configuration::{Concurrency, TrustedHost};
use uv_distribution_filename::DistFilename;
use uv_distribution_types::IndexCapabilities;
use uv_fs::Simplified;
use uv_tool::InstalledTools;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_python::{
EnvironmentPreference, PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest,
PythonVariant, VersionRequest,
};
use uv_resolver::RequiresPython;
use uv_settings::ResolverInstallerOptions;
use uv_tool::{InstalledTools, Tool};
use uv_warnings::warn_user;

use crate::commands::pip::latest::LatestClient;
use crate::commands::reporters::LatestVersionReporter;
use crate::commands::ExitStatus;
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;

/// List installed tools.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn list(
show_paths: bool,
show_version_specifiers: bool,
outdated: bool,
python_preference: PythonPreference,
connectivity: Connectivity,
concurrency: Concurrency,
native_tls: bool,
allow_insecure_host: &[TrustedHost],
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
Expand All @@ -37,6 +63,91 @@ pub(crate) async fn list(
return Ok(ExitStatus::Success);
}

// Get the versions of the installed tools.
let versions = tools
.iter()
.map(|(name, _)| {
let version = installed_tools.version(name, cache);
(name.clone(), version)
})
.collect::<FxHashMap<_, _>>();

let latest = if outdated {
let reporter = LatestVersionReporter::from(printer).with_length(tools.len() as u64);

// Filter out malformed tools.
let tools = tools
.iter()
.filter_map(|(name, tool)| {
if let Ok(ref tool) = tool {
if versions[name].is_ok() {
return Some((name, tool));
}
};
None
})
.collect_vec();

// Fetch the latest version for each tool.
let mut fetches = futures::stream::iter(tools)
.map(|(name, tool)| {
// SAFETY: The tool is known to be well-formed, get_environment will not fail.
let environment = installed_tools
.get_environment(name, cache)
.unwrap()
.unwrap();
async move {
let latest = find_latest(
name,
tool,
&environment,
python_preference,
connectivity,
native_tls,
allow_insecure_host,
cache,
)
.await?;
anyhow::Ok((name, latest))
}
})
.buffer_unordered(concurrency.downloads);

let mut map = FxHashMap::default();
while let Some((package, version)) = fetches.next().await.transpose()? {
if let Some(version) = version.as_ref() {
reporter.on_fetch_version(package, version.version());
map.insert(package.clone(), version.clone());
} else {
reporter.on_fetch_progress();
}
}
reporter.on_fetch_complete();
map
} else {
FxHashMap::default()
};

// Remove tools that are up-to-date.
let tools = if outdated {
tools
.into_iter()
.filter(|(name, tool)| {
if tool.is_err() {
return true;
}
let Ok(version) = versions[name].as_ref() else {
return true;
};
latest
.get(name)
.map_or(true, |filename| filename.version() > version)
})
.collect_vec()
} else {
tools
};

for (name, tool) in tools {
// Skip invalid tools
let Ok(tool) = tool else {
Expand All @@ -48,7 +159,7 @@ pub(crate) async fn list(
};

// Output tool name and version
let version = match installed_tools.version(&name, cache) {
let version = match &versions[&name] {
Ok(version) => version,
Err(e) => {
writeln!(printer.stderr(), "{e}")?;
Expand All @@ -73,18 +184,35 @@ pub(crate) async fn list(
String::new()
};

let latest = if outdated {
let latest = latest.get(&name).map(DistFilename::version);
if let Some(latest) = latest {
format!(" (latest: v{latest})")
} else {
String::new()
}
} else {
String::new()
};

if show_paths {
writeln!(
printer.stdout(),
"{} ({})",
"{}{}{}",
format!("{name} v{version}{version_specifier}").bold(),
installed_tools.tool_dir(&name).simplified_display().cyan(),
latest.bold().cyan(),
format!(
" ({})",
installed_tools.tool_dir(&name).simplified_display()
)
.cyan(),
)?;
} else {
writeln!(
printer.stdout(),
"{}",
format!("{name} v{version}{version_specifier}").bold()
"{}{}",
format!("{name} v{version}{version_specifier}").bold(),
latest.bold().cyan(),
)?;
}

Expand All @@ -105,3 +233,107 @@ pub(crate) async fn list(

Ok(ExitStatus::Success)
}

/// Find the latest version of a tool.
async fn find_latest(
name: &PackageName,
tool: &Tool,
environment: &PythonEnvironment,
python_preference: PythonPreference,
connectivity: Connectivity,
native_tls: bool,
allow_insecure_host: &[TrustedHost],
cache: &Cache,
) -> Result<Option<DistFilename>> {
let capabilities = IndexCapabilities::default();

let ResolverInstallerSettings {
index_locations,
index_strategy,
keyring_provider,
prerelease,
exclude_newer,
..
} = ResolverInstallerOptions::from(tool.options().clone()).into();

// Initialize the registry client.
let client =
RegistryClientBuilder::new(cache.clone().with_refresh(Refresh::All(Timestamp::now())))
.native_tls(native_tls)
.connectivity(connectivity)
.index_urls(index_locations.index_urls())
.index_strategy(index_strategy)
.keyring(keyring_provider)
.allow_insecure_host(allow_insecure_host.to_vec())
.markers(environment.interpreter().markers())
.platform(environment.interpreter().platform())
.build();

// Determine the platform tags.
let interpreter = environment.interpreter();
let tags = interpreter.tags()?;

// Determine the `requires-python` specifier.
let python_request = tool.python().as_deref().map(PythonRequest::parse);
let requires_python = if let Some(python_request) = python_request {
match python_request {
PythonRequest::Version(VersionRequest::MajorMinor(
major,
minor,
PythonVariant::Default,
)) => RequiresPython::greater_than_equal_version(&Version::new([
u64::from(major),
u64::from(minor),
])),
PythonRequest::Version(VersionRequest::MajorMinorPatch(
major,
minor,
patch,
PythonVariant::Default,
)) => RequiresPython::greater_than_equal_version(&Version::new([
u64::from(major),
u64::from(minor),
u64::from(patch),
])),
PythonRequest::Version(VersionRequest::Range(ref specifiers, _)) => {
RequiresPython::from_specifiers(specifiers)
}
python_request => {
match PythonInstallation::find(
&python_request,
EnvironmentPreference::OnlySystem,
python_preference,
cache,
) {
Ok(installation) => {
let interpreter = installation.into_interpreter();
RequiresPython::greater_than_equal_version(
&interpreter.python_minor_version(),
)
}
Err(err) => {
warn!(
"Failed to find a Python interpreter for tool `{name}` by request `{python_request}`, use current tool interpreter instead: {err}",
);
RequiresPython::greater_than_equal_version(
&interpreter.python_minor_version(),
)
}
}
}
}
} else {
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version())
};

let client = LatestClient {
client: &client,
capabilities: &capabilities,
prerelease,
exclude_newer,
tags: Some(tags),
requires_python: &requires_python,
};

Ok(client.find_latest(name, None).await?)
}
6 changes: 6 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,12 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
commands::tool_list(
args.show_paths,
args.show_version_specifiers,
args.outdated,
globals.python_preference,
globals.connectivity,
globals.concurrency,
globals.native_tls,
&globals.allow_insecure_host,
&cache,
printer,
)
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ impl ToolUpgradeSettings {
pub(crate) struct ToolListSettings {
pub(crate) show_paths: bool,
pub(crate) show_version_specifiers: bool,
pub(crate) outdated: bool,
}

impl ToolListSettings {
Expand All @@ -652,13 +653,15 @@ impl ToolListSettings {
let ToolListArgs {
show_paths,
show_version_specifiers,
outdated,
python_preference: _,
no_python_downloads: _,
} = args;

Self {
show_paths,
show_version_specifiers,
outdated,
}
}
}
Expand Down
Loading

0 comments on commit 154f3fe

Please sign in to comment.