Omit compatible release desugaring for pre-release hints (#19267)

## Summary

`~=3.6` gets expanded to `>=3.6,<4.dev0`, which causes us to show a
pre-release hint. We now omit such ranges. This could lead to false
negatives (i.e., if a user _actually_ requested `>=3.6,<4.dev0`), but I
think this is the right tradeoff.

Closes https://github.com/astral-sh/uv/issues/19266.
This commit is contained in:
Charlie Marsh
2026-05-04 20:11:22 -04:00
committed by GitHub
parent fe7137f253
commit 77f2710639
2 changed files with 50 additions and 1 deletions
+11 -1
View File
@@ -1157,7 +1157,9 @@ impl PubGrubReportFormatter<'_> {
let is_pre2 = match end {
Bound::Included(version) => version.any_prerelease(),
Bound::Excluded(version) => version.any_prerelease(),
Bound::Excluded(version) => {
version.any_prerelease() && !is_compatible_release_upper_bound(version)
}
Bound::Unbounded => false,
};
if is_pre2 {
@@ -1209,6 +1211,14 @@ impl PubGrubReportFormatter<'_> {
}
}
/// Return `true` for the excluded `.dev0` upper bounds used to desugar compatible releases.
///
/// For example, `~=3.6` becomes `>=3.6,<4.dev0`. The `<4.dev0` boundary preserves PEP 440's
/// ordering semantics, but it does not mean the user requested pre-releases.
fn is_compatible_release_upper_bound(version: &Version) -> bool {
version.dev() == Some(0) && !version.is_pre() && !version.is_post() && !version.is_local()
}
#[derive(Debug, Clone)]
pub(crate) struct ExcludeNewerVersionDetail {
version: Version,
+39
View File
@@ -34018,6 +34018,45 @@ fn lock_exclude_newer_hint_pinned_version() -> Result<()> {
Ok(())
}
/// Test that `~=` requirements don't emit a misleading pre-release hint when the compatible
/// release upper bound is excluded by `--exclude-newer`.
///
/// See: <https://github.com/astral-sh/uv/issues/19266>
#[test]
fn lock_exclude_newer_hint_compatible_release() -> Result<()> {
let context = uv_test::test_context!("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig~=2.0"]
"#,
)?;
// Use a cutoff that excludes `iniconfig 2.0.0` (2023-01-07) but not `1.1.1` (2020-10-18).
uv_snapshot!(context.filters(), context
.lock()
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
.arg("--exclude-newer")
.arg("2022-01-01T00:00:00Z"), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No solution found when resolving dependencies:
Because only iniconfig<=1.1.1 is available and your project depends on iniconfig>=2.0,<3.dev0, we can conclude that your project's requirements are unsatisfiable.
hint: `iniconfig` was filtered by `exclude-newer` to only include packages uploaded before 2022-01-01T00:00:00Z. The latest version satisfying the requirement is v2.0.0, published at 2023-01-07T11:08:09.864Z. Consider using `exclude-newer-package` to override the cutoff for this package.
");
Ok(())
}
/// Test that lockfile validation includes explicit indexes from path dependencies.
/// <https://github.com/astral-sh/uv/issues/11419>
#[tokio::test]