Allow yanked versions when specified via == (#561)

## Summary

This enables users to rely on yanked versions via explicit `==` markers,
which is necessary in some projects (and, in my opinion, reasonable).

Closes #551.
This commit is contained in:
Charlie Marsh 2023-12-05 03:44:06 -05:00 committed by GitHub
parent c3a917bbf6
commit 2d1e19e474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 178 additions and 8 deletions

View file

@ -56,7 +56,7 @@ impl From<&[Requirement]> for Preferences {
else {
return None;
};
let [version_specifier] = &**version_specifiers else {
let [version_specifier] = version_specifiers.as_ref() else {
return None;
};
let version = PubGrubVersion::from(version_specifier.version().clone());

View file

@ -20,3 +20,4 @@ mod resolution_mode;
mod resolution_options;
mod resolver;
mod version_map;
mod yanks;

View file

@ -36,6 +36,7 @@ use crate::pubgrub::{
};
use crate::resolution::Graph;
use crate::version_map::VersionMap;
use crate::yanks::AllowedYanks;
use crate::ResolutionOptions;
pub struct Resolver<'a, Context: BuildContext + Send + Sync> {
@ -43,6 +44,7 @@ pub struct Resolver<'a, Context: BuildContext + Send + Sync> {
requirements: Vec<Requirement>,
constraints: Vec<Requirement>,
allowed_urls: AllowedUrls,
allowed_yanks: AllowedYanks,
markers: &'a MarkerEnvironment,
tags: &'a Tags,
client: &'a RegistryClient,
@ -79,6 +81,11 @@ impl<'a, Context: BuildContext + Send + Sync> Resolver<'a, Context> {
}
})
.collect(),
allowed_yanks: manifest
.requirements
.iter()
.chain(manifest.constraints.iter())
.collect(),
project: manifest.project,
requirements: manifest.requirements,
constraints: manifest.constraints,
@ -553,6 +560,7 @@ impl<'a, Context: BuildContext + Send + Sync> Resolver<'a, Context> {
self.tags,
self.markers,
self.build_context.interpreter(),
&self.allowed_yanks,
self.exclude_newer.as_ref(),
);
self.index

View file

@ -15,6 +15,7 @@ use pypi_types::{SimpleJson, Yanked};
use crate::file::{DistFile, SdistFile, WheelFile};
use crate::pubgrub::PubGrubVersion;
use crate::yanks::AllowedYanks;
/// A map from versions to distributions.
#[derive(Debug, Default)]
@ -28,6 +29,7 @@ impl VersionMap {
tags: &Tags,
markers: &MarkerEnvironment,
interpreter: &Interpreter,
allowed_yanks: &AllowedYanks,
exclude_newer: Option<&DateTime<Utc>>,
) -> Self {
let mut version_map: BTreeMap<PubGrubVersion, PrioritizedDistribution> =
@ -72,14 +74,16 @@ impl VersionMap {
}
}
// When resolving, exclude yanked files.
// TODO(konstin): When we fail resolving due to a dependency locked to yanked version,
// we should tell the user.
if file.yanked.as_ref().is_some_and(Yanked::is_yanked) {
continue;
}
if let Ok(filename) = WheelFilename::from_str(file.filename.as_str()) {
// When resolving, exclude yanked files.
if file.yanked.as_ref().is_some_and(Yanked::is_yanked) {
if allowed_yanks.allowed(package_name, &filename.version) {
warn!("Allowing yanked version: {}", file.filename);
} else {
continue;
}
}
let priority = filename.compatibility(tags);
match version_map.entry(filename.version.into()) {
@ -96,6 +100,15 @@ impl VersionMap {
} else if let Ok(filename) =
SourceDistFilename::parse(file.filename.as_str(), package_name)
{
// When resolving, exclude yanked files.
if file.yanked.as_ref().is_some_and(Yanked::is_yanked) {
if allowed_yanks.allowed(package_name, &filename.version) {
warn!("Allowing yanked version: {}", file.filename);
} else {
continue;
}
}
match version_map.entry(filename.version.into()) {
Entry::Occupied(mut entry) => {
entry.get_mut().insert_source(SdistFile(file));

View file

@ -0,0 +1,47 @@
use fxhash::{FxHashMap, FxHashSet};
use pep440_rs::Version;
use pep508_rs::Requirement;
use puffin_normalize::PackageName;
/// 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)
.map(|allowed_yanks| allowed_yanks.contains(version))
.unwrap_or_default()
}
}
impl<'a> FromIterator<&'a Requirement> for AllowedYanks {
fn from_iter<T: IntoIterator<Item = &'a Requirement>>(iter: T) -> Self {
let mut allowed_yanks = FxHashMap::<PackageName, FxHashSet<Version>>::default();
for requirement in iter {
let Some(pep508_rs::VersionOrUrl::VersionSpecifier(specifiers)) =
&requirement.version_or_url
else {
continue;
};
let [specifier] = specifiers.as_ref() else {
continue;
};
if matches!(
specifier.operator(),
pep440_rs::Operator::Equal | pep440_rs::Operator::ExactEqual
) {
allowed_yanks
.entry(requirement.name.clone())
.or_default()
.insert(specifier.version().clone());
}
}
Self(allowed_yanks)
}
}