diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 0a803ebdebb14..f8d61244abdda 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -5,7 +5,7 @@ use std::ops::Bound; use indexmap::IndexSet; use itertools::Itertools; use owo_colors::OwoColorize; -use pubgrub::{DerivationTree, Derived, External, Map, Range, ReportFormatter, Term}; +use pubgrub::{DerivationTree, Derived, External, Map, Range, Ranges, ReportFormatter, Term}; use rustc_hash::FxHashMap; use uv_configuration::{IndexStrategy, NoBinary, NoBuild}; @@ -29,7 +29,9 @@ use crate::resolver::{ MetadataUnavailable, UnavailableErrorChain, UnavailablePackage, UnavailableReason, UnavailableVersion, }; -use crate::{Flexibility, InMemoryIndex, Options, ResolverEnvironment, VersionsResponse}; +use crate::{ + ExcludeNewerValue, Flexibility, InMemoryIndex, Options, ResolverEnvironment, VersionsResponse, +}; #[derive(Debug)] pub(crate) struct PubGrubReportFormatter<'a> { @@ -638,6 +640,21 @@ impl PubGrubReportFormatter<'_> { incomplete_packages, output_hints, ); + + if let Some(exclude_newer) = options.exclude_newer.exclude_newer_package(name) { + if self + .available_versions + .get(name) + .is_some_and(BTreeSet::is_empty) + && Self::has_versions_in_index(name, index, fork_indexes) + { + output_hints.insert(PubGrubHint::ExcludeNewer { + package: name.clone(), + per_package: options.exclude_newer.package.contains_key(name), + exclude_newer, + }); + } + } } } DerivationTree::External(External::FromDependencyOf( @@ -1036,6 +1053,30 @@ impl PubGrubReportFormatter<'_> { } } } + + fn has_versions_in_index( + name: &PackageName, + index: &InMemoryIndex, + fork_indexes: &ForkIndexes, + ) -> bool { + let response = if let Some(url) = fork_indexes.get(name).map(IndexMetadata::url) { + index.explicit().get(&(name.clone(), url.clone())) + } else { + index.implicit().get(name) + }; + + let Some(response) = response else { + return false; + }; + + let VersionsResponse::Found(ref version_maps) = *response else { + return false; + }; + + version_maps + .iter() + .any(|vm| vm.iter(&Ranges::full()).next().is_some()) + } } #[derive(Debug, Clone)] @@ -1204,6 +1245,13 @@ pub(crate) enum PubGrubHint { // excluded from `PartialEq` and `Hash` tags: BTreeSet, }, + /// All versions of a package were excluded by `exclude-newer`. + ExcludeNewer { + package: PackageName, + per_package: bool, + // excluded from `PartialEq` and `Hash` + exclude_newer: ExcludeNewerValue, + }, /// The resolution failed for a Python version that is different from the current Python version. DisjointPythonVersion { // excluded from `PartialEq` and `Hash` @@ -1287,6 +1335,10 @@ enum PubGrubHintCore { PlatformTags { package: PackageName, }, + ExcludeNewer { + package: PackageName, + per_package: bool, + }, DisjointPythonVersion, DisjointEnvironment, } @@ -1355,6 +1407,14 @@ impl From for PubGrubHintCore { PubGrubHint::LanguageTags { package, .. } => Self::LanguageTags { package }, PubGrubHint::AbiTags { package, .. } => Self::AbiTags { package }, PubGrubHint::PlatformTags { package, .. } => Self::PlatformTags { package }, + PubGrubHint::ExcludeNewer { + package, + per_package, + .. + } => Self::ExcludeNewer { + package, + per_package, + }, PubGrubHint::DisjointPythonVersion { .. } => Self::DisjointPythonVersion, PubGrubHint::DisjointEnvironment => Self::DisjointEnvironment, } @@ -1787,6 +1847,36 @@ impl std::fmt::Display for PubGrubHint { .join(", "), ) } + Self::ExcludeNewer { + package, + per_package, + exclude_newer, + } => { + if *per_package { + write!( + f, + "{}{} `{}` was filtered by `{}` to only include packages uploaded \ + before {}. Consider removing the setting or updating it to a later date.", + "hint".bold().cyan(), + ":".bold(), + package.cyan(), + "exclude-newer-package".green(), + exclude_newer.cyan(), + ) + } else { + write!( + f, + "{}{} `{}` was filtered by `{}` to only include packages uploaded \ + before {}. Consider using `{}` to override the cutoff for this package.", + "hint".bold().cyan(), + ":".bold(), + package.cyan(), + "exclude-newer".green(), + exclude_newer.cyan(), + "exclude-newer-package".green(), + ) + } + } Self::DisjointPythonVersion { python_version } => { write!( f, diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index d9be06ba651be..22abe546e95f7 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -324,6 +324,9 @@ impl ResolverState= exclude_newer.timestamp_millis() => { + trace!( + "Excluding `{}` (uploaded {upload_time}) due to exclude-newer ({exclude_newer})", + file.filename + ); (true, Some(upload_time)) } None => { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index b4b149017739a..f9c60527a391b 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -18940,6 +18940,7 @@ fn lock_explicit_default_index() -> Result<()> { Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }, given: None }), format: Simple }), conflict: None }, origin: None }} DEBUG Solving with installed Python version: 3.12.[X] DEBUG Solving with target Python version: >=3.12 + DEBUG Solving with exclude-newer: global: 2024-03-25T00:00:00Z DEBUG Adding direct dependency: project* DEBUG Searching for a compatible version of project @ file://[TEMP_DIR]/ (*) DEBUG Adding direct dependency: anyio* @@ -32485,6 +32486,43 @@ fn lock_exclude_newer_package() -> Result<()> { Ok(()) } +/// Test that the resolver emits a hint when all versions are excluded by `--exclude-newer`. +/// +/// See: +#[test] +fn lock_exclude_newer_hint() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context + .lock() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--exclude-newer") + .arg("2000-01-01T00:00:00Z"), @" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because there are no versions of iniconfig and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable. + + hint: `iniconfig` was filtered by `exclude-newer` to only include packages uploaded before 2000-01-01T00:00:00Z. Consider using `exclude-newer-package` to override the cutoff for this package. + "); + + Ok(()) +} + /// Test that lockfile validation includes explicit indexes from path dependencies. /// #[tokio::test] diff --git a/crates/uv/tests/it/lock_exclude_newer_relative.rs b/crates/uv/tests/it/lock_exclude_newer_relative.rs index a1d11ce8db2fd..7a23aba58f068 100644 --- a/crates/uv/tests/it/lock_exclude_newer_relative.rs +++ b/crates/uv/tests/it/lock_exclude_newer_relative.rs @@ -1125,7 +1125,7 @@ fn lock_exclude_newer_relative_values() -> Result<()> { uv_snapshot!(context.filters(), context .lock() .arg("--exclude-newer") - .arg("2006-12-02T02:07:43"), @" + .arg("2006-12-02T02:07:43Z"), @" success: false exit_code: 1 ----- stdout ----- @@ -1135,6 +1135,8 @@ fn lock_exclude_newer_relative_values() -> Result<()> { × No solution found when resolving dependencies: ╰─▶ Because there are no versions of iniconfig and iniconfig==2.0.0 was published after the exclude newer time, we can conclude that all versions of iniconfig cannot be used. And because your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable. + + hint: `iniconfig` was filtered by `exclude-newer` to only include packages uploaded before 2006-12-02T02:07:43Z. Consider using `exclude-newer-package` to override the cutoff for this package. "); uv_snapshot!(context.filters(), context