Make requires-python inference robust to == (#12091)

## Summary

Instead of using a high patch version, attempt to detect the
minimum-supported minor.

Closes #12088.
This commit is contained in:
Charlie Marsh 2024-06-28 09:38:17 -04:00 committed by GitHub
parent 434ce307a7
commit c326778652
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 217 additions and 12 deletions

View file

@ -1618,3 +1618,189 @@ print(
Ok(()) Ok(())
} }
/// Infer `3.11` from `requires-python` in `pyproject.toml`.
#[test]
fn requires_python() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("pyproject.toml");
fs::write(
&ruff_toml,
r#"[project]
requires-python = ">= 3.11"
[tool.ruff.lint]
select = ["UP006"]
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.args(["--stdin-filename", "test.py"])
.arg("-")
.pass_stdin(r#"from typing import List; foo: List[int]"#), @r###"
success: false
exit_code: 1
----- stdout -----
test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
let pyproject_toml = tempdir.path().join("pyproject.toml");
fs::write(
&pyproject_toml,
r#"[project]
requires-python = ">= 3.8"
[tool.ruff.lint]
select = ["UP006"]
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&pyproject_toml)
.args(["--stdin-filename", "test.py"])
.arg("-")
.pass_stdin(r#"from typing import List; foo: List[int]"#), @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
});
Ok(())
}
/// Infer `3.11` from `requires-python` in `pyproject.toml`.
#[test]
fn requires_python_patch() -> Result<()> {
let tempdir = TempDir::new()?;
let pyproject_toml = tempdir.path().join("pyproject.toml");
fs::write(
&pyproject_toml,
r#"[project]
requires-python = ">= 3.11.4"
[tool.ruff.lint]
select = ["UP006"]
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&pyproject_toml)
.args(["--stdin-filename", "test.py"])
.arg("-")
.pass_stdin(r#"from typing import List; foo: List[int]"#), @r###"
success: false
exit_code: 1
----- stdout -----
test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}
/// Infer `3.11` from `requires-python` in `pyproject.toml`.
#[test]
fn requires_python_equals() -> Result<()> {
let tempdir = TempDir::new()?;
let pyproject_toml = tempdir.path().join("pyproject.toml");
fs::write(
&pyproject_toml,
r#"[project]
requires-python = "== 3.11"
[tool.ruff.lint]
select = ["UP006"]
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&pyproject_toml)
.args(["--stdin-filename", "test.py"])
.arg("-")
.pass_stdin(r#"from typing import List; foo: List[int]"#), @r###"
success: false
exit_code: 1
----- stdout -----
test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}
/// Infer `3.11` from `requires-python` in `pyproject.toml`.
#[test]
fn requires_python_equals_patch() -> Result<()> {
let tempdir = TempDir::new()?;
let pyproject_toml = tempdir.path().join("pyproject.toml");
fs::write(
&pyproject_toml,
r#"[project]
requires-python = "== 3.11.4"
[tool.ruff.lint]
select = ["UP006"]
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&pyproject_toml)
.args(["--stdin-filename", "test.py"])
.arg("-")
.pass_stdin(r#"from typing import List; foo: List[int]"#), @r###"
success: false
exit_code: 1
----- stdout -----
test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}

View file

@ -9,7 +9,8 @@ use std::string::ToString;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use pep440_rs::{Version as Pep440Version, VersionSpecifier, VersionSpecifiers}; use log::debug;
use pep440_rs::{Operator, Version as Pep440Version, Version, VersionSpecifier, VersionSpecifiers};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{de, Deserialize, Deserializer, Serialize}; use serde::{de, Deserialize, Deserializer, Serialize};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
@ -59,7 +60,7 @@ pub enum PythonVersion {
impl From<PythonVersion> for Pep440Version { impl From<PythonVersion> for Pep440Version {
fn from(version: PythonVersion) -> Self { fn from(version: PythonVersion) -> Self {
let (major, minor) = version.as_tuple(); let (major, minor) = version.as_tuple();
Self::from_str(&format!("{major}.{minor}.100")).unwrap() Self::new([u64::from(major), u64::from(minor)])
} }
} }
@ -89,18 +90,36 @@ impl PythonVersion {
self.as_tuple().1 self.as_tuple().1
} }
/// Infer the minimum supported [`PythonVersion`] from a `requires-python` specifier.
pub fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option<Self> { pub fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option<Self> {
let mut minimum_version = None; /// Truncate a version to its major and minor components.
for python_version in PythonVersion::iter() { fn major_minor(version: &Version) -> Option<Version> {
if requires_version let major = version.release().first()?;
.iter() let minor = version.release().get(1)?;
.all(|specifier| specifier.contains(&python_version.into())) Some(Version::new([major, minor]))
{
minimum_version = Some(python_version);
break;
}
} }
minimum_version
// Extract the minimum supported version from the specifiers.
let minimum_version = requires_version
.iter()
.filter(|specifier| {
matches!(
specifier.operator(),
Operator::Equal
| Operator::EqualStar
| Operator::ExactEqual
| Operator::TildeEqual
| Operator::GreaterThan
| Operator::GreaterThanEqual
)
})
.filter_map(|specifier| major_minor(specifier.version()))
.min()?;
debug!("Detected minimum supported `requires-python` version: {minimum_version}");
// Find the Python version that matches the minimum supported version.
PythonVersion::iter().find(|version| Version::from(*version) == minimum_version)
} }
/// Return `true` if the current version supports [PEP 701]. /// Return `true` if the current version supports [PEP 701].