Track yanked versions as incompatibilities (#1290)

Moves yanked version filtering from `VersionMap::from_metadata` to the
resolver and tracks it as a PubGrub unavailable incompatibility so
yanked versions are reflected in error messages.

e.g. before
```
╰─▶ Because only albatross<=0.1.0 is available and you require albatross>0.1.0, 
       we can conclude that the requirements are unsatisfiable.
```

after

```
╰─▶ Because only the following versions of albatross are available:
            albatross<=0.1.0
            albatross==1.0.0
      and albatross==1.0.0 is unusable because it was yanked, we can conclude that albatross>0.1.0 cannot be used.
      And because you require albatross>0.1.0, we can conclude that the requirements are unsatisfiable.
```
This commit is contained in:
Zanie Blue 2024-02-12 22:01:17 -06:00 committed by GitHub
parent d8619f668a
commit b5dd8b7de2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 263 additions and 138 deletions

34
Cargo.lock generated
View file

@ -633,9 +633,9 @@ dependencies = [
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.3.2" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -1263,7 +1263,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http",
"indexmap 2.2.2", "indexmap 2.2.3",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -1485,9 +1485,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.2.2" version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.3", "hashbrown 0.14.3",
@ -2050,9 +2050,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-src" name = "openssl-src"
version = "300.2.2+3.2.1" version = "300.2.3+3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bbfad0063610ac26ee79f7484739e2b07555a75c42453b89263830b5c8103bc" checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843"
dependencies = [ dependencies = [
"cc", "cc",
] ]
@ -2218,7 +2218,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [ dependencies = [
"fixedbitset", "fixedbitset",
"indexmap 2.2.2", "indexmap 2.2.3",
] ]
[[package]] [[package]]
@ -2313,7 +2313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"indexmap 2.2.2", "indexmap 2.2.3",
"line-wrap", "line-wrap",
"quick-xml", "quick-xml",
"serde", "serde",
@ -2434,7 +2434,7 @@ name = "pubgrub"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/zanieb/pubgrub?rev=1b150cdbd1e6f93b1f465de9d08f499660d7f708#1b150cdbd1e6f93b1f465de9d08f499660d7f708" source = "git+https://github.com/zanieb/pubgrub?rev=1b150cdbd1e6f93b1f465de9d08f499660d7f708#1b150cdbd1e6f93b1f465de9d08f499660d7f708"
dependencies = [ dependencies = [
"indexmap 2.2.2", "indexmap 2.2.3",
"log", "log",
"priority-queue", "priority-queue",
"rustc-hash", "rustc-hash",
@ -2859,7 +2859,7 @@ dependencies = [
"fs-err", "fs-err",
"futures", "futures",
"gourgeist", "gourgeist",
"indexmap 2.2.2", "indexmap 2.2.3",
"insta", "insta",
"install-wheel-rs", "install-wheel-rs",
"itertools 0.12.1", "itertools 0.12.1",
@ -3020,7 +3020,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46d4a5e69187f23a29f8aa0ea57491d104ba541bc55f76552c2a74962aa20e04" checksum = "46d4a5e69187f23a29f8aa0ea57491d104ba541bc55f76552c2a74962aa20e04"
dependencies = [ dependencies = [
"indexmap 2.2.2", "indexmap 2.2.3",
"pep440_rs 0.3.12", "pep440_rs 0.3.12",
"pep508_rs", "pep508_rs",
"serde", "serde",
@ -4007,18 +4007,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.56" version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.56" version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4250,7 +4250,7 @@ version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951" checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951"
dependencies = [ dependencies = [
"indexmap 2.2.2", "indexmap 2.2.3",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",

View file

@ -1,6 +1,6 @@
use pep440_rs::VersionSpecifiers; use pep440_rs::VersionSpecifiers;
use platform_tags::TagPriority; use platform_tags::TagPriority;
use pypi_types::Hashes; use pypi_types::{Hashes, Yanked};
use crate::Dist; use crate::Dist;
@ -24,6 +24,8 @@ struct PrioritizedDistributionInner {
compatible_wheel: Option<(DistRequiresPython, TagPriority)>, compatible_wheel: Option<(DistRequiresPython, TagPriority)>,
/// An arbitrary, platform-incompatible wheel for the package version. /// An arbitrary, platform-incompatible wheel for the package version.
incompatible_wheel: Option<DistRequiresPython>, incompatible_wheel: Option<DistRequiresPython>,
/// Is the distribution yanked
yanked: Yanked,
/// The hashes for each distribution. /// The hashes for each distribution.
hashes: Vec<Hashes>, hashes: Vec<Hashes>,
} }
@ -35,6 +37,7 @@ impl PrioritizedDistribution {
requires_python: Option<VersionSpecifiers>, requires_python: Option<VersionSpecifiers>,
hash: Option<Hashes>, hash: Option<Hashes>,
priority: Option<TagPriority>, priority: Option<TagPriority>,
yanked: Yanked,
) -> Self { ) -> Self {
if let Some(priority) = priority { if let Some(priority) = priority {
Self(Box::new(PrioritizedDistributionInner { Self(Box::new(PrioritizedDistributionInner {
@ -42,13 +45,13 @@ impl PrioritizedDistribution {
compatible_wheel: Some(( compatible_wheel: Some((
DistRequiresPython { DistRequiresPython {
dist, dist,
requires_python, requires_python,
}, },
priority, priority,
)), )),
incompatible_wheel: None, incompatible_wheel: None,
hashes: hash.map(|hash| vec![hash]).unwrap_or_default(), hashes: hash.map(|hash| vec![hash]).unwrap_or_default(),
yanked,
})) }))
} else { } else {
Self(Box::new(PrioritizedDistributionInner { Self(Box::new(PrioritizedDistributionInner {
@ -59,6 +62,7 @@ impl PrioritizedDistribution {
requires_python, requires_python,
}), }),
hashes: hash.map(|hash| vec![hash]).unwrap_or_default(), hashes: hash.map(|hash| vec![hash]).unwrap_or_default(),
yanked,
})) }))
} }
} }
@ -68,6 +72,7 @@ impl PrioritizedDistribution {
dist: Dist, dist: Dist,
requires_python: Option<VersionSpecifiers>, requires_python: Option<VersionSpecifiers>,
hash: Option<Hashes>, hash: Option<Hashes>,
yanked: Yanked,
) -> Self { ) -> Self {
Self(Box::new(PrioritizedDistributionInner { Self(Box::new(PrioritizedDistributionInner {
source: Some(DistRequiresPython { source: Some(DistRequiresPython {
@ -77,6 +82,7 @@ impl PrioritizedDistribution {
compatible_wheel: None, compatible_wheel: None,
incompatible_wheel: None, incompatible_wheel: None,
hashes: hash.map(|hash| vec![hash]).unwrap_or_default(), hashes: hash.map(|hash| vec![hash]).unwrap_or_default(),
yanked,
})) }))
} }
@ -87,7 +93,12 @@ impl PrioritizedDistribution {
requires_python: Option<VersionSpecifiers>, requires_python: Option<VersionSpecifiers>,
hash: Option<Hashes>, hash: Option<Hashes>,
priority: Option<TagPriority>, priority: Option<TagPriority>,
yanked: Yanked,
) { ) {
if yanked.is_yanked() {
self.0.yanked = yanked;
}
// Prefer the highest-priority, platform-compatible wheel. // Prefer the highest-priority, platform-compatible wheel.
if let Some(priority) = priority { if let Some(priority) = priority {
if let Some((.., existing_priority)) = &self.0.compatible_wheel { if let Some((.., existing_priority)) = &self.0.compatible_wheel {
@ -127,7 +138,12 @@ impl PrioritizedDistribution {
dist: Dist, dist: Dist,
requires_python: Option<VersionSpecifiers>, requires_python: Option<VersionSpecifiers>,
hash: Option<Hashes>, hash: Option<Hashes>,
yanked: Yanked,
) { ) {
if yanked.is_yanked() {
self.0.yanked = yanked;
}
if self.0.source.is_none() { if self.0.source.is_none() {
self.0.source = Some(DistRequiresPython { self.0.source = Some(DistRequiresPython {
dist, dist,
@ -148,18 +164,24 @@ impl PrioritizedDistribution {
&self.0.incompatible_wheel, &self.0.incompatible_wheel,
) { ) {
// Prefer the highest-priority, platform-compatible wheel. // Prefer the highest-priority, platform-compatible wheel.
(Some((wheel, tag_priority)), _, _) => { (Some((wheel, tag_priority)), _, _) => Some(ResolvableDist::CompatibleWheel(
Some(ResolvableDist::CompatibleWheel(wheel, *tag_priority)) wheel,
} &self.0.yanked,
*tag_priority,
)),
// If we have a compatible source distribution and an incompatible wheel, return the // If we have a compatible source distribution and an incompatible wheel, return the
// wheel. We assume that all distributions have the same metadata for a given package // wheel. We assume that all distributions have the same metadata for a given package
// version. If a compatible source distribution exists, we assume we can build it, but // version. If a compatible source distribution exists, we assume we can build it, but
// using the wheel is faster. // using the wheel is faster.
(_, Some(source_dist), Some(wheel)) => { (_, Some(source_dist), Some(wheel)) => Some(ResolvableDist::IncompatibleWheel {
Some(ResolvableDist::IncompatibleWheel { source_dist, wheel }) source_dist,
} yanked: &self.0.yanked,
wheel,
}),
// Otherwise, if we have a source distribution, return it. // Otherwise, if we have a source distribution, return it.
(_, Some(source_dist), _) => Some(ResolvableDist::SourceDist(source_dist)), (_, Some(source_dist), _) => {
Some(ResolvableDist::SourceDist(source_dist, &self.0.yanked))
}
_ => None, _ => None,
} }
} }
@ -191,39 +213,54 @@ impl PrioritizedDistribution {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ResolvableDist<'a> { pub enum ResolvableDist<'a> {
/// The distribution should be resolved and installed using a source distribution. /// The distribution should be resolved and installed using a source distribution.
SourceDist(&'a DistRequiresPython), SourceDist(&'a DistRequiresPython, &'a Yanked),
/// The distribution should be resolved and installed using a wheel distribution. /// The distribution should be resolved and installed using a wheel distribution.
CompatibleWheel(&'a DistRequiresPython, TagPriority), CompatibleWheel(&'a DistRequiresPython, &'a Yanked, TagPriority),
/// The distribution should be resolved using an incompatible wheel distribution, but /// The distribution should be resolved using an incompatible wheel distribution, but
/// installed using a source distribution. /// installed using a source distribution.
IncompatibleWheel { IncompatibleWheel {
source_dist: &'a DistRequiresPython, source_dist: &'a DistRequiresPython,
yanked: &'a Yanked,
wheel: &'a DistRequiresPython, wheel: &'a DistRequiresPython,
}, },
} }
impl<'a> ResolvableDist<'a> { impl<'a> ResolvableDist<'a> {
/// Return the [`DistRequiresPython`] to use during resolution. /// Return the [`DistRequiresPython`] to use during resolution.
pub fn resolve(&self) -> &DistRequiresPython { pub fn for_resolution(&self) -> &DistRequiresPython {
match *self { match *self {
ResolvableDist::SourceDist(sdist) => sdist, ResolvableDist::SourceDist(sdist, _) => sdist,
ResolvableDist::CompatibleWheel(wheel, _) => wheel, ResolvableDist::CompatibleWheel(wheel, _, _) => wheel,
ResolvableDist::IncompatibleWheel { ResolvableDist::IncompatibleWheel {
source_dist: _, source_dist: _,
yanked: _,
wheel, wheel,
} => wheel, } => wheel,
} }
} }
/// Return the [`DistRequiresPython`] to use during installation. /// Return the [`DistRequiresPython`] to use during installation.
pub fn install(&self) -> &DistRequiresPython { pub fn for_installation(&self) -> &DistRequiresPython {
match *self { match *self {
ResolvableDist::SourceDist(sdist) => sdist, ResolvableDist::SourceDist(sdist, _) => sdist,
ResolvableDist::CompatibleWheel(wheel, _) => wheel, ResolvableDist::CompatibleWheel(wheel, _, _) => wheel,
ResolvableDist::IncompatibleWheel { ResolvableDist::IncompatibleWheel {
source_dist, source_dist,
yanked: _,
wheel: _, wheel: _,
} => source_dist, } => source_dist,
} }
} }
pub fn yanked(&self) -> &Yanked {
match *self {
ResolvableDist::SourceDist(_, yanked) => yanked,
ResolvableDist::CompatibleWheel(_, yanked, _) => yanked,
ResolvableDist::IncompatibleWheel {
source_dist: _,
yanked,
wheel: _,
} => yanked,
}
}
} }

View file

@ -17,7 +17,7 @@ use pep440_rs::Version;
use platform_tags::Tags; use platform_tags::Tags;
use puffin_cache::{Cache, CacheBucket}; use puffin_cache::{Cache, CacheBucket};
use puffin_normalize::PackageName; use puffin_normalize::PackageName;
use pypi_types::Hashes; use pypi_types::{Hashes, Yanked};
use crate::cached_client::{CacheControl, CachedClientError}; use crate::cached_client::{CacheControl, CachedClientError};
use crate::html::SimpleHtml; use crate::html::SimpleHtml;
@ -298,11 +298,17 @@ impl FlatIndex {
})); }));
match distributions.0.entry(version) { match distributions.0.entry(version) {
Entry::Occupied(mut entry) => { Entry::Occupied(mut entry) => {
entry.get_mut().insert_built(dist, None, None, priority); entry
.get_mut()
.insert_built(dist, None, None, priority, Yanked::default());
} }
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
entry.insert(PrioritizedDistribution::from_built( entry.insert(PrioritizedDistribution::from_built(
dist, None, None, priority, dist,
None,
None,
priority,
Yanked::default(),
)); ));
} }
} }
@ -315,10 +321,17 @@ impl FlatIndex {
})); }));
match distributions.0.entry(filename.version.clone()) { match distributions.0.entry(filename.version.clone()) {
Entry::Occupied(mut entry) => { Entry::Occupied(mut entry) => {
entry.get_mut().insert_source(dist, None, None); entry
.get_mut()
.insert_source(dist, None, None, Yanked::default());
} }
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
entry.insert(PrioritizedDistribution::from_source(dist, None, None)); entry.insert(PrioritizedDistribution::from_source(
dist,
None,
None,
Yanked::default(),
));
} }
} }
} }

View file

@ -1,4 +1,5 @@
use pubgrub::range::Range; use pubgrub::range::Range;
use pypi_types::Yanked;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use distribution_types::{Dist, DistributionMetadata, Name}; use distribution_types::{Dist, DistributionMetadata, Name};
@ -247,19 +248,22 @@ impl<'a> Candidate<'a> {
} }
/// Return the [`DistFile`] to use when resolving the package. /// Return the [`DistFile`] to use when resolving the package.
pub(crate) fn resolve(&self) -> &DistRequiresPython { pub(crate) fn resolution_dist(&self) -> &DistRequiresPython {
self.dist.resolve() self.dist.for_resolution()
} }
/// Return the [`DistFile`] to use when installing the package. /// Return the [`DistFile`] to use when installing the package.
pub(crate) fn install(&self) -> &DistRequiresPython { pub(crate) fn installation_dist(&self) -> &DistRequiresPython {
self.dist.install() self.dist.for_installation()
} }
/// If the candidate doesn't match the given requirement, return the version specifiers. /// If the candidate doesn't match the given Python requirement, return the version specifiers.
pub(crate) fn validate(&self, requirement: &PythonRequirement) -> Option<&VersionSpecifiers> { pub(crate) fn validate_python(
&self,
requirement: &PythonRequirement,
) -> Option<&VersionSpecifiers> {
// Validate the _installed_ file. // Validate the _installed_ file.
let requires_python = self.install().requires_python.as_ref()?; let requires_python = self.installation_dist().requires_python.as_ref()?;
// If the candidate doesn't support the target Python version, return the failing version // If the candidate doesn't support the target Python version, return the failing version
// specifiers. // specifiers.
@ -269,20 +273,20 @@ impl<'a> Candidate<'a> {
// If the candidate is a source distribution, and doesn't support the installed Python // If the candidate is a source distribution, and doesn't support the installed Python
// version, return the failing version specifiers, since we won't be able to build it. // version, return the failing version specifiers, since we won't be able to build it.
if matches!(self.install().dist, Dist::Source(_)) { if matches!(self.installation_dist().dist, Dist::Source(_)) {
if !requires_python.contains(requirement.installed()) { if !requires_python.contains(requirement.installed()) {
return Some(requires_python); return Some(requires_python);
} }
} }
// Validate the resolved file. // Validate the resolved file.
let requires_python = self.resolve().requires_python.as_ref()?; let requires_python = self.resolution_dist().requires_python.as_ref()?;
// If the candidate is a source distribution, and doesn't support the installed Python // If the candidate is a source distribution, and doesn't support the installed Python
// version, return the failing version specifiers, since we won't be able to build it. // version, return the failing version specifiers, since we won't be able to build it.
// This isn't strictly necessary, since if `self.resolve()` is a source distribution, it // This isn't strictly necessary, since if `self.resolve()` is a source distribution, it
// should be the same file as `self.install()` (validated above). // should be the same file as `self.install()` (validated above).
if matches!(self.resolve().dist, Dist::Source(_)) { if matches!(self.resolution_dist().dist, Dist::Source(_)) {
if !requires_python.contains(requirement.installed()) { if !requires_python.contains(requirement.installed()) {
return Some(requires_python); return Some(requires_python);
} }
@ -290,6 +294,10 @@ impl<'a> Candidate<'a> {
None None
} }
pub(crate) fn yanked(&self) -> &Yanked {
return self.dist.yanked();
}
} }
impl Name for Candidate<'_> { impl Name for Candidate<'_> {

View file

@ -17,7 +17,7 @@ impl FilePins {
pub(crate) fn insert(&mut self, candidate: &Candidate) { pub(crate) fn insert(&mut self, candidate: &Candidate) {
self.0.entry(candidate.name().clone()).or_default().insert( self.0.entry(candidate.name().clone()).or_default().insert(
candidate.version().clone(), candidate.version().clone(),
candidate.install().dist.clone(), candidate.installation_dist().dist.clone(),
); );
} }

View file

@ -15,13 +15,13 @@ use rustc_hash::{FxHashMap, FxHashSet};
use tokio::select; use tokio::select;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tracing::{debug, info_span, instrument, trace, Instrument}; use tracing::{debug, info_span, instrument, trace, warn, Instrument};
use url::Url; use url::Url;
use distribution_filename::WheelFilename; use distribution_filename::WheelFilename;
use distribution_types::{ use distribution_types::{
BuiltDist, Dist, DistributionMetadata, LocalEditable, Name, PackageId, RemoteSource, BuiltDist, Dist, DistributionMetadata, LocalEditable, Name, RemoteSource, SourceDist,
SourceDist, VersionOrUrl, VersionOrUrl,
}; };
use pep440_rs::{Version, VersionSpecifiers, MIN_VERSION}; use pep440_rs::{Version, VersionSpecifiers, MIN_VERSION};
use pep508_rs::{MarkerEnvironment, Requirement}; use pep508_rs::{MarkerEnvironment, Requirement};
@ -31,7 +31,7 @@ use puffin_distribution::DistributionDatabase;
use puffin_interpreter::Interpreter; use puffin_interpreter::Interpreter;
use puffin_normalize::PackageName; use puffin_normalize::PackageName;
use puffin_traits::BuildContext; use puffin_traits::BuildContext;
use pypi_types::Metadata21; use pypi_types::{Metadata21, Yanked};
use crate::candidate_selector::CandidateSelector; use crate::candidate_selector::CandidateSelector;
use crate::error::ResolveError; use crate::error::ResolveError;
@ -51,6 +51,7 @@ pub use crate::resolver::provider::ResolverProvider;
pub(crate) use crate::resolver::provider::VersionsResponse; pub(crate) use crate::resolver::provider::VersionsResponse;
use crate::resolver::reporter::Facade; use crate::resolver::reporter::Facade;
pub use crate::resolver::reporter::{BuildId, Reporter}; pub use crate::resolver::reporter::{BuildId, Reporter};
use crate::yanks::AllowedYanks;
use crate::{DependencyMode, Options}; use crate::{DependencyMode, Options};
mod allowed_urls; mod allowed_urls;
@ -64,6 +65,8 @@ mod reporter;
pub(crate) enum UnavailableVersion { pub(crate) enum UnavailableVersion {
/// Version is incompatible due to the `Requires-Python` version specifiers for that package. /// Version is incompatible due to the `Requires-Python` version specifiers for that package.
RequiresPython(VersionSpecifiers), RequiresPython(VersionSpecifiers),
/// Version is incompatible because it is yanked
Yanked(Yanked),
} }
/// The package is unavailable and cannot be used /// The package is unavailable and cannot be used
@ -77,19 +80,25 @@ pub(crate) enum UnavailablePackage {
NotFound, NotFound,
} }
enum ResolverVersion {
/// A usable version
Available(Version),
/// A version that is not usable for some reaosn
Unavailable(Version, UnavailableVersion),
}
pub struct Resolver<'a, Provider: ResolverProvider> { pub struct Resolver<'a, Provider: ResolverProvider> {
project: Option<PackageName>, project: Option<PackageName>,
requirements: Vec<Requirement>, requirements: Vec<Requirement>,
constraints: Vec<Requirement>, constraints: Vec<Requirement>,
overrides: Overrides, overrides: Overrides,
allowed_yanks: AllowedYanks,
allowed_urls: AllowedUrls, allowed_urls: AllowedUrls,
dependency_mode: DependencyMode, dependency_mode: DependencyMode,
markers: &'a MarkerEnvironment, markers: &'a MarkerEnvironment,
python_requirement: PythonRequirement, python_requirement: PythonRequirement,
selector: CandidateSelector, selector: CandidateSelector,
index: &'a InMemoryIndex, index: &'a InMemoryIndex,
/// Incompatibilities for specific package versions
unavailable_versions: DashMap<PackageId, UnavailableVersion>,
/// Incompatibilities for packages that are entirely unavailable /// Incompatibilities for packages that are entirely unavailable
unavailable_packages: DashMap<PackageName, UnavailablePackage>, unavailable_packages: DashMap<PackageName, UnavailablePackage>,
/// The set of all registry-based packages visited during resolution. /// The set of all registry-based packages visited during resolution.
@ -122,11 +131,6 @@ impl<'a, Context: BuildContext + Send + Sync> Resolver<'a, DefaultResolverProvid
tags, tags,
PythonRequirement::new(interpreter, markers), PythonRequirement::new(interpreter, markers),
options.exclude_newer, options.exclude_newer,
manifest
.requirements
.iter()
.chain(manifest.constraints.iter())
.collect(),
build_context.no_binary(), build_context.no_binary(),
); );
Self::new_custom_io( Self::new_custom_io(
@ -190,13 +194,20 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
) )
.collect(); .collect();
// Determine the allowed yanked package versions
let allowed_yanks = manifest
.requirements
.iter()
.chain(manifest.constraints.iter())
.collect();
Self { Self {
index, index,
unavailable_versions: DashMap::default(),
unavailable_packages: DashMap::default(), unavailable_packages: DashMap::default(),
visited: DashSet::default(), visited: DashSet::default(),
selector, selector,
allowed_urls, allowed_urls,
allowed_yanks,
dependency_mode: options.dependency_mode, dependency_mode: options.dependency_mode,
project: manifest.project, project: manifest.project,
requirements: manifest.requirements, requirements: manifest.requirements,
@ -369,6 +380,48 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
} }
Some(version) => version, Some(version) => version,
}; };
let version = match version {
ResolverVersion::Available(version) => version,
ResolverVersion::Unavailable(version, unavailable) => {
let reason = match unavailable {
UnavailableVersion::RequiresPython(requires_python) => {
// Incompatible requires-python versions are special in that we track
// them as incompatible dependencies instead of marking the package version
// as unavailable directly
let python_version = requires_python
.iter()
.map(PubGrubSpecifier::try_from)
.fold_ok(Range::full(), |range, specifier| {
range.intersection(&specifier.into())
})?;
let package = &next;
for kind in [PubGrubPython::Installed, PubGrubPython::Target] {
state.add_incompatibility(Incompatibility::from_dependency(
package.clone(),
Range::singleton(version.clone()),
(&PubGrubPackage::Python(kind), &python_version),
));
}
state.partial_solution.add_decision(next.clone(), version);
continue;
}
UnavailableVersion::Yanked(yanked) => match yanked {
Yanked::Bool(_) => "it was yanked".to_string(),
Yanked::Reason(reason) => format!(
"it was yanked (reason: {})",
reason.trim().trim_end_matches('.')
),
},
};
state.add_incompatibility(Incompatibility::unavailable(
next.clone(),
version.clone(),
reason,
));
continue;
}
};
self.on_progress(&next, &version); self.on_progress(&next, &version);
@ -483,6 +536,8 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
/// Given a set of candidate packages, choose the next package (and version) to add to the /// Given a set of candidate packages, choose the next package (and version) to add to the
/// partial solution. /// partial solution.
///
/// Returns [None] when there are no versions in the given range.
#[instrument(skip_all, fields(%package))] #[instrument(skip_all, fields(%package))]
async fn choose_version( async fn choose_version(
&self, &self,
@ -490,14 +545,14 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
range: &Range<Version>, range: &Range<Version>,
pins: &mut FilePins, pins: &mut FilePins,
request_sink: &tokio::sync::mpsc::Sender<Request>, request_sink: &tokio::sync::mpsc::Sender<Request>,
) -> Result<Option<Version>, ResolveError> { ) -> Result<Option<ResolverVersion>, ResolveError> {
match package { match package {
PubGrubPackage::Root(_) => Ok(Some(MIN_VERSION.clone())), PubGrubPackage::Root(_) => Ok(Some(ResolverVersion::Available(MIN_VERSION.clone()))),
PubGrubPackage::Python(PubGrubPython::Installed) => { PubGrubPackage::Python(PubGrubPython::Installed) => {
let version = self.python_requirement.installed(); let version = self.python_requirement.installed();
if range.contains(version) { if range.contains(version) {
Ok(Some(version.clone())) Ok(Some(ResolverVersion::Available(version.clone())))
} else { } else {
Ok(None) Ok(None)
} }
@ -506,7 +561,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
PubGrubPackage::Python(PubGrubPython::Target) => { PubGrubPackage::Python(PubGrubPython::Target) => {
let version = self.python_requirement.target(); let version = self.python_requirement.target();
if range.contains(version) { if range.contains(version) {
Ok(Some(version.clone())) Ok(Some(ResolverVersion::Available(version.clone())))
} else { } else {
Ok(None) Ok(None)
} }
@ -535,7 +590,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
// If the URL is that of a wheel, extract the version. // If the URL is that of a wheel, extract the version.
let version = wheel_filename.version; let version = wheel_filename.version;
if range.contains(&version) { if range.contains(&version) {
Ok(Some(version)) Ok(Some(ResolverVersion::Available(version)))
} else { } else {
Ok(None) Ok(None)
} }
@ -550,7 +605,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
.ok_or(ResolveError::Unregistered)?; .ok_or(ResolveError::Unregistered)?;
let version = &metadata.version; let version = &metadata.version;
if range.contains(version) { if range.contains(version) {
Ok(Some(version.clone())) Ok(Some(ResolverVersion::Available(version.clone())))
} else { } else {
Ok(None) Ok(None)
} }
@ -605,13 +660,27 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
return Ok(None); return Ok(None);
}; };
// If the version is incompatible, short-circuit. // If the version is incompatible because it was yanked
if let Some(requires_python) = candidate.validate(&self.python_requirement) { if candidate.yanked().is_yanked() {
self.unavailable_versions.insert( if self
candidate.package_id(), .allowed_yanks
.allowed(package_name, candidate.version())
{
warn!("Allowing yanked version: {}", candidate.package_id());
} else {
return Ok(Some(ResolverVersion::Unavailable(
candidate.version().clone(),
UnavailableVersion::Yanked(candidate.yanked().clone()),
)));
}
}
// If the version is incompatible because of its Python requirement
if let Some(requires_python) = candidate.validate_python(&self.python_requirement) {
return Ok(Some(ResolverVersion::Unavailable(
candidate.version().clone(),
UnavailableVersion::RequiresPython(requires_python.clone()), UnavailableVersion::RequiresPython(requires_python.clone()),
); )));
return Ok(Some(candidate.version().clone()));
} }
if let Some(extra) = extra { if let Some(extra) = extra {
@ -621,7 +690,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
extra, extra,
candidate.version(), candidate.version(),
candidate candidate
.resolve() .resolution_dist()
.dist .dist
.filename() .filename()
.unwrap_or("unknown filename") .unwrap_or("unknown filename")
@ -632,7 +701,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
candidate.name(), candidate.name(),
candidate.version(), candidate.version(),
candidate candidate
.resolve() .resolution_dist()
.dist .dist
.filename() .filename()
.unwrap_or("unknown filename") .unwrap_or("unknown filename")
@ -647,11 +716,11 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
// Emit a request to fetch the metadata for this version. // Emit a request to fetch the metadata for this version.
if self.index.distributions.register(candidate.package_id()) { if self.index.distributions.register(candidate.package_id()) {
let dist = candidate.resolve().dist.clone(); let dist = candidate.resolution_dist().dist.clone();
request_sink.send(Request::Dist(dist)).await?; request_sink.send(Request::Dist(dist)).await?;
} }
Ok(Some(version)) Ok(Some(ResolverVersion::Available(version)))
} }
} }
} }
@ -746,27 +815,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
)); ));
} }
// If the package is known to be incompatible, return the Python version as an
// incompatibility, and skip fetching the metadata.
if let Some(entry) = self.unavailable_versions.get(&package_id) {
// TODO(zanieb): Handle additional variants here
let UnavailableVersion::RequiresPython(requires_python) = entry.value();
let version = requires_python
.iter()
.map(PubGrubSpecifier::try_from)
.fold_ok(Range::full(), |range, specifier| {
range.intersection(&specifier.into())
})?;
let mut constraints = DependencyConstraints::default();
constraints.insert(
PubGrubPackage::Python(PubGrubPython::Installed),
version.clone(),
);
constraints.insert(PubGrubPackage::Python(PubGrubPython::Target), version);
return Ok(Dependencies::Available(constraints));
}
// Wait for the metadata to be available. // Wait for the metadata to be available.
let metadata = self let metadata = self
.index .index
@ -942,17 +990,16 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
}; };
// If the version is incompatible, short-circuit. // If the version is incompatible, short-circuit.
if let Some(requires_python) = candidate.validate(&self.python_requirement) { if candidate
self.unavailable_versions.insert( .validate_python(&self.python_requirement)
candidate.package_id(), .is_some()
UnavailableVersion::RequiresPython(requires_python.clone()), {
);
return Ok(None); return Ok(None);
} }
// Emit a request to fetch the metadata for this version. // Emit a request to fetch the metadata for this version.
if self.index.distributions.register(candidate.package_id()) { if self.index.distributions.register(candidate.package_id()) {
let dist = candidate.resolve().dist.clone(); let dist = candidate.resolution_dist().dist.clone();
let (metadata, precise) = self let (metadata, precise) = self
.provider .provider

View file

@ -16,7 +16,6 @@ use pypi_types::Metadata21;
use crate::python_requirement::PythonRequirement; use crate::python_requirement::PythonRequirement;
use crate::version_map::VersionMap; use crate::version_map::VersionMap;
use crate::yanks::AllowedYanks;
type PackageVersionsResult = Result<VersionsResponse, puffin_client::Error>; type PackageVersionsResult = Result<VersionsResponse, puffin_client::Error>;
type WheelMetadataResult = Result<(Metadata21, Option<Url>), puffin_distribution::Error>; type WheelMetadataResult = Result<(Metadata21, Option<Url>), puffin_distribution::Error>;
@ -75,7 +74,6 @@ pub struct DefaultResolverProviderInner {
tags: Tags, tags: Tags,
python_requirement: PythonRequirement, python_requirement: PythonRequirement,
exclude_newer: Option<DateTime<Utc>>, exclude_newer: Option<DateTime<Utc>>,
allowed_yanks: AllowedYanks,
no_binary: NoBinary, no_binary: NoBinary,
} }
@ -97,7 +95,6 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex
tags: &'a Tags, tags: &'a Tags,
python_requirement: PythonRequirement, python_requirement: PythonRequirement,
exclude_newer: Option<DateTime<Utc>>, exclude_newer: Option<DateTime<Utc>>,
allowed_yanks: AllowedYanks,
no_binary: &'a NoBinary, no_binary: &'a NoBinary,
) -> Self { ) -> Self {
Self { Self {
@ -108,7 +105,6 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex
tags: tags.clone(), tags: tags.clone(),
python_requirement, python_requirement,
exclude_newer, exclude_newer,
allowed_yanks,
no_binary: no_binary.clone(), no_binary: no_binary.clone(),
}), }),
} }
@ -138,7 +134,6 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider
&index, &index,
&self_send.tags, &self_send.tags,
&self_send.python_requirement, &self_send.python_requirement,
&self_send.allowed_yanks,
self_send.exclude_newer.as_ref(), self_send.exclude_newer.as_ref(),
self_send.flat_index.get(&package_name_owned).cloned(), self_send.flat_index.get(&package_name_owned).cloned(),
&self_send.no_binary, &self_send.no_binary,

