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

@ -1552,3 +1552,57 @@ fn compile_wheel_path_dependency_missing() -> Result<()> {
Ok(())
}
/// Resolve a yanked version of `attrs` by specifying the version directly.
#[test]
fn compile_yanked_version_direct() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_dir = TempDir::new()?;
let venv = create_venv_py312(&temp_dir, &cache_dir);
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("attrs==21.1.0")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-compile")
.arg("requirements.in")
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir));
});
Ok(())
}
/// Fail to resolve `attrs` due to the indirect use of a yanked version (`21.1.0`).
#[test]
fn compile_yanked_version_indirect() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_dir = TempDir::new()?;
let venv = create_venv_py312(&temp_dir, &cache_dir);
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("attrs>20.3.0,<21.2.0")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-compile")
.arg("requirements.in")
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir));
});
Ok(())
}

View file

@ -0,0 +1,24 @@
---
source: crates/puffin-cli/tests/pip_compile.rs
info:
program: puffin
args:
- pip-compile
- requirements.in
- "--cache-dir"
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmplcbD5Q
- "--exclude-newer"
- "2023-11-18T12:00:00Z"
env:
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpRGHD2t/.venv
---
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by Puffin v0.0.1 via the following command:
# puffin pip-compile requirements.in --cache-dir [CACHE_DIR]
attrs==21.1.0
----- stderr -----
Resolved 1 package in [TIME]

View file

@ -0,0 +1,23 @@
---
source: crates/puffin-cli/tests/pip_compile.rs
info:
program: puffin
args:
- pip-compile
- requirements.in
- "--cache-dir"
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpKpwYoD
- "--exclude-newer"
- "2023-11-18T12:00:00Z"
env:
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmptdJhvi/.venv
---
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because there is no version of attrs available matching >20.3.0, <21.2.0
and root depends on attrs>20.3.0, <21.2.0, version solving failed.

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)
}
}