uv-resolver: support conflicting groups

Surprisingly, this seems to be all that's necessary.

Previously, we were only extracting an extra from a
PubGrubPackage to test for conflicts. But now we extract
either an extra or a group. The surrounding code all
remains the same.

We do need to add some extra checking for groups
specifically, but I believe that's it.
This commit is contained in:
Andrew Gallant 2024-11-13 13:35:51 -05:00 committed by Andrew Gallant
parent 3f483d5911
commit 277e7f8dd0
4 changed files with 77 additions and 21 deletions

View File

@ -169,6 +169,8 @@ impl PubGrubPackage {
/// Returns the extra name associated with this PubGrub package, if it has
/// one.
///
/// Note that if this returns `Some`, then `dev` must return `None`.
pub(crate) fn extra(&self) -> Option<&ExtraName> {
match &**self {
// A root can never be a dependency of another package, and a `Python` pubgrub
@ -186,14 +188,44 @@ impl PubGrubPackage {
}
}
/// Returns the dev (aka "group") name associated with this PubGrub
/// package, if it has one.
///
/// Note that if this returns `Some`, then `extra` must return `None`.
pub(crate) fn dev(&self) -> Option<&GroupName> {
match &**self {
// A root can never be a dependency of another package, and a `Python` pubgrub
// package is never returned by `get_dependencies`. So these cases never occur.
PubGrubPackageInner::Root(_)
| PubGrubPackageInner::Python(_)
| PubGrubPackageInner::Package { dev: None, .. }
| PubGrubPackageInner::Extra { .. }
| PubGrubPackageInner::Marker { .. } => None,
PubGrubPackageInner::Package {
dev: Some(ref dev), ..
}
| PubGrubPackageInner::Dev { ref dev, .. } => Some(dev),
}
}
/// Extracts a possible conflicting group from this package.
///
/// If this package can't possibly be classified as a conflicting group,
/// then this returns `None`.
pub(crate) fn conflicting_item(&self) -> Option<ConflictItemRef<'_>> {
let package = self.name_no_root()?;
let extra = self.extra()?;
Some(ConflictItemRef::from((package, extra)))
match (self.extra(), self.dev()) {
(None, None) => None,
(Some(extra), None) => Some(ConflictItemRef::from((package, extra))),
(None, Some(group)) => Some(ConflictItemRef::from((package, group))),
(Some(extra), Some(group)) => {
unreachable!(
"PubGrub package cannot have both an extra and a group, \
but found extra=`{extra}` and group=`{group}` for \
package `{package}`",
)
}
}
}
/// Returns `true` if this PubGrub package is a proxy package.

View File

@ -19,7 +19,7 @@ use uv_distribution_types::{
use uv_fs::{Simplified, CWD};
use uv_git::ResolvedRepositoryReference;
use uv_installer::{SatisfiesResult, SitePackages};
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::MarkerTreeContents;
use uv_pypi_types::{ConflictPackage, ConflictSet, Conflicts, Requirement};
@ -82,8 +82,13 @@ pub(crate) enum ProjectError {
LockedPlatformIncompatibility(String),
#[error(
"The requested extras ({}) are incompatible with the declared conflicting extra: {{{}}}",
_1.iter().map(|extra| format!("`{extra}`")).collect::<Vec<String>>().join(", "),
"{} are incompatible with the declared conflicts: {{{}}}",
_1.iter().map(|conflict| {
match conflict {
ConflictPackage::Extra(ref extra) => format!("extra `{extra}`"),
ConflictPackage::Group(ref group) => format!("group `{group}`"),
}
}).collect::<Vec<String>>().join(", "),
_0
.iter()
.map(|item| {
@ -95,7 +100,7 @@ pub(crate) enum ProjectError {
.collect::<Vec<String>>()
.join(", "),
)]
ExtraIncompatibility(ConflictSet, Vec<ExtraName>),
ConflictIncompatibility(ConflictSet, Vec<ConflictPackage>),
#[error("The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`")]
RequestedPythonProjectIncompatibility(Version, RequiresPython),

View File

@ -15,10 +15,11 @@ use uv_configuration::{
use uv_dispatch::BuildDispatch;
use uv_distribution_types::{DirectorySourceDist, Dist, Index, ResolvedDist, SourceDist};
use uv_installer::SitePackages;
use uv_normalize::{ExtraName, PackageName};
use uv_normalize::PackageName;
use uv_pep508::{MarkerTree, Requirement, VersionOrUrl};
use uv_pypi_types::{
LenientRequirement, ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl,
ConflictPackage, LenientRequirement, ParsedArchiveUrl, ParsedGitUrl, ParsedUrl,
VerbatimParsedUrl,
};
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_resolver::{FlatIndex, InstallTarget};
@ -281,18 +282,36 @@ pub(super) async fn do_sync(
));
}
// Validate that we aren't trying to install extras that are
// declared as conflicting.
// Validate that we aren't trying to install extras or groups that
// are declared as conflicting. Note that we need to collect all
// extras and groups that match in a particular set, since extras
// can be declared as conflicting with groups. So if extra `x` and
// group `g` are declared as conflicting, then enabling both of
// those should result in an error.
let conflicts = target.lock().conflicts();
for set in conflicts.iter() {
let conflicting = set
.iter()
.filter_map(|item| item.extra())
.filter(|extra| extras.contains(extra))
.map(|extra| extra.clone())
.collect::<Vec<ExtraName>>();
if conflicting.len() >= 2 {
return Err(ProjectError::ExtraIncompatibility(set.clone(), conflicting));
let mut conflicts: Vec<ConflictPackage> = vec![];
for item in set.iter() {
if item
.extra()
.map(|extra| extras.contains(extra))
.unwrap_or(false)
{
conflicts.push(item.conflict().clone());
}
if item
.group()
.map(|group1| dev.iter().any(|group2| group1 == group2))
.unwrap_or(false)
{
conflicts.push(item.conflict().clone());
}
}
if conflicts.len() >= 2 {
return Err(ProjectError::ConflictIncompatibility(
set.clone(),
conflicts,
));
}
}

View File

@ -2361,7 +2361,7 @@ fn lock_conflicting_extra_basic() -> Result<()> {
----- stdout -----
----- stderr -----
error: The requested extras (`project1`, `project2`) are incompatible with the declared conflicting extra: {`project[project1]`, `project[project2]`}
error: extra `project1`, extra `project2` are incompatible with the declared conflicts: {`project[project1]`, `project[project2]`}
"###);
Ok(())
@ -2595,7 +2595,7 @@ fn lock_conflicting_extra_multiple_not_conflicting1() -> Result<()> {
----- stdout -----
----- stderr -----
error: The requested extras (`project1`, `project2`) are incompatible with the declared conflicting extra: {`project[project1]`, `project[project2]`}
error: extra `project1`, extra `project2` are incompatible with the declared conflicts: {`project[project1]`, `project[project2]`}
"###);
// project3/project4 conflict!
uv_snapshot!(
@ -2607,7 +2607,7 @@ fn lock_conflicting_extra_multiple_not_conflicting1() -> Result<()> {
----- stdout -----
----- stderr -----
error: The requested extras (`project3`, `project4`) are incompatible with the declared conflicting extra: {`project[project3]`, `project[project4]`}
error: extra `project3`, extra `project4` are incompatible with the declared conflicts: {`project[project3]`, `project[project4]`}
"###);
// ... but project1/project3 does not.
uv_snapshot!(