View file

@ -14,7 +14,6 @@ use puffin_warnings::warn_user_once;
use pypi_types::{Hashes, Yanked}; use pypi_types::{Hashes, Yanked};
use crate::python_requirement::PythonRequirement; use crate::python_requirement::PythonRequirement;
use crate::yanks::AllowedYanks;
/// A map from versions to distributions. /// A map from versions to distributions.
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -30,7 +29,6 @@ impl VersionMap {
index: &IndexUrl, index: &IndexUrl,
tags: &Tags, tags: &Tags,
python_requirement: &PythonRequirement, python_requirement: &PythonRequirement,
allowed_yanks: &AllowedYanks,
exclude_newer: Option<&DateTime<Utc>>, exclude_newer: Option<&DateTime<Utc>>,
mut flat_index: Option<FlatDistributions>, mut flat_index: Option<FlatDistributions>,
no_binary: &NoBinary, no_binary: &NoBinary,
@ -80,14 +78,15 @@ impl VersionMap {
} }
} }
// When resolving, exclude yanked files. // It is possible for files to have a different yank status per PEP 592 but in the official
if file.yanked.as_ref().is_some_and(Yanked::is_yanked) { // PyPI warehouse this cannot happen.
if allowed_yanks.allowed(package_name, &version) { // If any file is yanked, the version will be marked as yanked.
warn!("Allowing yanked version: {}", file.filename); // <https://peps.python.org/pep-0592/#warehouse-pypi-implementation-notes>
} else { let yanked = if let Some(ref yanked) = file.yanked {
continue; yanked.clone()
} } else {
} Yanked::default()
};
// Prioritize amongst all available files. // Prioritize amongst all available files.
let requires_python = file.requires_python.clone(); let requires_python = file.requires_python.clone();
@ -113,7 +112,13 @@ impl VersionMap {
file, file,
index.clone(), index.clone(),
); );
priority_dist.insert_built(dist, requires_python, Some(hash), priority); priority_dist.insert_built(
dist,
requires_python,
Some(hash),
priority,
yanked,
);
} }
DistFilename::SourceDistFilename(filename) => { DistFilename::SourceDistFilename(filename) => {
let dist = Dist::from_registry( let dist = Dist::from_registry(
@ -121,7 +126,7 @@ impl VersionMap {
file, file,
index.clone(), index.clone(),
); );
priority_dist.insert_source(dist, requires_python, Some(hash)); priority_dist.insert_source(dist, requires_python, Some(hash), yanked);
} }
} }
} }

