Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions crates/uv-resolver/src/pubgrub/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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> {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -1204,6 +1245,13 @@ pub(crate) enum PubGrubHint {
// excluded from `PartialEq` and `Hash`
tags: BTreeSet<PlatformTag>,
},
/// 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`
Expand Down Expand Up @@ -1287,6 +1335,10 @@ enum PubGrubHintCore {
PlatformTags {
package: PackageName,
},
ExcludeNewer {
package: PackageName,
per_package: bool,
},
DisjointPythonVersion,
DisjointEnvironment,
}
Expand Down Expand Up @@ -1355,6 +1407,14 @@ impl From<PubGrubHint> 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,
}
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
"Solving with target Python version: {}",
self.python_requirement.target()
);
if !self.options.exclude_newer.is_empty() {
debug!("Solving with exclude-newer: {}", self.options.exclude_newer);
}

let mut visited = FxHashSet::default();

Expand Down
6 changes: 5 additions & 1 deletion crates/uv-resolver/src/version_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::sync::OnceLock;

use pubgrub::Ranges;
use rustc_hash::FxHashMap;
use tracing::instrument;
use tracing::{instrument, trace};

use uv_client::{FlatIndexEntry, OwnedArchive, SimpleDetailMetadata, VersionFiles};
use uv_configuration::BuildOptions;
Expand Down Expand Up @@ -448,6 +448,10 @@ impl VersionMapLazy {
let (excluded, upload_time) = if let Some(exclude_newer) = &self.exclude_newer {
match file.upload_time_utc_ms.as_ref() {
Some(&upload_time) if upload_time >= exclude_newer.timestamp_millis() => {
trace!(
"Excluding `{}` (uploaded {upload_time}) due to exclude-newer ({exclude_newer})",
file.filename
);
(true, Some(upload_time))
}
None => {
Expand Down
38 changes: 38 additions & 0 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down Expand Up @@ -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: <https://github.com/astral-sh/uv/issues/18014>
#[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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit of an aside for this pull request, but why does the other one say

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 this one does not?

Copy link
Member

@zanieb zanieb Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah it's a version in the lockfile thing


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.
/// <https://github.com/astral-sh/uv/issues/11419>
#[tokio::test]
Expand Down
4 changes: 3 additions & 1 deletion crates/uv/tests/it/lock_exclude_newer_relative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----
Expand All @@ -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
Expand Down
Loading