Enable first-class dependency exclusions (#16528)

## Summary

This PR adds an `exclude-dependencies` setting that allows users to omit
a dependency during resolution. It's effectively a formalized version of
the `flask ; python_version < '0'` hack that we've suggested to users in
various issues.

Closes #12616.
This commit is contained in:
Charlie Marsh 2025-10-31 10:14:12 -04:00 committed by GitHub
parent 7978122837
commit 5c71b5c124
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 711 additions and 21 deletions

View file

@ -1163,6 +1163,23 @@ impl Lock {
manifest_table.insert("overrides", value(overrides));
}
if !self.manifest.excludes.is_empty() {
let excludes = self
.manifest
.excludes
.iter()
.map(|name| {
serde::Serialize::serialize(&name, toml_edit::ser::ValueSerializer::new())
})
.collect::<Result<Vec<_>, _>>()?;
let excludes = match excludes.as_slice() {
[] => Array::new(),
[name] => Array::from_iter([name]),
excludes => each_element_on_its_line_array(excludes.iter()),
};
manifest_table.insert("excludes", value(excludes));
}
if !self.manifest.build_constraints.is_empty() {
let build_constraints = self
.manifest
@ -1447,6 +1464,7 @@ impl Lock {
requirements: &[Requirement],
constraints: &[Requirement],
overrides: &[Requirement],
excludes: &[PackageName],
build_constraints: &[Requirement],
dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
dependency_metadata: &DependencyMetadata,
@ -1563,6 +1581,15 @@ impl Lock {
}
}
// Validate that the lockfile was generated with the same excludes.
{
let expected: BTreeSet<_> = excludes.iter().cloned().collect();
let actual: BTreeSet<_> = self.manifest.excludes.iter().cloned().collect();
if expected != actual {
return Ok(SatisfiesResult::MismatchedExcludes(expected, actual));
}
}
// Validate that the lockfile was generated with the same build constraints.
{
let expected: BTreeSet<_> = build_constraints
@ -2049,6 +2076,8 @@ pub enum SatisfiesResult<'lock> {
MismatchedConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
/// The lockfile uses a different set of overrides.
MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
/// The lockfile uses a different set of excludes.
MismatchedExcludes(BTreeSet<PackageName>, BTreeSet<PackageName>),
/// The lockfile uses a different set of build constraints.
MismatchedBuildConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
/// The lockfile uses a different set of dependency groups.
@ -2148,6 +2177,9 @@ pub struct ResolverManifest {
/// The overrides provided to the resolver.
#[serde(default)]
overrides: BTreeSet<Requirement>,
/// The excludes provided to the resolver.
#[serde(default)]
excludes: BTreeSet<PackageName>,
/// The build constraints provided to the resolver.
#[serde(default)]
build_constraints: BTreeSet<Requirement>,
@ -2164,6 +2196,7 @@ impl ResolverManifest {
requirements: impl IntoIterator<Item = Requirement>,
constraints: impl IntoIterator<Item = Requirement>,
overrides: impl IntoIterator<Item = Requirement>,
excludes: impl IntoIterator<Item = PackageName>,
build_constraints: impl IntoIterator<Item = Requirement>,
dependency_groups: impl IntoIterator<Item = (GroupName, Vec<Requirement>)>,
dependency_metadata: impl IntoIterator<Item = StaticMetadata>,
@ -2173,6 +2206,7 @@ impl ResolverManifest {
requirements: requirements.into_iter().collect(),
constraints: constraints.into_iter().collect(),
overrides: overrides.into_iter().collect(),
excludes: excludes.into_iter().collect(),
build_constraints: build_constraints.into_iter().collect(),
dependency_groups: dependency_groups
.into_iter()
@ -2201,6 +2235,7 @@ impl ResolverManifest {
.into_iter()
.map(|requirement| requirement.relative_to(root))
.collect::<Result<BTreeSet<_>, _>>()?,
excludes: self.excludes,
build_constraints: self
.build_constraints
.into_iter()