mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Update the tilde version specifier warning to include more context (#14335)
Follows https://github.com/astral-sh/uv/pull/14008
This commit is contained in:
parent
a7aa46acc5
commit
43f67a4a4c
5 changed files with 115 additions and 33 deletions
|
@ -5,11 +5,10 @@ use version_ranges::Ranges;
|
|||
use uv_distribution_filename::WheelFilename;
|
||||
use uv_pep440::{
|
||||
LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
|
||||
release_specifier_to_range, release_specifiers_to_ranges,
|
||||
release_specifiers_to_ranges,
|
||||
};
|
||||
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
|
||||
use uv_platform_tags::{AbiTag, LanguageTag};
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
/// The `Requires-Python` requirement specifier.
|
||||
///
|
||||
|
@ -67,27 +66,7 @@ impl RequiresPython {
|
|||
) -> Option<Self> {
|
||||
// Convert to PubGrub range and perform an intersection.
|
||||
let range = specifiers
|
||||
.map(|specs| {
|
||||
// Warn if there’s exactly one `~=` specifier without a patch.
|
||||
if let [spec] = &specs[..] {
|
||||
if spec.is_tilde_without_patch() {
|
||||
if let Some((lo_b, hi_b)) = release_specifier_to_range(spec.clone(), false)
|
||||
.bounding_range()
|
||||
.map(|(l, u)| (l.cloned(), u.cloned()))
|
||||
{
|
||||
let lo_spec = LowerBound::new(lo_b).specifier().unwrap();
|
||||
let hi_spec = UpperBound::new(hi_b).specifier().unwrap();
|
||||
warn_user_once!(
|
||||
"The release specifier (`{spec}`) contains a compatible release \
|
||||
match without a patch version. This will be interpreted as \
|
||||
`{lo_spec}, {hi_spec}`. Did you mean `{spec}.0` to freeze the \
|
||||
minor version?"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
release_specifiers_to_ranges(specs.clone())
|
||||
})
|
||||
.map(|specs| release_specifiers_to_ranges(specs.clone()))
|
||||
.reduce(|acc, r| acc.intersection(&r))?;
|
||||
|
||||
// If the intersection is empty, return `None`.
|
||||
|
|
|
@ -34,7 +34,7 @@ pub use {
|
|||
VersionPatternParseError,
|
||||
},
|
||||
version_specifier::{
|
||||
VersionSpecifier, VersionSpecifierBuildError, VersionSpecifiers,
|
||||
TildeVersionSpecifier, VersionSpecifier, VersionSpecifierBuildError, VersionSpecifiers,
|
||||
VersionSpecifiersParseError,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -665,11 +665,6 @@ impl VersionSpecifier {
|
|||
| Operator::NotEqual => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this is a `~=` specifier without a patch version (e.g. `~=3.11`).
|
||||
pub fn is_tilde_without_patch(&self) -> bool {
|
||||
self.operator == Operator::TildeEqual && self.version.release().len() == 2
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for VersionSpecifier {
|
||||
|
@ -893,6 +888,90 @@ pub(crate) fn parse_version_specifiers(
|
|||
Ok(version_ranges)
|
||||
}
|
||||
|
||||
/// A simple `~=` version specifier with a major, minor and (optional) patch version, e.g., `~=3.13`
|
||||
/// or `~=3.13.0`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TildeVersionSpecifier<'a> {
|
||||
inner: Cow<'a, VersionSpecifier>,
|
||||
}
|
||||
|
||||
impl<'a> TildeVersionSpecifier<'a> {
|
||||
/// Create a new [`TildeVersionSpecifier`] from a [`VersionSpecifier`] value.
|
||||
///
|
||||
/// If a [`Operator::TildeEqual`] is not used, or the version includes more than minor and patch
|
||||
/// segments, this will return [`None`].
|
||||
pub fn from_specifier(specifier: VersionSpecifier) -> Option<TildeVersionSpecifier<'a>> {
|
||||
TildeVersionSpecifier::new(Cow::Owned(specifier))
|
||||
}
|
||||
|
||||
/// Create a new [`TildeVersionSpecifier`] from a [`VersionSpecifier`] reference.
|
||||
///
|
||||
/// See [`TildeVersionSpecifier::from_specifier`].
|
||||
pub fn from_specifier_ref(
|
||||
specifier: &'a VersionSpecifier,
|
||||
) -> Option<TildeVersionSpecifier<'a>> {
|
||||
TildeVersionSpecifier::new(Cow::Borrowed(specifier))
|
||||
}
|
||||
|
||||
fn new(specifier: Cow<'a, VersionSpecifier>) -> Option<Self> {
|
||||
if specifier.operator != Operator::TildeEqual {
|
||||
return None;
|
||||
}
|
||||
if specifier.version().release().len() < 2 || specifier.version().release().len() > 3 {
|
||||
return None;
|
||||
}
|
||||
if specifier.version().any_prerelease()
|
||||
|| specifier.version().is_local()
|
||||
|| specifier.version().is_post()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(Self { inner: specifier })
|
||||
}
|
||||
|
||||
/// Whether a patch version is present in this tilde version specifier.
|
||||
pub fn has_patch(&self) -> bool {
|
||||
self.inner.version.release().len() == 3
|
||||
}
|
||||
|
||||
/// Construct the lower and upper bounding version specifiers for this tilde version specifier,
|
||||
/// e.g., for `~=3.13` this would return `>=3.13` and `<4` and for `~=3.13.0` it would
|
||||
/// return `>=3.13.0` and `<3.14`.
|
||||
pub fn bounding_specifiers(&self) -> (VersionSpecifier, VersionSpecifier) {
|
||||
let release = self.inner.version().release();
|
||||
let lower = self.inner.version.clone();
|
||||
let upper = if self.has_patch() {
|
||||
Version::new([release[0], release[1] + 1])
|
||||
} else {
|
||||
Version::new([release[0] + 1])
|
||||
};
|
||||
(
|
||||
VersionSpecifier::greater_than_equal_version(lower),
|
||||
VersionSpecifier::less_than_version(upper),
|
||||
)
|
||||
}
|
||||
|
||||
/// Construct a new tilde `VersionSpecifier` with the given patch version appended.
|
||||
pub fn with_patch_version(&self, patch: u64) -> TildeVersionSpecifier {
|
||||
let mut release = self.inner.version.release().to_vec();
|
||||
if self.has_patch() {
|
||||
release.pop();
|
||||
}
|
||||
release.push(patch);
|
||||
TildeVersionSpecifier::from_specifier(
|
||||
VersionSpecifier::from_version(Operator::TildeEqual, Version::new(release))
|
||||
.expect("We should always derive a valid new version specifier"),
|
||||
)
|
||||
.expect("We should always derive a new tilde version specifier")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TildeVersionSpecifier<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cmp::Ordering, str::FromStr};
|
||||
|
|
|
@ -25,7 +25,7 @@ use uv_fs::{CWD, LockedFile, Simplified};
|
|||
use uv_git::ResolvedRepositoryReference;
|
||||
use uv_installer::{SatisfiesResult, SitePackages};
|
||||
use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers};
|
||||
use uv_pep508::MarkerTreeContents;
|
||||
use uv_pypi_types::{ConflictPackage, ConflictSet, Conflicts};
|
||||
use uv_python::{
|
||||
|
@ -421,6 +421,30 @@ pub(crate) fn find_requires_python(
|
|||
if requires_python.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
for ((package, group), specifiers) in &requires_python {
|
||||
if let [spec] = &specifiers[..] {
|
||||
if let Some(spec) = TildeVersionSpecifier::from_specifier_ref(spec) {
|
||||
if spec.has_patch() {
|
||||
continue;
|
||||
}
|
||||
let (lower, upper) = spec.bounding_specifiers();
|
||||
let spec_0 = spec.with_patch_version(0);
|
||||
let (lower_0, upper_0) = spec_0.bounding_specifiers();
|
||||
warn_user_once!(
|
||||
"The `requires-python` specifier (`{spec}`) in `{package}{group}` \
|
||||
uses the tilde specifier (`~=`) without a patch version. This will be \
|
||||
interpreted as `{lower}, {upper}`. Did you mean `{spec_0}` to constrain the \
|
||||
version as `{lower_0}, {upper_0}`? We recommend only using \
|
||||
the tilde specifier with a patch version to avoid ambiguity.",
|
||||
group = if let Some(group) = group {
|
||||
format!(":{group}")
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
match RequiresPython::intersection(requires_python.iter().map(|(.., specifiers)| specifiers)) {
|
||||
Some(requires_python) => Ok(Some(requires_python)),
|
||||
None => Err(ProjectError::DisjointRequiresPython(requires_python)),
|
||||
|
|
|
@ -4551,15 +4551,15 @@ fn lock_requires_python_compatible_specifier() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: The release specifier (`~=3.13`) contains a compatible release match without a patch version. This will be interpreted as `>=3.13, <4`. Did you mean `~=3.13.0` to freeze the minor version?
|
||||
warning: The `requires-python` specifier (`~=3.13`) in `warehouse` uses the tilde specifier (`~=`) without a patch version. This will be interpreted as `>=3.13, <4`. Did you mean `~=3.13.0` to constrain the version as `>=3.13.0, <3.14`? We recommend only using the tilde specifier with a patch version to avoid ambiguity.
|
||||
Resolved 1 package in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue