Refactor incompatiblity tracking for distributions (#1298)

Extends the "compatibility" types introduced in #1293 to apply to source
distributions as well as wheels.

- We now track the most-relevant incompatible source distribution
- Exclude newer, Python requirements, and yanked versions are all
tracked as incompatibilities in the new model (this lets us remove
`DistMetadata`!)
This commit is contained in:
Zanie Blue 2024-03-08 11:02:31 -06:00 committed by GitHub
parent 1181aa9be4
commit 10c4effbd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 498 additions and 343 deletions

1
Cargo.lock generated
View file

@ -915,6 +915,7 @@ version = "0.0.1"
dependencies = [
"anyhow",
"cache-key",
"chrono",
"data-encoding",
"distribution-filename",
"fs-err",

View file

@ -25,6 +25,7 @@ uv-normalize = { path = "../uv-normalize" }
pypi-types = { path = "../pypi-types" }
anyhow = { workspace = true }
chrono = { workspace = true }
data-encoding = { workspace = true }
fs-err = { workspace = true }
itertools = { workspace = true }

View file

@ -11,95 +11,88 @@ pub struct PrioritizedDist(Box<PrioritizedDistInner>);
/// [`PrioritizedDist`] is boxed because [`Dist`] is large.
#[derive(Debug, Default, Clone)]
struct PrioritizedDistInner {
/// An arbitrary source distribution for the package version.
source: Option<DistMetadata>,
/// The most-compatible wheel distribution for the package version.
wheel: Option<(DistMetadata, WheelCompatibility)>,
/// The highest-priority source distribution. Between compatible source distributions this priority is arbitrary.
source: Option<(Dist, SourceDistCompatibility)>,
/// The highest-priority wheel.
wheel: Option<(Dist, WheelCompatibility)>,
/// The hashes for each distribution.
hashes: Vec<Hashes>,
/// If exclude newer filtered files from this distribution
exclude_newer: bool,
}
/// A distribution that can be used for both resolution and installation.
#[derive(Debug, Clone)]
pub enum CompatibleDist<'a> {
/// The distribution should be resolved and installed using a source distribution.
SourceDist(&'a DistMetadata),
SourceDist(&'a Dist),
/// The distribution should be resolved and installed using a wheel distribution.
CompatibleWheel(&'a DistMetadata, TagPriority),
CompatibleWheel(&'a Dist, TagPriority),
/// The distribution should be resolved using an incompatible wheel distribution, but
/// installed using a source distribution.
IncompatibleWheel {
source_dist: &'a DistMetadata,
wheel: &'a DistMetadata,
source_dist: &'a Dist,
wheel: &'a Dist,
},
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum IncompatibleDist {
/// An incompatible wheel is available.
Wheel(IncompatibleWheel),
/// An incompatible source distribution is available.
Source(IncompatibleSource),
/// No distributions are available
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WheelCompatibility {
Incompatible(IncompatibleWheel),
Compatible(TagPriority),
}
#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Clone)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum IncompatibleWheel {
ExcludeNewer(Option<i64>),
Tag(IncompatibleTag),
RequiresPython,
RequiresPython(VersionSpecifiers),
Yanked(Yanked),
NoBinary,
}
/// A [`Dist`] and metadata about it required for downstream filtering.
#[derive(Debug, Clone)]
pub struct DistMetadata {
/// The distribution.
pub dist: Dist,
/// The version of Python required by the distribution.
pub requires_python: Option<VersionSpecifiers>,
/// If the distribution file is yanked.
pub yanked: Yanked,
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceDistCompatibility {
Incompatible(IncompatibleSource),
Compatible,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum IncompatibleSource {
ExcludeNewer(Option<i64>),
RequiresPython(VersionSpecifiers),
Yanked(Yanked),
NoBuild,
}
impl PrioritizedDist {
/// Create a new [`PrioritizedDist`] from the given wheel distribution.
pub fn from_built(
dist: Dist,
requires_python: Option<VersionSpecifiers>,
yanked: Yanked,
hash: Option<Hashes>,
compatibility: WheelCompatibility,
) -> Self {
pub fn from_built(dist: Dist, hash: Option<Hashes>, compatibility: WheelCompatibility) -> Self {
Self(Box::new(PrioritizedDistInner {
wheel: Some((dist, compatibility)),
source: None,
wheel: Some((
DistMetadata {
dist,
requires_python,
yanked,
},
compatibility,
)),
hashes: hash.map(|hash| vec![hash]).unwrap_or_default(),
exclude_newer: false,
}))
}
/// Create a new [`PrioritizedDist`] from the given source distribution.
pub fn from_source(
dist: Dist,
requires_python: Option<VersionSpecifiers>,
yanked: Yanked,
hash: Option<Hashes>,
compatibility: SourceDistCompatibility,
) -> Self {
Self(Box::new(PrioritizedDistInner {
source: Some(DistMetadata {
dist,
requires_python,
yanked,
}),
wheel: None,
source: Some((dist, compatibility)),
hashes: hash.map(|hash| vec![hash]).unwrap_or_default(),
exclude_newer: false,
}))
}
@ -107,32 +100,16 @@ impl PrioritizedDist {
pub fn insert_built(
&mut self,
dist: Dist,
requires_python: Option<VersionSpecifiers>,
yanked: Yanked,
hash: Option<Hashes>,
compatibility: WheelCompatibility,
) {
// Track the highest-priority wheel.
if let Some((.., existing_compatibility)) = &self.0.wheel {
if compatibility > *existing_compatibility {
self.0.wheel = Some((
DistMetadata {
dist,
requires_python,
yanked,
},
compatibility,
));
if compatibility.is_more_compatible(existing_compatibility) {
self.0.wheel = Some((dist, compatibility));
}
} else {
self.0.wheel = Some((
DistMetadata {
dist,
requires_python,
yanked,
},
compatibility,
));
self.0.wheel = Some((dist, compatibility));
}
if let Some(hash) = hash {
@ -144,16 +121,16 @@ impl PrioritizedDist {
pub fn insert_source(
&mut self,
dist: Dist,
requires_python: Option<VersionSpecifiers>,
yanked: Yanked,
hash: Option<Hashes>,
compatibility: SourceDistCompatibility,
) {
if self.0.source.is_none() {
self.0.source = Some(DistMetadata {
dist,
requires_python,
yanked,
});
// Track the highest-priority source.
if let Some((.., existing_compatibility)) = &self.0.source {
if compatibility.is_more_compatible(existing_compatibility) {
self.0.source = Some((dist, compatibility));
}
} else {
self.0.source = Some((dist, compatibility));
}
if let Some(hash) = hash {
@ -172,22 +149,44 @@ impl PrioritizedDist {
// 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
// using the wheel is faster.
(Some((wheel, _)), Some(source_dist)) => {
Some(CompatibleDist::IncompatibleWheel { source_dist, wheel })
}
(
Some((wheel, WheelCompatibility::Incompatible(_))),
Some((source_dist, SourceDistCompatibility::Compatible)),
) => Some(CompatibleDist::IncompatibleWheel { source_dist, wheel }),
// Otherwise, if we have a source distribution, return it.
(_, Some(source_dist)) => Some(CompatibleDist::SourceDist(source_dist)),
(None, Some((source_dist, SourceDistCompatibility::Compatible))) => {
Some(CompatibleDist::SourceDist(source_dist))
}
_ => None,
}
}
/// Return the source distribution, if any.
pub fn source(&self) -> Option<&DistMetadata> {
self.0.source.as_ref()
/// Return the compatible source distribution, if any.
pub fn compatible_source(&self) -> Option<&Dist> {
self.0
.source
.as_ref()
.and_then(|(dist, compatibility)| match compatibility {
SourceDistCompatibility::Compatible => Some(dist),
SourceDistCompatibility::Incompatible(_) => None,
})
}
/// Return the incompatible source distribution, if any.
pub fn incompatible_source(&self) -> Option<(&Dist, &IncompatibleSource)> {
self.0
.source
.as_ref()
.and_then(|(dist, compatibility)| match compatibility {
SourceDistCompatibility::Compatible => None,
SourceDistCompatibility::Incompatible(incompatibility) => {
Some((dist, incompatibility))
}
})
}
/// Return the compatible built distribution, if any.
pub fn compatible_wheel(&self) -> Option<(&DistMetadata, TagPriority)> {
pub fn compatible_wheel(&self) -> Option<(&Dist, TagPriority)> {
self.0
.wheel
.as_ref()
@ -198,7 +197,7 @@ impl PrioritizedDist {
}
/// Return the incompatible built distribution, if any.
pub fn incompatible_wheel(&self) -> Option<(&DistMetadata, &IncompatibleWheel)> {
pub fn incompatible_wheel(&self) -> Option<(&Dist, &IncompatibleWheel)> {
self.0
.wheel
.as_ref()
@ -208,16 +207,6 @@ impl PrioritizedDist {
})
}
/// Set the `exclude_newer` flag
pub fn set_exclude_newer(&mut self) {
self.0.exclude_newer = true;
}
/// Check if any distributions were excluded by the `exclude_newer` option
pub fn exclude_newer(&self) -> bool {
self.0.exclude_newer
}
/// Return the hashes for each distribution.
pub fn hashes(&self) -> &[Hashes] {
&self.0.hashes
@ -231,8 +220,8 @@ impl PrioritizedDist {
}
impl<'a> CompatibleDist<'a> {
/// Return the [`DistMetadata`] to use during resolution.
pub fn for_resolution(&self) -> &DistMetadata {
/// Return the [`Dist`] to use during resolution.
pub fn for_resolution(&self) -> &Dist {
match *self {
CompatibleDist::SourceDist(sdist) => sdist,
CompatibleDist::CompatibleWheel(wheel, _) => wheel,
@ -243,8 +232,8 @@ impl<'a> CompatibleDist<'a> {
}
}
/// Return the [`DistMetadata`] to use during installation.
pub fn for_installation(&self) -> &DistMetadata {
/// Return the [`Dist`] to use during installation.
pub fn for_installation(&self) -> &Dist {
match *self {
CompatibleDist::SourceDist(sdist) => sdist,
CompatibleDist::CompatibleWheel(wheel, _) => wheel,
@ -254,42 +243,47 @@ impl<'a> CompatibleDist<'a> {
} => source_dist,
}
}
/// Return the [`Yanked`] status of the distribution.
///
/// It is possible for files to have a different yank status per PEP 592 but in the official
/// PyPI warehouse this cannot happen.
///
/// Here, we will treat the distribution is yanked if the file we will install with
/// is yanked.
///
/// PEP 592: <https://peps.python.org/pep-0592/#warehouse-pypi-implementation-notes>
pub fn yanked(&self) -> &Yanked {
&self.for_installation().yanked
}
}
impl Ord for WheelCompatibility {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(Self::Compatible(p_self), Self::Compatible(p_other)) => p_self.cmp(p_other),
(Self::Incompatible(_), Self::Compatible(_)) => std::cmp::Ordering::Less,
(Self::Compatible(_), Self::Incompatible(_)) => std::cmp::Ordering::Greater,
(Self::Incompatible(t_self), Self::Incompatible(t_other)) => t_self.cmp(t_other),
}
}
}
impl PartialOrd for WheelCompatibility {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(Self::cmp(self, other))
}
}
impl WheelCompatibility {
pub fn is_compatible(&self) -> bool {
matches!(self, Self::Compatible(_))
}
/// Return `true` if the current compatibility is more compatible than another.
///
/// Compatible wheels are always higher more compatible than incompatible wheels.
/// Compatible wheel ordering is determined by tag priority.
pub fn is_more_compatible(&self, other: &Self) -> bool {
match (self, other) {
(Self::Compatible(_), Self::Incompatible(_)) => true,
(Self::Compatible(tag_priority), Self::Compatible(other_tag_priority)) => {
tag_priority > other_tag_priority
}
(Self::Incompatible(_), Self::Compatible(_)) => false,
(Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
incompatibility.is_more_compatible(other_incompatibility)
}
}
}
}
impl SourceDistCompatibility {
/// Return the higher priority compatibility.
///
/// Compatible source distributions are always higher priority than incompatible source distributions.
/// Compatible source distribution priority is arbitrary.
/// Incompatible source distribution priority selects a source distribution that was "closest" to being usable.
pub fn is_more_compatible(&self, other: &Self) -> bool {
match (self, other) {
(Self::Compatible, Self::Incompatible(_)) => true,
(Self::Compatible, Self::Compatible) => false, // Arbitary
(Self::Incompatible(_), Self::Compatible) => false,
(Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => {
incompatibility.is_more_compatible(other_incompatibility)
}
}
}
}
impl From<TagCompatibility> for WheelCompatibility {
@ -300,3 +294,64 @@ impl From<TagCompatibility> for WheelCompatibility {
}
}
}
impl IncompatibleSource {
fn is_more_compatible(&self, other: &Self) -> bool {
match self {
Self::ExcludeNewer(timestamp_self) => match other {
// Smaller timestamps are closer to the cut-off time
Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self,
Self::NoBuild | Self::RequiresPython(_) | Self::Yanked(_) => true,
},
Self::RequiresPython(_) => match other {
Self::ExcludeNewer(_) => false,
// Version specifiers cannot be reasonably compared
Self::RequiresPython(_) => false,
Self::NoBuild | Self::Yanked(_) => true,
},
Self::Yanked(_) => match other {
Self::ExcludeNewer(_) | Self::RequiresPython(_) => false,
// Yanks with a reason are more helpful for errors
Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
Self::NoBuild => true,
},
Self::NoBuild => false,
}
}
}
impl IncompatibleWheel {
fn is_more_compatible(&self, other: &Self) -> bool {
match self {
Self::ExcludeNewer(timestamp_self) => match other {
// Smaller timestamps are closer to the cut-off time
Self::ExcludeNewer(timestamp_other) => match (timestamp_self, timestamp_other) {
(None, _) => true,
(_, None) => false,
(Some(timestamp_self), Some(timestamp_other)) => {
timestamp_other < timestamp_self
}
},
Self::NoBinary | Self::RequiresPython(_) | Self::Tag(_) | Self::Yanked(_) => true,
},
Self::Tag(tag_self) => match other {
Self::ExcludeNewer(_) => false,
Self::Tag(tag_other) => tag_other > tag_self,
Self::NoBinary | Self::RequiresPython(_) | Self::Yanked(_) => true,
},
Self::RequiresPython(_) => match other {
Self::ExcludeNewer(_) | Self::Tag(_) => false,
// Version specifiers cannot be reasonably compared
Self::RequiresPython(_) => false,
Self::NoBinary | Self::Yanked(_) => true,
},
Self::Yanked(_) => match other {
Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_) => false,
// Yanks with a reason are more helpful for errors
Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
Self::NoBinary => true,
},
Self::NoBinary => false,
}
}
}

View file

@ -89,7 +89,15 @@ impl DistInfoMetadata {
}
#[derive(
Debug, Clone, Serialize, Deserialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
)]
#[archive(check_bytes)]
#[archive_attr(derive(Debug))]

View file

@ -11,12 +11,12 @@ use url::Url;
use distribution_filename::DistFilename;
use distribution_types::{
BuiltDist, Dist, File, FileLocation, FlatIndexLocation, IndexUrl, PrioritizedDist,
RegistryBuiltDist, RegistrySourceDist, SourceDist,
RegistryBuiltDist, RegistrySourceDist, SourceDist, SourceDistCompatibility,
};
use pep440_rs::Version;
use pep508_rs::VerbatimUrl;
use platform_tags::Tags;
use pypi_types::{Hashes, Yanked};
use pypi_types::Hashes;
use uv_auth::safe_copy_url_auth;
use uv_cache::{Cache, CacheBucket};
use uv_normalize::PackageName;
@ -307,20 +307,14 @@ impl FlatIndex {
}));
match distributions.0.entry(version) {
Entry::Occupied(mut entry) => {
entry.get_mut().insert_built(
dist,
None,
Yanked::default(),
None,
compatibility.into(),
);
entry
.get_mut()
.insert_built(dist, None, compatibility.into());
}
Entry::Vacant(entry) => {
entry.insert(PrioritizedDist::from_built(
dist,
None,
Yanked::default(),
None,
compatibility.into(),
));
}
@ -334,16 +328,17 @@ impl FlatIndex {
}));
match distributions.0.entry(filename.version) {
Entry::Occupied(mut entry) => {
entry
.get_mut()
.insert_source(dist, None, Yanked::default(), None);
entry.get_mut().insert_source(
dist,
None,
SourceDistCompatibility::Compatible,
);
}
Entry::Vacant(entry) => {
entry.insert(PrioritizedDist::from_source(
dist,
None,
Yanked::default(),
None,
SourceDistCompatibility::Compatible,
));
}
}

View file

@ -122,6 +122,7 @@ async fn install_chunk(
venv.interpreter(),
&FlatIndex::default(),
&NoBinary::None,
&NoBuild::None,
)
.resolve_stream(requirements)
.collect()

View file

@ -1,7 +1,7 @@
use pubgrub::range::Range;
use rustc_hash::FxHashMap;
use distribution_types::CompatibleDist;
use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource};
use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
@ -192,7 +192,7 @@ impl CandidateSelector {
for (version, maybe_dist) in versions {
steps += 1;
let dist = if version.any_prerelease() {
let candidate = if version.any_prerelease() {
if range.contains(version) {
match allow_prerelease {
AllowPreRelease::Yes => {
@ -209,7 +209,7 @@ impl CandidateSelector {
);
// If pre-releases are allowed, treat them equivalently
// to stable distributions.
dist
Candidate::new(package_name, version, dist)
}
AllowPreRelease::IfNecessary => {
let Some(dist) = maybe_dist.prioritized_dist() else {
@ -235,7 +235,7 @@ impl CandidateSelector {
// current range.
prerelease = Some(PreReleaseCandidate::NotNecessary);
// Always return the first-matching stable distribution.
// Return the first-matching stable distribution.
if range.contains(version) {
let Some(dist) = maybe_dist.prioritized_dist() else {
continue;
@ -248,18 +248,29 @@ impl CandidateSelector {
steps,
version,
);
dist
Candidate::new(package_name, version, dist)
} else {
continue;
}
};
// Skip empty candidates due to exclude newer
if dist.exclude_newer() && dist.incompatible_wheel().is_none() && dist.get().is_none() {
// If candidate is not compatible due to exclude newer, continue searching.
// This is a special case — we pretend versions with exclude newer incompatibilities
// do not exist so that they are not present in error messages in our test suite.
// TODO(zanieb): Now that `--exclude-newer` is user facing we may want to consider
// flagging this behavior such that we _will_ report filtered distributions due to
// exclude-newer in our error messages.
if matches!(
candidate.dist(),
CandidateDist::Incompatible(
IncompatibleDist::Source(IncompatibleSource::ExcludeNewer(_))
| IncompatibleDist::Wheel(IncompatibleWheel::ExcludeNewer(_))
)
) {
continue;
}
return Some(Candidate::new(package_name, version, dist));
return Some(candidate);
}
tracing::trace!(
"exhausted all candidates for package {:?} with range {:?} \
@ -281,23 +292,26 @@ impl CandidateSelector {
#[derive(Debug, Clone)]
pub(crate) enum CandidateDist<'a> {
Compatible(CompatibleDist<'a>),
Incompatible(Option<&'a IncompatibleWheel>),
ExcludeNewer,
Incompatible(IncompatibleDist),
}
impl<'a> From<&'a PrioritizedDist> for CandidateDist<'a> {
fn from(value: &'a PrioritizedDist) -> Self {
if let Some(dist) = value.get() {
CandidateDist::Compatible(dist)
} else if value.exclude_newer() && value.incompatible_wheel().is_none() {
// If empty because of exclude-newer, mark as a special case
CandidateDist::ExcludeNewer
} else {
CandidateDist::Incompatible(
value
.incompatible_wheel()
.map(|(_, incompatibility)| incompatibility),
)
// TODO(zanieb)
// We always return the source distribution (if one exists) instead of the wheel
// but in the future we may want to return both so the resolver can explain
// why neither distribution kind can be used.
let dist = if let Some((_, incompatibility)) = value.incompatible_source() {
IncompatibleDist::Source(incompatibility.clone())
} else if let Some((_, incompatibility)) = value.incompatible_wheel() {
IncompatibleDist::Wheel(incompatibility.clone())
} else {
IncompatibleDist::Unavailable
};
CandidateDist::Incompatible(dist)
}
}
}

View file

@ -5,7 +5,7 @@
use anyhow::Result;
use futures::{stream, Stream, StreamExt, TryStreamExt};
use rustc_hash::FxHashMap;
use uv_traits::NoBinary;
use uv_traits::{NoBinary, NoBuild};
use distribution_filename::DistFilename;
use distribution_types::{Dist, IndexUrl, Resolution};
@ -26,6 +26,7 @@ pub struct DistFinder<'a> {
interpreter: &'a Interpreter,
flat_index: &'a FlatIndex,
no_binary: &'a NoBinary,
no_build: &'a NoBuild,
}
impl<'a> DistFinder<'a> {
@ -36,6 +37,7 @@ impl<'a> DistFinder<'a> {
interpreter: &'a Interpreter,
flat_index: &'a FlatIndex,
no_binary: &'a NoBinary,
no_build: &'a NoBuild,
) -> Self {
Self {
tags,
@ -44,6 +46,7 @@ impl<'a> DistFinder<'a> {
interpreter,
flat_index,
no_binary,
no_build,
}
}
@ -135,6 +138,11 @@ impl<'a> DistFinder<'a> {
NoBinary::All => true,
NoBinary::Packages(packages) => packages.contains(&requirement.name),
};
let no_build = match self.no_build {
NoBuild::None => false,
NoBuild::All => true,
NoBuild::Packages(packages) => packages.contains(&requirement.name),
};
// Prioritize the flat index by initializing the "best" matches with its entries.
let matching_override = if let Some(flat_index) = flat_index {
@ -154,8 +162,10 @@ impl<'a> DistFinder<'a> {
Some(version.clone()),
resolvable_dist
.compatible_wheel()
.map(|(dist, tag_priority)| (dist.dist.clone(), tag_priority)),
resolvable_dist.source().map(|dist| dist.dist.clone()),
.map(|(dist, tag_priority)| (dist.clone(), tag_priority)),
resolvable_dist
.compatible_source()
.map(std::clone::Clone::clone),
)
} else {
(None, None, None)
@ -213,7 +223,7 @@ impl<'a> DistFinder<'a> {
}
// Find the most-compatible sdist, if no wheel was found.
if best_wheel.is_none() {
if !no_build && best_wheel.is_none() {
for version_sdist in files.source_dists {
// Only add dists compatible with the python version.
// This is relevant for source dists which give no other indication of their

View file

@ -15,10 +15,10 @@ pub(crate) struct FilePins(FxHashMap<PackageName, FxHashMap<pep440_rs::Version,
impl FilePins {
/// Pin a candidate package.
pub(crate) fn insert(&mut self, candidate: &Candidate, dist: &CompatibleDist) {
self.0.entry(candidate.name().clone()).or_default().insert(
candidate.version().clone(),
dist.for_installation().dist.clone(),
);
self.0
.entry(candidate.name().clone())
.or_default()
.insert(candidate.version().clone(), dist.for_installation().clone());
}
/// Return the pinned file for the given package name and version, if it exists.

View file

@ -1,5 +1,4 @@
use distribution_types::{CompatibleDist, Dist};
use pep440_rs::{Version, VersionSpecifiers};
use pep440_rs::Version;
use pep508_rs::MarkerEnvironment;
use uv_interpreter::Interpreter;
@ -30,42 +29,4 @@ impl PythonRequirement {
pub fn target(&self) -> &Version {
&self.target
}
/// If the dist doesn't match the given Python requirement, return the version specifiers.
pub(crate) fn validate_dist<'a>(
&self,
dist: &'a CompatibleDist,
) -> Option<&'a VersionSpecifiers> {
// Validate the _installed_ file.
let requires_python = dist.for_installation().requires_python.as_ref()?;
// If the dist doesn't support the target Python version, return the failing version
// specifiers.
if !requires_python.contains(self.target()) {
return Some(requires_python);
}
// If the dist 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.
if matches!(dist.for_installation().dist, Dist::Source(_))
&& !requires_python.contains(self.installed())
{
return Some(requires_python);
}
// Validate the resolved file.
let requires_python = dist.for_resolution().requires_python.as_ref()?;
// If the dist 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.
// This isn't strictly necessary, since if `dist.resolve_metadata()` is a source distribution, it
// should be the same file as `dist.install_metadata()` (validated above).
if matches!(dist.for_resolution().dist, Dist::Source(_))
&& !requires_python.contains(self.installed())
{
return Some(requires_python);
}
None
}
}

View file

@ -14,15 +14,15 @@ use pubgrub::solver::{Incompatibility, State};
use rustc_hash::{FxHashMap, FxHashSet};
use tokio::select;
use tokio_stream::wrappers::ReceiverStream;
use tracing::{debug, info_span, instrument, trace, warn, Instrument};
use tracing::{debug, info_span, instrument, trace, Instrument};
use url::Url;
use distribution_filename::WheelFilename;
use distribution_types::{
BuiltDist, Dist, DistributionMetadata, IncompatibleWheel, Name, RemoteSource, SourceDist,
VersionOrUrl,
BuiltDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel,
Name, RemoteSource, SourceDist, VersionOrUrl,
};
use pep440_rs::{Version, VersionSpecifiers, MIN_VERSION};
use pep440_rs::{Version, MIN_VERSION};
use pep508_rs::{MarkerEnvironment, Requirement};
use platform_tags::{IncompatibleTag, Tags};
use pypi_types::{Metadata23, Yanked};
@ -53,6 +53,7 @@ pub use crate::resolver::provider::{
};
use crate::resolver::reporter::Facade;
pub use crate::resolver::reporter::{BuildId, Reporter};
use crate::yanks::AllowedYanks;
use crate::{DependencyMode, Options};
@ -65,12 +66,8 @@ mod urls;
/// Unlike [`PackageUnavailable`] this applies to a single version of the package
#[derive(Debug, Clone)]
pub(crate) enum UnavailableVersion {
/// Version is incompatible due to the `Requires-Python` version specifiers for that package.
RequiresPython(VersionSpecifiers),
/// Version is incompatible because it is yanked
Yanked(Yanked),
/// Version is incompatible because it has no usable distributions
NoDistributions(Option<IncompatibleWheel>),
IncompatibleDist(IncompatibleDist),
}
/// The package is unavailable and cannot be used
@ -97,7 +94,6 @@ pub struct Resolver<'a, Provider: ResolverProvider> {
constraints: Constraints,
overrides: Overrides,
editables: Editables,
allowed_yanks: AllowedYanks,
urls: Urls,
dependency_mode: DependencyMode,
markers: &'a MarkerEnvironment,
@ -134,8 +130,10 @@ impl<'a, Context: BuildContext + Send + Sync> Resolver<'a, DefaultResolverProvid
flat_index,
tags,
PythonRequirement::new(interpreter, markers),
AllowedYanks::from_manifest(&manifest, markers),
options.exclude_newer,
build_context.no_binary(),
build_context.no_build(),
);
Self::new_custom_io(
manifest,
@ -163,7 +161,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
unavailable_packages: DashMap::default(),
visited: DashSet::default(),
selector: CandidateSelector::for_resolution(options, &manifest, markers),
allowed_yanks: AllowedYanks::from_manifest(&manifest, markers),
dependency_mode: options.dependency_mode,
urls: Urls::from_manifest(&manifest, markers)?,
project: manifest.project,
@ -341,10 +338,13 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
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
// Incompatible requires-python versions are special in that we track
// them as incompatible dependencies instead of marking the package version
// as unavailable directly
UnavailableVersion::IncompatibleDist(
IncompatibleDist::Source(IncompatibleSource::RequiresPython(requires_python))
| IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython(requires_python))
) => {
let python_version = requires_python
.iter()
.map(PubGrubSpecifier::try_from)
@ -363,30 +363,51 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
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('.')
),
},
UnavailableVersion::NoDistributions(best_incompatible) => {
if let Some(best_incompatible) = best_incompatible {
match best_incompatible {
IncompatibleWheel::NoBinary => "no source distribution is available and using wheels is disabled".to_string(),
IncompatibleWheel::RequiresPython => "no wheels are available that meet your required Python version".to_string(),
IncompatibleWheel::Tag(tag) => {
match tag {
IncompatibleTag::Invalid => "no wheels are available with valid tags".to_string(),
IncompatibleTag::Python => "no wheels are available with a matching Python implementation".to_string(),
IncompatibleTag::Abi => "no wheels are available with a matching Python ABI".to_string(),
IncompatibleTag::Platform => "no wheels are available with a matching platform".to_string(),
UnavailableVersion::IncompatibleDist(incompatibility) => {
match incompatibility {
IncompatibleDist::Wheel(incompatibility) => {
match incompatibility {
IncompatibleWheel::NoBinary => "no source distribution is available and using wheels is disabled".to_string(),
IncompatibleWheel::Tag(tag) => {
match tag {
IncompatibleTag::Invalid => "no wheels are available with valid tags".to_string(),
IncompatibleTag::Python => "no wheels are available with a matching Python implementation".to_string(),
IncompatibleTag::Abi => "no wheels are available with a matching Python ABI".to_string(),
IncompatibleTag::Platform => "no wheels are available with a matching platform".to_string(),
}
}
IncompatibleWheel::Yanked(yanked) => match yanked {
Yanked::Bool(_) => "it was yanked".to_string(),
Yanked::Reason(reason) => format!(
"it was yanked (reason: {})",
reason.trim().trim_end_matches('.')
),
},
IncompatibleWheel::ExcludeNewer(ts) => match ts {
Some(_) => "it was published after the exclude newer time".to_string(),
None => "it has no publish time".to_string()
}
IncompatibleWheel::RequiresPython(_) => unreachable!(),
}
}
} else {
// TODO(zanieb): It's unclear why we would encounter this case still
"no wheels are available for your system".to_string()
IncompatibleDist::Source(incompatibility) => {
match incompatibility {
IncompatibleSource::NoBuild => "no wheels are usable and building from source is disabled".to_string(),
IncompatibleSource::Yanked(yanked) => match yanked {
Yanked::Bool(_) => "it was yanked".to_string(),
Yanked::Reason(reason) => format!(
"it was yanked (reason: {})",
reason.trim().trim_end_matches('.')
),
},
IncompatibleSource::ExcludeNewer(ts) => match ts {
Some(_) => "it was published after the exclude newer time".to_string(),
None => "it has no publish time".to_string()
}
IncompatibleSource::RequiresPython(_) => unreachable!(),
}
}
IncompatibleDist::Unavailable => "no distributions are available".to_string()
}
}
};
@ -521,7 +542,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
#[instrument(skip_all, fields(%package))]
async fn choose_version(
&self,
package: &PubGrubPackage,
package: &'a PubGrubPackage,
range: &Range<Version>,
pins: &mut FilePins,
request_sink: &tokio::sync::mpsc::Sender<Request>,
@ -644,42 +665,15 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
let dist = match candidate.dist() {
CandidateDist::Compatible(dist) => dist,
CandidateDist::ExcludeNewer => {
// If the version is incompatible because of `exclude_newer`, pretend the versions do not exist
return Ok(None);
}
CandidateDist::Incompatible(incompatibility) => {
// If the version is incompatible because no distributions match, exit early.
// If the version is incompatible because no distributions are compatible, exit early.
return Ok(Some(ResolverVersion::Unavailable(
candidate.version().clone(),
UnavailableVersion::NoDistributions(incompatibility.cloned()),
UnavailableVersion::IncompatibleDist(incompatibility.clone()),
)));
}
};
// If the version is incompatible because it was yanked, exit early.
if dist.yanked().is_yanked() {
if self
.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(dist.yanked().clone()),
)));
}
}
// If the version is incompatible because of its Python requirement
if let Some(requires_python) = self.python_requirement.validate_dist(dist) {
return Ok(Some(ResolverVersion::Unavailable(
candidate.version().clone(),
UnavailableVersion::RequiresPython(requires_python.clone()),
)));
}
if let Some(extra) = extra {
debug!(
"Selecting: {}[{}]=={} ({})",
@ -687,7 +681,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
extra,
candidate.version(),
dist.for_resolution()
.dist
.filename()
.unwrap_or(Cow::Borrowed("unknown filename"))
);
@ -697,7 +690,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
candidate.name(),
candidate.version(),
dist.for_resolution()
.dist
.filename()
.unwrap_or(Cow::Borrowed("unknown filename"))
);
@ -711,7 +703,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
// Emit a request to fetch the metadata for this version.
if self.index.distributions.register(candidate.package_id()) {
let dist = dist.for_resolution().dist.clone();
let dist = dist.for_resolution().clone();
request_sink.send(Request::Dist(dist)).await?;
}
@ -1023,7 +1015,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
};
// Try to find a compatible version. If there aren't any compatible versions,
// short-circuit and return `None`.
// short-circuit.
let Some(candidate) = self.selector.select(&package_name, &range, version_map)
else {
return Ok(None);
@ -1034,14 +1026,9 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
return Ok(None);
};
// If the Python version is incompatible, short-circuit.
if self.python_requirement.validate_dist(dist).is_some() {
return Ok(None);
}
// Emit a request to fetch the metadata for this version.
if self.index.distributions.register(candidate.package_id()) {
let dist = dist.for_resolution().dist.clone();
let dist = dist.for_resolution().clone();
let (metadata, precise) = self
.provider

View file

@ -10,10 +10,11 @@ use pypi_types::Metadata23;
use uv_client::{FlatIndex, RegistryClient};
use uv_distribution::DistributionDatabase;
use uv_normalize::PackageName;
use uv_traits::{BuildContext, NoBinary};
use uv_traits::{BuildContext, NoBinary, NoBuild};
use crate::python_requirement::PythonRequirement;
use crate::version_map::VersionMap;
use crate::yanks::AllowedYanks;
pub type PackageVersionsResult = Result<VersionsResponse, uv_client::Error>;
pub type WheelMetadataResult = Result<(Metadata23, Option<Url>), uv_distribution::Error>;
@ -66,8 +67,10 @@ pub struct DefaultResolverProvider<'a, Context: BuildContext + Send + Sync> {
flat_index: FlatIndex,
tags: Tags,
python_requirement: PythonRequirement,
allowed_yanks: AllowedYanks,
exclude_newer: Option<DateTime<Utc>>,
no_binary: NoBinary,
no_build: NoBuild,
}
impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Context> {
@ -79,8 +82,10 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex
flat_index: &'a FlatIndex,
tags: &'a Tags,
python_requirement: PythonRequirement,
allowed_yanks: AllowedYanks,
exclude_newer: Option<DateTime<Utc>>,
no_binary: &'a NoBinary,
no_build: &'a NoBuild,
) -> Self {
Self {
fetcher,
@ -88,8 +93,10 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex
flat_index: flat_index.clone(),
tags: tags.clone(),
python_requirement,
allowed_yanks,
exclude_newer,
no_binary: no_binary.clone(),
no_build: no_build.clone(),
}
}
}
@ -113,9 +120,11 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider
&index,
&self.tags,
&self.python_requirement,
&self.allowed_yanks,
self.exclude_newer.as_ref(),
self.flat_index.get(package_name).cloned(),
&self.no_binary,
&self.no_build,
))),
Err(err) => match err.into_kind() {
uv_client::ErrorKind::PackageNotFound(_) => {

View file

@ -2,20 +2,24 @@ use std::collections::btree_map::{BTreeMap, Entry};
use std::sync::OnceLock;
use chrono::{DateTime, Utc};
use rustc_hash::FxHashSet;
use tracing::{instrument, warn};
use distribution_filename::DistFilename;
use distribution_types::{Dist, IncompatibleWheel, IndexUrl, PrioritizedDist, WheelCompatibility};
use pep440_rs::Version;
use distribution_filename::{DistFilename, WheelFilename};
use distribution_types::{
Dist, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist,
SourceDistCompatibility, WheelCompatibility,
};
use pep440_rs::{Version, VersionSpecifiers};
use platform_tags::Tags;
use pypi_types::Hashes;
use pypi_types::{Hashes, Yanked};
use rkyv::{de::deserializers::SharedDeserializeMap, Deserialize};
use uv_client::{FlatDistributions, OwnedArchive, SimpleMetadata, VersionFiles};
use uv_normalize::PackageName;
use uv_traits::NoBinary;
use uv_traits::{NoBinary, NoBuild};
use uv_warnings::warn_user_once;
use crate::python_requirement::PythonRequirement;
use crate::{python_requirement::PythonRequirement, yanks::AllowedYanks};
/// A map from versions to distributions.
#[derive(Debug)]
@ -25,6 +29,14 @@ pub struct VersionMap {
impl VersionMap {
/// Initialize a [`VersionMap`] from the given metadata.
///
/// Note it is possible for files to have a different yank status per PEP 592 but in the official
/// PyPI warehouse this cannot happen.
///
/// Here, we track if each file is yanked separately. If a release is partially yanked, the
/// unyanked distributions _can_ be used.
///
/// PEP 592: <https://peps.python.org/pep-0592/#warehouse-pypi-implementation-notes>
#[instrument(skip_all, fields(package_name))]
#[allow(clippy::too_many_arguments)]
pub(crate) fn from_metadata(
@ -33,9 +45,11 @@ impl VersionMap {
index: &IndexUrl,
tags: &Tags,
python_requirement: &PythonRequirement,
allowed_yanks: &AllowedYanks,
exclude_newer: Option<&DateTime<Utc>>,
flat_index: Option<FlatDistributions>,
no_binary: &NoBinary,
no_build: &NoBuild,
) -> Self {
let mut map = BTreeMap::new();
// Create stubs for each entry in simple metadata. The full conversion
@ -85,15 +99,27 @@ impl VersionMap {
NoBinary::All => true,
NoBinary::Packages(packages) => packages.contains(package_name),
};
// Check if source distributions are allowed for this package.
let no_build = match no_build {
NoBuild::None => false,
NoBuild::All => true,
NoBuild::Packages(packages) => packages.contains(package_name),
};
let allowed_yanks = allowed_yanks
.allowed_versions(package_name)
.cloned()
.unwrap_or_default();
Self {
inner: VersionMapInner::Lazy(VersionMapLazy {
map,
simple_metadata,
no_binary,
no_build,
index: index.clone(),
tags: tags.clone(),
python_requirement: python_requirement.clone(),
exclude_newer: exclude_newer.copied(),
allowed_yanks,
}),
}
}
@ -256,6 +282,8 @@ struct VersionMapLazy {
simple_metadata: OwnedArchive<SimpleMetadata>,
/// When true, wheels aren't allowed.
no_binary: bool,
/// When true, source dists aren't allowed.
no_build: bool,
/// The URL of the index where this package came from.
index: IndexUrl,
/// The set of compatibility tags that determines whether a wheel is usable
@ -267,6 +295,8 @@ struct VersionMapLazy {
python_requirement: PythonRequirement,
/// Whether files newer than this timestamp should be excluded or not.
exclude_newer: Option<DateTime<Utc>>,
/// Which yanked versions are allowed
allowed_yanks: FxHashSet<Version>,
}
impl VersionMapLazy {
@ -319,69 +349,62 @@ impl VersionMapLazy {
.expect("archived version files should deserialize");
let mut priority_dist = init.cloned().unwrap_or_default();
for (filename, file) in files.all() {
if let Some(exclude_newer) = self.exclude_newer {
// Support resolving as if it were an earlier timestamp, at least as long files have
// upload time information.
let (excluded, upload_time) = if let Some(exclude_newer) = self.exclude_newer {
match file.upload_time_utc_ms.as_ref() {
Some(&upload_time) if upload_time >= exclude_newer.timestamp_millis() => {
priority_dist.set_exclude_newer();
continue;
(true, Some(upload_time))
}
None => {
warn_user_once!(
"{} is missing an upload date, but user provided: {exclude_newer}",
file.filename,
);
priority_dist.set_exclude_newer();
continue;
(true, None)
}
_ => {}
_ => (false, None),
}
}
let yanked = file.yanked.clone().unwrap_or_default();
} else {
(false, None)
};
// Prioritize amongst all available files.
let version = filename.version().clone();
let requires_python = file.requires_python.clone();
let yanked = file.yanked.clone();
let hash = file.hashes.clone();
match filename {
DistFilename::WheelFilename(filename) => {
// Determine a compatibility for the wheel based on tags
let mut compatibility =
WheelCompatibility::from(filename.compatibility(&self.tags));
if compatibility.is_compatible() {
// Check for Python version incompatibility
if let Some(ref requires_python) = file.requires_python {
if !requires_python.contains(self.python_requirement.target()) {
compatibility = WheelCompatibility::Incompatible(
IncompatibleWheel::RequiresPython,
);
}
}
// Mark all wheels as incompatibility when binaries are disabled
if self.no_binary {
compatibility =
WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
}
};
let compatibility = self.wheel_compatibility(
&filename,
&version,
requires_python,
yanked,
excluded,
upload_time,
);
let dist = Dist::from_registry(
DistFilename::WheelFilename(filename),
file,
self.index.clone(),
);
priority_dist.insert_built(
dist,
requires_python,
yanked,
Some(hash),
compatibility,
);
priority_dist.insert_built(dist, Some(hash), compatibility);
}
DistFilename::SourceDistFilename(filename) => {
let compatibility = self.source_dist_compatibility(
&version,
requires_python,
yanked,
excluded,
upload_time,
);
let dist = Dist::from_registry(
DistFilename::SourceDistFilename(filename),
file,
self.index.clone(),
);
priority_dist.insert_source(dist, requires_python, yanked, Some(hash));
priority_dist.insert_source(dist, Some(hash), compatibility);
}
}
}
@ -393,6 +416,88 @@ impl VersionMapLazy {
};
simple.dist.get_or_init(get_or_init).as_ref()
}
fn source_dist_compatibility(
&self,
version: &Version,
requires_python: Option<VersionSpecifiers>,
yanked: Option<Yanked>,
excluded: bool,
upload_time: Option<i64>,
) -> SourceDistCompatibility {
// Check if builds are disabled
if self.no_build {
return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
}
// Check if after upload time cutoff
if excluded {
return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
upload_time,
));
}
// Check if yanked
if let Some(yanked) = yanked {
if yanked.is_yanked() && !self.allowed_yanks.contains(version) {
return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(yanked));
}
}
// Check if Python version is supported
// Source distributions must meet both the _target_ Python version and the
// _installed_ Python version (to build successfully)
if let Some(requires_python) = requires_python {
if !requires_python.contains(self.python_requirement.target())
|| !requires_python.contains(self.python_requirement.installed())
{
return SourceDistCompatibility::Incompatible(IncompatibleSource::RequiresPython(
requires_python,
));
}
}
SourceDistCompatibility::Compatible
}
fn wheel_compatibility(
&self,
filename: &WheelFilename,
version: &Version,
requires_python: Option<VersionSpecifiers>,
yanked: Option<Yanked>,
excluded: bool,
upload_time: Option<i64>,
) -> WheelCompatibility {
// Check if binaries are disabled
if self.no_binary {
return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
}
// Check if after upload time cutoff
if excluded {
return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
}
// Check if yanked
if let Some(yanked) = yanked {
if yanked.is_yanked() && !self.allowed_yanks.contains(version) {
return WheelCompatibility::Incompatible(IncompatibleWheel::Yanked(yanked));
}
}
// Check for a Python version incompatibility`
if let Some(requires_python) = requires_python {
if !requires_python.contains(self.python_requirement.target()) {
return WheelCompatibility::Incompatible(IncompatibleWheel::RequiresPython(
requires_python,
));
}
}
// Determine a compatibility for the wheel based on tags
WheelCompatibility::from(filename.compatibility(&self.tags))
}
}
/// Represents a possibly initialized [`PrioritizedDist`] for
@ -402,7 +507,7 @@ enum LazyPrioritizedDist {
/// Represents a eagerly constructed distribution from a
/// `FlatDistributions`.
OnlyFlat(PrioritizedDist),
/// Represents a lazyily constructed distribution from an index into a
/// Represents a lazily constructed distribution from an index into a
/// `VersionFiles` from `SimpleMetadata`.
OnlySimple(SimplePrioritizedDist),
/// Combines the above. This occurs when we have data from both a flat

View file

@ -8,8 +8,8 @@ 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>>);
#[derive(Debug, Default, Clone)]
pub struct AllowedYanks(FxHashMap<PackageName, FxHashSet<Version>>);
impl AllowedYanks {
pub(crate) fn from_manifest(manifest: &Manifest, markers: &MarkerEnvironment) -> Self {
@ -49,11 +49,12 @@ impl 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))
/// Returns versions for the given package which are allowed even if marked as yanked by the
/// relevant index.
pub(crate) fn allowed_versions(
&self,
package_name: &PackageName,
) -> Option<&FxHashSet<Version>> {
self.0.get(package_name)
}
}

View file

@ -220,9 +220,15 @@ pub(crate) async fn pip_sync(
} else {
let start = std::time::Instant::now();
let wheel_finder =
uv_resolver::DistFinder::new(tags, &client, venv.interpreter(), &flat_index, no_binary)
.with_reporter(FinderReporter::from(printer).with_length(remote.len() as u64));
let wheel_finder = uv_resolver::DistFinder::new(
tags,
&client,
venv.interpreter(),
&flat_index,
no_binary,
no_build,
)
.with_reporter(FinderReporter::from(printer).with_length(remote.len() as u64));
let resolution = wheel_finder.resolve(&remote).await?;
let s = if resolution.len() == 1 { "" } else { "s" };

View file

@ -3445,12 +3445,13 @@ fn no_wheels_no_build() {
.arg("no-wheels-no-build-a")
, @r###"
success: false
exit_code: 2
exit_code: 1
----- stdout -----
----- stderr -----
error: Failed to download and build: albatross==1.0.0
Caused by: Building source distributions is disabled
× No solution found when resolving dependencies:
Because only albatross==1.0.0 is available and albatross==1.0.0 is unusable because no wheels are usable and building from source is disabled, we can conclude that all versions of albatross cannot be used.
And because you require albatross, we can conclude that the requirements are unsatisfiable.
"###);
assert_not_installed(&context.venv, "no_wheels_no_build_a", &context.temp_dir);