Support recursive extras with marker in pip compile -r pyproject.toml (#9535)

## Summary

Closes https://github.com/astral-sh/uv/issues/9530.
This commit is contained in:
Charlie Marsh 2024-11-29 22:40:22 -05:00 committed by GitHub
parent 891e02d586
commit 69811837e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 209 additions and 54 deletions

View file

@ -1,10 +1,13 @@
use std::borrow::Cow;
use std::collections::VecDeque;
use std::path::Path;
use std::slice;
use std::sync::Arc;
use anyhow::{Context, Result};
use futures::stream::FuturesOrdered;
use futures::TryStreamExt;
use rustc_hash::FxHashSet;
use url::Url;
use uv_configuration::ExtrasSpecification;
@ -14,7 +17,7 @@ use uv_distribution_types::{
};
use uv_fs::Simplified;
use uv_normalize::{ExtraName, PackageName};
use uv_pep508::RequirementOrigin;
use uv_pep508::{MarkerTree, RequirementOrigin};
use uv_pypi_types::Requirement;
use uv_resolver::{InMemoryIndex, MetadataResponse};
use uv_types::{BuildContext, HashStrategy};
@ -89,16 +92,13 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
let origin = RequirementOrigin::Project(path.to_path_buf(), metadata.name.clone());
// Determine the extras to include when resolving the requirements.
let extras: Vec<_> = self
let extras = self
.extras
.extra_names(metadata.provides_extras.iter())
.cloned()
.collect();
.collect::<Vec<_>>();
// Determine the appropriate requirements to return based on the extras. This involves
// evaluating the `extras` expression in any markers, but preserving the remaining marker
// conditions.
let mut requirements: Vec<Requirement> = metadata
let dependencies = metadata
.requires_dist
.into_iter()
.map(|requirement| Requirement {
@ -106,30 +106,61 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
marker: requirement.marker.simplify_extras(&extras),
..requirement
})
.collect::<Vec<_>>();
// Transitively process all extras that are recursively included, starting with the current
// extra.
let mut requirements = dependencies.clone();
let mut seen = FxHashSet::<(ExtraName, MarkerTree)>::default();
let mut queue: VecDeque<_> = requirements
.iter()
.filter(|req| req.name == metadata.name)
.flat_map(|req| {
req.extras
.iter()
.cloned()
.map(|extra| (extra, req.marker.clone().simplify_extras(&extras)))
})
.collect();
while let Some((extra, marker)) = queue.pop_front() {
if !seen.insert((extra.clone(), marker.clone())) {
continue;
}
// Resolve any recursive extras.
loop {
// Find the first recursive requirement.
// TODO(charlie): Respect markers on recursive extras.
let Some(index) = requirements.iter().position(|requirement| {
requirement.name == metadata.name && requirement.marker.is_true()
}) else {
break;
};
// Remove the requirement that points to us.
let recursive = requirements.remove(index);
// Re-simplify the requirements.
for requirement in &mut requirements {
requirement.marker = requirement
.marker
.clone()
.simplify_extras(&recursive.extras);
// Find the requirements for the extra.
for requirement in &dependencies {
if requirement.marker.top_level_extra_name().as_ref() == Some(&extra) {
let requirement = {
let mut marker = marker.clone();
marker.and(requirement.marker.clone());
Requirement {
name: requirement.name.clone(),
extras: requirement.extras.clone(),
source: requirement.source.clone(),
origin: requirement.origin.clone(),
marker: marker.simplify_extras(slice::from_ref(&extra)),
}
};
if requirement.name == metadata.name {
// Add each transitively included extra.
queue.extend(
requirement
.extras
.iter()
.cloned()
.map(|extra| (extra, requirement.marker.clone())),
);
} else {
// Add the requirements for that extra.
requirements.push(requirement);
}
}
}
}
// Drop all the self-requirements now that we flattened them out.
requirements.retain(|req| req.name != metadata.name);
let project = metadata.name;
let extras = metadata.provides_extras;