Skip to content

Commit

Permalink
Improve display of available package ranges
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Aug 15, 2024
1 parent 3ee8658 commit 7cd52c5
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 25 deletions.
109 changes: 103 additions & 6 deletions crates/uv-resolver/src/pubgrub/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,28 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
format!("there is no version of {package}{set}")
} else {
let complement = set.complement();
let segments = complement.iter().count();
// Simple case, there's a single range to report
if segments == 1 {
let range =
// Note that sometimes we do not have a range of available versions, e.g.,
// when a package is from a non-registry source. In that case, we cannot
// perform further simplicifaction of the range.
if let Some(available_versions) = self.available_versions.get(package) {
update_availability_range(&complement, available_versions)
} else {
complement
};
if range.is_empty() {
return format!("there are no versions of {package}");
}
if range.iter().count() == 1 {
format!(
"only {} is available",
self.compatible_range(package, &complement)
self.availability_range(package, &range)
)
// Complex case, there are multiple ranges
} else {
format!(
"only the following versions of {} {}",
package,
self.availability_range(package, &complement)
self.availability_range(package, &range)
)
}
}
Expand Down Expand Up @@ -1066,6 +1075,94 @@ impl PackageRange<'_> {
}
}

/// Create a range with improved segments for reporting the available versions for a package.
fn update_availability_range(
range: &Range<Version>,
available_versions: &BTreeSet<Version>,
) -> Range<Version> {
let mut new_range = Range::empty();

// Construct an available range to help guide simplification. Note this is not strictly correct,
// as the available range should have many holes in it. However, for this use-case it should be
// okay — we just may avoid simplifying some segments _inside_ the available range.
let (available_range, first_available, last_available) =
match (available_versions.first(), available_versions.last()) {
// At least one version is available
(Some(first), Some(last)) => {
let range = Range::<Version>::from_range_bounds((
Bound::Included(first.clone()),
Bound::Included(last.clone()),
));
// If only one version is available, return this as the bound immediately
if first == last {
return range;
}
(range, first, last)
}
// SAFETY: If there's only a single item, `first` and `last` should both
// return `Some`.
(Some(_), None) | (None, Some(_)) => unreachable!(),
// No versions are available; nothing to do
(None, None) => return Range::empty(),
};

for segment in range.iter() {
let (lower, upper) = segment;
let segment_range = Range::from_range_bounds((lower.clone(), upper.clone()));

// Drop the segment if it's disjoint with the available range, e.g., if the segment is
// `foo>999`, and the the available versions are all `<10` it's useless to show.
if segment_range.is_disjoint(&available_range) {
continue;
}

// Replace the segment if it's captured by the available range, e.g., if the segment is
// `foo<1000` and the available versions are all `<10` we can simplify to `foo<10`.
if available_range.subset_of(&segment_range) {
// If the segment only has a lower or upper bound, only take the relevant part of the
// available range. This avoids replacing `foo<100` with `foo>1,<2`, instead using
// `foo<2` to avoid extra noise.
if matches!(lower, Bound::Unbounded) {
new_range = new_range.union(&Range::from_range_bounds((
Bound::Unbounded,
Bound::Included(last_available.clone()),
)));
} else if matches!(upper, Bound::Unbounded) {
new_range = new_range.union(&Range::from_range_bounds((
Bound::Included(first_available.clone()),
Bound::Unbounded,
)));
} else {
new_range = new_range.union(&available_range);
}
continue;
}

// If the bound is inclusive, and the version is _not_ available, change it to an exclusive
// bound to avoid confusion, e.g., if the segment is `foo<=10` and the available versions
// do not include `foo 10`, we should instead say `foo<10`.
let lower = match lower {
Bound::Included(version) if !available_versions.contains(version) => {
Bound::Excluded(version.clone())
}
_ => (*lower).clone(),
};
let upper = match upper {
Bound::Included(version) if !available_versions.contains(version) => {
Bound::Excluded(version.clone())
}
_ => (*upper).clone(),
};

// Note this repeated-union construction is not particularly efficient, but there's not
// better API exposed by PubGrub. Since we're just generating an error message, it's
// probably okay, but we should investigate a better upstream API.
new_range = new_range.union(&Range::from_range_bounds((lower, upper)));
}

new_range
}

