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

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