Make direct dependency detection respect markers (#2207)

## Summary

When determining "direct" dependencies, we need to ensure that we
respect markers. In the linked issue, the user had an optional
dependency like:

```toml
[project.optional-dependencies]
dev = [
  "setuptools>=64",
  "setuptools_scm>=8"
]
```

By not respecting markers, we tried to resolve `setuptools` to the
lowest-available version. However, since `setuptools>=64` _isn't_
enabled (since it's optional), we won't respect _that_ constraint.

To be consistent, we need to omit optional dependencies just as we will
at resolution time.

Closes https://github.com/astral-sh/uv/issues/2203.

## Test Plan

`cargo test`
This commit is contained in:
Charlie Marsh 2024-03-05 09:25:06 -08:00 committed by GitHub
parent 7f07ada24c
commit 8620b5a52f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 135 additions and 57 deletions

View file

@ -1,28 +1,33 @@
use rustc_hash::{FxHashMap, FxHashSet};
use pep440_rs::Version;
use pep508_rs::Requirement;
use pep508_rs::MarkerEnvironment;
use uv_normalize::PackageName;
use crate::Manifest;
/// A set of package versions that are permitted, even if they're marked as yanked by the
/// relevant index.
#[derive(Debug, Default)]
pub(crate) struct AllowedYanks(FxHashMap<PackageName, FxHashSet<Version>>);
impl AllowedYanks {
/// Returns `true` if the given package version is allowed, even if it's marked as yanked by
/// the relevant index.
pub(crate) fn allowed(&self, package_name: &PackageName, version: &Version) -> bool {
self.0
.get(package_name)
.is_some_and(|allowed_yanks| allowed_yanks.contains(version))
}
}
impl<'a> FromIterator<&'a Requirement> for AllowedYanks {
fn from_iter<T: IntoIterator<Item = &'a Requirement>>(iter: T) -> Self {
pub(crate) fn from_manifest(manifest: &Manifest, markers: &MarkerEnvironment) -> Self {
let mut allowed_yanks = FxHashMap::<PackageName, FxHashSet<Version>>::default();
for requirement in iter {
for requirement in manifest
.requirements
.iter()
.chain(manifest.constraints.iter())
.chain(manifest.overrides.iter())
.chain(manifest.preferences.iter())
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata
.requires_dist
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
}))
{
let Some(pep508_rs::VersionOrUrl::VersionSpecifier(specifiers)) =
&requirement.version_or_url
else {
@ -43,4 +48,12 @@ impl<'a> FromIterator<&'a Requirement> for AllowedYanks {
}
Self(allowed_yanks)
}
/// Returns `true` if the given package version is allowed, even if it's marked as yanked by
/// the relevant index.
pub(crate) fn allowed(&self, package_name: &PackageName, version: &Version) -> bool {
self.0
.get(package_name)
.is_some_and(|allowed_yanks| allowed_yanks.contains(version))
}
}