uv-resolver: use Requires-Python to filter dependencies during universal resolution

In the time before universal resolving, we would always pass a
`MarkerEnvironment`, and this environment would capture any relevant
`Requires-Python` specifier (including if `-p/--python` was provided on
the CLI).

But in universal resolution, we very specifically do not use a
`MarkerEnvironment` because we want to produce a resolution that is
compatible across potentially multiple environments. This in turn meant
that we lost `Requires-Python` filtering.

This PR adds it back. We do this by converting our `PythonRequirement`
into a `MarkerTree` that encodes the version specifiers in a
`Requires-Python` specifier. We then ask whether that `MarkerTree` is
disjoint with a dependency specification's `MarkerTree`. If it is, then
we know it's impossible for that dependency specification to every be
true, and we can completely ignore it.
This commit is contained in:
Andrew Gallant 2024-06-11 09:57:10 -04:00 committed by Andrew Gallant
parent c32667caec
commit 75b323232d
8 changed files with 147 additions and 422 deletions

View file

@ -47,6 +47,7 @@ use crate::pubgrub::{
PubGrubPriorities, PubGrubPython, PubGrubSpecifier,
};
use crate::python_requirement::PythonRequirement;
use crate::requires_python::RequiresPython;
use crate::resolution::ResolutionGraph;
pub(crate) use crate::resolver::availability::{
IncompletePackage, ResolverVersion, UnavailablePackage, UnavailableReason, UnavailableVersion,
@ -94,6 +95,14 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
/// When not set, the resolver is in "universal" mode.
markers: Option<MarkerEnvironment>,
python_requirement: PythonRequirement,
/// This is derived from `PythonRequirement` once at initialization
/// time. It's used in universal mode to filter our dependencies with
/// a `python_version` marker expression that has no overlap with the
/// `Requires-Python` specifier.
///
/// This is non-None if and only if the resolver is operating in
/// universal mode. (i.e., when `markers` is `None`.)
requires_python: Option<MarkerTree>,
selector: CandidateSelector,
index: InMemoryIndex,
installed_packages: InstalledPackages,
@ -181,6 +190,16 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
provider: Provider,
installed_packages: InstalledPackages,
) -> Result<Self, ResolveError> {
let requires_python = if markers.is_some() {
None
} else {
Some(
python_requirement
.requires_python()
.map(RequiresPython::to_marker_tree)
.unwrap_or_else(|| MarkerTree::And(vec![])),
)
};
let state = ResolverState {
index: index.clone(),
git: git.clone(),
@ -200,6 +219,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
hasher: hasher.clone(),
markers: markers.cloned(),
python_requirement: python_requirement.clone(),
requires_python,
reporter: None,
installed_packages,
};
@ -959,6 +979,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&self.locals,
&self.git,
self.markers.as_ref(),
self.requires_python.as_ref(),
);
let dependencies = match dependencies {
@ -1109,6 +1130,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&self.locals,
&self.git,
self.markers.as_ref(),
self.requires_python.as_ref(),
)?;
for (dep_package, dep_version) in dependencies.iter() {