Allow conflicting prerelease strategies when forking (#5150)

## Summary

Similar to https://github.com/astral-sh/uv/pull/5232, we should also
track prerelease strategies per-fork, instead of globally per package.
The common functionality for tracking locals and prerelease versions
across forks is extracted into the `ForkMap` type.

Resolves https://github.com/astral-sh/uv/issues/4579. This doesn't quite
solve https://github.com/astral-sh/uv/issues/4959, as that issue relies
on overlapping markers.
This commit is contained in:
Ibraheem Ahmed
2024-07-23 11:57:14 -04:00
committed by GitHub
parent bea8bc6c61
commit c8ac8ee57a
9 changed files with 449 additions and 132 deletions
+43 -44
View File
@@ -12,10 +12,10 @@ use uv_normalize::PackageName;
use uv_types::InstalledPackagesProvider;
use crate::preferences::Preferences;
use crate::prerelease_mode::PreReleaseStrategy;
use crate::prerelease_mode::{AllowPreRelease, PreReleaseStrategy};
use crate::resolution_mode::ResolutionStrategy;
use crate::version_map::{VersionMap, VersionMapDistHandle};
use crate::{Exclusions, Manifest, Options};
use crate::{Exclusions, Manifest, Options, ResolverMarkers};
#[derive(Debug, Clone)]
#[allow(clippy::struct_field_names)]
@@ -68,13 +68,6 @@ impl CandidateSelector {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum AllowPreRelease {
Yes,
No,
IfNecessary,
}
impl CandidateSelector {
/// Select a [`Candidate`] from a set of candidate versions and files.
///
@@ -89,35 +82,52 @@ impl CandidateSelector {
preferences: &'a Preferences,
installed_packages: &'a InstalledPackages,
exclusions: &'a Exclusions,
markers: &ResolverMarkers,
) -> Option<Candidate<'a>> {
if let Some(preferred) = Self::get_preferred(
if let Some(preferred) = self.get_preferred(
package_name,
range,
version_maps,
preferences,
installed_packages,
exclusions,
markers,
) {
return Some(preferred);
}
self.select_no_preference(package_name, range, version_maps)
self.select_no_preference(package_name, range, version_maps, markers)
}
/// Get a preferred version if one exists. This is the preference from a lockfile or a locally
/// installed version.
fn get_preferred<'a, InstalledPackages: InstalledPackagesProvider>(
&self,
package_name: &'a PackageName,
range: &Range<Version>,
version_maps: &'a [VersionMap],
preferences: &'a Preferences,
installed_packages: &'a InstalledPackages,
exclusions: &'a Exclusions,
markers: &ResolverMarkers,
) -> Option<Candidate<'a>> {
// If the package has a preference (e.g., an existing version from an existing lockfile),
// and the preference satisfies the current range, use that.
if let Some(version) = preferences.version(package_name) {
if range.contains(version) {
'preference: {
// Respect the version range for this requirement.
if !range.contains(version) {
break 'preference;
}
// Respect the pre-release strategy for this fork.
if version.any_prerelease()
&& self.prerelease_strategy.allows(package_name, markers)
!= AllowPreRelease::Yes
{
break 'preference;
}
// Check for a locally installed distribution that matches the preferred version
if !exclusions.contains(package_name) {
let installed_dists = installed_packages.get_packages(package_name);
@@ -167,16 +177,27 @@ impl CandidateSelector {
[] => {}
[dist] => {
let version = dist.version();
if range.contains(version) {
debug!("Found installed version of {dist} that satisfies {range}");
return Some(Candidate {
name: package_name,
version,
dist: CandidateDist::Compatible(CompatibleDist::InstalledDist(dist)),
choice_kind: VersionChoiceKind::Installed,
});
// Respect the version range for this requirement.
if !range.contains(version) {
return None;
}
// Respect the pre-release strategy for this fork.
if version.any_prerelease()
&& self.prerelease_strategy.allows(package_name, markers)
!= AllowPreRelease::Yes
{
return None;
}
debug!("Found installed version of {dist} that satisfies {range}");
return Some(Candidate {
name: package_name,
version,
dist: CandidateDist::Compatible(CompatibleDist::InstalledDist(dist)),
choice_kind: VersionChoiceKind::Installed,
});
}
// We do not consider installed distributions with multiple versions because
// during installation these must be reinstalled from the remote
@@ -189,29 +210,6 @@ impl CandidateSelector {
None
}
/// Determine the appropriate prerelease strategy for the current package.
fn allow_prereleases(&self, package_name: &PackageName) -> AllowPreRelease {
match &self.prerelease_strategy {
PreReleaseStrategy::Disallow => AllowPreRelease::No,
PreReleaseStrategy::Allow => AllowPreRelease::Yes,
PreReleaseStrategy::IfNecessary => AllowPreRelease::IfNecessary,
PreReleaseStrategy::Explicit(packages) => {
if packages.contains(package_name) {
AllowPreRelease::Yes
} else {
AllowPreRelease::No
}
}
PreReleaseStrategy::IfNecessaryOrExplicit(packages) => {
if packages.contains(package_name) {
AllowPreRelease::Yes
} else {
AllowPreRelease::IfNecessary
}
}
}
}
/// Select a [`Candidate`] without checking for version preference such as an existing
/// lockfile.
pub(crate) fn select_no_preference<'a>(
@@ -219,13 +217,14 @@ impl CandidateSelector {
package_name: &'a PackageName,
range: &Range<Version>,
version_maps: &'a [VersionMap],
markers: &ResolverMarkers,
) -> Option<Candidate> {
tracing::trace!(
"selecting candidate for package {package_name} with range {range:?} with {} remote versions",
version_maps.iter().map(VersionMap::len).sum::<usize>(),
);
let highest = self.use_highest_version(package_name);
let allow_prerelease = self.allow_prereleases(package_name);
let allow_prerelease = self.prerelease_strategy.allows(package_name, markers);
if self.index_strategy == IndexStrategy::UnsafeBestMatch {
if highest {
+1
View File
@@ -232,6 +232,7 @@ impl std::fmt::Display for NoSolutionError {
&self.unavailable_packages,
&self.incomplete_packages,
&self.fork_urls,
&self.markers,
) {
write!(f, "\n\n{hint}")?;
}
+61 -40
View File
@@ -1,10 +1,10 @@
use pypi_types::RequirementSource;
use rustc_hash::FxHashSet;
use pep508_rs::MarkerEnvironment;
use uv_normalize::PackageName;
use crate::{DependencyMode, Manifest};
use crate::resolver::ForkSet;
use crate::{DependencyMode, Manifest, ResolverMarkers};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
@@ -57,11 +57,11 @@ pub(crate) enum PreReleaseStrategy {
/// Allow pre-release versions for first-party packages with explicit pre-release markers in
/// their version requirements.
Explicit(FxHashSet<PackageName>),
Explicit(ForkSet),
/// Allow pre-release versions if all versions of a package are pre-release, or if the package
/// has an explicit pre-release marker in its version requirements.
IfNecessaryOrExplicit(FxHashSet<PackageName>),
IfNecessaryOrExplicit(ForkSet),
}
impl PreReleaseStrategy {
@@ -71,51 +71,72 @@ impl PreReleaseStrategy {
markers: Option<&MarkerEnvironment>,
dependencies: DependencyMode,
) -> Self {
let mut packages = ForkSet::default();
match mode {
PreReleaseMode::Disallow => Self::Disallow,
PreReleaseMode::Allow => Self::Allow,
PreReleaseMode::IfNecessary => Self::IfNecessary,
PreReleaseMode::Explicit => Self::Explicit(
manifest
.requirements(markers, dependencies)
.filter(|requirement| {
let RequirementSource::Registry { specifier, .. } = &requirement.source
else {
return false;
};
specifier
.iter()
.any(pep440_rs::VersionSpecifier::any_prerelease)
})
.map(|requirement| requirement.name.clone())
.collect(),
),
PreReleaseMode::IfNecessaryOrExplicit => Self::IfNecessaryOrExplicit(
manifest
.requirements(markers, dependencies)
.filter(|requirement| {
let RequirementSource::Registry { specifier, .. } = &requirement.source
else {
return false;
};
specifier
.iter()
.any(pep440_rs::VersionSpecifier::any_prerelease)
})
.map(|requirement| requirement.name.clone())
.collect(),
),
_ => {
for requirement in manifest.requirements(markers, dependencies) {
let RequirementSource::Registry { specifier, .. } = &requirement.source else {
continue;
};
if specifier
.iter()
.any(pep440_rs::VersionSpecifier::any_prerelease)
{
packages.add(&requirement, ());
}
}
match mode {
PreReleaseMode::Explicit => Self::Explicit(packages),
PreReleaseMode::IfNecessaryOrExplicit => Self::IfNecessaryOrExplicit(packages),
_ => unreachable!(),
}
}
}
}
/// Returns `true` if a [`PackageName`] is allowed to have pre-release versions.
pub(crate) fn allows(&self, package: &PackageName) -> bool {
pub(crate) fn allows(
&self,
package_name: &PackageName,
markers: &ResolverMarkers,
) -> AllowPreRelease {
match self {
Self::Disallow => false,
Self::Allow => true,
Self::IfNecessary => false,
Self::Explicit(packages) => packages.contains(package),
Self::IfNecessaryOrExplicit(packages) => packages.contains(package),
PreReleaseStrategy::Disallow => AllowPreRelease::No,
PreReleaseStrategy::Allow => AllowPreRelease::Yes,
PreReleaseStrategy::IfNecessary => AllowPreRelease::IfNecessary,
PreReleaseStrategy::Explicit(packages) => {
if packages.contains(package_name, markers) {
AllowPreRelease::Yes
} else {
AllowPreRelease::No
}
}
PreReleaseStrategy::IfNecessaryOrExplicit(packages) => {
if packages.contains(package_name, markers) {
AllowPreRelease::Yes
} else {
AllowPreRelease::IfNecessary
}
}
}
}
}
/// The pre-release strategy for a given package.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum AllowPreRelease {
/// Allow all pre-release versions.
Yes,
/// Disallow all pre-release versions.
No,
/// Allow pre-release versions if all versions of this package are pre-release.
IfNecessary,
}
+11 -4
View File
@@ -18,9 +18,10 @@ use uv_normalize::PackageName;
use crate::candidate_selector::CandidateSelector;
use crate::fork_urls::ForkUrls;
use crate::prerelease_mode::AllowPreRelease;
use crate::python_requirement::{PythonRequirement, PythonTarget};
use crate::resolver::{IncompletePackage, UnavailablePackage, UnavailableReason};
use crate::RequiresPython;
use crate::{RequiresPython, ResolverMarkers};
use super::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
@@ -407,6 +408,7 @@ impl PubGrubReportFormatter<'_> {
unavailable_packages: &FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: &FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
fork_urls: &ForkUrls,
markers: &ResolverMarkers,
) -> IndexSet<PubGrubHint> {
let mut hints = IndexSet::default();
match derivation_tree {
@@ -416,7 +418,9 @@ impl PubGrubReportFormatter<'_> {
if let PubGrubPackageInner::Package { name, .. } = &**package {
// Check for no versions due to pre-release options.
if !fork_urls.contains_key(name) {
self.prerelease_available_hint(package, name, set, selector, &mut hints);
self.prerelease_available_hint(
package, name, set, selector, markers, &mut hints,
);
}
}
@@ -465,6 +469,7 @@ impl PubGrubReportFormatter<'_> {
unavailable_packages,
incomplete_packages,
fork_urls,
markers,
));
hints.extend(self.hints(
&derived.cause2,
@@ -473,6 +478,7 @@ impl PubGrubReportFormatter<'_> {
unavailable_packages,
incomplete_packages,
fork_urls,
markers,
));
}
}
@@ -569,6 +575,7 @@ impl PubGrubReportFormatter<'_> {
name: &PackageName,
set: &Range<Version>,
selector: &CandidateSelector,
markers: &ResolverMarkers,
hints: &mut IndexSet<PubGrubHint>,
) {
let any_prerelease = set.iter().any(|(start, end)| {
@@ -587,7 +594,7 @@ impl PubGrubReportFormatter<'_> {
if any_prerelease {
// A pre-release marker appeared in the version requirements.
if !selector.prerelease_strategy().allows(name) {
if selector.prerelease_strategy().allows(name, markers) != AllowPreRelease::Yes {
hints.insert(PubGrubHint::PreReleaseRequested {
package: package.clone(),
range: self.simplify_set(set, package).into_owned(),
@@ -601,7 +608,7 @@ impl PubGrubReportFormatter<'_> {
.find(|version| set.contains(version))
}) {
// There are pre-release versions available for the package.
if !selector.prerelease_strategy().allows(name) {
if selector.prerelease_strategy().allows(name, markers) != AllowPreRelease::Yes {
hints.insert(PubGrubHint::PreReleaseAvailable {
package: package.clone(),
version: version.clone(),
@@ -12,7 +12,7 @@ use pep440_rs::Version;
use crate::candidate_selector::CandidateSelector;
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner};
use crate::resolver::Request;
use crate::{InMemoryIndex, PythonRequirement, ResolveError, VersionsResponse};
use crate::{InMemoryIndex, PythonRequirement, ResolveError, ResolverMarkers, VersionsResponse};
enum BatchPrefetchStrategy {
/// Go through the next versions assuming the existing selection and its constraints
@@ -53,6 +53,7 @@ impl BatchPrefetcher {
request_sink: &Sender<Request>,
index: &InMemoryIndex,
selector: &CandidateSelector,
markers: &ResolverMarkers,
) -> anyhow::Result<(), ResolveError> {
let PubGrubPackageInner::Package {
name,
@@ -92,7 +93,7 @@ impl BatchPrefetcher {
previous,
} => {
if let Some(candidate) =
selector.select_no_preference(name, &compatible, version_map)
selector.select_no_preference(name, &compatible, version_map, markers)
{
let compatible = compatible.intersection(
&Range::singleton(candidate.version().clone()).complement(),
@@ -116,7 +117,7 @@ impl BatchPrefetcher {
Range::strictly_higher_than(previous)
};
if let Some(candidate) =
selector.select_no_preference(name, &range, version_map)
selector.select_no_preference(name, &range, version_map, markers)
{
phase = BatchPrefetchStrategy::InOrder {
previous: candidate.version().clone(),
@@ -0,0 +1,84 @@
use pep508_rs::{MarkerTree, PackageName};
use pypi_types::Requirement;
use rustc_hash::FxHashMap;
use crate::marker::is_disjoint;
use crate::ResolverMarkers;
/// A set of package names associated with a given fork.
pub(crate) type ForkSet = ForkMap<()>;
/// A map from package names to their values for a given fork.
#[derive(Debug, Clone)]
pub(crate) struct ForkMap<T>(FxHashMap<PackageName, Vec<Entry<T>>>);
/// An entry in a [`ForkMap`].
#[derive(Debug, Clone)]
struct Entry<T> {
value: T,
marker: Option<MarkerTree>,
}
impl<T> Default for ForkMap<T> {
fn default() -> Self {
Self(FxHashMap::default())
}
}
impl<T> ForkMap<T> {
/// Associate a value with the [`Requirement`] in a given fork.
pub(crate) fn add(&mut self, requirement: &Requirement, value: T) {
let entry = Entry {
value,
marker: requirement.marker.clone(),
};
self.0
.entry(requirement.name.clone())
.or_default()
.push(entry);
}
/// Returns `true` if the map contains any values for a package that are compatible with the
/// given fork.
pub(crate) fn contains(&self, package_name: &PackageName, markers: &ResolverMarkers) -> bool {
!self.get(package_name, markers).is_empty()
}
/// Returns a list of values associated with a package that are compatible with the given fork.
///
/// Compatibility implies that the markers on the requirement that contained this value
/// are not disjoint with the given fork. Note that this does not imply that the requirement
/// diverged in the given fork - values from overlapping forks may be combined.
pub(crate) fn get(&self, package_name: &PackageName, markers: &ResolverMarkers) -> Vec<&T> {
let Some(values) = self.0.get(package_name) else {
return Vec::new();
};
match markers {
// If we are solving for a specific environment we already filtered
// compatible requirements `from_manifest`.
ResolverMarkers::SpecificEnvironment(_) => values
.first()
.map(|entry| &entry.value)
.into_iter()
.collect(),
// Return all values that were requested with markers that are compatible
// with the current fork, i.e. the markers are not disjoint.
ResolverMarkers::Fork(fork) => values
.iter()
.filter(|entry| {
!entry
.marker
.as_ref()
.is_some_and(|marker| is_disjoint(fork, marker))
})
.map(|entry| &entry.value)
.collect(),
// If we haven't forked yet, all values are potentially compatible.
ResolverMarkers::Universal => values.iter().map(|entry| &entry.value).collect(),
}
}
}
+8 -39
View File
@@ -3,15 +3,15 @@ use std::str::FromStr;
use distribution_filename::{SourceDistFilename, WheelFilename};
use distribution_types::RemoteSource;
use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifierBuildError};
use pep508_rs::{MarkerEnvironment, MarkerTree, PackageName};
use pep508_rs::{MarkerEnvironment, PackageName};
use pypi_types::RequirementSource;
use rustc_hash::FxHashMap;
use crate::{marker::is_disjoint, DependencyMode, Manifest, ResolverMarkers};
use crate::resolver::ForkMap;
use crate::{DependencyMode, Manifest, ResolverMarkers};
/// A map of package names to their associated, required local versions across all forks.
#[derive(Debug, Default, Clone)]
pub(crate) struct Locals(FxHashMap<PackageName, Vec<(Option<MarkerTree>, Version)>>);
pub(crate) struct Locals(ForkMap<Version>);
impl Locals {
/// Determine the set of permitted local versions in the [`Manifest`].
@@ -20,20 +20,17 @@ impl Locals {
markers: Option<&MarkerEnvironment>,
dependencies: DependencyMode,
) -> Self {
let mut required: FxHashMap<PackageName, Vec<_>> = FxHashMap::default();
let mut locals = ForkMap::default();
// Add all direct requirements and constraints. There's no need to look for conflicts,
// since conflicts will be enforced by the solver.
for requirement in manifest.requirements(markers, dependencies) {
if let Some(local) = from_source(&requirement.source) {
required
.entry(requirement.name.clone())
.or_default()
.push((requirement.marker.clone(), local));
locals.add(&requirement, local);
}
}
Self(required)
Self(locals)
}
/// Return a list of local versions that are compatible with a package in the given fork.
@@ -42,35 +39,7 @@ impl Locals {
package_name: &PackageName,
markers: &ResolverMarkers,
) -> Vec<&Version> {
let Some(locals) = self.0.get(package_name) else {
return Vec::new();
};
match markers {
// If we are solving for a specific environment we already filtered
// compatible requirements `from_manifest`.
ResolverMarkers::SpecificEnvironment(_) => {
locals.first().map(|(_, local)| local).into_iter().collect()
}
// Return all locals that were requested with markers that are compatible
// with the current fork.
//
// Compatibility implies that the markers are not disjoint. The resolver will
// choose the most compatible local when it narrows to the specific fork.
ResolverMarkers::Fork(fork) => locals
.iter()
.filter(|(marker, _)| {
!marker
.as_ref()
.is_some_and(|marker| is_disjoint(fork, marker))
})
.map(|(_, local)| local)
.collect(),
// If we haven't forked yet, all locals are potentially compatible.
ResolverMarkers::Universal => locals.iter().map(|(_, local)| local).collect(),
}
self.0.get(package_name, markers)
}
/// Given a specifier that may include the version _without_ a local segment, return a specifier
+12 -1
View File
@@ -26,7 +26,8 @@ use distribution_types::{
IncompatibleWheel, IndexLocations, InstalledDist, PythonRequirementKind, RemoteSource,
ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef,
};
pub(crate) use locals::Locals;
pub(crate) use fork_map::{ForkMap, ForkSet};
use locals::Locals;
use pep440_rs::{Version, MIN_VERSION};
use pep508_rs::MarkerTree;
use platform_tags::Tags;
@@ -72,6 +73,7 @@ use crate::{DependencyMode, Exclusions, FlatIndex, Options};
mod availability;
mod batch_prefetch;
mod fork_map;
mod index;
mod locals;
mod provider;
@@ -422,6 +424,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&mut state.pins,
&preferences,
&state.fork_urls,
&state.markers,
&state.python_requirement,
&mut visited,
&request_sink,
@@ -480,6 +483,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&request_sink,
&self.index,
&self.selector,
&state.markers,
)?;
}
@@ -812,6 +816,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
pins: &mut FilePins,
preferences: &Preferences,
fork_urls: &ForkUrls,
fork_markers: &ResolverMarkers,
python_requirement: &PythonRequirement,
visited: &mut FxHashSet<PackageName>,
request_sink: &Sender<Request>,
@@ -840,6 +845,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
range,
package,
preferences,
fork_markers,
python_requirement,
pins,
visited,
@@ -953,6 +959,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
range: &Range<Version>,
package: &PubGrubPackage,
preferences: &Preferences,
fork_markers: &ResolverMarkers,
python_requirement: &PythonRequirement,
pins: &mut FilePins,
visited: &mut FxHashSet<PackageName>,
@@ -995,6 +1002,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
preferences,
&self.installed_packages,
&self.exclusions,
fork_markers,
) else {
// Short circuit: we couldn't find _any_ versions for a package.
return Ok(None);
@@ -1767,6 +1775,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&self.preferences,
&self.installed_packages,
&self.exclusions,
// We don't have access to the fork state when prefetching, so assume that
// pre-release versions are allowed.
&ResolverMarkers::Universal,
) else {
return Ok(None);
};
+225 -1
View File
@@ -6960,7 +6960,7 @@ fn universal_local_path_requirement() -> Result<()> {
}
/// If a dependency requests a local version with an overlapping marker expression,
/// we should prefer the local in all cases.
/// we should prefer the local in both forks.
#[test]
fn universal_overlapping_local_requirement() -> Result<()> {
let context = TestContext::new("3.12");
@@ -7464,6 +7464,230 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> {
Ok(())
}
// Requested distinct prerelease strategies with disjoint markers.
#[test]
fn universal_disjoint_prereleases() -> Result<()> {
static EXCLUDE_NEWER: &str = "2024-07-17T00:00:00Z";
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
cffi ; os_name == 'linux'
cffi >= 1.17.0rc1 ; os_name != 'linux'
"})?;
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
.env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER)
.arg("requirements.in")
.arg("--universal"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
cffi==1.17.0rc1 ; os_name != 'linux'
# via -r requirements.in
cffi==1.16.0 ; os_name == 'linux'
# via -r requirements.in
pycparser==2.22
# via cffi
----- stderr -----
Resolved 3 packages in [TIME]
"###
);
Ok(())
}
// Requested distinct prerelease strategies with disjoint markers for a package
// that is also present as a transitive dependency.
#[test]
fn universal_transitive_disjoint_prerelease_requirement() -> Result<()> {
static EXCLUDE_NEWER: &str = "2024-07-17T00:00:00Z";
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
cffi ; os_name == 'linux'
cffi >= 1.17.0rc1 ; os_name != 'linux'
cryptography
"})?;
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
.env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER)
.arg("requirements.in")
.arg("--universal"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
cffi==1.17.0rc1 ; os_name != 'linux' or platform_python_implementation != 'PyPy'
# via
# -r requirements.in
# cryptography
cffi==1.16.0 ; os_name == 'linux' or platform_python_implementation != 'PyPy'
# via
# -r requirements.in
# cryptography
cryptography==42.0.8
# via -r requirements.in
pycparser==2.22
# via cffi
----- stderr -----
Resolved 4 packages in [TIME]
"###
);
Ok(())
}
// Ensure that the global prerelease mode is respected across forks.
#[test]
fn universal_prerelease_mode() -> Result<()> {
static EXCLUDE_NEWER: &str = "2024-07-17T00:00:00Z";
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc::indoc! {r"
cffi ; os_name == 'linux'
cffi >= 1.17.0rc1 ; os_name != 'linux'
"})?;
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
.env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER)
.arg("--prerelease=allow")
.arg("requirements.in")
.arg("--universal"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --prerelease=allow requirements.in --universal
cffi==1.17.0rc1
# via -r requirements.in
pycparser==2.22
# via cffi
----- stderr -----
Resolved 2 packages in [TIME]
"###
);
Ok(())
}
/// If a dependency requests a prerelease version with an overlapping marker expression,
/// we should prefer the prerelease version in both forks.
#[test]
fn universal_overlapping_prerelease_requirement() -> Result<()> {
static EXCLUDE_NEWER: &str = "2024-07-17T00:00:00Z";
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "example"
version = "0.0.0"
dependencies = [
"cffi >= 1.17.0rc1 ; os_name == 'Linux'"
]
requires-python = ">=3.11"
"#})?;
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc! {"
cffi
.
"})?;
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
.env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER)
.arg("requirements.in")
.arg("--universal"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
cffi==1.17.0rc1
# via
# -r requirements.in
# example
.
# via -r requirements.in
pycparser==2.22
# via cffi
----- stderr -----
Resolved 3 packages in [TIME]
"###
);
Ok(())
}
/// If a dependency requests distinct prerelease strategies with disjoint marker expressions,
/// we should fork the root requirement.
#[test]
fn universal_disjoint_prerelease_requirement() -> Result<()> {
static EXCLUDE_NEWER: &str = "2024-07-17T00:00:00Z";
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "example"
version = "0.0.0"
dependencies = [
"cffi >= 1.17.0rc1 ; os_name == 'Linux'",
"cffi==1.15.0 ; os_name != 'Linux'"
]
requires-python = ">=3.11"
"#})?;
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc! {"
cffi
.
"})?;
// Some marker expressions on the output here are missing due to https://github.com/astral-sh/uv/issues/5086,
// but the prerelease versions are still respected correctly.
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
.env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER)
.arg("requirements.in")
.arg("--universal"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
cffi==1.17.0rc1
# via
# -r requirements.in
# example
cffi==1.15.0
# via
# -r requirements.in
# example
.
# via -r requirements.in
pycparser==2.22
# via cffi
----- stderr -----
Resolved 4 packages in [TIME]
"###
);
Ok(())
}
/// Perform a universal resolution that requires narrowing the supported Python range in one of the
/// fork branches.
///