From 2e3e6a01aa82027bbfc68be88403a08f554ae638 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 14 Aug 2024 21:41:31 -0500 Subject: [PATCH] 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. --- crates/uv-resolver/src/error.rs | 56 ++- crates/uv-resolver/src/manifest.rs | 7 + crates/uv-resolver/src/pubgrub/report.rs | 344 ++++++++++++------ .../uv-resolver/src/resolver/availability.rs | 3 + crates/uv-resolver/src/resolver/mod.rs | 20 +- crates/uv/src/commands/pip/compile.rs | 1 + crates/uv/src/commands/pip/install.rs | 1 + crates/uv/src/commands/pip/operations.rs | 3 + crates/uv/src/commands/pip/sync.rs | 1 + crates/uv/src/commands/project/lock.rs | 1 + crates/uv/src/commands/project/mod.rs | 2 + crates/uv/tests/edit.rs | 4 +- crates/uv/tests/lock.rs | 11 +- crates/uv/tests/lock_scenarios.rs | 28 +- crates/uv/tests/pip_install.rs | 2 +- crates/uv/tests/workspace.rs | 18 +- 16 files changed, 353 insertions(+), 149 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 513b99731..32436a2ce 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -125,6 +125,7 @@ pub struct NoSolutionError { incomplete_packages: FxHashMap>, fork_urls: ForkUrls, markers: ResolverMarkers, + workspace_members: BTreeSet, } impl NoSolutionError { @@ -139,6 +140,7 @@ impl NoSolutionError { incomplete_packages: FxHashMap>, fork_urls: ForkUrls, markers: ResolverMarkers, + workspace_members: BTreeSet, ) -> 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 ==0.1.0 is available". +fn collapse_unavailable_workspace_members( + tree: &mut DerivationTree, 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. diff --git a/crates/uv-resolver/src/manifest.rs b/crates/uv-resolver/src/manifest.rs index 91a2d68e5..741ae98d0 100644 --- a/crates/uv-resolver/src/manifest.rs +++ b/crates/uv-resolver/src/manifest.rs @@ -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, + /// Members of the project's workspace. + pub(crate) workspace_members: BTreeSet, + /// 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, preferences: Preferences, project: Option, + workspace_members: Option>, exclusions: Exclusions, lookaheads: Vec, ) -> 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(), } } diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 5f8606d2a..32b235221 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -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, } impl ReportFormatter, UnavailableReason> @@ -53,12 +55,12 @@ impl ReportFormatter, 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, 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, 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, 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, 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, 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, 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 { + 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 { + 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 { + 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, + ) -> 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, + ) -> 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, + ) -> 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>, + 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>) -> PackageTerm<'a> { - PackageTerm { package, term } + fn new<'a>( + package: &'a PubGrubPackage, + term: &'a Term>, + 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, kind: PackageRangeKind, + formatter: Option<&'a PubGrubReportFormatter<'a>>, } impl PackageRange<'_> { + fn compatibility<'a>( + package: &'a PubGrubPackage, + range: &'a Range, + formatter: Option<&'a PubGrubReportFormatter<'a>>, + ) -> PackageRange<'a> { + PackageRange { + package, + range, + kind: PackageRangeKind::Compatibility, + formatter, + } + } + + fn dependency<'a>( + package: &'a PubGrubPackage, + range: &'a Range, + formatter: Option<&'a PubGrubReportFormatter<'a>>, + ) -> PackageRange<'a> { + PackageRange { + package, + range, + kind: PackageRangeKind::Dependency, + formatter, + } + } + + fn availability<'a>( + package: &'a PubGrubPackage, + range: &'a Range, + 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 " depends on <...>" and /// if true use " 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, - ) -> PackageRange<'a> { - PackageRange { - package, - range, - kind: PackageRangeKind::Compatibility, - } - } - - fn dependency<'a>(package: &'a PubGrubPackage, range: &'a Range) -> PackageRange<'a> { - PackageRange { - package, - range, - kind: PackageRangeKind::Dependency, - } - } - - fn available<'a>(package: &'a PubGrubPackage, range: &'a Range) -> 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) -> DependsOn<'a> { - self.dependency2 = Some(PackageRange::dependency(package, range)); + self.dependency2 = Some(PackageRange { + package, + range, + kind: PackageRangeKind::Dependency, + formatter: self.package.formatter, + }); self } } diff --git a/crates/uv-resolver/src/resolver/availability.rs b/crates/uv-resolver/src/resolver/availability.rs index 1b01cb25f..f40dfe499 100644 --- a/crates/uv-resolver/src/resolver/availability.rs +++ b/crates/uv-resolver/src/resolver/availability.rs @@ -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", } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 1963dae41..52bdd861e 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -102,6 +102,7 @@ struct ResolverState { hasher: HashStrategy, markers: ResolverMarkers, python_requirement: PythonRequirement, + workspace_members: BTreeSet, /// 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 ), 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 ResolverState ResolverState( dev: Vec, source_trees: Vec, mut project: Option, + workspace_members: Option>, extras: &ExtrasSpecification, preferences: Vec, installed_packages: InstalledPackages, @@ -230,6 +232,7 @@ pub(crate) async fn resolve( dev, preferences, project, + workspace_members, exclusions, lookaheads, ); diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 32d58db41..89466fdaf 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -288,6 +288,7 @@ pub(crate) async fn pip_sync( dev, source_trees, project, + None, &extras, preferences, site_packages.clone(), diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index b928cbad6..0c64a2f6c 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -596,6 +596,7 @@ async fn do_lock( dev, source_trees, None, + Some(workspace.packages().keys().cloned().collect()), &extras, preferences, EmptyInstalledPackages, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 52a434ef4..4cf74c6b4 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -630,6 +630,7 @@ pub(crate) async fn resolve_environment<'a>( dev, source_trees, project, + None, &extras, preferences, EmptyInstalledPackages, @@ -952,6 +953,7 @@ pub(crate) async fn update_environment( dev, source_trees, project, + None, &extras, preferences, site_packages.clone(), diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index 23ac75268..2cca891ce 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -2578,8 +2578,8 @@ fn add_error() -> Result<()> { ----- stderr ----- warning: `uv add` is experimental and may change without warning × No solution found when resolving dependencies: - ╰─▶ Because there are no versions of xyz and project==0.1.0 depends on xyz, we can conclude that project==0.1.0 cannot be used. - And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because there are no versions of xyz and project depends on xyz, we can conclude that project's requirements are unsatisfiable. + And because your workspace requires project, we can conclude that your workspace's requirements are unsatisfiable. help: If this is intentional, run `uv add --frozen` to skip the lock and sync steps. "###); diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 38cb17ea0..33170b3f6 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -2748,8 +2748,7 @@ fn lock_requires_python() -> Result<()> { pygls>=1.1.0,<1.3.0 pygls>1.3.0 cannot be used, we can conclude that pygls>=1.1.0 cannot be used. - And because project==0.1.0 depends on pygls>=1.1.0, we can conclude that project==0.1.0 cannot be used. - And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable. + And because project depends on pygls>=1.1.0 and your workspace requires project, we can conclude that your workspace's requirements are unsatisfiable. hint: The `requires-python` value (>=3.7) includes Python versions that are not supported by your dependencies (e.g., pygls>=1.1.0,<=1.2.1 only supports >=3.7.9, <4). Consider using a more restrictive `requires-python` value (like >=3.7.9, <4). "###); @@ -5016,8 +5015,8 @@ fn lock_requires_python_no_wheels() -> Result<()> { ----- stderr ----- warning: `uv lock` is experimental and may change without warning × No solution found when resolving dependencies: - ╰─▶ Because dearpygui==1.9.1 has no wheels with a matching Python ABI tag and project==0.1.0 depends on dearpygui==1.9.1, we can conclude that project==0.1.0 cannot be used. - And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because dearpygui==1.9.1 has no wheels with a matching Python ABI tag and project depends on dearpygui==1.9.1, we can conclude that project's requirements are unsatisfiable. + And because your workspace requires project, we can conclude that your workspace's requirements are unsatisfiable. "###); Ok(()) @@ -8211,8 +8210,8 @@ fn unconditional_overlapping_marker_disjoint_version_constraints() -> Result<()> ----- stderr ----- warning: `uv lock` is experimental and may change without warning × No solution found when resolving dependencies for split (python_version > '3.10'): - ╰─▶ Because only datasets{python_version > '3.10'}<2.19 is available and project==0.1.0 depends on datasets{python_version > '3.10'}>=2.19, we can conclude that project==0.1.0 cannot be used. - And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because only datasets{python_version > '3.10'}<2.19 is available and project depends on datasets{python_version > '3.10'}>=2.19, we can conclude that project's requirements are unsatisfiable. + And because your workspace requires project, we can conclude that your workspace's requirements are unsatisfiable. "###); Ok(()) diff --git a/crates/uv/tests/lock_scenarios.rs b/crates/uv/tests/lock_scenarios.rs index 558cefe16..4e76f13aa 100644 --- a/crates/uv/tests/lock_scenarios.rs +++ b/crates/uv/tests/lock_scenarios.rs @@ -468,13 +468,13 @@ fn conflict_in_fork() -> Result<()> { warning: `uv lock` is experimental and may change without warning × No solution found when resolving dependencies for split (sys_platform == 'darwin'): ╰─▶ Because only package-b==1.0.0 is available and package-b==1.0.0 depends on package-d==1, we can conclude that all versions of package-b depend on package-d==1. - And because package-c==1.0.0 depends on package-d==2 and only package-c==1.0.0 is available, we can conclude that all versions of package-b and all versions of package-c are incompatible. - And because package-a{sys_platform == 'darwin'}==1.0.0 depends on package-b and package-c, we can conclude that package-a{sys_platform == 'darwin'}==1.0.0 cannot be used. - And because only the following versions of package-a{sys_platform == 'darwin'} are available: + And because package-c==1.0.0 depends on package-d==2, we can conclude that all versions of package-b and package-c==1.0.0 are incompatible. + And because only package-c==1.0.0 is available and package-a{sys_platform == 'darwin'}==1.0.0 depends on package-b, we can conclude that package-a{sys_platform == 'darwin'}==1.0.0 and all versions of package-c are incompatible. + And because package-a{sys_platform == 'darwin'}==1.0.0 depends on package-c and only the following versions of package-a{sys_platform == 'darwin'} are available: package-a{sys_platform == 'darwin'}==1.0.0 package-a{sys_platform == 'darwin'}>=2 - and project==0.1.0 depends on package-a{sys_platform == 'darwin'}<2, we can conclude that project==0.1.0 cannot be used. - And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable. + we can conclude that package-a{sys_platform == 'darwin'}<2 is incompatible. + And because project depends on package-a{sys_platform == 'darwin'}<2 and your workspace requires project, we can conclude that your workspace's requirements are unsatisfiable. "### ); @@ -537,8 +537,8 @@ fn fork_conflict_unsatisfiable() -> Result<()> { ----- stderr ----- warning: `uv lock` is experimental and may change without warning × No solution found when resolving dependencies: - ╰─▶ Because project==0.1.0 depends on package-a>=2 and package-a<2, we can conclude that project==0.1.0 cannot be used. - And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because project depends on package-a>=2 and package-a<2, we can conclude that project's requirements are unsatisfiable. + And because your workspace requires project, we can conclude that your workspace's requirements are unsatisfiable. "### ); @@ -1262,8 +1262,8 @@ fn fork_marker_disjoint() -> Result<()> { ----- stderr ----- warning: `uv lock` is experimental and may change without warning × No solution found when resolving dependencies for split (sys_platform == 'linux'): - ╰─▶ Because project==0.1.0 depends on package-a{sys_platform == 'linux'}>=2 and package-a{sys_platform == 'linux'}<2, we can conclude that project==0.1.0 cannot be used. - And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because project depends on package-a{sys_platform == 'linux'}>=2 and package-a{sys_platform == 'linux'}<2, we can conclude that project's requirements are unsatisfiable. + And because your workspace requires project, we can conclude that your workspace's requirements are unsatisfiable. "### ); @@ -3024,8 +3024,8 @@ fn fork_non_local_fork_marker_direct() -> Result<()> { warning: `uv lock` is experimental and may change without warning × No solution found when resolving dependencies: ╰─▶ Because package-b{sys_platform == 'darwin'}==1.0.0 depends on package-c>=2.0.0 and package-a{sys_platform == 'linux'}==1.0.0 depends on package-c<2.0.0, we can conclude that package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0 are incompatible. - And because project==0.1.0 depends on package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0, we can conclude that project==0.1.0 cannot be used. - And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable. + And because project depends on package-a{sys_platform == 'linux'}==1.0.0, we can conclude that project and package-b{sys_platform == 'darwin'}==1.0.0 are incompatible. + And because project depends on package-b{sys_platform == 'darwin'}==1.0.0 and your workspace requires project, we can conclude that your workspace's requirements are unsatisfiable. "### ); @@ -3101,9 +3101,9 @@ fn fork_non_local_fork_marker_transitive() -> Result<()> { And because only the following versions of package-c{sys_platform == 'linux'} are available: package-c{sys_platform == 'linux'}==1.0.0 package-c{sys_platform == 'linux'}>=2.0.0 - and package-a==1.0.0 depends on package-c{sys_platform == 'linux'}<2.0.0, we can conclude that package-a==1.0.0 and package-b==1.0.0 are incompatible. - And because project==0.1.0 depends on package-a==1.0.0 and package-b==1.0.0, we can conclude that project==0.1.0 cannot be used. - And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable. + we can conclude that package-b==1.0.0 and package-c{sys_platform == 'linux'}<2.0.0 are incompatible. + And because package-a==1.0.0 depends on package-c{sys_platform == 'linux'}<2.0.0 and project depends on package-a==1.0.0, we can conclude that package-b==1.0.0 and project are incompatible. + And because project depends on package-b==1.0.0 and your workspace requires project, we can conclude that your workspace's requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 78351b7c5..23c1fe60b 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -387,7 +387,7 @@ werkzeug==3.0.1 ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because flask==3.0.2 depends on click>=8.1.3 and you require click==7.0.0, we can conclude that your requirements and flask==3.0.2 are incompatible. + ╰─▶ Because flask==3.0.2 depends on click>=8.1.3 and you require click==7.0.0, we can conclude that the requirements and flask==3.0.2 are incompatible. And because you require flask==3.0.2, we can conclude that the requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/workspace.rs b/crates/uv/tests/workspace.rs index f4ebfc3d3..6c8a32fe0 100644 --- a/crates/uv/tests/workspace.rs +++ b/crates/uv/tests/workspace.rs @@ -1008,8 +1008,8 @@ fn workspace_inherit_sources() -> Result<()> { ----- stderr ----- Using Python 3.12.[X] interpreter at: [PYTHON-3.12] × No solution found when resolving dependencies: - ╰─▶ Because library was not found in the cache and leaf==0.1.0 depends on library, we can conclude that leaf==0.1.0 cannot be used. - And because only leaf==0.1.0 is available and you require leaf, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because library was not found in the cache and leaf depends on library, 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. hint: Packages were unavailable because the network was disabled "### @@ -1200,8 +1200,8 @@ 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==0.1.0 depends on httpx>9999, we can conclude that leaf==0.1.0 cannot be used. - And because only leaf==0.1.0 is available and you require leaf, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because only httpx<=9999 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. "### ); @@ -1256,9 +1256,8 @@ fn workspace_unsatisfiable_member_dependencies_conflicting() -> Result<()> { ----- stderr ----- Using Python 3.12.[X] interpreter at: [PYTHON-3.12] × No solution found when resolving dependencies: - ╰─▶ Because only bar==0.1.0 is available and bar==0.1.0 depends on anyio==4.2.0, we can conclude that all versions of bar depend on anyio==4.2.0. - And because foo==0.1.0 depends on anyio==4.1.0 and only foo==0.1.0 is available, we can conclude that all versions of bar and all versions of foo are incompatible. - And because you require bar and foo, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because bar depends on anyio==4.2.0 and foo depends on anyio==4.1.0, we can conclude that bar and foo are incompatible. + And because your workspace requires bar and foo, we can conclude that your workspace's requirements are unsatisfiable. "### ); @@ -1324,9 +1323,8 @@ fn workspace_unsatisfiable_member_dependencies_conflicting_threeway() -> Result< ----- stderr ----- Using Python 3.12.[X] interpreter at: [PYTHON-3.12] × No solution found when resolving dependencies: - ╰─▶ 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. - And because knot==0.1.0 depends on anyio==4.2.0 and only knot==0.1.0 is available, we can conclude that all versions of bird and all versions of knot are incompatible. - And because you require bird and knot, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because bird depends on anyio==4.3.0 and knot depends on anyio==4.2.0, we can conclude that bird and knot are incompatible. + And because your workspace requires bird and knot, we can conclude that your workspace's requirements are unsatisfiable. "### );