From bca76afb9c459dc27287ee98e90ed833fb3a9f95 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 7 Apr 2026 09:44:27 -0400 Subject: [PATCH] Respect `--exclude-newer` in `uv tool list --outdated` (#18861) ## Summary Closes https://github.com/astral-sh/uv/issues/18819. --- crates/uv-cli/src/lib.rs | 13 ++++++++++++ crates/uv/src/commands/tool/list.rs | 10 +++++++--- crates/uv/src/lib.rs | 2 ++ crates/uv/src/settings.rs | 17 ++++++++++++++-- crates/uv/tests/it/tool_list.rs | 31 +++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 793977eae..7f8a0d723 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -5805,6 +5805,19 @@ pub struct ToolListArgs { #[arg(long, overrides_with("outdated"), hide = true)] pub no_outdated: bool, + /// Limit candidate packages to those that were uploaded prior to the given date. + /// + /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format + /// (e.g., `2006-12-02`) resolved based on your system's configured time zone, a "friendly" + /// duration (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, + /// `P7D`, `P30D`). + /// + /// Durations do not respect semantics of the local time zone and are always resolved to a fixed + /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). + /// Calendar units such as months and years are not allowed. + #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] + pub exclude_newer: Option, + // Hide unused global Python options. #[arg(long, hide = true)] pub python_preference: Option, diff --git a/crates/uv/src/commands/tool/list.rs b/crates/uv/src/commands/tool/list.rs index a2674f23a..8f8d3e26f 100644 --- a/crates/uv/src/commands/tool/list.rs +++ b/crates/uv/src/commands/tool/list.rs @@ -15,7 +15,7 @@ use uv_distribution_types::{IndexCapabilities, RequiresPython}; use uv_fs::Simplified; use uv_normalize::PackageName; use uv_python::LenientImplementationName; -use uv_settings::ResolverInstallerOptions; +use uv_settings::{Combine, ResolverInstallerOptions}; use uv_tool::InstalledTools; use uv_warnings::warn_user; @@ -34,6 +34,8 @@ pub(crate) async fn list( show_extras: bool, show_python: bool, outdated: bool, + args: ResolverInstallerOptions, + filesystem: ResolverInstallerOptions, client_builder: BaseClientBuilder<'_>, concurrency: Concurrency, cache: &Cache, @@ -124,10 +126,12 @@ pub(crate) async fn list( .map(|(name, tool, tool_env, _version)| { let client_builder = client_builder.clone(); let download_concurrency = download_concurrency.clone(); + let args = args.clone(); + let filesystem = filesystem.clone(); async move { let capabilities = IndexCapabilities::default(); - let settings = ResolverInstallerSettings::from(ResolverInstallerOptions::from( - tool.options().clone(), + let settings = ResolverInstallerSettings::from(args.combine( + ResolverInstallerOptions::from(tool.options().clone()).combine(filesystem), )); let interpreter = tool_env.environment().interpreter(); diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ea3d9ba89..075ac91ac 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1627,6 +1627,8 @@ async fn run(cli: Cli) -> Result { args.show_extras, args.show_python, args.outdated, + args.args, + args.filesystem, client_builder.subcommand(vec!["tool".to_owned(), "list".to_owned()]), globals.concurrency, &cache, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index e0230020f..bbd300796 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1111,12 +1111,13 @@ pub(crate) struct ToolListSettings { pub(crate) show_extras: bool, pub(crate) show_python: bool, pub(crate) outdated: bool, + pub(crate) args: ResolverInstallerOptions, + pub(crate) filesystem: ResolverInstallerOptions, } impl ToolListSettings { /// Resolve the [`ToolListSettings`] from the CLI and filesystem configuration. - #[expect(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: ToolListArgs, _filesystem: Option) -> Self { + pub(crate) fn resolve(args: ToolListArgs, filesystem: Option) -> Self { let ToolListArgs { show_paths, show_version_specifiers, @@ -1125,10 +1126,17 @@ impl ToolListSettings { show_python, outdated, no_outdated, + exclude_newer, python_preference: _, no_python_downloads: _, } = args; + let filesystem = filesystem.map(FilesystemOptions::into_options); + let filesystem = ResolverInstallerOptions { + exclude_newer: filesystem.and_then(|options| options.top_level.exclude_newer), + ..ResolverInstallerOptions::default() + }; + Self { show_paths, show_version_specifiers, @@ -1136,6 +1144,11 @@ impl ToolListSettings { show_extras, show_python, outdated: flag(outdated, no_outdated, "outdated").unwrap_or(false), + args: ResolverInstallerOptions { + exclude_newer, + ..ResolverInstallerOptions::default() + }, + filesystem, } } } diff --git a/crates/uv/tests/it/tool_list.rs b/crates/uv/tests/it/tool_list.rs index 7b2dfdc89..8d55ad937 100644 --- a/crates/uv/tests/it/tool_list.rs +++ b/crates/uv/tests/it/tool_list.rs @@ -196,6 +196,37 @@ fn tool_list_outdated_respects_exclude_newer() { "); } +#[test] +fn tool_list_outdated_cli_exclude_newer() { + let context = uv_test::test_context!("3.12").with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Install an older version of `black`. + 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(); + + // `--exclude-newer` should filter out releases newer than the cutoff when determining the + // latest available tool version. + uv_snapshot!(context.filters(), context.tool_list() + .arg("--outdated") + .arg("--exclude-newer") + .arg("2024-03-01T00:00:00Z") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); +} + #[test] fn tool_list_missing_receipt() { let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();