mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 10:33:49 +00:00
Improve resolver error messages referencing workspace members (#6092)
An extension of #6090 that replaces #6066. In brief, 1. Workspace member names are passed to the resolver for no solution errors 2. There is a new derivation tree pre-processing step that trims `NoVersion` incompatibilities for workspace members from the derivation tree. This avoids showing redundant clauses like `Because only bird==0.1.0 is available and bird==0.1.0 depends on anyio==4.3.0, we can conclude that all versions of bird depend on anyio==4.3.0.`. As a minor note, we use a custom incompatibility kind to mark these incompatibilities at resolution-time instead of afterwards. 3. Root dependencies on workspace members say `your workspace requires bird` rather than `you require bird` 4. Workspace member package display omits the version, e.g., `bird` instead of `bird==0.1.0` 5. Instead of reporting a workspace member as unusable we note that its requirements cannot be solved, e.g., `bird's requirements are unsatisfiable` instead of `bird cannot be used`. 6. Instead of saying `your requirements are unsatisfiable` we say `your workspace's requirements are unsatisfiable` when in a workspace, since we're not in a "provide direct requirements" paradigm. As an annoying but minor implementation detail, `PackageRange` now requires access to the `PubGrubReportFormatter` so it can determine if it is formatting a workspace member or not. We could probably improve the abstractions in the future. As a follow-up, we should additional special casing for "single project" workspaces to avoid mention of the workspace concept in simple projects. However, it looks like this will require additional tree manipulations so I'm going to keep it separate.
This commit is contained in:
parent
f28ce546cf
commit
2e3e6a01aa
16 changed files with 353 additions and 149 deletions
|
@ -125,6 +125,7 @@ pub struct NoSolutionError {
|
|||
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
|
||||
fork_urls: ForkUrls,
|
||||
markers: ResolverMarkers,
|
||||
workspace_members: BTreeSet<PackageName>,
|
||||
}
|
||||
|
||||
impl NoSolutionError {
|
||||
|
@ -139,6 +140,7 @@ impl NoSolutionError {
|
|||
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
|
||||
fork_urls: ForkUrls,
|
||||
markers: ResolverMarkers,
|
||||
workspace_members: BTreeSet<PackageName>,
|
||||
) -> Self {
|
||||
Self {
|
||||
error,
|
||||
|
@ -150,6 +152,7 @@ impl NoSolutionError {
|
|||
incomplete_packages,
|
||||
fork_urls,
|
||||
markers,
|
||||
workspace_members,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,8 +214,14 @@ impl std::fmt::Display for NoSolutionError {
|
|||
let formatter = PubGrubReportFormatter {
|
||||
available_versions: &self.available_versions,
|
||||
python_requirement: &self.python_requirement,
|
||||
workspace_members: &self.workspace_members,
|
||||
};
|
||||
let report = DefaultStringReporter::report_with_formatter(&self.error, &formatter);
|
||||
|
||||
// Transform the error tree for reporting
|
||||
let mut tree = self.error.clone();
|
||||
collapse_unavailable_workspace_members(&mut tree);
|
||||
|
||||
let report = DefaultStringReporter::report_with_formatter(&tree, &formatter);
|
||||
write!(f, "{report}")?;
|
||||
|
||||
// Include any additional hints.
|
||||
|
@ -232,6 +241,51 @@ impl std::fmt::Display for NoSolutionError {
|
|||
}
|
||||
}
|
||||
|
||||
/// Given a [`DerivationTree`], collapse any [`UnavailablePackage::WorkspaceMember`] incompatibilities
|
||||
/// to avoid saying things like "only <workspace-member>==0.1.0 is available".
|
||||
fn collapse_unavailable_workspace_members(
|
||||
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
||||
) {
|
||||
match tree {
|
||||
DerivationTree::External(_) => {}
|
||||
DerivationTree::Derived(derived) => {
|
||||
match (
|
||||
Arc::make_mut(&mut derived.cause1),
|
||||
Arc::make_mut(&mut derived.cause2),
|
||||
) {
|
||||
// If one node is an unavailable workspace member...
|
||||
(
|
||||
DerivationTree::External(External::Custom(
|
||||
_,
|
||||
_,
|
||||
UnavailableReason::Package(UnavailablePackage::WorkspaceMember),
|
||||
)),
|
||||
ref mut other,
|
||||
)
|
||||
| (
|
||||
ref mut other,
|
||||
DerivationTree::External(External::Custom(
|
||||
_,
|
||||
_,
|
||||
UnavailableReason::Package(UnavailablePackage::WorkspaceMember),
|
||||
)),
|
||||
) => {
|
||||
// First, recursively collapse the other side of the tree
|
||||
collapse_unavailable_workspace_members(other);
|
||||
|
||||
// Then, replace this node with the other tree
|
||||
*tree = other.clone();
|
||||
}
|
||||
// If not, just recurse
|
||||
_ => {
|
||||
collapse_unavailable_workspace_members(Arc::make_mut(&mut derived.cause1));
|
||||
collapse_unavailable_workspace_members(Arc::make_mut(&mut derived.cause2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NoSolutionHeader {
|
||||
/// The [`ResolverMarkers`] that caused the failure.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use either::Either;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use pep508_rs::MarkerEnvironment;
|
||||
use pypi_types::Requirement;
|
||||
|
@ -36,6 +37,9 @@ pub struct Manifest {
|
|||
/// The name of the project.
|
||||
pub(crate) project: Option<PackageName>,
|
||||
|
||||
/// Members of the project's workspace.
|
||||
pub(crate) workspace_members: BTreeSet<PackageName>,
|
||||
|
||||
/// The installed packages to exclude from consideration during resolution.
|
||||
///
|
||||
/// These typically represent packages that are being upgraded or reinstalled
|
||||
|
@ -58,6 +62,7 @@ impl Manifest {
|
|||
dev: Vec<GroupName>,
|
||||
preferences: Preferences,
|
||||
project: Option<PackageName>,
|
||||
workspace_members: Option<BTreeSet<PackageName>>,
|
||||
exclusions: Exclusions,
|
||||
lookaheads: Vec<RequestedRequirements>,
|
||||
) -> Self {
|
||||
|
@ -68,6 +73,7 @@ impl Manifest {
|
|||
dev,
|
||||
preferences,
|
||||
project,
|
||||
workspace_members: workspace_members.unwrap_or_default(),
|
||||
exclusions,
|
||||
lookaheads,
|
||||
}
|
||||
|
@ -82,6 +88,7 @@ impl Manifest {
|
|||
preferences: Preferences::default(),
|
||||
project: None,
|
||||
exclusions: Exclusions::default(),
|
||||
workspace_members: BTreeSet::new(),
|
||||
lookaheads: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ pub(crate) struct PubGrubReportFormatter<'a> {
|
|||
|
||||
/// The versions that were available for each package
|
||||
pub(crate) python_requirement: &'a PythonRequirement,
|
||||
|
||||
pub(crate) workspace_members: &'a BTreeSet<PackageName>,
|
||||
}
|
||||
|
||||
impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
|
||||
|
@ -53,12 +55,12 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
|
|||
return if let Some(target) = self.python_requirement.target() {
|
||||
format!(
|
||||
"the requested {package} version ({target}) does not satisfy {}",
|
||||
PackageRange::compatibility(package, set)
|
||||
self.compatible_range(package, set)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"the requested {package} version does not satisfy {}",
|
||||
PackageRange::compatibility(package, set)
|
||||
self.compatible_range(package, set)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
@ -69,7 +71,7 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
|
|||
return format!(
|
||||
"the current {package} version ({}) does not satisfy {}",
|
||||
self.python_requirement.installed(),
|
||||
PackageRange::compatibility(package, set)
|
||||
self.compatible_range(package, set)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -86,57 +88,51 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
|
|||
if segments == 1 {
|
||||
format!(
|
||||
"only {} is available",
|
||||
PackageRange::compatibility(package, &complement)
|
||||
self.compatible_range(package, &complement)
|
||||
)
|
||||
// Complex case, there are multiple ranges
|
||||
} else {
|
||||
format!(
|
||||
"only the following versions of {} {}",
|
||||
package,
|
||||
PackageRange::available(package, &complement)
|
||||
self.availability_range(package, &complement)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
External::Custom(package, set, reason) => match &**package {
|
||||
PubGrubPackageInner::Root(Some(name)) => {
|
||||
format!("{name} cannot be used because {reason}")
|
||||
}
|
||||
PubGrubPackageInner::Root(None) => {
|
||||
format!("your requirements cannot be used because {reason}")
|
||||
}
|
||||
_ => match reason {
|
||||
UnavailableReason::Package(reason) => {
|
||||
// While there may be a term attached, this error applies to the entire
|
||||
// package, so we show it for the entire package
|
||||
format!("{}{reason}", Padded::new("", &package, " "))
|
||||
External::Custom(package, set, reason) => {
|
||||
if let Some(root) = self.format_root(package) {
|
||||
format!("{root} cannot be used because {reason}")
|
||||
} else {
|
||||
match reason {
|
||||
UnavailableReason::Package(reason) => {
|
||||
// While there may be a term attached, this error applies to the entire
|
||||
// package, so we show it for the entire package
|
||||
format!("{}{reason}", Padded::new("", &package, " "))
|
||||
}
|
||||
UnavailableReason::Version(reason) => {
|
||||
format!(
|
||||
"{}{reason}",
|
||||
Padded::new("", &self.compatible_range(package, set), " ")
|
||||
)
|
||||
}
|
||||
}
|
||||
UnavailableReason::Version(reason) => {
|
||||
format!(
|
||||
"{}{reason}",
|
||||
Padded::new("", &PackageRange::compatibility(package, set), " ")
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
External::FromDependencyOf(package, package_set, dependency, dependency_set) => {
|
||||
let package_set = self.simplify_set(package_set, package);
|
||||
let dependency_set = self.simplify_set(dependency_set, dependency);
|
||||
match &**package {
|
||||
PubGrubPackageInner::Root(Some(name)) => format!(
|
||||
"{name} depends on {}",
|
||||
PackageRange::dependency(dependency, &dependency_set)
|
||||
),
|
||||
PubGrubPackageInner::Root(None) => format!(
|
||||
"you require {}",
|
||||
PackageRange::dependency(dependency, &dependency_set)
|
||||
),
|
||||
_ => format!(
|
||||
"{}",
|
||||
PackageRange::compatibility(package, &package_set)
|
||||
.depends_on(dependency, &dependency_set),
|
||||
),
|
||||
if let Some(root) = self.format_root_requires(package) {
|
||||
return format!(
|
||||
"{root} {}",
|
||||
self.dependency_range(dependency, &dependency_set)
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"{}",
|
||||
self.compatible_range(package, &package_set)
|
||||
.depends_on(dependency, &dependency_set),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -150,25 +146,24 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
|
|||
match terms_vec.as_slice() {
|
||||
[] => "the requirements are unsatisfiable".into(),
|
||||
[(root, _)] if matches!(&**(*root), PubGrubPackageInner::Root(_)) => {
|
||||
"the requirements are unsatisfiable".into()
|
||||
let root = self.format_root(root).unwrap();
|
||||
format!("{root} are unsatisfiable")
|
||||
}
|
||||
[(package, Term::Positive(range))]
|
||||
if matches!(&**(*package), PubGrubPackageInner::Package { .. }) =>
|
||||
{
|
||||
let range = self.simplify_set(range, package);
|
||||
format!(
|
||||
"{} cannot be used",
|
||||
PackageRange::compatibility(package, &range)
|
||||
)
|
||||
if let Some(member) = self.format_workspace_member(package) {
|
||||
format!("{member}'s requirements are unsatisfiable")
|
||||
} else {
|
||||
format!("{} cannot be used", self.compatible_range(package, &range))
|
||||
}
|
||||
}
|
||||
[(package, Term::Negative(range))]
|
||||
if matches!(&**(*package), PubGrubPackageInner::Package { .. }) =>
|
||||
{
|
||||
let range = self.simplify_set(range, package);
|
||||
format!(
|
||||
"{} must be used",
|
||||
PackageRange::compatibility(package, &range)
|
||||
)
|
||||
format!("{} must be used", self.compatible_range(package, &range))
|
||||
}
|
||||
[(p1, Term::Positive(r1)), (p2, Term::Negative(r2))] => self.format_external(
|
||||
&External::FromDependencyOf((*p1).clone(), r1.clone(), (*p2).clone(), r2.clone()),
|
||||
|
@ -180,7 +175,7 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
|
|||
let mut result = String::new();
|
||||
let str_terms: Vec<_> = slice
|
||||
.iter()
|
||||
.map(|(p, t)| format!("{}", PackageTerm::new(p, t)))
|
||||
.map(|(p, t)| format!("{}", PackageTerm::new(p, t, self)))
|
||||
.collect();
|
||||
for (index, term) in str_terms.iter().enumerate() {
|
||||
result.push_str(term);
|
||||
|
@ -195,7 +190,7 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
|
|||
}
|
||||
}
|
||||
if let [(p, t)] = slice {
|
||||
if PackageTerm::new(p, t).plural() {
|
||||
if PackageTerm::new(p, t, self).plural() {
|
||||
result.push_str(" are incompatible");
|
||||
} else {
|
||||
result.push_str(" is incompatible");
|
||||
|
@ -328,6 +323,88 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
|
|||
}
|
||||
|
||||
impl PubGrubReportFormatter<'_> {
|
||||
/// Return the formatting for "the root package requires", if the given
|
||||
/// package is the root package.
|
||||
///
|
||||
/// If not given the root package, returns `None`.
|
||||
fn format_root_requires(&self, package: &PubGrubPackage) -> Option<String> {
|
||||
if self.is_workspace() {
|
||||
if matches!(&**package, PubGrubPackageInner::Root(_)) {
|
||||
return Some("your workspace requires".to_string());
|
||||
}
|
||||
}
|
||||
match &**package {
|
||||
PubGrubPackageInner::Root(Some(name)) => Some(format!("{name} depends on")),
|
||||
PubGrubPackageInner::Root(None) => Some("you require".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the formatting for "the root package", if the given
|
||||
/// package is the root package.
|
||||
///
|
||||
/// If not given the root package, returns `None`.
|
||||
fn format_root(&self, package: &PubGrubPackage) -> Option<String> {
|
||||
if self.is_workspace() {
|
||||
if matches!(&**package, PubGrubPackageInner::Root(_)) {
|
||||
return Some("your workspace's requirements".to_string());
|
||||
}
|
||||
}
|
||||
match &**package {
|
||||
PubGrubPackageInner::Root(Some(_)) => Some("the requirements".to_string()),
|
||||
PubGrubPackageInner::Root(None) => Some("the requirements".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the resolution error is for a workspace.
|
||||
fn is_workspace(&self) -> bool {
|
||||
!self.workspace_members.is_empty()
|
||||
}
|
||||
|
||||
/// Return a display name for the package if it is a workspace member.
|
||||
fn format_workspace_member(&self, package: &PubGrubPackage) -> Option<String> {
|
||||
match &**package {
|
||||
PubGrubPackageInner::Package { name, .. }
|
||||
| PubGrubPackageInner::Extra { name, .. }
|
||||
| PubGrubPackageInner::Dev { name, .. } => {
|
||||
if self.workspace_members.contains(name) {
|
||||
Some(format!("{name}"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`PackageRange::compatibility`] display with this formatter attached.
|
||||
fn compatible_range<'a>(
|
||||
&'a self,
|
||||
package: &'a PubGrubPackage,
|
||||
range: &'a Range<Version>,
|
||||
) -> PackageRange<'a> {
|
||||
PackageRange::compatibility(package, range, Some(self))
|
||||
}
|
||||
|
||||
/// Create a [`PackageRange::dependency`] display with this formatter attached.
|
||||
fn dependency_range<'a>(
|
||||
&'a self,
|
||||
package: &'a PubGrubPackage,
|
||||
range: &'a Range<Version>,
|
||||
) -> PackageRange<'a> {
|
||||
PackageRange::dependency(package, range, Some(self))
|
||||
}
|
||||
|
||||
/// Create a [`PackageRange::availability`] display with this formatter attached.
|
||||
fn availability_range<'a>(
|
||||
&'a self,
|
||||
package: &'a PubGrubPackage,
|
||||
range: &'a Range<Version>,
|
||||
) -> PackageRange<'a> {
|
||||
PackageRange::availability(package, range, Some(self))
|
||||
}
|
||||
|
||||
/// Format two external incompatibilities, combining them if possible.
|
||||
fn format_both_external(
|
||||
&self,
|
||||
|
@ -340,33 +417,26 @@ impl PubGrubReportFormatter<'_> {
|
|||
External::FromDependencyOf(package2, _, dependency2, dependency_set2),
|
||||
) if package1 == package2 => {
|
||||
let dependency_set1 = self.simplify_set(dependency_set1, dependency1);
|
||||
let dependency1 = PackageRange::dependency(dependency1, &dependency_set1);
|
||||
let dependency1 = self.dependency_range(dependency1, &dependency_set1);
|
||||
|
||||
let dependency_set2 = self.simplify_set(dependency_set2, dependency2);
|
||||
let dependency2 = PackageRange::dependency(dependency2, &dependency_set2);
|
||||
let dependency2 = self.dependency_range(dependency2, &dependency_set2);
|
||||
|
||||
match &**package1 {
|
||||
PubGrubPackageInner::Root(Some(name)) => format!(
|
||||
"{name} depends on {}and {}",
|
||||
if let Some(root) = self.format_root_requires(package1) {
|
||||
return format!(
|
||||
"{root} {}and {}",
|
||||
Padded::new("", &dependency1, " "),
|
||||
dependency2,
|
||||
),
|
||||
PubGrubPackageInner::Root(None) => format!(
|
||||
"you require {}and {}",
|
||||
Padded::new("", &dependency1, " "),
|
||||
dependency2,
|
||||
),
|
||||
_ => {
|
||||
let package_set = self.simplify_set(package_set1, package1);
|
||||
|
||||
format!(
|
||||
"{}",
|
||||
PackageRange::compatibility(package1, &package_set)
|
||||
.depends_on(dependency1.package, &dependency_set1)
|
||||
.and(dependency2.package, &dependency_set2),
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
let package_set = self.simplify_set(package_set1, package1);
|
||||
|
||||
format!(
|
||||
"{}",
|
||||
self.compatible_range(package1, &package_set)
|
||||
.depends_on(dependency1.package, &dependency_set1)
|
||||
.and(dependency2.package, &dependency_set2),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
let external1 = self.format_external(external1);
|
||||
|
@ -521,7 +591,7 @@ impl PubGrubReportFormatter<'_> {
|
|||
reason: reason.clone(),
|
||||
});
|
||||
}
|
||||
Some(UnavailablePackage::NotFound) => {}
|
||||
Some(UnavailablePackage::NotFound | UnavailablePackage::WorkspaceMember) => {}
|
||||
None => {}
|
||||
}
|
||||
|
||||
|
@ -716,7 +786,7 @@ impl std::fmt::Display for PubGrubHint {
|
|||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package.bold(),
|
||||
PackageRange::compatibility(package, range).bold()
|
||||
PackageRange::compatibility(package, range, None).bold()
|
||||
)
|
||||
}
|
||||
Self::NoIndex => {
|
||||
|
@ -831,7 +901,7 @@ impl std::fmt::Display for PubGrubHint {
|
|||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
requires_python.bold(),
|
||||
PackageRange::compatibility(package, package_set).bold(),
|
||||
PackageRange::compatibility(package, package_set, None).bold(),
|
||||
package_requires_python.bold(),
|
||||
package_requires_python.bold(),
|
||||
)
|
||||
|
@ -844,12 +914,15 @@ impl std::fmt::Display for PubGrubHint {
|
|||
struct PackageTerm<'a> {
|
||||
package: &'a PubGrubPackage,
|
||||
term: &'a Term<Range<Version>>,
|
||||
formatter: &'a PubGrubReportFormatter<'a>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PackageTerm<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.term {
|
||||
Term::Positive(set) => write!(f, "{}", PackageRange::compatibility(self.package, set)),
|
||||
Term::Positive(set) => {
|
||||
write!(f, "{}", self.formatter.compatible_range(self.package, set))
|
||||
}
|
||||
Term::Negative(set) => {
|
||||
if let Some(version) = set.as_singleton() {
|
||||
// Note we do not handle the "root" package here but we should never
|
||||
|
@ -860,7 +933,8 @@ impl std::fmt::Display for PackageTerm<'_> {
|
|||
write!(
|
||||
f,
|
||||
"{}",
|
||||
PackageRange::compatibility(self.package, &set.complement())
|
||||
self.formatter
|
||||
.compatible_range(self.package, &set.complement())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -870,19 +944,29 @@ impl std::fmt::Display for PackageTerm<'_> {
|
|||
|
||||
impl PackageTerm<'_> {
|
||||
/// Create a new [`PackageTerm`] from a [`PubGrubPackage`] and a [`Term`].
|
||||
fn new<'a>(package: &'a PubGrubPackage, term: &'a Term<Range<Version>>) -> PackageTerm<'a> {
|
||||
PackageTerm { package, term }
|
||||
fn new<'a>(
|
||||
package: &'a PubGrubPackage,
|
||||
term: &'a Term<Range<Version>>,
|
||||
formatter: &'a PubGrubReportFormatter<'a>,
|
||||
) -> PackageTerm<'a> {
|
||||
PackageTerm {
|
||||
package,
|
||||
term,
|
||||
formatter,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the predicate following this package term should be singular or plural.
|
||||
fn plural(&self) -> bool {
|
||||
match self.term {
|
||||
Term::Positive(set) => PackageRange::compatibility(self.package, set).plural(),
|
||||
Term::Positive(set) => self.formatter.compatible_range(self.package, set).plural(),
|
||||
Term::Negative(set) => {
|
||||
if set.as_singleton().is_some() {
|
||||
false
|
||||
} else {
|
||||
PackageRange::compatibility(self.package, &set.complement()).plural()
|
||||
self.formatter
|
||||
.compatible_range(self.package, &set.complement())
|
||||
.plural()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -903,9 +987,49 @@ struct PackageRange<'a> {
|
|||
package: &'a PubGrubPackage,
|
||||
range: &'a Range<Version>,
|
||||
kind: PackageRangeKind,
|
||||
formatter: Option<&'a PubGrubReportFormatter<'a>>,
|
||||
}
|
||||
|
||||
impl PackageRange<'_> {
|
||||
fn compatibility<'a>(
|
||||
package: &'a PubGrubPackage,
|
||||
range: &'a Range<Version>,
|
||||
formatter: Option<&'a PubGrubReportFormatter<'a>>,
|
||||
) -> PackageRange<'a> {
|
||||
PackageRange {
|
||||
package,
|
||||
range,
|
||||
kind: PackageRangeKind::Compatibility,
|
||||
formatter,
|
||||
}
|
||||
}
|
||||
|
||||
fn dependency<'a>(
|
||||
package: &'a PubGrubPackage,
|
||||
range: &'a Range<Version>,
|
||||
formatter: Option<&'a PubGrubReportFormatter<'a>>,
|
||||
) -> PackageRange<'a> {
|
||||
PackageRange {
|
||||
package,
|
||||
range,
|
||||
kind: PackageRangeKind::Dependency,
|
||||
formatter,
|
||||
}
|
||||
}
|
||||
|
||||
fn availability<'a>(
|
||||
package: &'a PubGrubPackage,
|
||||
range: &'a Range<Version>,
|
||||
formatter: Option<&'a PubGrubReportFormatter<'a>>,
|
||||
) -> PackageRange<'a> {
|
||||
PackageRange {
|
||||
package,
|
||||
range,
|
||||
kind: PackageRangeKind::Available,
|
||||
formatter,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a boolean indicating if the predicate following this package range should
|
||||
/// be singular or plural e.g. if false use "<range> depends on <...>" and
|
||||
/// if true use "<range> depend on <...>"
|
||||
|
@ -930,11 +1054,20 @@ impl PackageRange<'_> {
|
|||
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
|
||||
let package = match &**self.package {
|
||||
PubGrubPackageInner::Root(Some(name)) => return write!(f, "{name}"),
|
||||
PubGrubPackageInner::Root(None) => return write!(f, "your requirements"),
|
||||
_ => self.package,
|
||||
};
|
||||
if let Some(root) = self
|
||||
.formatter
|
||||
.and_then(|formatter| formatter.format_root(self.package))
|
||||
{
|
||||
return write!(f, "{root}");
|
||||
}
|
||||
// Exit early for workspace members, only a single version is available
|
||||
if let Some(member) = self
|
||||
.formatter
|
||||
.and_then(|formatter| formatter.format_workspace_member(self.package))
|
||||
{
|
||||
return write!(f, "{member}");
|
||||
}
|
||||
let package = self.package;
|
||||
|
||||
if self.range.is_empty() {
|
||||
return write!(f, "{package} ∅");
|
||||
|
@ -982,33 +1115,6 @@ impl std::fmt::Display for PackageRange<'_> {
|
|||
}
|
||||
|
||||
impl PackageRange<'_> {
|
||||
fn compatibility<'a>(
|
||||
package: &'a PubGrubPackage,
|
||||
range: &'a Range<Version>,
|
||||
) -> PackageRange<'a> {
|
||||
PackageRange {
|
||||
package,
|
||||
range,
|
||||
kind: PackageRangeKind::Compatibility,
|
||||
}
|
||||
}
|
||||
|
||||
fn dependency<'a>(package: &'a PubGrubPackage, range: &'a Range<Version>) -> PackageRange<'a> {
|
||||
PackageRange {
|
||||
package,
|
||||
range,
|
||||
kind: PackageRangeKind::Dependency,
|
||||
}
|
||||
}
|
||||
|
||||
fn available<'a>(package: &'a PubGrubPackage, range: &'a Range<Version>) -> PackageRange<'a> {
|
||||
PackageRange {
|
||||
package,
|
||||
range,
|
||||
kind: PackageRangeKind::Available,
|
||||
}
|
||||
}
|
||||
|
||||
fn depends_on<'a>(
|
||||
&'a self,
|
||||
package: &'a PubGrubPackage,
|
||||
|
@ -1016,7 +1122,12 @@ impl PackageRange<'_> {
|
|||
) -> DependsOn<'a> {
|
||||
DependsOn {
|
||||
package: self,
|
||||
dependency1: PackageRange::dependency(package, range),
|
||||
dependency1: PackageRange {
|
||||
package,
|
||||
range,
|
||||
kind: PackageRangeKind::Dependency,
|
||||
formatter: self.formatter,
|
||||
},
|
||||
dependency2: None,
|
||||
}
|
||||
}
|
||||
|
@ -1035,7 +1146,12 @@ impl<'a> DependsOn<'a> {
|
|||
///
|
||||
/// Note this overwrites previous calls to `DependsOn::and`.
|
||||
fn and(mut self, package: &'a PubGrubPackage, range: &'a Range<Version>) -> DependsOn<'a> {
|
||||
self.dependency2 = Some(PackageRange::dependency(package, range));
|
||||
self.dependency2 = Some(PackageRange {
|
||||
package,
|
||||
range,
|
||||
kind: PackageRangeKind::Dependency,
|
||||
formatter: self.package.formatter,
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,8 @@ pub(crate) enum UnavailablePackage {
|
|||
InvalidMetadata(String),
|
||||
/// The package has an invalid structure.
|
||||
InvalidStructure(String),
|
||||
/// No other versions of the package can be used because it is a workspace member
|
||||
WorkspaceMember,
|
||||
}
|
||||
|
||||
impl UnavailablePackage {
|
||||
|
@ -85,6 +87,7 @@ impl UnavailablePackage {
|
|||
UnavailablePackage::MissingMetadata => "does not include a `METADATA` file",
|
||||
UnavailablePackage::InvalidMetadata(_) => "has invalid metadata",
|
||||
UnavailablePackage::InvalidStructure(_) => "has an invalid package format",
|
||||
UnavailablePackage::WorkspaceMember => "is a workspace member",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
|
|||
hasher: HashStrategy,
|
||||
markers: ResolverMarkers,
|
||||
python_requirement: PythonRequirement,
|
||||
workspace_members: BTreeSet<PackageName>,
|
||||
/// This is derived from `PythonRequirement` once at initialization
|
||||
/// time. It's used in universal mode to filter our dependencies with
|
||||
/// a `python_version` marker expression that has no overlap with the
|
||||
|
@ -225,6 +226,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
|
|||
),
|
||||
groups: Groups::from_manifest(&manifest, markers.marker_environment()),
|
||||
project: manifest.project,
|
||||
workspace_members: manifest.workspace_members,
|
||||
requirements: manifest.requirements,
|
||||
constraints: manifest.constraints,
|
||||
overrides: manifest.overrides,
|
||||
|
@ -449,8 +451,23 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
.term_intersection_for_package(&state.next)
|
||||
.expect("a package was chosen but we don't have a term");
|
||||
|
||||
// Check if the decision was due to the package being unavailable
|
||||
if let PubGrubPackageInner::Package { ref name, .. } = &*state.next {
|
||||
// Check if the decision was due to the package being a
|
||||
// workspace member
|
||||
if self.workspace_members.contains(name) {
|
||||
state
|
||||
.pubgrub
|
||||
.add_incompatibility(Incompatibility::custom_term(
|
||||
state.next.clone(),
|
||||
term_intersection.clone(),
|
||||
UnavailableReason::Package(
|
||||
UnavailablePackage::WorkspaceMember,
|
||||
),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the decision was due to the package being unavailable
|
||||
if let Some(entry) = self.unavailable_packages.get(name) {
|
||||
state
|
||||
.pubgrub
|
||||
|
@ -1988,6 +2005,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
incomplete_packages,
|
||||
fork_urls,
|
||||
markers,
|
||||
self.workspace_members.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue