mirror of
https://github.com/astral-sh/uv.git
synced 2026-05-06 08:56:53 -04:00
Improve --exclude-newer hints (#18952)
## Summary
We now show a hint if there's an available version that's excluded and
would satisfy the range (and include that version in the hint).
Previously, we only showed this if _all_ versions were omitted.
For example:
```
× 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. The latest version satisfying the requirement is v2.0.0, published on 2023-01-07. Consider using `exclude-newer-
package` to override the cutoff for this package.
```
Closes https://github.com/astral-sh/uv/issues/18220.
---------
Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
@@ -162,6 +162,17 @@ pub type ErrorTree = DerivationTree<PubGrubPackage, Range<Version>, UnavailableR
|
||||
pub struct NoSolutionError {
|
||||
error: pubgrub::NoSolutionError<UvDependencyProvider>,
|
||||
index: InMemoryIndex,
|
||||
/// The versions that were available for each package after `exclude-newer` filtering.
|
||||
///
|
||||
/// For versions available before filtering, see [`NoSolutionError::available_versions`].
|
||||
included_versions: FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
/// The versions available for each package.
|
||||
///
|
||||
/// These version sets are not filtered by `exclude-newer`. See
|
||||
/// [`NoSolutionError::included_versions`] instead if filtered versions are needed.
|
||||
///
|
||||
/// These versions are filtered by [`EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF`] for
|
||||
/// deterministic output in tests.
|
||||
available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
|
||||
selector: CandidateSelector,
|
||||
@@ -184,6 +195,7 @@ impl NoSolutionError {
|
||||
pub(crate) fn new(
|
||||
error: pubgrub::NoSolutionError<UvDependencyProvider>,
|
||||
index: InMemoryIndex,
|
||||
included_versions: FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
|
||||
selector: CandidateSelector,
|
||||
@@ -203,6 +215,7 @@ impl NoSolutionError {
|
||||
Self {
|
||||
error,
|
||||
index,
|
||||
included_versions,
|
||||
available_versions,
|
||||
available_indexes,
|
||||
selector,
|
||||
@@ -388,6 +401,7 @@ impl std::fmt::Debug for NoSolutionError {
|
||||
let Self {
|
||||
error,
|
||||
index: _,
|
||||
included_versions,
|
||||
available_versions,
|
||||
available_indexes,
|
||||
selector,
|
||||
@@ -406,6 +420,7 @@ impl std::fmt::Debug for NoSolutionError {
|
||||
} = self;
|
||||
f.debug_struct("NoSolutionError")
|
||||
.field("error", error)
|
||||
.field("included_versions", included_versions)
|
||||
.field("available_versions", available_versions)
|
||||
.field("available_indexes", available_indexes)
|
||||
.field("selector", selector)
|
||||
@@ -431,6 +446,7 @@ impl std::fmt::Display for NoSolutionError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// Write the derivation report.
|
||||
let formatter = PubGrubReportFormatter {
|
||||
included_versions: &self.included_versions,
|
||||
available_versions: &self.available_versions,
|
||||
python_requirement: &self.python_requirement,
|
||||
workspace_members: &self.workspace_members,
|
||||
@@ -460,7 +476,7 @@ impl std::fmt::Display for NoSolutionError {
|
||||
|
||||
simplify_derivation_tree_ranges(
|
||||
&mut tree,
|
||||
&self.available_versions,
|
||||
&self.included_versions,
|
||||
&self.selector,
|
||||
&self.env,
|
||||
);
|
||||
@@ -481,6 +497,7 @@ impl std::fmt::Display for NoSolutionError {
|
||||
|
||||
// Include any additional hints.
|
||||
let mut additional_hints = IndexSet::default();
|
||||
let inherited_exclude_newer_ranges = FxHashMap::default();
|
||||
formatter.generate_hints(
|
||||
&tree,
|
||||
&self.index,
|
||||
@@ -497,6 +514,7 @@ impl std::fmt::Display for NoSolutionError {
|
||||
self.tags.as_ref(),
|
||||
&self.workspace_members,
|
||||
&self.options,
|
||||
&inherited_exclude_newer_ranges,
|
||||
&mut additional_hints,
|
||||
);
|
||||
for hint in additional_hints {
|
||||
@@ -1301,11 +1319,11 @@ impl std::fmt::Display for NoSolutionHeader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a [`DerivationTree`], simplify version ranges using the available versions for each
|
||||
/// Given a [`DerivationTree`], simplify version ranges using the included versions for each
|
||||
/// package.
|
||||
fn simplify_derivation_tree_ranges(
|
||||
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
||||
available_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
candidate_selector: &CandidateSelector,
|
||||
resolver_environment: &ResolverEnvironment,
|
||||
) {
|
||||
@@ -1315,7 +1333,7 @@ fn simplify_derivation_tree_ranges(
|
||||
if let Some(simplified) = simplify_range(
|
||||
versions1,
|
||||
package1,
|
||||
available_versions,
|
||||
included_versions,
|
||||
candidate_selector,
|
||||
resolver_environment,
|
||||
) {
|
||||
@@ -1324,7 +1342,7 @@ fn simplify_derivation_tree_ranges(
|
||||
if let Some(simplified) = simplify_range(
|
||||
versions2,
|
||||
package2,
|
||||
available_versions,
|
||||
included_versions,
|
||||
candidate_selector,
|
||||
resolver_environment,
|
||||
) {
|
||||
@@ -1335,7 +1353,7 @@ fn simplify_derivation_tree_ranges(
|
||||
if let Some(simplified) = simplify_range(
|
||||
versions,
|
||||
package,
|
||||
available_versions,
|
||||
included_versions,
|
||||
candidate_selector,
|
||||
resolver_environment,
|
||||
) {
|
||||
@@ -1346,7 +1364,7 @@ fn simplify_derivation_tree_ranges(
|
||||
if let Some(simplified) = simplify_range(
|
||||
versions,
|
||||
package,
|
||||
available_versions,
|
||||
included_versions,
|
||||
candidate_selector,
|
||||
resolver_environment,
|
||||
) {
|
||||
@@ -1359,13 +1377,13 @@ fn simplify_derivation_tree_ranges(
|
||||
// Recursively simplify both sides of the tree
|
||||
simplify_derivation_tree_ranges(
|
||||
Arc::make_mut(&mut derived.cause1),
|
||||
available_versions,
|
||||
included_versions,
|
||||
candidate_selector,
|
||||
resolver_environment,
|
||||
);
|
||||
simplify_derivation_tree_ranges(
|
||||
Arc::make_mut(&mut derived.cause2),
|
||||
available_versions,
|
||||
included_versions,
|
||||
candidate_selector,
|
||||
resolver_environment,
|
||||
);
|
||||
@@ -1373,13 +1391,13 @@ fn simplify_derivation_tree_ranges(
|
||||
// Simplify the terms
|
||||
derived.terms = std::mem::take(&mut derived.terms)
|
||||
.into_iter()
|
||||
.map(|(pkg, term)| {
|
||||
.map(|(package, term)| {
|
||||
let term = match term {
|
||||
Term::Positive(versions) => Term::Positive(
|
||||
simplify_range(
|
||||
&versions,
|
||||
&pkg,
|
||||
available_versions,
|
||||
&package,
|
||||
included_versions,
|
||||
candidate_selector,
|
||||
resolver_environment,
|
||||
)
|
||||
@@ -1388,34 +1406,34 @@ fn simplify_derivation_tree_ranges(
|
||||
Term::Negative(versions) => Term::Negative(
|
||||
simplify_range(
|
||||
&versions,
|
||||
&pkg,
|
||||
available_versions,
|
||||
&package,
|
||||
included_versions,
|
||||
candidate_selector,
|
||||
resolver_environment,
|
||||
)
|
||||
.unwrap_or(versions),
|
||||
),
|
||||
};
|
||||
(pkg, term)
|
||||
(package, term)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to simplify a version range using available versions for a package.
|
||||
/// Helper function to simplify a version range using included versions for a package.
|
||||
///
|
||||
/// If the range cannot be simplified, `None` is returned.
|
||||
fn simplify_range(
|
||||
range: &Range<Version>,
|
||||
package: &PubGrubPackage,
|
||||
available_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
candidate_selector: &CandidateSelector,
|
||||
resolver_environment: &ResolverEnvironment,
|
||||
) -> Option<Range<Version>> {
|
||||
// If there's not a package name or available versions, we can't simplify anything
|
||||
// If there's not a package name or included versions, we can't simplify anything
|
||||
let name = package.name()?;
|
||||
let versions = available_versions.get(name)?;
|
||||
let versions = included_versions.get(name)?;
|
||||
|
||||
// If this is a full range, there's nothing to simplify
|
||||
if range == &Range::full() {
|
||||
|
||||
@@ -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, Ranges, ReportFormatter, Term};
|
||||
use pubgrub::{DerivationTree, Derived, External, Map, Range, ReportFormatter, Term};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use uv_configuration::{IndexStrategy, NoBinary, NoBuild};
|
||||
@@ -36,10 +36,13 @@ use crate::{
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PubGrubReportFormatter<'a> {
|
||||
/// The versions that were available for each package.
|
||||
/// See [`crate::error::NoSolutionError::included_versions`].
|
||||
pub(crate) included_versions: &'a FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
|
||||
/// See [`crate::error::NoSolutionError::available_versions`].
|
||||
pub(crate) available_versions: &'a FxHashMap<PackageName, BTreeSet<Version>>,
|
||||
|
||||
/// The versions that were available for each package.
|
||||
/// The Python requirement for the resolution.
|
||||
pub(crate) python_requirement: &'a PythonRequirement,
|
||||
|
||||
/// The members of the workspace.
|
||||
@@ -91,11 +94,11 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
|
||||
} else {
|
||||
let complement = set.complement();
|
||||
let range =
|
||||
// Note that sometimes we do not have a range of available versions, e.g.,
|
||||
// Note that sometimes we do not have a range of included versions, e.g.,
|
||||
// when a package is from a non-registry source. In that case, we cannot
|
||||
// perform further simplification of the range.
|
||||
if let Some(available_versions) = package.name().and_then(|name| self.available_versions.get(name)) {
|
||||
update_availability_range(&complement, available_versions)
|
||||
if let Some(included_versions) = package.name().and_then(|name| self.included_versions.get(name)) {
|
||||
update_availability_range(&complement, included_versions)
|
||||
} else {
|
||||
complement
|
||||
};
|
||||
@@ -543,6 +546,7 @@ impl PubGrubReportFormatter<'_> {
|
||||
tags: Option<&Tags>,
|
||||
workspace_members: &BTreeSet<PackageName>,
|
||||
options: &Options,
|
||||
inherited_exclude_newer_ranges: &FxHashMap<PackageName, Range<Version>>,
|
||||
output_hints: &mut IndexSet<PubGrubHint>,
|
||||
) {
|
||||
// Check for disjoint target hints (only applicable to universal resolution).
|
||||
@@ -664,16 +668,34 @@ impl PubGrubReportFormatter<'_> {
|
||||
};
|
||||
|
||||
if let Some((exclude_newer, source)) = exclude_newer {
|
||||
if self
|
||||
// Check if there are no included versions in the requested
|
||||
// range, but there are still available versions in that range
|
||||
// (i.e., they were filtered out by `exclude-newer`).
|
||||
let no_included_in_set = self
|
||||
.included_versions
|
||||
.get(name)
|
||||
.is_none_or(|versions| !versions.iter().any(|v| set.contains(v)));
|
||||
let available_has_versions_in_set = self
|
||||
.available_versions
|
||||
.get(name)
|
||||
.is_some_and(BTreeSet::is_empty)
|
||||
&& Self::has_versions_in_index(name, index, fork_indexes)
|
||||
{
|
||||
.is_some_and(|versions| versions.iter().any(|v| set.contains(v)));
|
||||
if no_included_in_set && available_has_versions_in_set {
|
||||
let version_hint_set =
|
||||
inherited_exclude_newer_ranges.get(name).map_or_else(
|
||||
|| set.clone(),
|
||||
|exclude_newer_range| set.union(exclude_newer_range),
|
||||
);
|
||||
let matching_version = self.exclude_newer_version_hint(
|
||||
name,
|
||||
&version_hint_set,
|
||||
index,
|
||||
fork_indexes,
|
||||
);
|
||||
output_hints.insert(PubGrubHint::ExcludeNewer {
|
||||
package: name.clone(),
|
||||
source,
|
||||
exclude_newer,
|
||||
matching_version,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -730,6 +752,29 @@ impl PubGrubReportFormatter<'_> {
|
||||
}
|
||||
DerivationTree::External(External::NotRoot(..)) => {}
|
||||
DerivationTree::Derived(derived) => {
|
||||
let cause1_exclude_newer_ranges =
|
||||
Self::subtree_exclude_newer_ranges(&derived.cause1);
|
||||
let cause2_exclude_newer_ranges =
|
||||
Self::subtree_exclude_newer_ranges(&derived.cause2);
|
||||
|
||||
let mut cause1_inherited_exclude_newer_ranges =
|
||||
inherited_exclude_newer_ranges.clone();
|
||||
for (name, range) in &cause2_exclude_newer_ranges {
|
||||
cause1_inherited_exclude_newer_ranges
|
||||
.entry(name.clone())
|
||||
.and_modify(|existing| *existing = existing.union(range))
|
||||
.or_insert_with(|| range.clone());
|
||||
}
|
||||
|
||||
let mut cause2_inherited_exclude_newer_ranges =
|
||||
inherited_exclude_newer_ranges.clone();
|
||||
for (name, range) in &cause1_exclude_newer_ranges {
|
||||
cause2_inherited_exclude_newer_ranges
|
||||
.entry(name.clone())
|
||||
.and_modify(|existing| *existing = existing.union(range))
|
||||
.or_insert_with(|| range.clone());
|
||||
}
|
||||
|
||||
self.generate_hints(
|
||||
&derived.cause1,
|
||||
index,
|
||||
@@ -746,6 +791,7 @@ impl PubGrubReportFormatter<'_> {
|
||||
tags,
|
||||
workspace_members,
|
||||
options,
|
||||
&cause1_inherited_exclude_newer_ranges,
|
||||
output_hints,
|
||||
);
|
||||
self.generate_hints(
|
||||
@@ -764,12 +810,104 @@ impl PubGrubReportFormatter<'_> {
|
||||
tags,
|
||||
workspace_members,
|
||||
options,
|
||||
&cause2_inherited_exclude_newer_ranges,
|
||||
output_hints,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect the version ranges in `derivation_tree` that were excluded solely by
|
||||
/// `exclude-newer`, grouped by package name.
|
||||
fn subtree_exclude_newer_ranges(
|
||||
derivation_tree: &ErrorTree,
|
||||
) -> FxHashMap<PackageName, Range<Version>> {
|
||||
fn collect(
|
||||
derivation_tree: &ErrorTree,
|
||||
exclude_newer_ranges: &mut FxHashMap<PackageName, Range<Version>>,
|
||||
) {
|
||||
match derivation_tree {
|
||||
DerivationTree::External(External::Custom(package, versions, reason)) => {
|
||||
if matches!(
|
||||
reason,
|
||||
UnavailableReason::Version(UnavailableVersion::IncompatibleDist(
|
||||
IncompatibleDist::Wheel(IncompatibleWheel::ExcludeNewer(_))
|
||||
| IncompatibleDist::Source(IncompatibleSource::ExcludeNewer(_))
|
||||
))
|
||||
) {
|
||||
if let Some(name) = package.name() {
|
||||
exclude_newer_ranges
|
||||
.entry(name.clone())
|
||||
.and_modify(|set| *set = set.union(versions))
|
||||
.or_insert_with(|| versions.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
DerivationTree::External(_) => {}
|
||||
DerivationTree::Derived(derived) => {
|
||||
collect(&derived.cause1, exclude_newer_ranges);
|
||||
collect(&derived.cause2, exclude_newer_ranges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut exclude_newer_ranges = FxHashMap::default();
|
||||
collect(derivation_tree, &mut exclude_newer_ranges);
|
||||
exclude_newer_ranges
|
||||
}
|
||||
|
||||
/// Return the latest version in `set` that is available for resolver error reporting,
|
||||
/// along with the earliest known publish date for that version.
|
||||
fn exclude_newer_version_hint(
|
||||
&self,
|
||||
name: &PackageName,
|
||||
set: &Range<Version>,
|
||||
index: &InMemoryIndex,
|
||||
fork_indexes: &ForkIndexes,
|
||||
) -> Option<ExcludeNewerVersionDetail> {
|
||||
let version = self.available_versions.get(name).and_then(|versions| {
|
||||
versions
|
||||
.iter()
|
||||
.rfind(|version| set.contains(version))
|
||||
.cloned()
|
||||
})?;
|
||||
|
||||
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 VersionsResponse::Found(version_maps) = &*response else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let publish_date = version_maps
|
||||
.iter()
|
||||
.filter_map(|version_map| {
|
||||
version_map.get(&version).and_then(|prioritized| {
|
||||
prioritized
|
||||
.files()
|
||||
.filter_map(|file| file.upload_time_utc_ms)
|
||||
.min()
|
||||
})
|
||||
})
|
||||
.min()
|
||||
.and_then(|upload_time| {
|
||||
Some(
|
||||
jiff::Timestamp::from_millisecond(upload_time)
|
||||
.ok()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
|
||||
Some(ExcludeNewerVersionDetail {
|
||||
version,
|
||||
publish_date,
|
||||
singleton: set.as_singleton().is_some(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a [`PubGrubHint`] for a package that doesn't have any wheels matching the current
|
||||
/// Python version, ABI, or platform.
|
||||
fn tag_hint(
|
||||
@@ -1051,7 +1189,7 @@ impl PubGrubReportFormatter<'_> {
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if let Some(version) = self.available_versions.get(name).and_then(|versions| {
|
||||
} else if let Some(version) = self.included_versions.get(name).and_then(|versions| {
|
||||
versions
|
||||
.iter()
|
||||
.rev()
|
||||
@@ -1075,30 +1213,13 @@ 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)]
|
||||
pub(crate) struct ExcludeNewerVersionDetail {
|
||||
version: Version,
|
||||
publish_date: Option<String>,
|
||||
singleton: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -1267,12 +1388,14 @@ pub(crate) enum PubGrubHint {
|
||||
// excluded from `PartialEq` and `Hash`
|
||||
tags: BTreeSet<PlatformTag>,
|
||||
},
|
||||
/// All versions of a package were excluded by `exclude-newer`.
|
||||
/// Versions of a package were excluded by `exclude-newer`.
|
||||
ExcludeNewer {
|
||||
package: PackageName,
|
||||
source: EffectiveExcludeNewerSource,
|
||||
// excluded from `PartialEq` and `Hash`
|
||||
exclude_newer: ExcludeNewerValue,
|
||||
// excluded from `PartialEq` and `Hash`
|
||||
matching_version: Option<ExcludeNewerVersionDetail>,
|
||||
},
|
||||
/// The resolution failed for a Python version that is different from the current Python version.
|
||||
DisjointPythonVersion {
|
||||
@@ -1868,41 +1991,78 @@ impl std::fmt::Display for PubGrubHint {
|
||||
package,
|
||||
source,
|
||||
exclude_newer,
|
||||
} => match source {
|
||||
EffectiveExcludeNewerSource::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(),
|
||||
),
|
||||
EffectiveExcludeNewerSource::Global => 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(),
|
||||
),
|
||||
EffectiveExcludeNewerSource::Index => write!(
|
||||
f,
|
||||
"{}{} `{}` was filtered by the index-specific `{}` setting to only include \
|
||||
packages uploaded before {}. Consider updating that index's cutoff, setting \
|
||||
matching_version,
|
||||
} => {
|
||||
let latest = match matching_version {
|
||||
Some(ExcludeNewerVersionDetail {
|
||||
version,
|
||||
publish_date: Some(publish_date),
|
||||
singleton: true,
|
||||
}) => format!(
|
||||
" The requested version, {}, was published at {}.",
|
||||
format!("v{version}").cyan(),
|
||||
publish_date.cyan()
|
||||
),
|
||||
Some(ExcludeNewerVersionDetail {
|
||||
version: _,
|
||||
publish_date: None,
|
||||
singleton: true,
|
||||
}) => String::new(),
|
||||
Some(ExcludeNewerVersionDetail {
|
||||
version,
|
||||
publish_date: Some(publish_date),
|
||||
singleton: false,
|
||||
}) => format!(
|
||||
" The latest version satisfying the requirement is {}, published at {}.",
|
||||
format!("v{version}").cyan(),
|
||||
publish_date.cyan()
|
||||
),
|
||||
Some(ExcludeNewerVersionDetail {
|
||||
version,
|
||||
publish_date: None,
|
||||
singleton: false,
|
||||
}) => format!(
|
||||
" The latest version satisfying the requirement is {}.",
|
||||
format!("v{version}").cyan()
|
||||
),
|
||||
None => String::new(),
|
||||
};
|
||||
match source {
|
||||
EffectiveExcludeNewerSource::Package => write!(
|
||||
f,
|
||||
"{}{} `{}` was filtered by `{}` to only include packages uploaded \
|
||||
before {}.{latest} Consider removing the setting or updating it to a later date.",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package.cyan(),
|
||||
"exclude-newer-package".green(),
|
||||
exclude_newer.cyan(),
|
||||
),
|
||||
EffectiveExcludeNewerSource::Global => write!(
|
||||
f,
|
||||
"{}{} `{}` was filtered by `{}` to only include packages uploaded \
|
||||
before {}.{latest} 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(),
|
||||
),
|
||||
EffectiveExcludeNewerSource::Index => write!(
|
||||
f,
|
||||
"{}{} `{}` was filtered by the index-specific `{}` setting to only include \
|
||||
packages uploaded before {}.{latest} Consider updating that index's cutoff, setting \
|
||||
it to `false`, or using `{}` to override the cutoff for this package.",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package.cyan(),
|
||||
"exclude-newer".green(),
|
||||
exclude_newer.cyan(),
|
||||
"exclude-newer-package".green(),
|
||||
),
|
||||
},
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package.cyan(),
|
||||
"exclude-newer".green(),
|
||||
exclude_newer.cyan(),
|
||||
"exclude-newer-package".green(),
|
||||
),
|
||||
}
|
||||
}
|
||||
Self::DisjointPythonVersion { python_version } => {
|
||||
write!(
|
||||
f,
|
||||
|
||||
@@ -23,10 +23,10 @@ use tracing::{Level, debug, info, instrument, trace, warn};
|
||||
use uv_configuration::{Constraints, Excludes, Overrides};
|
||||
use uv_distribution::{ArchiveMetadata, DistributionDatabase};
|
||||
use uv_distribution_types::{
|
||||
BuiltDist, CompatibleDist, DerivationChain, Dist, DistErrorKind, Identifier, IncompatibleDist,
|
||||
IncompatibleSource, IncompatibleWheel, IndexCapabilities, IndexLocations, IndexMetadata,
|
||||
IndexUrl, InstalledDist, Name, PythonRequirementKind, RemoteSource, Requirement, ResolvedDist,
|
||||
ResolvedDistRef, SourceDist, VersionOrUrlRef, implied_markers,
|
||||
BuiltDist, CompatibleDist, DerivationChain, Dist, DistErrorKind, ExcludeNewerValue, Identifier,
|
||||
IncompatibleDist, IncompatibleSource, IncompatibleWheel, IndexCapabilities, IndexLocations,
|
||||
IndexMetadata, IndexUrl, InstalledDist, Name, PythonRequirementKind, RemoteSource, Requirement,
|
||||
ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef, implied_markers,
|
||||
};
|
||||
use uv_git::GitResolver;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
@@ -36,6 +36,7 @@ use uv_pep508::{
|
||||
};
|
||||
use uv_platform_tags::{IncompatibleTag, Tags};
|
||||
use uv_pypi_types::{ConflictItem, ConflictItemRef, ConflictKindRef, Conflicts, VerbatimParsedUrl};
|
||||
use uv_static::EnvVars;
|
||||
use uv_torch::TorchStrategy;
|
||||
use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider};
|
||||
use uv_warnings::warn_user_once;
|
||||
@@ -2721,14 +2722,21 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||
}
|
||||
|
||||
let mut available_indexes = FxHashMap::default();
|
||||
let mut included_versions = FxHashMap::default();
|
||||
let mut available_versions = FxHashMap::default();
|
||||
|
||||
let available_version_cutoff: Option<ExcludeNewerValue> =
|
||||
std::env::var(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF)
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok());
|
||||
|
||||
for package in err.packages() {
|
||||
let Some(name) = package.name() else { continue };
|
||||
if !visited.contains(name) {
|
||||
// Avoid including available versions for packages that exist in the derivation
|
||||
// Avoid including version data for packages that exist in the derivation
|
||||
// tree, but were never visited during resolution. We _may_ have metadata for
|
||||
// these packages, but it's non-deterministic, and omitting them ensures that
|
||||
// we represent the self of the resolver at the time of failure.
|
||||
// we represent the state of the resolver at the time of failure.
|
||||
continue;
|
||||
}
|
||||
let versions_response = if let Some(index) = fork_indexes.get(name) {
|
||||
@@ -2740,28 +2748,60 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||
};
|
||||
if let Some(response) = versions_response {
|
||||
if let VersionsResponse::Found(ref version_maps) = *response {
|
||||
// Track the available versions, across all indexes.
|
||||
// Track included and available versions, across all indexes.
|
||||
for version_map in version_maps {
|
||||
let package_versions = available_versions
|
||||
let package_included_versions = included_versions
|
||||
.entry(name.clone())
|
||||
.or_insert_with(BTreeSet::new);
|
||||
let package_available_versions = available_versions
|
||||
.entry(name.clone())
|
||||
.or_insert_with(BTreeSet::new);
|
||||
|
||||
for (version, dists) in version_map.iter(&Ranges::full()) {
|
||||
// Don't show versions removed by excluded-newer in hints.
|
||||
if let Some(exclude_newer) = version_map.exclude_newer() {
|
||||
let Some(prioritized_dist) = dists.prioritized_dist() else {
|
||||
continue;
|
||||
// Included versions are those that survive the effective
|
||||
// `exclude-newer` filter used during resolution. Files with
|
||||
// missing upload times are treated as excluded (matching
|
||||
// the resolution behavior in `version_map.rs`).
|
||||
let excluded_from_included = || {
|
||||
let Some(exclude_newer) = version_map.exclude_newer() else {
|
||||
return false;
|
||||
};
|
||||
if prioritized_dist.files().all(|file| {
|
||||
let Some(prioritized_dist) = dists.prioritized_dist() else {
|
||||
return true;
|
||||
};
|
||||
prioritized_dist.files().all(|file| {
|
||||
file.upload_time_utc_ms.is_none_or(|upload_time| {
|
||||
upload_time >= exclude_newer.timestamp_millis()
|
||||
})
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if !excluded_from_included() {
|
||||
package_included_versions.insert(version.clone());
|
||||
}
|
||||
|
||||
package_versions.insert(version.clone());
|
||||
// Available versions are used in resolver error reporting,
|
||||
// and can be bounded by a test-only cutoff for deterministic
|
||||
// snapshots. Files with missing upload times are *not*
|
||||
// excluded, since we only filter versions we can confirm
|
||||
// were published after the cutoff.
|
||||
let excluded_from_available = || {
|
||||
let Some(ref exclude_newer) = available_version_cutoff else {
|
||||
return false;
|
||||
};
|
||||
let Some(prioritized_dist) = dists.prioritized_dist() else {
|
||||
return false;
|
||||
};
|
||||
prioritized_dist.files().all(|file| {
|
||||
file.upload_time_utc_ms.is_some_and(|upload_time| {
|
||||
upload_time >= exclude_newer.timestamp_millis()
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
if !excluded_from_available() {
|
||||
package_available_versions.insert(version.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2781,6 +2821,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||
ResolveError::NoSolution(Box::new(NoSolutionError::new(
|
||||
err,
|
||||
self.index.clone(),
|
||||
included_versions,
|
||||
available_versions,
|
||||
available_indexes,
|
||||
self.selector.clone(),
|
||||
|
||||
@@ -1174,6 +1174,19 @@ impl EnvVars {
|
||||
#[attr_added_in("0.9.8")]
|
||||
pub const UV_TEST_CURRENT_TIMESTAMP: &'static str = "UV_TEST_CURRENT_TIMESTAMP";
|
||||
|
||||
/// When set to a timestamp, applies an `exclude-newer` filter to the versions
|
||||
/// considered available from indexes.
|
||||
///
|
||||
/// This is used for reproducible resolver error messages. When `exclude-newer`
|
||||
/// is used, we retain information about the available versions to improve error
|
||||
/// messages. In contrast, versions published after this cutoff are considered
|
||||
/// non-existent.
|
||||
///
|
||||
/// Should be set to an RFC 3339 timestamp (e.g., `2024-03-25T00:00:00Z`).
|
||||
#[attr_hidden]
|
||||
#[attr_added_in("next release")]
|
||||
pub const UV_TEST_AVAILABLE_VERSION_CUTOFF: &'static str = "UV_TEST_AVAILABLE_VERSION_CUTOFF";
|
||||
|
||||
/// `.env` files from which to load environment variables when executing `uv run` commands.
|
||||
#[attr_added_in("0.4.30")]
|
||||
pub const UV_ENV_FILE: &'static str = "UV_ENV_FILE";
|
||||
|
||||
@@ -33,8 +33,8 @@ use uv_python::{
|
||||
};
|
||||
use uv_static::EnvVars;
|
||||
|
||||
// Exclude any packages uploaded after this date.
|
||||
static EXCLUDE_NEWER: &str = "2024-03-25T00:00:00Z";
|
||||
// Shared test timestamp for deterministic package availability and relative times.
|
||||
static TEST_TIMESTAMP: &str = "2024-03-25T00:00:00Z";
|
||||
|
||||
pub const PACKSE_VERSION: &str = "0.3.59";
|
||||
pub const DEFAULT_PYTHON_VERSION: &str = "3.12";
|
||||
@@ -138,7 +138,7 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[
|
||||
///
|
||||
/// * Set the current directory to a temporary directory (`temp_dir`).
|
||||
/// * Set the cache dir to a different temporary directory (`cache_dir`).
|
||||
/// * Set a cutoff for versions used in the resolution so the snapshots don't change after a new release.
|
||||
/// * Set a shared test timestamp so snapshots don't change after a new release.
|
||||
/// * Set the venv to a fresh `.venv` in `temp_dir`
|
||||
pub struct TestContext {
|
||||
pub root: ChildPath,
|
||||
@@ -1233,9 +1233,9 @@ impl TestContext {
|
||||
// Installations are not allowed by default; see `Self::with_managed_python_dirs`
|
||||
.env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
|
||||
.env(EnvVars::UV_TEST_PYTHON_PATH, self.python_path())
|
||||
// Lock to a point in time view of the world
|
||||
.env(EnvVars::UV_EXCLUDE_NEWER, EXCLUDE_NEWER)
|
||||
.env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, EXCLUDE_NEWER)
|
||||
.env(EnvVars::UV_EXCLUDE_NEWER, TEST_TIMESTAMP)
|
||||
.env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, TEST_TIMESTAMP)
|
||||
.env(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF, TEST_TIMESTAMP)
|
||||
// When installations are allowed, we don't want to write to global state, like the
|
||||
// Windows registry
|
||||
.env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "0")
|
||||
|
||||
@@ -33688,7 +33688,7 @@ fn lock_exclude_newer_hint() -> Result<()> {
|
||||
× 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.
|
||||
hint: `iniconfig` was filtered by `exclude-newer` to only include packages uploaded before 2000-01-01T00:00:00Z. The latest version satisfying the requirement is v2.0.0, published at 2023-01-07T11:08:09.864Z. Consider using `exclude-newer-package` to override the cutoff for this package.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
@@ -33738,7 +33738,7 @@ async fn lock_exclude_newer_index_disable() -> Result<()> {
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because there are no versions of iniconfig and your project depends on iniconfig>=2, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: `iniconfig` was filtered by `exclude-newer` to only include packages uploaded before 2024-03-25T00:00:00Z. Consider using `exclude-newer-package` to override the cutoff for this package.
|
||||
hint: `iniconfig` was filtered by `exclude-newer` to only include packages uploaded before 2024-03-25T00:00:00Z. The latest version satisfying the requirement is v2.0.0. Consider using `exclude-newer-package` to override the cutoff for this package.
|
||||
");
|
||||
|
||||
pyproject_toml.write_str(&format!(
|
||||
@@ -33822,7 +33822,7 @@ async fn lock_exclude_newer_index_value() -> Result<()> {
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because there are no versions of iniconfig and your project depends on iniconfig>=2, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: `iniconfig` was filtered by the index-specific `exclude-newer` setting to only include packages uploaded before 2025-01-01T00:00:00Z. Consider updating that index's cutoff, setting it to `false`, or using `exclude-newer-package` to override the cutoff for this package.
|
||||
hint: `iniconfig` was filtered by the index-specific `exclude-newer` setting to only include packages uploaded before 2025-01-01T00:00:00Z. The latest version satisfying the requirement is v2.0.0. Consider updating that index's cutoff, setting it to `false`, or using `exclude-newer-package` to override the cutoff for this package.
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context
|
||||
@@ -33839,7 +33839,7 @@ async fn lock_exclude_newer_index_value() -> Result<()> {
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because there are no versions of iniconfig and your project depends on iniconfig>=2, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: `iniconfig` was filtered by the index-specific `exclude-newer` setting to only include packages uploaded before 2025-01-01T00:00:00Z. Consider updating that index's cutoff, setting it to `false`, or using `exclude-newer-package` to override the cutoff for this package.
|
||||
hint: `iniconfig` was filtered by the index-specific `exclude-newer` setting to only include packages uploaded before 2025-01-01T00:00:00Z. The latest version satisfying the requirement is v2.0.0. Consider updating that index's cutoff, setting it to `false`, or using `exclude-newer-package` to override the cutoff for this package.
|
||||
");
|
||||
|
||||
pyproject_toml.write_str(&format!(
|
||||
@@ -33877,6 +33877,45 @@ async fn lock_exclude_newer_index_value() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that the resolver emits a hint when a pinned version is excluded by `--exclude-newer`,
|
||||
/// even though older versions of the same package are still available.
|
||||
///
|
||||
/// See: <https://github.com/astral-sh/uv/issues/18949>
|
||||
#[test]
|
||||
fn lock_exclude_newer_hint_pinned_version() -> 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==2.0.0"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Use a cutoff that excludes `iniconfig 2.0.0` (2023-01-07) but not `1.1.1` (2020-10-18).
|
||||
uv_snapshot!(context.filters(), context
|
||||
.lock()
|
||||
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
|
||||
.arg("--exclude-newer")
|
||||
.arg("2022-01-01T00:00:00Z"), @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because there is no version of iniconfig==2.0.0 and your project depends on iniconfig==2.0.0, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: `iniconfig` was filtered by `exclude-newer` to only include packages uploaded before 2022-01-01T00:00:00Z. The requested version, v2.0.0, was published at 2023-01-07T11:08:09.864Z. 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]
|
||||
|
||||
@@ -1136,7 +1136,7 @@ fn lock_exclude_newer_relative_values() -> Result<()> {
|
||||
╰─▶ 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.
|
||||
hint: `iniconfig` was filtered by `exclude-newer` to only include packages uploaded before 2006-12-02T02:07:43Z. The latest version satisfying the requirement is v2.0.0, published at 2023-01-07T11:08:09.864Z. Consider using `exclude-newer-package` to override the cutoff for this package.
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context
|
||||
|
||||
@@ -3232,7 +3232,8 @@ fn only_binary_editable_setup_py() {
|
||||
/// don't propagate the `--prerelease` flag to the source distribution build regardless.
|
||||
#[test]
|
||||
fn no_prerelease_hint_source_builds() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12").with_exclude_newer("2018-10-08");
|
||||
// Use an explicit UTC timestamp so the snapshot is stable across host time zones.
|
||||
let context = uv_test::test_context!("3.12").with_exclude_newer("2018-10-09T00:00:00Z");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
@@ -3258,6 +3259,8 @@ fn no_prerelease_hint_source_builds() -> Result<()> {
|
||||
├─▶ Failed to resolve requirements from `setup.py` build
|
||||
├─▶ No solution found when resolving: `setuptools>=40.8.0`
|
||||
╰─▶ Because only setuptools<=40.4.3 is available and you require setuptools>=40.8.0, we can conclude that your requirements are unsatisfiable.
|
||||
|
||||
hint: `setuptools` was filtered by `exclude-newer` to only include packages uploaded before 2018-10-09T00:00:00Z. The latest version satisfying the requirement is v69.2.0, published at 2024-03-13T11:20:54.103Z. Consider using `exclude-newer-package` to override the cutoff for this package.
|
||||
"
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user