mirror of https://github.com/astral-sh/uv
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:
parent
3f483d5911
commit
277e7f8dd0
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
Loading…
Reference in New Issue