impl std::fmt::Display for PackageRange<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Exit early for the root package — the range is not meaningful
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/tests/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2934,9 +2934,9 @@ fn compile_yanked_version_indirect() -> Result<()> {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only the following versions of attrs are available:
attrs<=20.3.0
attrs<20.3.0
attrs==21.1.0
attrs>=21.2.0
attrs>21.2.0
and attrs==21.1.0 was yanked (reason: Installable but not importable on Python 3.4), we can conclude that attrs>20.3.0,<21.2.0 cannot be used.
And because you require attrs>20.3.0,<21.2.0, we can conclude that your requirements are unsatisfiable.
"###
Expand Down
32 changes: 16 additions & 16 deletions crates/uv/tests/pip_install_scenarios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ fn dependency_excludes_range_of_compatible_versions() {
× No solution found when resolving dependencies:
╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and only the following versions of package-a are available:
package-a==1.0.0
package-a>=2.0.0,<=3.0.0
package-a>2.0.0,<=3.0.0
we can conclude that package-a<2.0.0 depends on package-b==1.0.0. (1)
Because only the following versions of package-c are available:
Expand Down Expand Up @@ -574,7 +574,7 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() {
× No solution found when resolving dependencies:
╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and only the following versions of package-a are available:
package-a==1.0.0
package-a>=2.0.0,<=3.0.0
package-a>2.0.0,<=3.0.0
we can conclude that package-a<2.0.0 depends on package-b==1.0.0. (1)
Because only the following versions of package-c are available:
Expand Down Expand Up @@ -1941,7 +1941,7 @@ fn local_greater_than() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a<=1.2.3 is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a==1.2.3+foo is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable.
"###);

assert_not_installed(&context.venv, "local_greater_than_a", &context.temp_dir);
Expand Down Expand Up @@ -2019,7 +2019,7 @@ fn local_less_than() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a>=1.2.3 is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a==1.2.3+foo is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable.
"###);

assert_not_installed(&context.venv, "local_less_than_a", &context.temp_dir);
Expand Down Expand Up @@ -2054,7 +2054,7 @@ fn local_less_than_or_equal() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a>1.2.3 is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a==1.2.3+foo is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable.
"###);

// The version '1.2.3+foo' satisfies the constraint '<=1.2.3'.
Expand Down Expand Up @@ -2172,7 +2172,7 @@ fn post_greater_than() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a<=1.2.3 is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable.
"###);

assert_not_installed(&context.venv, "post_greater_than_a", &context.temp_dir);
Expand Down Expand Up @@ -2298,7 +2298,7 @@ fn post_less_than_or_equal() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a>1.2.3 is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable.
"###);

assert_not_installed(
Expand Down Expand Up @@ -2337,7 +2337,7 @@ fn post_less_than() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a>=1.2.3 is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable.
"###);

assert_not_installed(&context.venv, "post_less_than_a", &context.temp_dir);
Expand Down Expand Up @@ -2374,7 +2374,7 @@ fn post_local_greater_than() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a<=1.2.3 is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a<=1.2.3.post1+local is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable.
"###);

assert_not_installed(
Expand Down Expand Up @@ -2415,7 +2415,7 @@ fn post_local_greater_than_post() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a<1.2.3.post2 is available and you require package-a>=1.2.3.post2, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a<=1.2.3.post1+local is available and you require package-a>=1.2.3.post2, we can conclude that your requirements are unsatisfiable.
"###);

assert_not_installed(
Expand Down Expand Up @@ -2543,7 +2543,7 @@ fn post_greater_than_post_not_available() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a<1.2.3.post3 is available and you require package-a>=1.2.3.post3, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a<=1.2.3.post1 is available and you require package-a>=1.2.3.post3, we can conclude that your requirements are unsatisfiable.
"###);

assert_not_installed(
Expand Down Expand Up @@ -2629,7 +2629,7 @@ fn package_only_prereleases_in_range() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-a<=0.1.0 is available and you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable.
╰─▶ Because only package-a<0.1.0 is available and you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable.
hint: Pre-releases are available for package-a in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`)
"###);
Expand Down Expand Up @@ -3077,7 +3077,7 @@ fn transitive_package_only_prereleases_in_range() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only package-b<=0.1 is available and package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used.
╰─▶ Because only package-b<0.1 is available and package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used.
And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable.
hint: Pre-releases are available for package-b in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`)
Expand Down Expand Up @@ -3451,7 +3451,7 @@ fn transitive_prerelease_and_stable_dependency_many_versions_holes() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only the following versions of package-c are available:
package-c<=1.0.0
package-c<1.0.0
package-c>=2.0.0a5,<=2.0.0a7
package-c==2.0.0b1
package-c>=2.0.0b5
Expand Down Expand Up @@ -4515,7 +4515,7 @@ fn package_only_yanked_in_range() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only the following versions of package-a are available:
package-a<=0.1.0
package-a<0.1.0
package-a==1.0.0
and package-a==1.0.0 was yanked (reason: Yanked for testing), we can conclude that package-a>0.1.0 cannot be used.
And because you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable.
Expand Down Expand Up @@ -4707,7 +4707,7 @@ fn transitive_package_only_yanked_in_range() {
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because only the following versions of package-b are available:
package-b<=0.1
package-b<0.1
package-b==1.0.0
and package-b==1.0.0 was yanked (reason: Yanked for testing), we can conclude that package-b>0.1 cannot be used.
And because package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used.
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/tests/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,7 @@ fn workspace_unsatisfiable_member_dependencies() -> Result<()> {
----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
× No solution found when resolving dependencies:
╰─▶ Because only httpx<=9999 is available and leaf depends on httpx>9999, we can conclude that leaf's requirements are unsatisfiable.
╰─▶ Because only httpx<=1.0.0b0 is available and leaf depends on httpx>9999, we can conclude that leaf's requirements are unsatisfiable.
And because your workspace requires leaf, we can conclude that your workspace's requirements are unsatisfiable.
"###
);
Expand Down

0 comments on commit 7cd52c5

Please sign in to comment.