View file

@ -7,7 +7,7 @@ use puffin_normalize::PackageName;
/// A set of package versions that are permitted, even if they're marked as yanked by the /// A set of package versions that are permitted, even if they're marked as yanked by the
/// relevant index. /// relevant index.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct AllowedYanks(FxHashMap<PackageName, FxHashSet<Version>>); pub(crate) struct AllowedYanks(FxHashMap<PackageName, FxHashSet<Version>>);
impl AllowedYanks { impl AllowedYanks {
/// Returns `true` if the given package version is allowed, even if it's marked as yanked by /// Returns `true` if the given package version is allowed, even if it's marked as yanked by

View file

@ -1719,16 +1719,16 @@ fn compile_yanked_version_direct() -> Result<()> {
puffin_snapshot!(context.compile() puffin_snapshot!(context.compile()
.arg("requirements.in"), @r###" .arg("requirements.in"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
# This file was autogenerated by Puffin v[VERSION] via the following command: # This file was autogenerated by Puffin v[VERSION] via the following command:
# puffin pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in # puffin pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in
attrs==21.1.0 attrs==21.1.0
----- stderr ----- ----- stderr -----
Resolved 1 package in [TIME] Resolved 1 package in [TIME]
"### "###
); );
Ok(()) Ok(())
@ -1751,8 +1751,12 @@ fn compile_yanked_version_indirect() -> Result<()> {
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because only the following versions of attrs are available: Because only the following versions of attrs are available:
attrs<=20.3.0 attrs<=20.3.0
attrs==21.1.0
attrs>=21.2.0 attrs>=21.2.0
and you require attrs>20.3.0,<21.2.0, we can conclude that the and attrs==21.1.0 is unusable because it was yanked (reason:
Installable but not importable on Python 3.4), we can conclude that
attrs>20.3.0,<21.2.0 cannot be used.
And because you require attrs>20.3.0,<21.2.0, we can conclude that the
requirements are unsatisfiable. requirements are unsatisfiable.
"### "###
); );

View file

@ -2694,7 +2694,8 @@ fn package_only_yanked() {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because there are no versions of albatross and you require albatross, we can conclude that the requirements are unsatisfiable. Because only albatross==1.0.0 is available and albatross==1.0.0 is unusable because it was yanked, we can conclude that all versions of albatross cannot be used.
And because you require albatross, we can conclude that the requirements are unsatisfiable.
"###); "###);
// Yanked versions should not be installed, even if they are the only one // Yanked versions should not be installed, even if they are the only one
@ -2735,7 +2736,11 @@ fn package_only_yanked_in_range() {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because only albatross<=0.1.0 is available and you require albatross>0.1.0, we can conclude that the requirements are unsatisfiable. Because only the following versions of albatross are available:
albatross<=0.1.0
albatross==1.0.0
and albatross==1.0.0 is unusable because it was yanked, we can conclude that albatross>0.1.0 cannot be used.
And because you require albatross>0.1.0, we can conclude that the requirements are unsatisfiable.
"###); "###);
// Since there are other versions of `a` available, yanked versions should not be // Since there are other versions of `a` available, yanked versions should not be
@ -2870,7 +2875,8 @@ fn transitive_package_only_yanked() {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because there are no versions of bluebird and albatross==0.1.0 depends on bluebird, we can conclude that albatross==0.1.0 cannot be used. Because only bluebird==1.0.0 is available and bluebird==1.0.0 is unusable because it was yanked, we can conclude that all versions of bluebird cannot be used.
And because albatross==0.1.0 depends on bluebird, we can conclude that albatross==0.1.0 cannot be used.
And because only albatross==0.1.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. And because only albatross==0.1.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable.
"###); "###);
@ -2918,7 +2924,11 @@ fn transitive_package_only_yanked_in_range() {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because only bluebird<=0.1 is available and albatross==0.1.0 depends on bluebird>0.1, we can conclude that albatross==0.1.0 cannot be used. Because only the following versions of bluebird are available:
bluebird<=0.1
bluebird==1.0.0
and bluebird==1.0.0 is unusable because it was yanked, we can conclude that bluebird>0.1 cannot be used.
And because albatross==0.1.0 depends on bluebird>0.1, we can conclude that albatross==0.1.0 cannot be used.
And because only albatross==0.1.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. And because only albatross==0.1.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable.
"###); "###);
@ -3030,7 +3040,7 @@ fn transitive_yanked_and_unyanked_dependency() {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because there is no version of crow==2.0.0 and albatross==1.0.0 depends on crow==2.0.0, we can conclude that albatross==1.0.0 cannot be used. Because crow==2.0.0 is unusable because it was yanked and albatross==1.0.0 depends on crow==2.0.0, we can conclude that albatross==1.0.0 cannot be used.
And because only albatross==1.0.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. And because only albatross==1.0.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable.
"###); "###);

View file

@ -108,6 +108,12 @@ impl Yanked {
} }
} }
impl Default for Yanked {
fn default() -> Self {
Self::Bool(false)
}
}
/// A dictionary mapping a hash name to a hex encoded digest of the file. /// A dictionary mapping a hash name to a hex encoded digest of the file.
/// ///
/// PEP 691 says multiple hashes can be included and the interpretation is left to the client, we /// PEP 691 says multiple hashes can be included and the interpretation is left to the client, we