diff --git a/crates/uv-resolver/src/prerelease.rs b/crates/uv-resolver/src/prerelease.rs index c452858775f1e..c207186d0a9dc 100644 --- a/crates/uv-resolver/src/prerelease.rs +++ b/crates/uv-resolver/src/prerelease.rs @@ -1,9 +1,9 @@ use uv_pypi_types::RequirementSource; -use uv_normalize::PackageName; - use crate::resolver::ForkSet; use crate::{DependencyMode, Manifest, ResolverMarkers}; +use uv_normalize::PackageName; +use uv_pep440::Operator; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] @@ -84,6 +84,9 @@ impl PrereleaseStrategy { if specifier .iter() + .filter(|spec| { + !matches!(spec.operator(), Operator::NotEqual | Operator::NotEqualStar) + }) .any(uv_pep440::VersionSpecifier::any_prerelease) { packages.add(&requirement, ()); diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 964542dfe2da9..68239d83b5ce2 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -12418,3 +12418,39 @@ fn prune_unreachable() -> Result<()> { Ok(()) } + +#[test] +fn negation_not_imply_prerelease() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("flask<2.0.1, !=2.0.0rc1")?; + uv_snapshot!(context + .pip_compile() + .arg("requirements.in"), @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 + click==8.1.7 + # via flask + flask==2.0.0 + # via -r requirements.in + itsdangerous==2.1.2 + # via flask + jinja2==3.1.3 + # via flask + markupsafe==2.1.5 + # via + # jinja2 + # werkzeug + werkzeug==3.0.1 + # via flask + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + Ok(()) +}