Infer target-version from project metadata (#3470)

* Infer target-version from project metadata

* Fix requires-python with ">=3.8.16"

* Load requires-python at runtime

* Use upstream VersionSpecifiers

* Add debug information when parsing ruff.toml

* Display debug only if target_version is not set

* Bump pep440-rs to add impl Error for Pep440Error
This commit is contained in:
Jonathan Plasse 2023-03-13 18:16:01 +01:00 committed by GitHub
parent 3a5fbd6d74
commit b540407b74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 151 additions and 7 deletions

26
Cargo.lock generated
View file

@ -1536,6 +1536,18 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739" checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739"
[[package]]
name = "pep440_rs"
version = "0.2.0"
source = "git+https://github.com/konstin/pep440-rs.git?rev=a8fef4ec47f4c25b070b39cdbe6a0b9847e49941#a8fef4ec47f4c25b070b39cdbe6a0b9847e49941"
dependencies = [
"lazy_static",
"regex",
"serde",
"tracing",
"unicode-width",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.2.0" version = "2.2.0"
@ -1986,6 +1998,8 @@ dependencies = [
"once_cell", "once_cell",
"path-absolutize", "path-absolutize",
"pathdiff", "pathdiff",
"pep440_rs",
"pretty_assertions",
"regex", "regex",
"result-like", "result-like",
"ruff_cache", "ruff_cache",
@ -2841,9 +2855,21 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"pin-project-lite", "pin-project-lite",
"tracing-attributes",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-attributes"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.30" version = "0.1.30"

View file

@ -46,8 +46,15 @@ fn main() -> Result<()> {
.map(|tool| ExternalConfig { .map(|tool| ExternalConfig {
black: tool.black.as_ref(), black: tool.black.as_ref(),
isort: tool.isort.as_ref(), isort: tool.isort.as_ref(),
..Default::default()
}) })
.unwrap_or_default(); .unwrap_or_default();
let external_config = ExternalConfig {
project: pyproject
.as_ref()
.and_then(|pyproject| pyproject.project.as_ref()),
..external_config
};
// Create Ruff's pyproject.toml section. // Create Ruff's pyproject.toml section.
let pyproject = flake8_to_ruff::convert(&config, &external_config, args.plugin)?; let pyproject = flake8_to_ruff::convert(&config, &external_config, args.plugin)?;

View file

@ -47,6 +47,9 @@ path-absolutize = { workspace = true, features = [
"use_unix_paths_on_wasm", "use_unix_paths_on_wasm",
] } ] }
pathdiff = { version = "0.2.1" } pathdiff = { version = "0.2.1" }
pep440_rs = { git = "https://github.com/konstin/pep440-rs.git", features = [
"serde",
], rev = "a8fef4ec47f4c25b070b39cdbe6a0b9847e49941" }
regex = { workspace = true } regex = { workspace = true }
result-like = { version = "0.4.6" } result-like = { version = "0.4.6" }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
@ -64,9 +67,10 @@ thiserror = { version = "1.0.38" }
toml = { workspace = true } toml = { workspace = true }
[dev-dependencies] [dev-dependencies]
insta = { workspace = true, features = ["yaml", "redactions"] }
test-case = { workspace = true }
criterion = { version = "0.4.0" } criterion = { version = "0.4.0" }
insta = { workspace = true, features = ["yaml", "redactions"] }
pretty_assertions = "1.3.0"
test-case = { workspace = true }
[features] [features]

View file

@ -20,6 +20,7 @@ use crate::rules::{
}; };
use crate::settings::options::Options; use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject; use crate::settings::pyproject::Pyproject;
use crate::settings::types::PythonVersion;
use crate::warn_user; use crate::warn_user;
const DEFAULT_SELECTORS: &[RuleSelector] = &[ const DEFAULT_SELECTORS: &[RuleSelector] = &[
@ -424,6 +425,15 @@ pub fn convert(
} }
} }
if let Some(project) = &external_config.project {
if let Some(requires_python) = &project.requires_python {
if options.target_version.is_none() {
options.target_version =
PythonVersion::get_minimum_supported_version(requires_python);
}
}
}
// Create the pyproject.toml. // Create the pyproject.toml.
Ok(Pyproject::new(options)) Ok(Pyproject::new(options))
} }
@ -439,13 +449,17 @@ fn resolve_select(plugins: &[Plugin]) -> HashSet<RuleSelector> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
use pep440_rs::VersionSpecifiers;
use pretty_assertions::assert_eq;
use super::super::plugin::Plugin; use super::super::plugin::Plugin;
use super::convert; use super::convert;
use crate::flake8_to_ruff::converter::DEFAULT_SELECTORS; use crate::flake8_to_ruff::converter::DEFAULT_SELECTORS;
use crate::flake8_to_ruff::pep621::Project;
use crate::flake8_to_ruff::ExternalConfig; use crate::flake8_to_ruff::ExternalConfig;
use crate::registry::Linter; use crate::registry::Linter;
use crate::rule_selector::RuleSelector; use crate::rule_selector::RuleSelector;
@ -453,6 +467,7 @@ mod tests {
use crate::rules::{flake8_quotes, pydocstyle}; use crate::rules::{flake8_quotes, pydocstyle};
use crate::settings::options::Options; use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject; use crate::settings::pyproject::Pyproject;
use crate::settings::types::PythonVersion;
fn default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> Options { fn default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> Options {
Options { Options {
@ -609,4 +624,25 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn it_converts_project_requires_python() -> Result<()> {
let actual = convert(
&HashMap::from([("flake8".to_string(), HashMap::default())]),
&ExternalConfig {
project: Some(&Project {
requires_python: Some(VersionSpecifiers::from_str(">=3.8.16, <3.11")?),
}),
..ExternalConfig::default()
},
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
target_version: Some(PythonVersion::Py38),
..default_options([])
});
assert_eq!(actual, expected);
Ok(())
}
} }

View file

@ -1,8 +1,10 @@
use super::black::Black; use super::black::Black;
use super::isort::Isort; use super::isort::Isort;
use super::pep621::Project;
#[derive(Default)] #[derive(Default)]
pub struct ExternalConfig<'a> { pub struct ExternalConfig<'a> {
pub black: Option<&'a Black>, pub black: Option<&'a Black>,
pub isort: Option<&'a Isort>, pub isort: Option<&'a Isort>,
pub project: Option<&'a Project>,
} }

View file

@ -3,6 +3,7 @@ mod converter;
mod external_config; mod external_config;
mod isort; mod isort;
mod parser; mod parser;
pub mod pep621;
mod plugin; mod plugin;
mod pyproject; mod pyproject;

View file

@ -0,0 +1,10 @@
//! Extract PEP 621 configuration settings from a pyproject.toml.
use pep440_rs::VersionSpecifiers;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Project {
#[serde(alias = "requires-python", alias = "requires_python")]
pub requires_python: Option<VersionSpecifiers>,
}

View file

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use super::black::Black; use super::black::Black;
use super::isort::Isort; use super::isort::Isort;
use super::pep621::Project;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tools { pub struct Tools {
@ -15,6 +16,7 @@ pub struct Tools {
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pyproject { pub struct Pyproject {
pub tool: Option<Tools>, pub tool: Option<Tools>,
pub project: Option<Project>,
} }
pub fn parse<P: AsRef<Path>>(path: P) -> Result<Pyproject> { pub fn parse<P: AsRef<Path>>(path: P) -> Result<Pyproject> {

View file

@ -3,18 +3,22 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::Result;
use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::flake8_to_ruff::pep621::Project;
use crate::settings::options::Options; use crate::settings::options::Options;
use crate::settings::types::PythonVersion;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Tools { struct Tools {
ruff: Option<Options>, ruff: Option<Options>,
} }
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Pyproject { pub struct Pyproject {
tool: Option<Tools>, tool: Option<Tools>,
project: Option<Project>,
} }
impl Pyproject { impl Pyproject {
@ -23,6 +27,7 @@ impl Pyproject {
tool: Some(Tools { tool: Some(Tools {
ruff: Some(options), ruff: Some(options),
}), }),
project: None,
} }
} }
} }
@ -114,12 +119,27 @@ pub fn find_user_settings_toml() -> Option<PathBuf> {
pub fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> { pub fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
if path.as_ref().ends_with("pyproject.toml") { if path.as_ref().ends_with("pyproject.toml") {
let pyproject = parse_pyproject_toml(&path)?; let pyproject = parse_pyproject_toml(&path)?;
Ok(pyproject let mut ruff = pyproject
.tool .tool
.and_then(|tool| tool.ruff) .and_then(|tool| tool.ruff)
.unwrap_or_default()) .unwrap_or_default();
if ruff.target_version.is_none() {
if let Some(project) = pyproject.project {
if let Some(requires_python) = project.requires_python {
ruff.target_version =
PythonVersion::get_minimum_supported_version(&requires_python);
}
}
}
Ok(ruff)
} else { } else {
parse_ruff_toml(path) let ruff = parse_ruff_toml(path);
if let Ok(ruff) = &ruff {
if ruff.target_version.is_none() {
debug!("`project.requires_python` in `pyproject.toml` will not be used to set `target_version` when using `ruff.toml`.");
}
}
ruff
} }
} }

View file

@ -2,22 +2,37 @@ use std::hash::{Hash, Hasher};
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use std::string::ToString;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use clap::ValueEnum; use clap::ValueEnum;
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
use pep440_rs::{Version as Pep440Version, VersionSpecifiers};
use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_macros::CacheKey; use ruff_macros::CacheKey;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{de, Deserialize, Deserializer, Serialize}; use serde::{de, Deserialize, Deserializer, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rule_selector::RuleSelector; use crate::rule_selector::RuleSelector;
use crate::{fs, warn_user_once}; use crate::{fs, warn_user_once};
#[derive( #[derive(
Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, JsonSchema, CacheKey, Clone,
Copy,
Debug,
PartialOrd,
Ord,
PartialEq,
Eq,
Serialize,
Deserialize,
JsonSchema,
CacheKey,
EnumIter,
)] )]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum PythonVersion { pub enum PythonVersion {
@ -50,6 +65,13 @@ impl FromStr for PythonVersion {
} }
} }
impl From<PythonVersion> for Pep440Version {
fn from(version: PythonVersion) -> Self {
let (major, minor) = version.as_tuple();
Self::from_str(&format!("{major}.{minor}.100")).unwrap()
}
}
impl PythonVersion { impl PythonVersion {
pub const fn as_tuple(&self) -> (u32, u32) { pub const fn as_tuple(&self) -> (u32, u32) {
match self { match self {
@ -60,6 +82,20 @@ impl PythonVersion {
Self::Py311 => (3, 11), Self::Py311 => (3, 11),
} }
} }
pub fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option<Self> {
let mut minimum_version = None;
for python_version in PythonVersion::iter() {
if requires_version
.iter()
.all(|specifier| specifier.contains(&python_version.into()))
{
minimum_version = Some(python_version);
break;
}
}
minimum_version
}
} }
#[derive(Debug, Clone, CacheKey, PartialEq, PartialOrd, Eq, Ord)] #[derive(Debug, Clone, CacheKey, PartialEq, PartialOrd, Eq, Ord)]