mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Avoid rendering desugared prefix matches in error messages (#14195)
## Summary When the user provides a requirement like `==2.4.*`, we desugar that to `>=2.4.dev0,<2.5.dev0`. These bounds then appear in error messages, and worse, they also trick the error message reporter into thinking that the user asked for a pre-release. This PR adds logic to convert to the more-concise `==2.4.*` representation when possible. We could probably do a similar thing for the compatible release operator (`~=`). Closes https://github.com/astral-sh/uv/issues/14177. Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
f892b8564f
commit
4eef79e5e8
3 changed files with 115 additions and 5 deletions
|
@ -1213,6 +1213,69 @@ impl SentinelRange<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A prefix match, e.g., `==2.4.*`, which is desugared to a range like `>=2.4.dev0,<2.5.dev0`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct PrefixMatch<'a> {
|
||||
version: &'a Version,
|
||||
}
|
||||
|
||||
impl<'a> PrefixMatch<'a> {
|
||||
/// Determine whether a given range is equivalent to a prefix match (e.g., `==2.4.*`).
|
||||
///
|
||||
/// Prefix matches are desugared to (e.g.) `>=2.4.dev0,<2.5.dev0`, but we want to render them
|
||||
/// as `==2.4.*` in error messages.
|
||||
pub(crate) fn from_range(lower: &'a Bound<Version>, upper: &'a Bound<Version>) -> Option<Self> {
|
||||
let Bound::Included(lower) = lower else {
|
||||
return None;
|
||||
};
|
||||
let Bound::Excluded(upper) = upper else {
|
||||
return None;
|
||||
};
|
||||
if lower.is_pre() || lower.is_post() || lower.is_local() {
|
||||
return None;
|
||||
}
|
||||
if upper.is_pre() || upper.is_post() || upper.is_local() {
|
||||
return None;
|
||||
}
|
||||
if lower.dev() != Some(0) {
|
||||
return None;
|
||||
}
|
||||
if upper.dev() != Some(0) {
|
||||
return None;
|
||||
}
|
||||
if lower.release().len() != upper.release().len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// All segments should be the same, except the last one, which should be incremented.
|
||||
let num_segments = lower.release().len();
|
||||
for (i, (lower, upper)) in lower
|
||||
.release()
|
||||
.iter()
|
||||
.zip(upper.release().iter())
|
||||
.enumerate()
|
||||
{
|
||||
if i == num_segments - 1 {
|
||||
if lower + 1 != *upper {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
if lower != upper {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(PrefixMatch { version: lower })
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PrefixMatch<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "=={}.*", self.version.only_release())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NoSolutionHeader {
|
||||
/// The [`ResolverEnvironment`] that caused the failure.
|
||||
|
|
|
@ -18,7 +18,7 @@ use uv_pep440::{Version, VersionSpecifiers};
|
|||
use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, Tags};
|
||||
|
||||
use crate::candidate_selector::CandidateSelector;
|
||||
use crate::error::ErrorTree;
|
||||
use crate::error::{ErrorTree, PrefixMatch};
|
||||
use crate::fork_indexes::ForkIndexes;
|
||||
use crate::fork_urls::ForkUrls;
|
||||
use crate::prerelease::AllowPrerelease;
|
||||
|
@ -944,17 +944,30 @@ impl PubGrubReportFormatter<'_> {
|
|||
hints: &mut IndexSet<PubGrubHint>,
|
||||
) {
|
||||
let any_prerelease = set.iter().any(|(start, end)| {
|
||||
// Ignore, e.g., `>=2.4.dev0,<2.5.dev0`, which is the desugared form of `==2.4.*`.
|
||||
if PrefixMatch::from_range(start, end).is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let is_pre1 = match start {
|
||||
Bound::Included(version) => version.any_prerelease(),
|
||||
Bound::Excluded(version) => version.any_prerelease(),
|
||||
Bound::Unbounded => false,
|
||||
};
|
||||
if is_pre1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let is_pre2 = match end {
|
||||
Bound::Included(version) => version.any_prerelease(),
|
||||
Bound::Excluded(version) => version.any_prerelease(),
|
||||
Bound::Unbounded => false,
|
||||
};
|
||||
is_pre1 || is_pre2
|
||||
if is_pre2 {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
if any_prerelease {
|
||||
|
@ -1928,11 +1941,11 @@ impl std::fmt::Display for PackageRange<'_> {
|
|||
PackageRangeKind::Available => write!(f, "are available:")?,
|
||||
}
|
||||
}
|
||||
for segment in &segments {
|
||||
for (lower, upper) in &segments {
|
||||
if segments.len() > 1 {
|
||||
write!(f, "\n ")?;
|
||||
}
|
||||
match segment {
|
||||
match (lower, upper) {
|
||||
(Bound::Unbounded, Bound::Unbounded) => match self.kind {
|
||||
PackageRangeKind::Dependency => write!(f, "{package}")?,
|
||||
PackageRangeKind::Compatibility => write!(f, "all versions of {package}")?,
|
||||
|
@ -1948,7 +1961,13 @@ impl std::fmt::Display for PackageRange<'_> {
|
|||
write!(f, "{package}>={v},<={b}")?;
|
||||
}
|
||||
}
|
||||
(Bound::Included(v), Bound::Excluded(b)) => write!(f, "{package}>={v},<{b}")?,
|
||||
(Bound::Included(v), Bound::Excluded(b)) => {
|
||||
if let Some(prefix) = PrefixMatch::from_range(lower, upper) {
|
||||
write!(f, "{package}{prefix}")?;
|
||||
} else {
|
||||
write!(f, "{package}>={v},<{b}")?;
|
||||
}
|
||||
}
|
||||
(Bound::Excluded(v), Bound::Unbounded) => write!(f, "{package}>{v}")?,
|
||||
(Bound::Excluded(v), Bound::Included(b)) => write!(f, "{package}>{v},<={b}")?,
|
||||
(Bound::Excluded(v), Bound::Excluded(b)) => write!(f, "{package}>{v},<{b}")?,
|
||||
|
|
|
@ -28452,3 +28452,31 @@ fn lock_with_index_trailing_slashes_in_lockfile() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_prefix_match() -> Result<()> {
|
||||
let context = TestContext::new("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 = ["anyio==5.4.*"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because only anyio<=4.3.0 is available and your project depends on anyio==5.4.*, we can conclude that your project's requirements are unsatisfiable.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue