diff --git a/crates/uv-resolver/src/candidate_selector.rs b/crates/uv-resolver/src/candidate_selector.rs index d866d00647dd..f4d17b813fea 100644 --- a/crates/uv-resolver/src/candidate_selector.rs +++ b/crates/uv-resolver/src/candidate_selector.rs @@ -206,7 +206,7 @@ impl CandidateSelector { exclusions: &Exclusions, resolver_markers: &ResolverMarkers, ) -> Option> { - for (_marker, version) in preferences { + for (marker, version) in preferences { // Respect the version range for this requirement. if !range.contains(version) { continue; @@ -240,13 +240,20 @@ impl CandidateSelector { } // Respect the pre-release strategy for this fork. - if version.any_prerelease() - && self + if version.any_prerelease() { + let allow = match self .prerelease_strategy .allows(package_name, resolver_markers) - != AllowPrerelease::Yes - { - continue; + { + AllowPrerelease::Yes => true, + AllowPrerelease::No => false, + // If the pre-release is "global" (i.e., provided via a lockfile, rather than + // a fork), accept it unless pre-releases are completely banned. + AllowPrerelease::IfNecessary => marker.is_none(), + }; + if !allow { + continue; + } } // Check for a remote distribution that matches the preferred version diff --git a/crates/uv-resolver/src/requires_python.rs b/crates/uv-resolver/src/requires_python.rs index a828210bd413..3002a0ea5b9f 100644 --- a/crates/uv-resolver/src/requires_python.rs +++ b/crates/uv-resolver/src/requires_python.rs @@ -494,7 +494,7 @@ mod tests { ]; for (i, v1) in versions.iter().enumerate() { for v2 in &versions[i + 1..] { - assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}",); + assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}"); } } } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index c5d563d810bd..de6c64eed8ed 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -7523,6 +7523,46 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> { Ok(()) } +/// Respect an existing pre-release preference, even if preferences aren't enabled. +#[test] +fn existing_prerelease_preference() -> 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 + "})?; + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc::indoc! {r" + cffi==1.17.0rc1 + pyparser==2.22 + "})?; + + uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile() + .env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER) + .arg("requirements.in") + .arg("-o") + .arg("requirements.txt"), @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 -o requirements.txt + cffi==1.17.0rc1 + # via -r requirements.in + pycparser==2.22 + # via cffi + + ----- stderr ----- + Resolved 2 packages in [TIME] + "### + ); + + Ok(()) +} + /// Requested distinct pre-release strategies with disjoint markers. #[test] fn universal_disjoint_prereleases() -> Result<()> { @@ -7531,8 +7571,8 @@ fn universal_disjoint_prereleases() -> Result<()> { 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' + cffi >= 1.16.0rc1 ; os_name != 'linux' + cffi >= 1.16.0rc1, <1.16.0rc2 ; os_name == 'linux' "})?; uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile() @@ -7544,15 +7584,137 @@ fn universal_disjoint_prereleases() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal - cffi==1.16.0 ; os_name == 'linux' + cffi==1.16.0rc1 # via -r requirements.in - cffi==1.17.0rc1 ; os_name != 'linux' + pycparser==2.22 + # via cffi + + ----- stderr ----- + Resolved 2 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Requested distinct pre-release strategies with disjoint markers. +#[test] +fn universal_disjoint_prereleases_preference() -> 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.16.0 ; os_name == 'linux' + "})?; + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc::indoc! {r" + cffi==1.17.0rc1 + pyparser==2.22 + "})?; + + uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile() + .env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER) + .arg("requirements.in") + .arg("-o") + .arg("requirements.txt") + .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 -o requirements.txt --universal + cffi==1.17.0rc1 # via -r requirements.in pycparser==2.22 # via cffi ----- stderr ----- - Resolved 3 packages in [TIME] + Resolved 2 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Requested distinct pre-release strategies with disjoint markers. +/// +/// TODO(charlie): This should resolve to two different `cffi` versions, one for each fork. +#[test] +fn universal_disjoint_prereleases_preference_marker() -> 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.16.0rc1 ; os_name == 'linux' + "})?; + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc::indoc! {r" + cffi==1.16.0 ; os_name != 'linux' + cffi==1.16.0rc1 ; os_name == 'linux' + pyparser==2.22 + "})?; + + uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile() + .env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER) + .arg("requirements.in") + .arg("-o") + .arg("requirements.txt") + .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 -o requirements.txt --universal + cffi==1.16.0 + # via -r requirements.in + pycparser==2.22 + # via cffi + + ----- stderr ----- + Resolved 2 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Resolve to a single version as `--prerelease=allow` is provided, even though the first branch +/// doesn't include a pre-release marker. +#[test] +fn universal_disjoint_prereleases_allow() -> 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 >= 1.15.0, < 1.17.0 ; os_name == 'linux' + cffi >= 1.15.0, <= 1.16.0rc2 ; os_name != 'linux' + "})?; + + uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile() + .env("UV_EXCLUDE_NEWER", EXCLUDE_NEWER) + .arg("requirements.in") + .arg("--universal") + .arg("--prerelease") + .arg("allow"), @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 --prerelease allow + cffi==1.16.0rc2 + # via -r requirements.in + pycparser==2.22 + # via cffi + + ----- stderr ----- + Resolved 2 packages in [TIME] "### );