Respect --exclude-newer in uv tool list --outdated (#18861)

## Summary

Closes https://github.com/astral-sh/uv/issues/18819.
This commit is contained in:
Charlie Marsh
2026-04-07 09:44:27 -04:00
committed by GitHub
parent 2cf99b91ec
commit bca76afb9c
5 changed files with 68 additions and 5 deletions
+13
View File
@@ -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<ExcludeNewerValue>,
// Hide unused global Python options.
#[arg(long, hide = true)]
pub python_preference: Option<PythonPreference>,
+7 -3
View File
@@ -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();
+2
View File
@@ -1627,6 +1627,8 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
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,
+15 -2
View File
@@ -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<FilesystemOptions>) -> Self {
pub(crate) fn resolve(args: ToolListArgs, filesystem: Option<FilesystemOptions>) -> 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,
}
}
}
+31
View File
@@ -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();