mirror of https://github.com/astral-sh/uv
Add support for `package`-level conflicts in workspaces (#14906)
Revives https://github.com/astral-sh/uv/pull/9130 Previously, we allowed scoping conflicting extras or groups to specific packages, e.g. ,`{ package = "foo", extra = "bar" }` for a conflict in `foo[bar]`. Now, we allow dropping the `extra` or `group` bit and using `{ package = "foo" }` directly which declares a conflict with `foo`'s production dependencies. This means you can declare conflicts between workspace members, e.g.: ``` [tool.uv] conflicts = [[{ package = "foo" }, { package = "bar" }]] ``` would not allow `foo` and `bar` to be installed at the same time. Similarly, a conflict can be declared between a package and a group: ``` [tool.uv] conflicts = [[{ package = "foo" }, { group = "lint" }]] ``` which would mean, e.g., that `--only-group lint` would be required for the invocation. As with our existing support for conflicting extras, there are edge-cases here where the resolver will _not_ fail even if there are conflicts that render a particular install target unusable. There's test coverage for some of these. We'll still error at install-time when the conflicting groups are selected. Due to the likelihood of bugs in this feature, I've marked it as a preview feature. I would not recommend reading the commits as there's some slop from not wanting to rebase Andrew's branch. --------- Co-authored-by: Andrew Gallant <andrew@astral.sh>
This commit is contained in:
parent
a9302906ce
commit
8f71d239f8
|
|
@ -14,7 +14,8 @@ bitflags::bitflags! {
|
|||
const JSON_OUTPUT = 1 << 2;
|
||||
const PYLOCK = 1 << 3;
|
||||
const ADD_BOUNDS = 1 << 4;
|
||||
const EXTRA_BUILD_DEPENDENCIES = 1 << 5;
|
||||
const PACKAGE_CONFLICTS = 1 << 5;
|
||||
const EXTRA_BUILD_DEPENDENCIES = 1 << 6;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ impl PreviewFeatures {
|
|||
Self::JSON_OUTPUT => "json-output",
|
||||
Self::PYLOCK => "pylock",
|
||||
Self::ADD_BOUNDS => "add-bounds",
|
||||
Self::PACKAGE_CONFLICTS => "package-conflicts",
|
||||
Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies",
|
||||
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
|
||||
}
|
||||
|
|
@ -72,6 +74,7 @@ impl FromStr for PreviewFeatures {
|
|||
"json-output" => Self::JSON_OUTPUT,
|
||||
"pylock" => Self::PYLOCK,
|
||||
"add-bounds" => Self::ADD_BOUNDS,
|
||||
"package-conflicts" => Self::PACKAGE_CONFLICTS,
|
||||
"extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES,
|
||||
_ => {
|
||||
warn_user_once!("Unknown preview feature: `{part}`");
|
||||
|
|
@ -235,6 +238,10 @@ mod tests {
|
|||
assert_eq!(PreviewFeatures::JSON_OUTPUT.flag_as_str(), "json-output");
|
||||
assert_eq!(PreviewFeatures::PYLOCK.flag_as_str(), "pylock");
|
||||
assert_eq!(PreviewFeatures::ADD_BOUNDS.flag_as_str(), "add-bounds");
|
||||
assert_eq!(
|
||||
PreviewFeatures::PACKAGE_CONFLICTS.flag_as_str(),
|
||||
"package-conflicts"
|
||||
);
|
||||
assert_eq!(
|
||||
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES.flag_as_str(),
|
||||
"extra-build-dependencies"
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ impl Conflicts {
|
|||
pub fn contains<'a>(
|
||||
&self,
|
||||
package: &PackageName,
|
||||
conflict: impl Into<ConflictPackageRef<'a>>,
|
||||
kind: impl Into<ConflictKindRef<'a>>,
|
||||
) -> bool {
|
||||
let conflict = conflict.into();
|
||||
self.iter().any(|set| set.contains(package, conflict))
|
||||
let kind = kind.into();
|
||||
self.iter().any(|set| set.contains(package, kind))
|
||||
}
|
||||
|
||||
/// Returns true if there are no conflicts.
|
||||
|
|
@ -106,7 +106,7 @@ impl Conflicts {
|
|||
for set in &self.0 {
|
||||
direct_conflict_sets.insert(set);
|
||||
for item in set.iter() {
|
||||
let ConflictPackage::Group(group) = &item.conflict else {
|
||||
let ConflictKind::Group(group) = &item.kind else {
|
||||
// TODO(john): Do we also want to handle extras here?
|
||||
continue;
|
||||
};
|
||||
|
|
@ -129,7 +129,7 @@ impl Conflicts {
|
|||
}
|
||||
let group_conflict_item = ConflictItem {
|
||||
package: package.clone(),
|
||||
conflict: ConflictPackage::Group(group.clone()),
|
||||
kind: ConflictKind::Group(group.clone()),
|
||||
};
|
||||
let node_id = graph.add_node(FxHashSet::default());
|
||||
group_node_idxs.insert(group, node_id);
|
||||
|
|
@ -242,11 +242,11 @@ impl ConflictSet {
|
|||
pub fn contains<'a>(
|
||||
&self,
|
||||
package: &PackageName,
|
||||
conflict: impl Into<ConflictPackageRef<'a>>,
|
||||
kind: impl Into<ConflictKindRef<'a>>,
|
||||
) -> bool {
|
||||
let conflict = conflict.into();
|
||||
let kind = kind.into();
|
||||
self.iter()
|
||||
.any(|set| set.package() == package && *set.conflict() == conflict)
|
||||
.any(|set| set.package() == package && *set.kind() == kind)
|
||||
}
|
||||
|
||||
/// Returns true if these conflicts contain any set that contains the given
|
||||
|
|
@ -326,7 +326,7 @@ impl TryFrom<Vec<ConflictItem>> for ConflictSet {
|
|||
)]
|
||||
pub struct ConflictItem {
|
||||
package: PackageName,
|
||||
conflict: ConflictPackage,
|
||||
kind: ConflictKind,
|
||||
}
|
||||
|
||||
impl ConflictItem {
|
||||
|
|
@ -338,40 +338,47 @@ impl ConflictItem {
|
|||
/// Returns the package-specific conflict.
|
||||
///
|
||||
/// i.e., Either an extra or a group name.
|
||||
pub fn conflict(&self) -> &ConflictPackage {
|
||||
&self.conflict
|
||||
pub fn kind(&self) -> &ConflictKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
/// Returns the extra name of this conflicting item.
|
||||
pub fn extra(&self) -> Option<&ExtraName> {
|
||||
self.conflict.extra()
|
||||
self.kind.extra()
|
||||
}
|
||||
|
||||
/// Returns the group name of this conflicting item.
|
||||
pub fn group(&self) -> Option<&GroupName> {
|
||||
self.conflict.group()
|
||||
self.kind.group()
|
||||
}
|
||||
|
||||
/// Returns this item as a new type with its fields borrowed.
|
||||
pub fn as_ref(&self) -> ConflictItemRef<'_> {
|
||||
ConflictItemRef {
|
||||
package: self.package(),
|
||||
conflict: self.conflict.as_ref(),
|
||||
kind: self.kind.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PackageName> for ConflictItem {
|
||||
fn from(package: PackageName) -> Self {
|
||||
let kind = ConflictKind::Project;
|
||||
Self { package, kind }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(PackageName, ExtraName)> for ConflictItem {
|
||||
fn from((package, extra): (PackageName, ExtraName)) -> Self {
|
||||
let conflict = ConflictPackage::Extra(extra);
|
||||
Self { package, conflict }
|
||||
let kind = ConflictKind::Extra(extra);
|
||||
Self { package, kind }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(PackageName, GroupName)> for ConflictItem {
|
||||
fn from((package, group): (PackageName, GroupName)) -> Self {
|
||||
let conflict = ConflictPackage::Group(group);
|
||||
Self { package, conflict }
|
||||
let kind = ConflictKind::Group(group);
|
||||
Self { package, kind }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -382,7 +389,7 @@ impl From<(PackageName, GroupName)> for ConflictItem {
|
|||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||
pub struct ConflictItemRef<'a> {
|
||||
package: &'a PackageName,
|
||||
conflict: ConflictPackageRef<'a>,
|
||||
kind: ConflictKindRef<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ConflictItemRef<'a> {
|
||||
|
|
@ -394,40 +401,47 @@ impl<'a> ConflictItemRef<'a> {
|
|||
/// Returns the package-specific conflict.
|
||||
///
|
||||
/// i.e., Either an extra or a group name.
|
||||
pub fn conflict(&self) -> ConflictPackageRef<'a> {
|
||||
self.conflict
|
||||
pub fn kind(&self) -> ConflictKindRef<'a> {
|
||||
self.kind
|
||||
}
|
||||
|
||||
/// Returns the extra name of this conflicting item.
|
||||
pub fn extra(&self) -> Option<&'a ExtraName> {
|
||||
self.conflict.extra()
|
||||
self.kind.extra()
|
||||
}
|
||||
|
||||
/// Returns the group name of this conflicting item.
|
||||
pub fn group(&self) -> Option<&'a GroupName> {
|
||||
self.conflict.group()
|
||||
self.kind.group()
|
||||
}
|
||||
|
||||
/// Converts this borrowed conflicting item to its owned variant.
|
||||
pub fn to_owned(&self) -> ConflictItem {
|
||||
ConflictItem {
|
||||
package: self.package().clone(),
|
||||
conflict: self.conflict.to_owned(),
|
||||
kind: self.kind.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PackageName> for ConflictItemRef<'a> {
|
||||
fn from(package: &'a PackageName) -> Self {
|
||||
let kind = ConflictKindRef::Project;
|
||||
Self { package, kind }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<(&'a PackageName, &'a ExtraName)> for ConflictItemRef<'a> {
|
||||
fn from((package, extra): (&'a PackageName, &'a ExtraName)) -> Self {
|
||||
let conflict = ConflictPackageRef::Extra(extra);
|
||||
ConflictItemRef { package, conflict }
|
||||
let kind = ConflictKindRef::Extra(extra);
|
||||
ConflictItemRef { package, kind }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<(&'a PackageName, &'a GroupName)> for ConflictItemRef<'a> {
|
||||
fn from((package, group): (&'a PackageName, &'a GroupName)) -> Self {
|
||||
let conflict = ConflictPackageRef::Group(group);
|
||||
ConflictItemRef { package, conflict }
|
||||
let kind = ConflictKindRef::Group(group);
|
||||
ConflictItemRef { package, kind }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -439,20 +453,22 @@ impl hashbrown::Equivalent<ConflictItem> for ConflictItemRef<'_> {
|
|||
|
||||
/// The actual conflicting data for a package.
|
||||
///
|
||||
/// That is, either an extra or a group name.
|
||||
/// That is, either an extra or a group name, or the entire project itself.
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||
pub enum ConflictPackage {
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum ConflictKind {
|
||||
Extra(ExtraName),
|
||||
Group(GroupName),
|
||||
Project,
|
||||
}
|
||||
|
||||
impl ConflictPackage {
|
||||
impl ConflictKind {
|
||||
/// If this conflict corresponds to an extra, then return the
|
||||
/// extra name.
|
||||
pub fn extra(&self) -> Option<&ExtraName> {
|
||||
match self {
|
||||
Self::Extra(extra) => Some(extra),
|
||||
Self::Group(_) => None,
|
||||
Self::Group(_) | Self::Project => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -461,15 +477,16 @@ impl ConflictPackage {
|
|||
pub fn group(&self) -> Option<&GroupName> {
|
||||
match self {
|
||||
Self::Group(group) => Some(group),
|
||||
Self::Extra(_) => None,
|
||||
Self::Extra(_) | Self::Project => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns this conflict as a new type with its fields borrowed.
|
||||
pub fn as_ref(&self) -> ConflictPackageRef<'_> {
|
||||
pub fn as_ref(&self) -> ConflictKindRef<'_> {
|
||||
match self {
|
||||
Self::Extra(extra) => ConflictPackageRef::Extra(extra),
|
||||
Self::Group(group) => ConflictPackageRef::Group(group),
|
||||
Self::Extra(extra) => ConflictKindRef::Extra(extra),
|
||||
Self::Group(group) => ConflictKindRef::Group(group),
|
||||
Self::Project => ConflictKindRef::Project,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -478,18 +495,19 @@ impl ConflictPackage {
|
|||
///
|
||||
/// That is, either a borrowed extra name or a borrowed group name.
|
||||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||
pub enum ConflictPackageRef<'a> {
|
||||
pub enum ConflictKindRef<'a> {
|
||||
Extra(&'a ExtraName),
|
||||
Group(&'a GroupName),
|
||||
Project,
|
||||
}
|
||||
|
||||
impl<'a> ConflictPackageRef<'a> {
|
||||
impl<'a> ConflictKindRef<'a> {
|
||||
/// If this conflict corresponds to an extra, then return the
|
||||
/// extra name.
|
||||
pub fn extra(&self) -> Option<&'a ExtraName> {
|
||||
match self {
|
||||
Self::Extra(extra) => Some(extra),
|
||||
Self::Group(_) => None,
|
||||
Self::Group(_) | Self::Project => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -498,45 +516,46 @@ impl<'a> ConflictPackageRef<'a> {
|
|||
pub fn group(&self) -> Option<&'a GroupName> {
|
||||
match self {
|
||||
Self::Group(group) => Some(group),
|
||||
Self::Extra(_) => None,
|
||||
Self::Extra(_) | Self::Project => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this borrowed conflict to its owned variant.
|
||||
pub fn to_owned(&self) -> ConflictPackage {
|
||||
match *self {
|
||||
Self::Extra(extra) => ConflictPackage::Extra(extra.clone()),
|
||||
Self::Group(group) => ConflictPackage::Group(group.clone()),
|
||||
pub fn to_owned(&self) -> ConflictKind {
|
||||
match self {
|
||||
Self::Extra(extra) => ConflictKind::Extra((*extra).clone()),
|
||||
Self::Group(group) => ConflictKind::Group((*group).clone()),
|
||||
Self::Project => ConflictKind::Project,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ExtraName> for ConflictPackageRef<'a> {
|
||||
impl<'a> From<&'a ExtraName> for ConflictKindRef<'a> {
|
||||
fn from(extra: &'a ExtraName) -> Self {
|
||||
Self::Extra(extra)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a GroupName> for ConflictPackageRef<'a> {
|
||||
impl<'a> From<&'a GroupName> for ConflictKindRef<'a> {
|
||||
fn from(group: &'a GroupName) -> Self {
|
||||
Self::Group(group)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<ConflictPackage> for ConflictPackageRef<'_> {
|
||||
fn eq(&self, other: &ConflictPackage) -> bool {
|
||||
impl PartialEq<ConflictKind> for ConflictKindRef<'_> {
|
||||
fn eq(&self, other: &ConflictKind) -> bool {
|
||||
other.as_ref() == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<ConflictPackageRef<'a>> for ConflictPackage {
|
||||
fn eq(&self, other: &ConflictPackageRef<'a>) -> bool {
|
||||
impl<'a> PartialEq<ConflictKindRef<'a>> for ConflictKind {
|
||||
fn eq(&self, other: &ConflictKindRef<'a>) -> bool {
|
||||
self.as_ref() == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl hashbrown::Equivalent<ConflictPackage> for ConflictPackageRef<'_> {
|
||||
fn equivalent(&self, key: &ConflictPackage) -> bool {
|
||||
impl hashbrown::Equivalent<ConflictKind> for ConflictKindRef<'_> {
|
||||
fn equivalent(&self, key: &ConflictKind) -> bool {
|
||||
key.as_ref() == *self
|
||||
}
|
||||
}
|
||||
|
|
@ -557,9 +576,9 @@ pub enum ConflictError {
|
|||
/// optional.)
|
||||
#[error("Expected `package` field in conflicting entry")]
|
||||
MissingPackage,
|
||||
/// An error that occurs when both `extra` and `group` are missing.
|
||||
#[error("Expected `extra` or `group` field in conflicting entry")]
|
||||
MissingExtraAndGroup,
|
||||
/// An error that occurs when all of `package`, `extra` and `group` are missing.
|
||||
#[error("Expected `package`, `extra` or `group` field in conflicting entry")]
|
||||
MissingPackageAndExtraAndGroup,
|
||||
/// An error that occurs when both `extra` and `group` are present.
|
||||
#[error("Expected one of `extra` or `group` in conflicting entry, but found both")]
|
||||
FoundExtraAndGroup,
|
||||
|
|
@ -596,7 +615,7 @@ impl SchemaConflicts {
|
|||
let package = item.package.clone().unwrap_or_else(|| package.clone());
|
||||
set.push(ConflictItem {
|
||||
package: package.clone(),
|
||||
conflict: item.conflict.clone(),
|
||||
kind: item.kind.clone(),
|
||||
});
|
||||
}
|
||||
// OK because we guarantee that
|
||||
|
|
@ -635,7 +654,7 @@ pub struct SchemaConflictSet(Vec<SchemaConflictItem>);
|
|||
)]
|
||||
pub struct SchemaConflictItem {
|
||||
package: Option<PackageName>,
|
||||
conflict: ConflictPackage,
|
||||
kind: ConflictKind,
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
|
|
@ -695,8 +714,8 @@ impl TryFrom<ConflictItemWire> for ConflictItem {
|
|||
return Err(ConflictError::MissingPackage);
|
||||
};
|
||||
match (wire.extra, wire.group) {
|
||||
(None, None) => Err(ConflictError::MissingExtraAndGroup),
|
||||
(Some(_), Some(_)) => Err(ConflictError::FoundExtraAndGroup),
|
||||
(None, None) => Ok(Self::from(package)),
|
||||
(Some(extra), None) => Ok(Self::from((package, extra))),
|
||||
(None, Some(group)) => Ok(Self::from((package, group))),
|
||||
}
|
||||
|
|
@ -705,17 +724,22 @@ impl TryFrom<ConflictItemWire> for ConflictItem {
|
|||
|
||||
impl From<ConflictItem> for ConflictItemWire {
|
||||
fn from(item: ConflictItem) -> Self {
|
||||
match item.conflict {
|
||||
ConflictPackage::Extra(extra) => Self {
|
||||
match item.kind {
|
||||
ConflictKind::Extra(extra) => Self {
|
||||
package: Some(item.package),
|
||||
extra: Some(extra),
|
||||
group: None,
|
||||
},
|
||||
ConflictPackage::Group(group) => Self {
|
||||
ConflictKind::Group(group) => Self {
|
||||
package: Some(item.package),
|
||||
extra: None,
|
||||
group: Some(group),
|
||||
},
|
||||
ConflictKind::Project => Self {
|
||||
package: Some(item.package),
|
||||
extra: None,
|
||||
group: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -726,15 +750,23 @@ impl TryFrom<ConflictItemWire> for SchemaConflictItem {
|
|||
fn try_from(wire: ConflictItemWire) -> Result<Self, ConflictError> {
|
||||
let package = wire.package;
|
||||
match (wire.extra, wire.group) {
|
||||
(None, None) => Err(ConflictError::MissingExtraAndGroup),
|
||||
(Some(_), Some(_)) => Err(ConflictError::FoundExtraAndGroup),
|
||||
(None, None) => {
|
||||
let Some(package) = package else {
|
||||
return Err(ConflictError::MissingPackageAndExtraAndGroup);
|
||||
};
|
||||
Ok(Self {
|
||||
package: Some(package),
|
||||
kind: ConflictKind::Project,
|
||||
})
|
||||
}
|
||||
(Some(extra), None) => Ok(Self {
|
||||
package,
|
||||
conflict: ConflictPackage::Extra(extra),
|
||||
kind: ConflictKind::Extra(extra),
|
||||
}),
|
||||
(None, Some(group)) => Ok(Self {
|
||||
package,
|
||||
conflict: ConflictPackage::Group(group),
|
||||
kind: ConflictKind::Group(group),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -742,17 +774,22 @@ impl TryFrom<ConflictItemWire> for SchemaConflictItem {
|
|||
|
||||
impl From<SchemaConflictItem> for ConflictItemWire {
|
||||
fn from(item: SchemaConflictItem) -> Self {
|
||||
match item.conflict {
|
||||
ConflictPackage::Extra(extra) => Self {
|
||||
match item.kind {
|
||||
ConflictKind::Extra(extra) => Self {
|
||||
package: item.package,
|
||||
extra: Some(extra),
|
||||
group: None,
|
||||
},
|
||||
ConflictPackage::Group(group) => Self {
|
||||
ConflictKind::Group(group) => Self {
|
||||
package: item.package,
|
||||
extra: None,
|
||||
group: Some(group),
|
||||
},
|
||||
ConflictKind::Project => Self {
|
||||
package: item.package,
|
||||
extra: None,
|
||||
group: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ pub(crate) fn simplify_conflict_markers(
|
|||
let mut new_set = BTreeSet::default();
|
||||
for item in set {
|
||||
for conflict_set in conflicts.iter() {
|
||||
if !conflict_set.contains(item.package(), item.as_ref().conflict()) {
|
||||
if !conflict_set.contains(item.package(), item.as_ref().kind()) {
|
||||
continue;
|
||||
}
|
||||
for conflict_item in conflict_set.iter() {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ pub trait Installable<'lock> {
|
|||
|
||||
let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
|
||||
let mut seen = FxHashSet::default();
|
||||
let mut activated_projects: Vec<&PackageName> = vec![];
|
||||
let mut activated_extras: Vec<(&PackageName, &ExtraName)> = vec![];
|
||||
let mut activated_groups: Vec<(&PackageName, &GroupName)> = vec![];
|
||||
|
||||
|
|
@ -74,6 +75,7 @@ pub trait Installable<'lock> {
|
|||
|
||||
// Track the activated extras.
|
||||
if dev.prod() {
|
||||
activated_projects.push(&dist.id.name);
|
||||
for extra in extras.extra_names(dist.optional_dependencies.keys()) {
|
||||
activated_extras.push((&dist.id.name, extra));
|
||||
}
|
||||
|
|
@ -143,6 +145,7 @@ pub trait Installable<'lock> {
|
|||
{
|
||||
if !dep.complexified_marker.evaluate(
|
||||
marker_env,
|
||||
activated_projects.iter().copied(),
|
||||
activated_extras.iter().copied(),
|
||||
activated_groups.iter().copied(),
|
||||
) {
|
||||
|
|
@ -367,6 +370,7 @@ pub trait Installable<'lock> {
|
|||
}
|
||||
if !dep.complexified_marker.evaluate(
|
||||
marker_env,
|
||||
activated_projects.iter().copied(),
|
||||
activated_extras
|
||||
.iter()
|
||||
.chain(additional_activated_extras.iter())
|
||||
|
|
@ -454,6 +458,7 @@ pub trait Installable<'lock> {
|
|||
for dep in deps {
|
||||
if !dep.complexified_marker.evaluate(
|
||||
marker_env,
|
||||
activated_projects.iter().copied(),
|
||||
activated_extras.iter().copied(),
|
||||
activated_groups.iter().copied(),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ use uv_platform_tags::{
|
|||
AbiTag, IncompatibleTag, LanguageTag, PlatformTag, TagCompatibility, TagPriority, Tags,
|
||||
};
|
||||
use uv_pypi_types::{
|
||||
ConflictPackage, Conflicts, HashAlgorithm, HashDigest, HashDigests, Hashes, ParsedArchiveUrl,
|
||||
ConflictKind, Conflicts, HashAlgorithm, HashDigest, HashDigests, Hashes, ParsedArchiveUrl,
|
||||
ParsedGitUrl,
|
||||
};
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
|
|
@ -1026,11 +1026,12 @@ impl Lock {
|
|||
list.push(each_element_on_its_line_array(set.iter().map(|item| {
|
||||
let mut table = InlineTable::new();
|
||||
table.insert("package", Value::from(item.package().to_string()));
|
||||
match item.conflict() {
|
||||
ConflictPackage::Extra(extra) => {
|
||||
match item.kind() {
|
||||
ConflictKind::Project => {}
|
||||
ConflictKind::Extra(extra) => {
|
||||
table.insert("extra", Value::from(extra.to_string()));
|
||||
}
|
||||
ConflictPackage::Group(group) => {
|
||||
ConflictKind::Group(group) => {
|
||||
table.insert("group", Value::from(group.to_string()));
|
||||
}
|
||||
}
|
||||
|
|
@ -3107,6 +3108,21 @@ impl Package {
|
|||
pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
|
||||
&self.metadata.dependency_groups
|
||||
}
|
||||
|
||||
/// Returns the dependencies of the package.
|
||||
pub fn dependencies(&self) -> &[Dependency] {
|
||||
&self.dependencies
|
||||
}
|
||||
|
||||
/// Returns the optional dependencies of the package.
|
||||
pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
|
||||
&self.optional_dependencies
|
||||
}
|
||||
|
||||
/// Returns the resolved PEP 735 dependency groups of the package.
|
||||
pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
|
||||
&self.dependency_groups
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to construct a `VerbatimUrl` from the given normalized `Path`.
|
||||
|
|
@ -4657,7 +4673,7 @@ impl TryFrom<WheelWire> for Wheel {
|
|||
|
||||
/// A single dependency of a package in a lockfile.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||
struct Dependency {
|
||||
pub struct Dependency {
|
||||
package_id: PackageId,
|
||||
extra: BTreeSet<ExtraName>,
|
||||
/// A marker simplified from the PEP 508 marker in `complexified_marker`
|
||||
|
|
@ -4742,6 +4758,16 @@ impl Dependency {
|
|||
|
||||
table
|
||||
}
|
||||
|
||||
/// Returns the package name of this dependency.
|
||||
pub fn package_name(&self) -> &PackageName {
|
||||
&self.package_id.name
|
||||
}
|
||||
|
||||
/// Returns the extras specified on this dependency.
|
||||
pub fn extra(&self) -> &BTreeSet<ExtraName> {
|
||||
&self.extra
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Dependency {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ use uv_distribution_types::{Requirement, RequirementSource};
|
|||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_pypi_types::{
|
||||
Conflicts, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl,
|
||||
VerbatimParsedUrl,
|
||||
ConflictItemRef, Conflicts, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl,
|
||||
ParsedUrl, VerbatimParsedUrl,
|
||||
};
|
||||
|
||||
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner};
|
||||
|
|
@ -19,6 +19,21 @@ pub(crate) struct PubGrubDependency {
|
|||
pub(crate) package: PubGrubPackage,
|
||||
pub(crate) version: Ranges<Version>,
|
||||
|
||||
/// When the parent that created this dependency is a "normal" package
|
||||
/// (non-extra non-group), this corresponds to its name.
|
||||
///
|
||||
/// This is used to create project-level `ConflictItemRef` for a specific
|
||||
/// package. In effect, this lets us "delay" filtering of project
|
||||
/// dependencies when a conflict is declared between the project and a
|
||||
/// group.
|
||||
///
|
||||
/// The main problem with dealing with project level conflicts is that if you
|
||||
/// declare a conflict between a package and a group, we represent that
|
||||
/// group as a dependency of that package. So if you filter out the package
|
||||
/// in a fork due to a conflict, you also filter out the group. Therefore,
|
||||
/// we introduce this parent field to enable "delayed" filtering.
|
||||
pub(crate) parent: Option<PackageName>,
|
||||
|
||||
/// This field is set if the [`Requirement`] had a URL. We still use a URL from [`Urls`]
|
||||
/// even if this field is None where there is an override with a URL or there is a different
|
||||
/// requirement or constraint for the same package that has a URL.
|
||||
|
|
@ -30,8 +45,12 @@ impl PubGrubDependency {
|
|||
conflicts: &Conflicts,
|
||||
requirement: Cow<'a, Requirement>,
|
||||
dev: Option<&'a GroupName>,
|
||||
source_name: Option<&'a PackageName>,
|
||||
parent_package: Option<&'a PubGrubPackage>,
|
||||
) -> impl Iterator<Item = Self> + 'a {
|
||||
let parent_name = parent_package.and_then(|package| package.name_no_root());
|
||||
let is_normal_parent = parent_package
|
||||
.map(|pp| pp.extra().is_none() && pp.dev().is_none())
|
||||
.unwrap_or(false);
|
||||
let iter = if !requirement.extras.is_empty() {
|
||||
// This is crazy subtle, but if any of the extras in the
|
||||
// requirement are part of a declared conflict, then we
|
||||
|
|
@ -80,50 +99,59 @@ impl PubGrubDependency {
|
|||
|
||||
// Add the package, plus any extra variants.
|
||||
iter.map(move |(extra, group)| {
|
||||
PubGrubRequirement::from_requirement(&requirement, extra, group)
|
||||
})
|
||||
.map(move |requirement| {
|
||||
let pubgrub_requirement =
|
||||
PubGrubRequirement::from_requirement(&requirement, extra, group);
|
||||
let PubGrubRequirement {
|
||||
package,
|
||||
version,
|
||||
url,
|
||||
} = requirement;
|
||||
} = pubgrub_requirement;
|
||||
match &*package {
|
||||
PubGrubPackageInner::Package { .. } => Self {
|
||||
package,
|
||||
version,
|
||||
parent: if is_normal_parent {
|
||||
parent_name.cloned()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
url,
|
||||
},
|
||||
PubGrubPackageInner::Marker { .. } => Self {
|
||||
package,
|
||||
version,
|
||||
parent: if is_normal_parent {
|
||||
parent_name.cloned()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
url,
|
||||
},
|
||||
PubGrubPackageInner::Extra { name, .. } => {
|
||||
// Detect self-dependencies.
|
||||
if dev.is_none() {
|
||||
debug_assert!(
|
||||
source_name.is_none_or(|source_name| source_name != name),
|
||||
parent_name.is_none_or(|parent_name| parent_name != name),
|
||||
"extras not flattened for {name}"
|
||||
);
|
||||
}
|
||||
Self {
|
||||
package,
|
||||
version,
|
||||
parent: None,
|
||||
url,
|
||||
}
|
||||
}
|
||||
PubGrubPackageInner::Dev { name, .. } => {
|
||||
// Detect self-dependencies.
|
||||
if dev.is_none() {
|
||||
debug_assert!(
|
||||
source_name.is_none_or(|source_name| source_name != name),
|
||||
parent_name.is_none_or(|parent_name| parent_name != name),
|
||||
"group not flattened for {name}"
|
||||
);
|
||||
}
|
||||
Self {
|
||||
package,
|
||||
version,
|
||||
parent: None,
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
|
@ -135,6 +163,14 @@ impl PubGrubDependency {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Extracts a possible conflicting item from this dependency.
|
||||
///
|
||||
/// If this package can't possibly be classified as conflicting, then this
|
||||
/// returns `None`.
|
||||
pub(crate) fn conflicting_item(&self) -> Option<ConflictItemRef<'_>> {
|
||||
self.package.conflicting_item()
|
||||
}
|
||||
}
|
||||
|
||||
/// A PubGrub-compatible package and version range.
|
||||
|
|
|
|||
|
|
@ -214,14 +214,14 @@ impl PubGrubPackage {
|
|||
}
|
||||
}
|
||||
|
||||
/// Extracts a possible conflicting group from this package.
|
||||
/// Extracts a possible conflicting item from this package.
|
||||
///
|
||||
/// If this package can't possibly be classified as a conflicting group,
|
||||
/// then this returns `None`.
|
||||
/// If this package can't possibly be classified as conflicting, then
|
||||
/// this returns `None`.
|
||||
pub(crate) fn conflicting_item(&self) -> Option<ConflictItemRef<'_>> {
|
||||
let package = self.name_no_root()?;
|
||||
match (self.extra(), self.dev()) {
|
||||
(None, None) => None,
|
||||
(None, None) => Some(ConflictItemRef::from(package)),
|
||||
(Some(extra), None) => Some(ConflictItemRef::from((package, extra))),
|
||||
(None, Some(group)) => Some(ConflictItemRef::from((package, group))),
|
||||
(Some(extra), Some(group)) => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use tracing::trace;
|
|||
use uv_distribution_types::{RequiresPython, RequiresPythonRange};
|
||||
use uv_pep440::VersionSpecifiers;
|
||||
use uv_pep508::{MarkerEnvironment, MarkerTree};
|
||||
use uv_pypi_types::{ConflictItem, ConflictItemRef, ConflictPackage, ResolverMarkerEnvironment};
|
||||
use uv_pypi_types::{ConflictItem, ConflictItemRef, ConflictKind, ResolverMarkerEnvironment};
|
||||
|
||||
use crate::pubgrub::{PubGrubDependency, PubGrubPackage};
|
||||
use crate::resolver::ForkState;
|
||||
|
|
@ -391,11 +391,12 @@ impl ResolverEnvironment {
|
|||
format!(
|
||||
"{}{}",
|
||||
conflict_item.package(),
|
||||
match conflict_item.conflict() {
|
||||
ConflictPackage::Extra(extra) => format!("[{extra}]"),
|
||||
ConflictPackage::Group(group) => {
|
||||
match conflict_item.kind() {
|
||||
ConflictKind::Extra(extra) => format!("[{extra}]"),
|
||||
ConflictKind::Group(group) => {
|
||||
format!("[group:{group}]")
|
||||
}
|
||||
ConflictKind::Project => String::new(),
|
||||
}
|
||||
)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ use uv_pep508::{
|
|||
MarkerEnvironment, MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString,
|
||||
};
|
||||
use uv_platform_tags::Tags;
|
||||
use uv_pypi_types::{ConflictItem, ConflictItemRef, Conflicts, VerbatimParsedUrl};
|
||||
use uv_pypi_types::{ConflictItem, ConflictItemRef, ConflictKindRef, Conflicts, VerbatimParsedUrl};
|
||||
use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider};
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
|
|
@ -946,6 +946,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
let PubGrubDependency {
|
||||
package,
|
||||
version: _,
|
||||
parent: _,
|
||||
url: _,
|
||||
} = dependency;
|
||||
let url = package.name().and_then(|name| state.fork_urls.get(name));
|
||||
|
|
@ -1750,7 +1751,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
&self.conflicts,
|
||||
requirement,
|
||||
None,
|
||||
None,
|
||||
Some(package),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -1866,7 +1867,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
&self.conflicts,
|
||||
requirement,
|
||||
dev.as_ref(),
|
||||
Some(name),
|
||||
Some(package),
|
||||
)
|
||||
})
|
||||
.chain(system_dependencies)
|
||||
|
|
@ -1890,6 +1891,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
marker,
|
||||
}),
|
||||
version: Range::singleton(version.clone()),
|
||||
parent: None,
|
||||
url: None,
|
||||
})
|
||||
.collect(),
|
||||
|
|
@ -1917,6 +1919,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
marker,
|
||||
}),
|
||||
version: Range::singleton(version.clone()),
|
||||
parent: None,
|
||||
url: None,
|
||||
})
|
||||
})
|
||||
|
|
@ -1938,6 +1941,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
marker,
|
||||
}),
|
||||
version: Range::singleton(version.clone()),
|
||||
parent: None,
|
||||
url: None,
|
||||
})
|
||||
.collect(),
|
||||
|
|
@ -2801,6 +2805,7 @@ impl ForkState {
|
|||
let PubGrubDependency {
|
||||
package,
|
||||
version,
|
||||
parent: _,
|
||||
url,
|
||||
} = dependency;
|
||||
|
||||
|
|
@ -2872,6 +2877,7 @@ impl ForkState {
|
|||
let PubGrubDependency {
|
||||
package,
|
||||
version,
|
||||
parent: _,
|
||||
url: _,
|
||||
} = dependency;
|
||||
(package, version)
|
||||
|
|
@ -3654,7 +3660,7 @@ impl Forks {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Create a fork that excludes ALL extras.
|
||||
// Create a fork that excludes ALL conflicts.
|
||||
if let Some(fork_none) = fork.clone().filter(set.iter().cloned().map(Err)) {
|
||||
new.push(fork_none);
|
||||
}
|
||||
|
|
@ -3740,7 +3746,7 @@ impl Fork {
|
|||
|
||||
/// Add a dependency to this fork.
|
||||
fn add_dependency(&mut self, dep: PubGrubDependency) {
|
||||
if let Some(conflicting_item) = dep.package.conflicting_item() {
|
||||
if let Some(conflicting_item) = dep.conflicting_item() {
|
||||
self.conflicts.insert(conflicting_item.to_owned());
|
||||
}
|
||||
self.dependencies.push(dep);
|
||||
|
|
@ -3757,7 +3763,7 @@ impl Fork {
|
|||
if self.env.included_by_marker(marker) {
|
||||
return true;
|
||||
}
|
||||
if let Some(conflicting_item) = dep.package.conflicting_item() {
|
||||
if let Some(conflicting_item) = dep.conflicting_item() {
|
||||
self.conflicts.remove(&conflicting_item);
|
||||
}
|
||||
false
|
||||
|
|
@ -3782,12 +3788,23 @@ impl Fork {
|
|||
) -> Option<Self> {
|
||||
self.env = self.env.filter_by_group(rules)?;
|
||||
self.dependencies.retain(|dep| {
|
||||
let Some(conflicting_item) = dep.package.conflicting_item() else {
|
||||
let Some(conflicting_item) = dep.conflicting_item() else {
|
||||
return true;
|
||||
};
|
||||
if self.env.included_by_group(conflicting_item) {
|
||||
return true;
|
||||
}
|
||||
match conflicting_item.kind() {
|
||||
// We should not filter entire projects unless they're a top-level dependency
|
||||
// Otherwise, we'll fail to solve for children of the project, like extras
|
||||
ConflictKindRef::Project => {
|
||||
if dep.parent.is_some() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ConflictKindRef::Group(_) => {}
|
||||
ConflictKindRef::Extra(_) => {}
|
||||
}
|
||||
self.conflicts.remove(&conflicting_item);
|
||||
false
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ impl From<SystemDependency> for PubGrubDependency {
|
|||
Self {
|
||||
package: PubGrubPackage::from(PubGrubPackageInner::System(value.name)),
|
||||
version: Ranges::singleton(value.version),
|
||||
parent: None,
|
||||
url: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use rustc_hash::FxHashMap;
|
|||
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep508::{ExtraOperator, MarkerEnvironment, MarkerExpression, MarkerOperator, MarkerTree};
|
||||
use uv_pypi_types::{ConflictItem, ConflictPackage, Conflicts, Inference};
|
||||
use uv_pypi_types::{ConflictItem, ConflictKind, Conflicts, Inference};
|
||||
|
||||
use crate::ResolveError;
|
||||
|
||||
|
|
@ -173,9 +173,10 @@ impl UniversalMarker {
|
|||
/// This may simplify the conflicting marker component of this universal
|
||||
/// marker.
|
||||
pub(crate) fn assume_conflict_item(&mut self, item: &ConflictItem) {
|
||||
match *item.conflict() {
|
||||
ConflictPackage::Extra(ref extra) => self.assume_extra(item.package(), extra),
|
||||
ConflictPackage::Group(ref group) => self.assume_group(item.package(), group),
|
||||
match *item.kind() {
|
||||
ConflictKind::Extra(ref extra) => self.assume_extra(item.package(), extra),
|
||||
ConflictKind::Group(ref group) => self.assume_group(item.package(), group),
|
||||
ConflictKind::Project => self.assume_project(item.package()),
|
||||
}
|
||||
self.pep508 = self.marker.without_extras();
|
||||
}
|
||||
|
|
@ -186,18 +187,45 @@ impl UniversalMarker {
|
|||
/// This may simplify the conflicting marker component of this universal
|
||||
/// marker.
|
||||
pub(crate) fn assume_not_conflict_item(&mut self, item: &ConflictItem) {
|
||||
match *item.conflict() {
|
||||
ConflictPackage::Extra(ref extra) => self.assume_not_extra(item.package(), extra),
|
||||
ConflictPackage::Group(ref group) => self.assume_not_group(item.package(), group),
|
||||
match *item.kind() {
|
||||
ConflictKind::Extra(ref extra) => self.assume_not_extra(item.package(), extra),
|
||||
ConflictKind::Group(ref group) => self.assume_not_group(item.package(), group),
|
||||
ConflictKind::Project => self.assume_not_project(item.package()),
|
||||
}
|
||||
self.pep508 = self.marker.without_extras();
|
||||
}
|
||||
|
||||
/// Assumes that the "production" dependencies for the given project are
|
||||
/// activated.
|
||||
///
|
||||
/// This may simplify the conflicting marker component of this universal
|
||||
/// marker.
|
||||
fn assume_project(&mut self, package: &PackageName) {
|
||||
let extra = encode_project(package);
|
||||
self.marker = self
|
||||
.marker
|
||||
.simplify_extras_with(|candidate| *candidate == extra);
|
||||
self.pep508 = self.marker.without_extras();
|
||||
}
|
||||
|
||||
/// Assumes that the "production" dependencies for the given project are
|
||||
/// not activated.
|
||||
///
|
||||
/// This may simplify the conflicting marker component of this universal
|
||||
/// marker.
|
||||
fn assume_not_project(&mut self, package: &PackageName) {
|
||||
let extra = encode_project(package);
|
||||
self.marker = self
|
||||
.marker
|
||||
.simplify_not_extras_with(|candidate| *candidate == extra);
|
||||
self.pep508 = self.marker.without_extras();
|
||||
}
|
||||
|
||||
/// Assumes that a given extra for the given package is activated.
|
||||
///
|
||||
/// This may simplify the conflicting marker component of this universal
|
||||
/// marker.
|
||||
pub(crate) fn assume_extra(&mut self, package: &PackageName, extra: &ExtraName) {
|
||||
fn assume_extra(&mut self, package: &PackageName, extra: &ExtraName) {
|
||||
let extra = encode_package_extra(package, extra);
|
||||
self.marker = self
|
||||
.marker
|
||||
|
|
@ -209,7 +237,7 @@ impl UniversalMarker {
|
|||
///
|
||||
/// This may simplify the conflicting marker component of this universal
|
||||
/// marker.
|
||||
pub(crate) fn assume_not_extra(&mut self, package: &PackageName, extra: &ExtraName) {
|
||||
fn assume_not_extra(&mut self, package: &PackageName, extra: &ExtraName) {
|
||||
let extra = encode_package_extra(package, extra);
|
||||
self.marker = self
|
||||
.marker
|
||||
|
|
@ -221,7 +249,7 @@ impl UniversalMarker {
|
|||
///
|
||||
/// This may simplify the conflicting marker component of this universal
|
||||
/// marker.
|
||||
pub(crate) fn assume_group(&mut self, package: &PackageName, group: &GroupName) {
|
||||
fn assume_group(&mut self, package: &PackageName, group: &GroupName) {
|
||||
let extra = encode_package_group(package, group);
|
||||
self.marker = self
|
||||
.marker
|
||||
|
|
@ -233,7 +261,7 @@ impl UniversalMarker {
|
|||
///
|
||||
/// This may simplify the conflicting marker component of this universal
|
||||
/// marker.
|
||||
pub(crate) fn assume_not_group(&mut self, package: &PackageName, group: &GroupName) {
|
||||
fn assume_not_group(&mut self, package: &PackageName, group: &GroupName) {
|
||||
let extra = encode_package_group(package, group);
|
||||
self.marker = self
|
||||
.marker
|
||||
|
|
@ -277,6 +305,7 @@ impl UniversalMarker {
|
|||
pub(crate) fn evaluate<P, E, G>(
|
||||
self,
|
||||
env: &MarkerEnvironment,
|
||||
projects: impl Iterator<Item = P>,
|
||||
extras: impl Iterator<Item = (P, E)>,
|
||||
groups: impl Iterator<Item = (P, G)>,
|
||||
) -> bool
|
||||
|
|
@ -285,12 +314,18 @@ impl UniversalMarker {
|
|||
E: Borrow<ExtraName>,
|
||||
G: Borrow<GroupName>,
|
||||
{
|
||||
let projects = projects.map(|package| encode_project(package.borrow()));
|
||||
let extras =
|
||||
extras.map(|(package, extra)| encode_package_extra(package.borrow(), extra.borrow()));
|
||||
let groups =
|
||||
groups.map(|(package, group)| encode_package_group(package.borrow(), group.borrow()));
|
||||
self.marker
|
||||
.evaluate(env, &extras.chain(groups).collect::<Vec<ExtraName>>())
|
||||
self.marker.evaluate(
|
||||
env,
|
||||
&projects
|
||||
.chain(extras)
|
||||
.chain(groups)
|
||||
.collect::<Vec<ExtraName>>(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if the marker always evaluates to true if the given set of extras is activated.
|
||||
|
|
@ -392,12 +427,23 @@ impl ConflictMarker {
|
|||
/// Create a conflict marker that is true only when the given extra or
|
||||
/// group (for a specific package) is activated.
|
||||
pub fn from_conflict_item(item: &ConflictItem) -> Self {
|
||||
match *item.conflict() {
|
||||
ConflictPackage::Extra(ref extra) => Self::extra(item.package(), extra),
|
||||
ConflictPackage::Group(ref group) => Self::group(item.package(), group),
|
||||
match *item.kind() {
|
||||
ConflictKind::Extra(ref extra) => Self::extra(item.package(), extra),
|
||||
ConflictKind::Group(ref group) => Self::group(item.package(), group),
|
||||
ConflictKind::Project => Self::project(item.package()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a conflict marker that is true only when the production
|
||||
/// dependencies for the given package are activated.
|
||||
pub fn project(package: &PackageName) -> Self {
|
||||
let operator = uv_pep508::ExtraOperator::Equal;
|
||||
let name = uv_pep508::MarkerValueExtra::Extra(encode_project(package));
|
||||
let expr = uv_pep508::MarkerExpression::Extra { operator, name };
|
||||
let marker = MarkerTree::expression(expr);
|
||||
Self { marker }
|
||||
}
|
||||
|
||||
/// Create a conflict marker that is true only when the given extra for the
|
||||
/// given package is activated.
|
||||
pub fn extra(package: &PackageName, extra: &ExtraName) -> Self {
|
||||
|
|
@ -504,9 +550,10 @@ impl std::fmt::Debug for ConflictMarker {
|
|||
|
||||
/// Encodes the given conflict into a valid `extra` value in a PEP 508 marker.
|
||||
fn encode_conflict_item(conflict: &ConflictItem) -> ExtraName {
|
||||
match conflict.conflict() {
|
||||
ConflictPackage::Extra(extra) => encode_package_extra(conflict.package(), extra),
|
||||
ConflictPackage::Group(group) => encode_package_group(conflict.package(), group),
|
||||
match conflict.kind() {
|
||||
ConflictKind::Extra(extra) => encode_package_extra(conflict.package(), extra),
|
||||
ConflictKind::Group(group) => encode_package_group(conflict.package(), group),
|
||||
ConflictKind::Project => encode_project(conflict.package()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -535,8 +582,17 @@ fn encode_package_group(package: &PackageName, group: &GroupName) -> ExtraName {
|
|||
ExtraName::from_owned(format!("group-{package_len}-{package}-{group}")).unwrap()
|
||||
}
|
||||
|
||||
/// Encodes the given project package name into a valid `extra` value in a PEP
|
||||
/// 508 marker.
|
||||
fn encode_project(package: &PackageName) -> ExtraName {
|
||||
// See `encode_package_extra`, the same considerations apply here.
|
||||
let package_len = package.as_str().len();
|
||||
ExtraName::from_owned(format!("project-{package_len}-{package}")).unwrap()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ParsedRawExtra<'a> {
|
||||
Project { package: &'a str },
|
||||
Extra { package: &'a str, extra: &'a str },
|
||||
Group { package: &'a str, group: &'a str },
|
||||
}
|
||||
|
|
@ -553,13 +609,13 @@ impl<'a> ParsedRawExtra<'a> {
|
|||
let Some((kind, tail)) = raw.split_once('-') else {
|
||||
return Err(mkerr(
|
||||
raw_extra,
|
||||
"expected to find leading `extra-` or `group-`",
|
||||
"expected to find leading `package`, `extra-` or `group-`",
|
||||
));
|
||||
};
|
||||
let Some((len, tail)) = tail.split_once('-') else {
|
||||
return Err(mkerr(
|
||||
raw_extra,
|
||||
"expected to find `{number}-` after leading `extra-` or `group-`",
|
||||
"expected to find `{number}-` after leading `package-`, `extra-` or `group-`",
|
||||
));
|
||||
};
|
||||
let len = len.parse::<usize>().map_err(|_| {
|
||||
|
|
@ -577,22 +633,28 @@ impl<'a> ParsedRawExtra<'a> {
|
|||
),
|
||||
));
|
||||
};
|
||||
if !tail.starts_with('-') {
|
||||
return Err(mkerr(
|
||||
raw_extra,
|
||||
format!("expected `-` after package name `{package}`"),
|
||||
));
|
||||
}
|
||||
let tail = &tail[1..];
|
||||
match kind {
|
||||
"extra" => Ok(ParsedRawExtra::Extra {
|
||||
package,
|
||||
extra: tail,
|
||||
}),
|
||||
"group" => Ok(ParsedRawExtra::Group {
|
||||
package,
|
||||
group: tail,
|
||||
}),
|
||||
"project" => Ok(ParsedRawExtra::Project { package }),
|
||||
"extra" | "group" => {
|
||||
if !tail.starts_with('-') {
|
||||
return Err(mkerr(
|
||||
raw_extra,
|
||||
format!("expected `-` after package name `{package}`"),
|
||||
));
|
||||
}
|
||||
let tail = &tail[1..];
|
||||
if kind == "extra" {
|
||||
Ok(ParsedRawExtra::Extra {
|
||||
package,
|
||||
extra: tail,
|
||||
})
|
||||
} else {
|
||||
Ok(ParsedRawExtra::Group {
|
||||
package,
|
||||
group: tail,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => Err(mkerr(
|
||||
raw_extra,
|
||||
format!("unrecognized kind `{kind}` (must be `extra` or `group`)"),
|
||||
|
|
@ -608,6 +670,7 @@ impl<'a> ParsedRawExtra<'a> {
|
|||
}
|
||||
})?;
|
||||
match self {
|
||||
Self::Project { .. } => Ok(ConflictItem::from(package)),
|
||||
Self::Extra { extra, .. } => {
|
||||
let extra = ExtraName::from_str(extra).map_err(|name_error| {
|
||||
ResolveError::InvalidValueInConflictMarker {
|
||||
|
|
@ -631,6 +694,7 @@ impl<'a> ParsedRawExtra<'a> {
|
|||
|
||||
fn package(&self) -> &'a str {
|
||||
match self {
|
||||
Self::Project { package, .. } => package,
|
||||
Self::Extra { package, .. } => package,
|
||||
Self::Group { package, .. } => package,
|
||||
}
|
||||
|
|
@ -719,6 +783,26 @@ pub(crate) fn resolve_conflicts(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for the conflict item as a project.
|
||||
if conflict_item.extra().is_none() && conflict_item.group().is_none() {
|
||||
let package = conflict_item.package();
|
||||
let encoded = encode_project(package);
|
||||
if encoded == *name {
|
||||
match operator {
|
||||
ExtraOperator::Equal => {
|
||||
or.and(*conflict_marker);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
ExtraOperator::NotEqual => {
|
||||
or.and(conflict_marker.negate());
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find the marker in the list of known conflicts, assume it's always
|
||||
|
|
|
|||
|
|
@ -210,9 +210,6 @@ pub(crate) async fn export(
|
|||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
// Validate that the set of requested extras and development groups are compatible.
|
||||
detect_conflicts(&lock, &extras, &groups)?;
|
||||
|
||||
// Identify the installation target.
|
||||
let target = match &target {
|
||||
ExportTarget::Project(VirtualProject::Project(project)) => {
|
||||
|
|
@ -262,6 +259,9 @@ pub(crate) async fn export(
|
|||
},
|
||||
};
|
||||
|
||||
// Validate that the set of requested extras and development groups are compatible.
|
||||
detect_conflicts(&target, &extras, &groups)?;
|
||||
|
||||
// Validate that the set of requested extras and development groups are defined in the lockfile.
|
||||
target.validate_extras(&extras)?;
|
||||
target.validate_groups(&groups)?;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
|
@ -7,7 +8,7 @@ use rustc_hash::FxHashSet;
|
|||
|
||||
use uv_configuration::{Constraints, DependencyGroupsWithDefaults, ExtrasSpecification};
|
||||
use uv_distribution_types::Index;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_normalize::{ExtraName, PackageName};
|
||||
use uv_pypi_types::{DependencyGroupSpecifier, LenientRequirement, VerbatimParsedUrl};
|
||||
use uv_resolver::{Installable, Lock, Package};
|
||||
use uv_scripts::Pep723Script;
|
||||
|
|
@ -369,4 +370,107 @@ impl<'lock> InstallTarget<'lock> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the names of all packages in the workspace that will be installed.
|
||||
///
|
||||
/// Note this only includes workspace members.
|
||||
pub(crate) fn packages(
|
||||
&self,
|
||||
extras: &ExtrasSpecification,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
) -> BTreeSet<&PackageName> {
|
||||
match self {
|
||||
Self::Project { name, lock, .. } => {
|
||||
// Collect the packages by name for efficient lookup
|
||||
let packages = lock
|
||||
.packages()
|
||||
.iter()
|
||||
.map(|p| (p.name(), p))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// We'll include the project itself
|
||||
let mut required_members = BTreeSet::new();
|
||||
required_members.insert(*name);
|
||||
|
||||
// Find all workspace member dependencies recursively
|
||||
let mut queue: VecDeque<(&PackageName, Option<&ExtraName>)> = VecDeque::new();
|
||||
let mut seen: FxHashSet<(&PackageName, Option<&ExtraName>)> = FxHashSet::default();
|
||||
|
||||
let Some(root_package) = packages.get(name) else {
|
||||
return required_members;
|
||||
};
|
||||
|
||||
if groups.prod() {
|
||||
// Add the root package
|
||||
queue.push_back((name, None));
|
||||
seen.insert((name, None));
|
||||
|
||||
// Add explicitly activated extras for the root package
|
||||
for extra in extras.extra_names(root_package.optional_dependencies().keys()) {
|
||||
if seen.insert((name, Some(extra))) {
|
||||
queue.push_back((name, Some(extra)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add activated dependency groups for the root package
|
||||
for (group_name, dependencies) in root_package.resolved_dependency_groups() {
|
||||
if !groups.contains(group_name) {
|
||||
continue;
|
||||
}
|
||||
for dependency in dependencies {
|
||||
let name = dependency.package_name();
|
||||
queue.push_back((name, None));
|
||||
for extra in dependency.extra() {
|
||||
queue.push_back((name, Some(extra)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some((pkg_name, extra)) = queue.pop_front() {
|
||||
if lock.members().contains(pkg_name) {
|
||||
required_members.insert(pkg_name);
|
||||
}
|
||||
|
||||
let Some(package) = packages.get(pkg_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(dependencies) = extra
|
||||
.map(|extra_name| {
|
||||
package
|
||||
.optional_dependencies()
|
||||
.get(extra_name)
|
||||
.map(Vec::as_slice)
|
||||
})
|
||||
.unwrap_or(Some(package.dependencies()))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for dependency in dependencies {
|
||||
let name = dependency.package_name();
|
||||
if seen.insert((name, None)) {
|
||||
queue.push_back((name, None));
|
||||
}
|
||||
for extra in dependency.extra() {
|
||||
if seen.insert((name, Some(extra))) {
|
||||
queue.push_back((name, Some(extra)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required_members
|
||||
}
|
||||
Self::Workspace { lock, .. } | Self::NonProjectWorkspace { lock, .. } => {
|
||||
// Return all workspace members
|
||||
lock.members().iter().collect()
|
||||
}
|
||||
Self::Script { .. } => {
|
||||
// Scripts don't have workspace members
|
||||
BTreeSet::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ use uv_distribution_types::{
|
|||
use uv_git::ResolvedRepositoryReference;
|
||||
use uv_normalize::{GroupName, PackageName};
|
||||
use uv_pep440::Version;
|
||||
use uv_pypi_types::{Conflicts, SupportedEnvironments};
|
||||
use uv_pypi_types::{ConflictKind, Conflicts, SupportedEnvironments};
|
||||
use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
|
||||
use uv_requirements::ExtrasResolver;
|
||||
use uv_requirements::upgrade::{LockedRequirements, read_lock_requirements};
|
||||
|
|
@ -487,6 +487,19 @@ async fn do_lock(
|
|||
}
|
||||
}
|
||||
|
||||
// Check if any conflicts contain project-level conflicts
|
||||
if !preview.is_enabled(PreviewFeatures::PACKAGE_CONFLICTS)
|
||||
&& conflicts.iter().any(|set| {
|
||||
set.iter()
|
||||
.any(|item| matches!(item.kind(), ConflictKind::Project))
|
||||
})
|
||||
{
|
||||
warn_user_once!(
|
||||
"Declaring conflicts for packages (`package = ...`) is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
|
||||
PreviewFeatures::PACKAGE_CONFLICTS
|
||||
);
|
||||
}
|
||||
|
||||
// Collect the list of supported environments.
|
||||
let environments = {
|
||||
let environments = target.environments();
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use uv_installer::{SatisfiesResult, SitePackages};
|
|||
use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers};
|
||||
use uv_pep508::MarkerTreeContents;
|
||||
use uv_pypi_types::{ConflictPackage, ConflictSet, Conflicts};
|
||||
use uv_pypi_types::{ConflictItem, ConflictKind, ConflictSet, Conflicts};
|
||||
use uv_python::{
|
||||
EnvironmentPreference, Interpreter, InvalidEnvironmentKind, PythonDownloads, PythonEnvironment,
|
||||
PythonInstallation, PythonPreference, PythonRequest, PythonSource, PythonVariant,
|
||||
|
|
@ -36,8 +36,8 @@ use uv_python::{
|
|||
use uv_requirements::upgrade::{LockedRequirements, read_lock_requirements};
|
||||
use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification};
|
||||
use uv_resolver::{
|
||||
FlatIndex, Lock, OptionsBuilder, Preference, PythonRequirement, ResolverEnvironment,
|
||||
ResolverOutput,
|
||||
FlatIndex, Installable, Lock, OptionsBuilder, Preference, PythonRequirement,
|
||||
ResolverEnvironment, ResolverOutput,
|
||||
};
|
||||
use uv_scripts::Pep723ItemRef;
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
|
|
@ -52,6 +52,7 @@ use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache};
|
|||
|
||||
use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
|
||||
use crate::commands::pip::operations::{Changelog, Modifications};
|
||||
use crate::commands::project::install_target::InstallTarget;
|
||||
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
|
||||
use crate::commands::{capitalize, conjunction, pip};
|
||||
use crate::printer::Printer;
|
||||
|
|
@ -274,7 +275,7 @@ pub(crate) struct ConflictError {
|
|||
/// The set from which the conflict was derived.
|
||||
pub(crate) set: ConflictSet,
|
||||
/// The items from the set that were enabled, and thus create the conflict.
|
||||
pub(crate) conflicts: Vec<ConflictPackage>,
|
||||
pub(crate) conflicts: Vec<ConflictItem>,
|
||||
/// Enabled dependency groups with defaults applied.
|
||||
pub(crate) groups: DependencyGroupsWithDefaults,
|
||||
}
|
||||
|
|
@ -285,9 +286,10 @@ impl std::fmt::Display for ConflictError {
|
|||
let set = self
|
||||
.set
|
||||
.iter()
|
||||
.map(|item| match item.conflict() {
|
||||
ConflictPackage::Extra(extra) => format!("`{}[{}]`", item.package(), extra),
|
||||
ConflictPackage::Group(group) => format!("`{}:{}`", item.package(), group),
|
||||
.map(|item| match item.kind() {
|
||||
ConflictKind::Project => format!("{}", item.package()),
|
||||
ConflictKind::Extra(extra) => format!("`{}[{}]`", item.package(), extra),
|
||||
ConflictKind::Group(group) => format!("`{}:{}`", item.package(), group),
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
|
|
@ -295,7 +297,7 @@ impl std::fmt::Display for ConflictError {
|
|||
if self
|
||||
.conflicts
|
||||
.iter()
|
||||
.all(|conflict| matches!(conflict, ConflictPackage::Extra(..)))
|
||||
.all(|conflict| matches!(conflict.kind(), ConflictKind::Extra(..)))
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
|
|
@ -303,9 +305,9 @@ impl std::fmt::Display for ConflictError {
|
|||
conjunction(
|
||||
self.conflicts
|
||||
.iter()
|
||||
.map(|conflict| match conflict {
|
||||
ConflictPackage::Extra(extra) => format!("`{extra}`"),
|
||||
ConflictPackage::Group(..) => unreachable!(),
|
||||
.map(|conflict| match conflict.kind() {
|
||||
ConflictKind::Extra(extra) => format!("`{extra}`"),
|
||||
ConflictKind::Group(..) | ConflictKind::Project => unreachable!(),
|
||||
})
|
||||
.collect()
|
||||
)
|
||||
|
|
@ -313,7 +315,7 @@ impl std::fmt::Display for ConflictError {
|
|||
} else if self
|
||||
.conflicts
|
||||
.iter()
|
||||
.all(|conflict| matches!(conflict, ConflictPackage::Group(..)))
|
||||
.all(|conflict| matches!(conflict.kind(), ConflictKind::Group(..)))
|
||||
{
|
||||
let conflict_source = if self.set.is_inferred_conflict() {
|
||||
"transitively inferred"
|
||||
|
|
@ -326,12 +328,12 @@ impl std::fmt::Display for ConflictError {
|
|||
conjunction(
|
||||
self.conflicts
|
||||
.iter()
|
||||
.map(|conflict| match conflict {
|
||||
ConflictPackage::Group(group)
|
||||
.map(|conflict| match conflict.kind() {
|
||||
ConflictKind::Group(group)
|
||||
if self.groups.contains_because_default(group) =>
|
||||
format!("`{group}` (enabled by default)"),
|
||||
ConflictPackage::Group(group) => format!("`{group}`"),
|
||||
ConflictPackage::Extra(..) => unreachable!(),
|
||||
ConflictKind::Group(group) => format!("`{group}`"),
|
||||
ConflictKind::Extra(..) | ConflictKind::Project => unreachable!(),
|
||||
})
|
||||
.collect()
|
||||
)
|
||||
|
|
@ -345,14 +347,17 @@ impl std::fmt::Display for ConflictError {
|
|||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, conflict)| {
|
||||
let conflict = match conflict {
|
||||
ConflictPackage::Extra(extra) => format!("extra `{extra}`"),
|
||||
ConflictPackage::Group(group)
|
||||
let conflict = match conflict.kind() {
|
||||
ConflictKind::Project => {
|
||||
format!("package `{}`", conflict.package())
|
||||
}
|
||||
ConflictKind::Extra(extra) => format!("extra `{extra}`"),
|
||||
ConflictKind::Group(group)
|
||||
if self.groups.contains_because_default(group) =>
|
||||
{
|
||||
format!("group `{group}` (enabled by default)")
|
||||
}
|
||||
ConflictPackage::Group(group) => format!("group `{group}`"),
|
||||
ConflictKind::Group(group) => format!("group `{group}`"),
|
||||
};
|
||||
if i == 0 {
|
||||
capitalize(&conflict)
|
||||
|
|
@ -2526,31 +2531,33 @@ pub(crate) fn default_dependency_groups(
|
|||
/// are declared as conflicting.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn detect_conflicts(
|
||||
lock: &Lock,
|
||||
target: &InstallTarget,
|
||||
extras: &ExtrasSpecification,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
) -> Result<(), ProjectError> {
|
||||
// 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.
|
||||
// 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 lock = target.lock();
|
||||
let packages = target.packages(extras, groups);
|
||||
let conflicts = lock.conflicts();
|
||||
for set in conflicts.iter() {
|
||||
let mut conflicts: Vec<ConflictPackage> = vec![];
|
||||
let mut conflicts: Vec<ConflictItem> = vec![];
|
||||
for item in set.iter() {
|
||||
if item
|
||||
.extra()
|
||||
.map(|extra| extras.contains(extra))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
conflicts.push(item.conflict().clone());
|
||||
if !packages.contains(item.package()) {
|
||||
// Ignore items that are not in the install targets
|
||||
continue;
|
||||
}
|
||||
if item
|
||||
.group()
|
||||
.map(|group| groups.contains(group))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
conflicts.push(item.conflict().clone());
|
||||
let is_conflicting = match item.kind() {
|
||||
ConflictKind::Project => groups.prod(),
|
||||
ConflictKind::Extra(extra) => extras.contains(extra),
|
||||
ConflictKind::Group(group1) => groups.contains(group1),
|
||||
};
|
||||
if is_conflicting {
|
||||
conflicts.push(item.clone());
|
||||
}
|
||||
}
|
||||
if conflicts.len() >= 2 {
|
||||
|
|
|
|||
|
|
@ -663,7 +663,7 @@ pub(super) async fn do_sync(
|
|||
}
|
||||
|
||||
// Validate that the set of requested extras and development groups are compatible.
|
||||
detect_conflicts(target.lock(), extras, groups)?;
|
||||
detect_conflicts(&target, extras, groups)?;
|
||||
|
||||
// Validate that the set of requested extras and development groups are defined in the lockfile.
|
||||
target.validate_extras(extras)?;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1049,24 +1049,24 @@ fn extra_unconditional() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
// This should error since we're enabling two conflicting extras.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Found conflicting extras `proxy1[extra1]` and `proxy1[extra2]` enabled simultaneously
|
||||
"###);
|
||||
");
|
||||
|
||||
root_pyproject_toml.write_str(
|
||||
r#"
|
||||
|
|
@ -1085,14 +1085,14 @@ fn extra_unconditional() -> Result<()> {
|
|||
proxy1 = { workspace = true }
|
||||
"#,
|
||||
)?;
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
// This is fine because we are only enabling one
|
||||
// extra, and thus, there is no conflict.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r"
|
||||
|
|
@ -1127,17 +1127,17 @@ fn extra_unconditional() -> Result<()> {
|
|||
proxy1 = { workspace = true }
|
||||
"#,
|
||||
)?;
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
// This is fine because we are only enabling one
|
||||
// extra, and thus, there is no conflict.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -1148,7 +1148,7 @@ fn extra_unconditional() -> Result<()> {
|
|||
Installed 1 package in [TIME]
|
||||
- anyio==4.1.0
|
||||
+ anyio==4.2.0
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1203,14 +1203,14 @@ fn extra_unconditional_non_conflicting() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
// This *should* install `anyio==4.1.0`, but when this
|
||||
// test was initially written, it didn't. This was because
|
||||
|
|
@ -1426,26 +1426,26 @@ fn extra_unconditional_non_local_conflict() -> Result<()> {
|
|||
// that can never be installed! Namely, because two different
|
||||
// conflicting extras are enabled unconditionally in all
|
||||
// configurations.
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
// This should fail. If it doesn't and we generated a lock
|
||||
// file above, then this will likely result in the installation
|
||||
// of two different versions of the same package.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Found conflicting extras `c[x1]` and `c[x2]` enabled simultaneously
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1955,14 +1955,14 @@ fn group_basic() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
|
|
@ -2110,14 +2110,14 @@ fn group_default() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
|
|
@ -2642,14 +2642,14 @@ fn multiple_sources_index_disjoint_groups() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
|
||||
|
|
@ -3125,7 +3125,7 @@ fn non_optional_dependency_extra() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -3135,7 +3135,7 @@ fn non_optional_dependency_extra() -> Result<()> {
|
|||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -3172,7 +3172,7 @@ fn non_optional_dependency_group() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -3182,7 +3182,7 @@ fn non_optional_dependency_group() -> Result<()> {
|
|||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -3222,7 +3222,7 @@ fn non_optional_dependency_mixed() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -3232,7 +3232,7 @@ fn non_optional_dependency_mixed() -> Result<()> {
|
|||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -3422,7 +3422,7 @@ fn shared_optional_dependency_group1() -> Result<()> {
|
|||
)?;
|
||||
|
||||
// This shouldn't install two versions of `idna`, only one, `idna==3.5`.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group=baz").arg("--group=foo"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group=baz").arg("--group=foo"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -3434,7 +3434,7 @@ fn shared_optional_dependency_group1() -> Result<()> {
|
|||
+ anyio==4.3.0
|
||||
+ idna==3.5
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
insta::with_settings!({
|
||||
|
|
@ -3849,7 +3849,7 @@ fn shared_optional_dependency_group2() -> Result<()> {
|
|||
)?;
|
||||
|
||||
// This shouldn't install two versions of `idna`, only one, `idna==3.5`.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group=bar"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group=bar"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -3861,7 +3861,7 @@ fn shared_optional_dependency_group2() -> Result<()> {
|
|||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
insta::with_settings!({
|
||||
|
|
@ -4139,7 +4139,7 @@ fn shared_dependency_extra() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4151,7 +4151,7 @@ fn shared_dependency_extra() -> Result<()> {
|
|||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
insta::with_settings!({
|
||||
|
|
@ -4240,7 +4240,7 @@ fn shared_dependency_extra() -> Result<()> {
|
|||
|
||||
// This shouldn't install two versions of `idna`, only one, `idna==3.5`.
|
||||
// So this should remove `idna==3.6` installed above.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--extra=foo"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--extra=foo"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4252,9 +4252,9 @@ fn shared_dependency_extra() -> Result<()> {
|
|||
Installed 1 package in [TIME]
|
||||
- idna==3.6
|
||||
+ idna==3.5
|
||||
"###);
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--extra=bar"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--extra=bar"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4265,9 +4265,9 @@ fn shared_dependency_extra() -> Result<()> {
|
|||
Installed 1 package in [TIME]
|
||||
- idna==3.5
|
||||
+ idna==3.6
|
||||
"###);
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4275,7 +4275,7 @@ fn shared_dependency_extra() -> Result<()> {
|
|||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
Audited 3 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -4314,7 +4314,7 @@ fn shared_dependency_group() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4326,7 +4326,7 @@ fn shared_dependency_group() -> Result<()> {
|
|||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
insta::with_settings!({
|
||||
|
|
@ -4490,7 +4490,7 @@ fn shared_dependency_mixed() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4502,7 +4502,7 @@ fn shared_dependency_mixed() -> Result<()> {
|
|||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
insta::with_settings!({
|
||||
|
|
@ -4595,7 +4595,7 @@ fn shared_dependency_mixed() -> Result<()> {
|
|||
|
||||
// This shouldn't install two versions of `idna`, only one, `idna==3.5`.
|
||||
// So this should remove `idna==3.6` installed above.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--extra=foo"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--extra=foo"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4607,9 +4607,9 @@ fn shared_dependency_mixed() -> Result<()> {
|
|||
Installed 1 package in [TIME]
|
||||
- idna==3.6
|
||||
+ idna==3.5
|
||||
"###);
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group=bar"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group=bar"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4620,9 +4620,9 @@ fn shared_dependency_mixed() -> Result<()> {
|
|||
Installed 1 package in [TIME]
|
||||
- idna==3.5
|
||||
+ idna==3.6
|
||||
"###);
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4630,7 +4630,7 @@ fn shared_dependency_mixed() -> Result<()> {
|
|||
----- stderr -----
|
||||
Resolved 5 packages in [TIME]
|
||||
Audited 3 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7720,7 +7720,7 @@ fn preview_features() {
|
|||
show_settings: true,
|
||||
preview: Preview {
|
||||
flags: PreviewFeatures(
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | EXTRA_BUILD_DEPENDENCIES,
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES,
|
||||
),
|
||||
},
|
||||
python_preference: Managed,
|
||||
|
|
@ -7946,7 +7946,7 @@ fn preview_features() {
|
|||
show_settings: true,
|
||||
preview: Preview {
|
||||
flags: PreviewFeatures(
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | EXTRA_BUILD_DEPENDENCIES,
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES,
|
||||
),
|
||||
},
|
||||
python_preference: Managed,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ The following preview features are available:
|
|||
- `add-bounds`: Allows configuring the
|
||||
[default bounds for `uv add`](../reference/settings.md#add-bounds) invocations.
|
||||
- `json-output`: Allows `--output-format json` for various uv commands.
|
||||
- `package-conflicts`: Allows defining workspace conflicts at the package level.
|
||||
- `pylock`: Allows installing from `pylock.toml` files.
|
||||
- `python-install-default`: Allows
|
||||
[installing `python` and `python3` executables](./python-versions.md#installing-python-executables).
|
||||
|
|
|
|||
Loading…
Reference in New Issue