mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Warn users on ~=
python version specifier (#14008)
Close #7426 ## Summary Picking up on #8284, I noticed that the `requires_python` object already has its specifiers canonicalized in the `intersection` method, meaning `~=3.12` is converted to `>=3.12, <4`. To fix this, we check and warn in `intersection`. ## Test Plan Used the same tests from #8284.
This commit is contained in:
parent
6a5d2f1ec4
commit
eab938b7b4
4 changed files with 97 additions and 15 deletions
|
@ -5,10 +5,11 @@ use version_ranges::Ranges;
|
||||||
use uv_distribution_filename::WheelFilename;
|
use uv_distribution_filename::WheelFilename;
|
||||||
use uv_pep440::{
|
use uv_pep440::{
|
||||||
LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
|
LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
|
||||||
release_specifiers_to_ranges,
|
release_specifier_to_range, release_specifiers_to_ranges,
|
||||||
};
|
};
|
||||||
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
|
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
|
||||||
use uv_platform_tags::{AbiTag, LanguageTag};
|
use uv_platform_tags::{AbiTag, LanguageTag};
|
||||||
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
/// The `Requires-Python` requirement specifier.
|
/// The `Requires-Python` requirement specifier.
|
||||||
///
|
///
|
||||||
|
@ -66,15 +67,28 @@ impl RequiresPython {
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
// Convert to PubGrub range and perform an intersection.
|
// Convert to PubGrub range and perform an intersection.
|
||||||
let range = specifiers
|
let range = specifiers
|
||||||
.into_iter()
|
.map(|specs| {
|
||||||
.map(|specifier| release_specifiers_to_ranges(specifier.clone()))
|
// Warn if there’s exactly one `~=` specifier without a patch.
|
||||||
.fold(None, |range: Option<Ranges<Version>>, requires_python| {
|
if let [spec] = &specs[..] {
|
||||||
if let Some(range) = range {
|
if spec.is_tilde_without_patch() {
|
||||||
Some(range.intersection(&requires_python))
|
if let Some((lo_b, hi_b)) = release_specifier_to_range(spec.clone())
|
||||||
} else {
|
.bounding_range()
|
||||||
Some(requires_python)
|
.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())
|
||||||
|
})
|
||||||
|
.reduce(|acc, r| acc.intersection(&r))?;
|
||||||
|
|
||||||
// If the intersection is empty, return `None`.
|
// If the intersection is empty, return `None`.
|
||||||
if range.is_empty() {
|
if range.is_empty() {
|
||||||
|
|
|
@ -130,11 +130,10 @@ impl From<VersionSpecifier> for Ranges<Version> {
|
||||||
///
|
///
|
||||||
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
|
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
|
||||||
pub fn release_specifiers_to_ranges(specifiers: VersionSpecifiers) -> Ranges<Version> {
|
pub fn release_specifiers_to_ranges(specifiers: VersionSpecifiers) -> Ranges<Version> {
|
||||||
let mut range = Ranges::full();
|
specifiers
|
||||||
for specifier in specifiers {
|
.into_iter()
|
||||||
range = range.intersection(&release_specifier_to_range(specifier));
|
.map(release_specifier_to_range)
|
||||||
}
|
.fold(Ranges::full(), |acc, range| acc.intersection(&range))
|
||||||
range
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using release-only
|
/// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using release-only
|
||||||
|
|
|
@ -416,7 +416,7 @@ impl VersionSpecifier {
|
||||||
&self.operator
|
&self.operator
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the version, e.g. `<=` in `<= 2.0.0`
|
/// Get the version, e.g. `2.0.0` in `<= 2.0.0`
|
||||||
pub fn version(&self) -> &Version {
|
pub fn version(&self) -> &Version {
|
||||||
&self.version
|
&self.version
|
||||||
}
|
}
|
||||||
|
@ -615,6 +615,11 @@ impl VersionSpecifier {
|
||||||
| Operator::NotEqual => false,
|
| 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 {
|
impl FromStr for VersionSpecifier {
|
||||||
|
|
|
@ -4535,6 +4535,70 @@ fn lock_requires_python_exact() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lock a requirement from PyPI with a compatible release Python bound.
|
||||||
|
#[cfg(feature = "python-patch")]
|
||||||
|
#[test]
|
||||||
|
fn lock_requires_python_compatible_specifier() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.13.0");
|
||||||
|
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "warehouse"
|
||||||
|
version = "1.0.0"
|
||||||
|
requires-python = "~=3.13"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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?
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "warehouse"
|
||||||
|
version = "1.0.0"
|
||||||
|
requires-python = "~=3.13, <3.14"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
");
|
||||||
|
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "warehouse"
|
||||||
|
version = "1.0.0"
|
||||||
|
requires-python = "~=3.13.0"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Fork, even with a single dependency, if the minimum Python version is increased.
|
/// Fork, even with a single dependency, if the minimum Python version is increased.
|
||||||
#[test]
|
#[test]
|
||||||
fn lock_requires_python_fork() -> Result<()> {
|
fn lock_requires_python_fork() -> Result<()> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue