Add a Flake8-to-Ruff configuration conversion tool (#527)

This commit is contained in:
Charlie Marsh 2022-10-31 11:34:40 -04:00 committed by GitHub
parent 062c41b6f5
commit 7e5e03fb15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 494 additions and 14 deletions

7
Cargo.lock generated
View File

@ -571,6 +571,12 @@ dependencies = [
"cache-padded", "cache-padded",
] ]
[[package]]
name = "configparser"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a"
[[package]] [[package]]
name = "console" name = "console"
version = "0.15.2" version = "0.15.2"
@ -2202,6 +2208,7 @@ dependencies = [
"codegen", "codegen",
"colored", "colored",
"common-path", "common-path",
"configparser",
"criterion", "criterion",
"dirs 4.0.0", "dirs 4.0.0",
"fern", "fern",

View File

@ -13,6 +13,7 @@ chrono = { version = "0.4.21" }
clap = { version = "4.0.1", features = ["derive"] } clap = { version = "4.0.1", features = ["derive"] }
colored = { version = "2.0.0" } colored = { version = "2.0.0" }
common-path = { version = "1.0.0" } common-path = { version = "1.0.0" }
configparser = { version = "3.0.2" }
dirs = { version = "4.0.0" } dirs = { version = "4.0.0" }
fern = { version = "0.6.1" } fern = { version = "0.6.1" }
filetime = { version = "0.2.17" } filetime = { version = "0.2.17" }

View File

@ -0,0 +1,35 @@
//! Utility to generate Ruff's pyproject.toml section from a Flake8 INI file.
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use configparser::ini::Ini;
use ruff::flake8_to_ruff;
#[derive(Parser)]
#[command(
about = "Convert an existing Flake8 configuration to Ruff.",
long_about = None
)]
struct Cli {
/// Path to the Flake8 configuration file (e.g., 'setup.cfg', 'tox.ini', or '.flake8').
#[arg(required = true)]
file: PathBuf,
}
fn main() -> Result<()> {
let cli = Cli::parse();
// Read the INI file.
let mut ini = Ini::new_cs();
ini.set_multiline(true);
let config = ini.load(cli.file).map_err(|msg| anyhow::anyhow!(msg))?;
// Create the pyproject.toml.
let pyproject = flake8_to_ruff::convert(config)?;
println!("{}", toml::to_string(&pyproject)?);
Ok(())
}

View File

@ -96,7 +96,7 @@ fn main() {
println!("//! File automatically generated by examples/generate_check_code_prefix.rs."); println!("//! File automatically generated by examples/generate_check_code_prefix.rs.");
println!(); println!();
println!("use serde::{{Deserialize, Serialize}};"); println!("use serde::{{Serialize, Deserialize}};");
println!("use strum_macros::EnumString;"); println!("use strum_macros::EnumString;");
println!(); println!();
println!("use crate::checks::CheckCode;"); println!("use crate::checks::CheckCode;");

View File

@ -1,11 +1,11 @@
//! File automatically generated by examples/generate_check_code_prefix.rs. //! File automatically generated by examples/generate_check_code_prefix.rs.
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum_macros::EnumString; use strum_macros::{AsRefStr, EnumString};
use crate::checks::CheckCode; use crate::checks::CheckCode;
#[derive(EnumString, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[derive(AsRefStr, EnumString, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub enum CheckCodePrefix { pub enum CheckCodePrefix {
A, A,
A0, A0,

View File

@ -2,14 +2,14 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")] #[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Quote { pub enum Quote {
Single, Single,
Double, Double,
} }
#[derive(Debug, PartialEq, Eq, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")] #[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options { pub struct Options {
pub inline_quotes: Option<Quote>, pub inline_quotes: Option<Quote>,

56
src/flake8_to_ruff/mod.rs Normal file
View File

@ -0,0 +1,56 @@
//! Utility to generate Ruff's pyproject.toml section from a Flake8 INI file.
use std::collections::HashMap;
use anyhow::Result;
use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
mod parser;
pub fn convert(config: HashMap<String, HashMap<String, Option<String>>>) -> Result<Pyproject> {
// Extract the Flake8 section.
let flake8 = config
.get("flake8")
.expect("Unable to find flake8 section in INI file.");
// Parse each supported option.
let mut options: Options = Default::default();
for (key, value) in flake8 {
match key.as_str() {
"line-length" | "line_length" => match value.clone().unwrap().parse::<usize>() {
Ok(line_length) => options.line_length = Some(line_length),
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
},
"select" => {
options.select = Some(parser::parse_prefix_codes(value.as_ref().unwrap()));
}
"extend-select" | "extend_select" => {
options.extend_select = parser::parse_prefix_codes(value.as_ref().unwrap());
}
"ignore" => {
options.ignore = parser::parse_prefix_codes(value.as_ref().unwrap());
}
"extend-ignore" | "extend_ignore" => {
options.extend_ignore = parser::parse_prefix_codes(value.as_ref().unwrap());
}
"exclude" => {
options.exclude = Some(parser::parse_strings(value.as_ref().unwrap()));
}
"extend-exclude" | "extend_exclude" => {
options.extend_exclude = parser::parse_strings(value.as_ref().unwrap());
}
"per-file-ignores" | "per_file_ignores" => {
match parser::parse_files_to_codes_mapping(value.as_ref().unwrap()) {
Ok(per_file_ignores) => options.per_file_ignores = per_file_ignores,
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
}
}
_ => eprintln!("Skipping unsupported property: {key}"),
}
}
// Create the pyproject.toml.
Ok(Pyproject::new(options))
}

View File

@ -0,0 +1,360 @@
use std::str::FromStr;
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::settings::types::StrCheckCodePair;
static COMMA_SEPARATED_LIST_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
/// Parse a comma-separated list of `CheckCodePrefix` values (e.g., "F401,E501").
pub fn parse_prefix_codes(value: &str) -> Vec<CheckCodePrefix> {
let mut codes: Vec<CheckCodePrefix> = vec![];
for code in COMMA_SEPARATED_LIST_RE.split(value) {
let code = code.trim();
if code.is_empty() {
continue;
}
if let Ok(code) = CheckCodePrefix::from_str(code) {
codes.push(code);
} else {
eprintln!("Unsupported prefix code: {code}");
}
}
codes
}
/// Parse a comma-separated list of strings (e.g., "__init__.py,__main__.py").
pub fn parse_strings(value: &str) -> Vec<String> {
COMMA_SEPARATED_LIST_RE
.split(value)
.map(|part| part.trim())
.filter(|part| !part.is_empty())
.map(String::from)
.collect()
}
#[derive(Debug)]
struct Token {
token_name: TokenType,
src: String,
}
#[derive(Debug)]
enum TokenType {
Code,
File,
Colon,
Comma,
Ws,
Eof,
}
struct State {
seen_sep: bool,
seen_colon: bool,
filenames: Vec<String>,
codes: Vec<String>,
}
impl State {
fn new() -> Self {
Self {
seen_sep: true,
seen_colon: false,
filenames: vec![],
codes: vec![],
}
}
/// Generate the list of `StrCheckCodePair` pairs for the current state.
fn parse(&self) -> Vec<StrCheckCodePair> {
let mut codes: Vec<StrCheckCodePair> = vec![];
for code in &self.codes {
match CheckCodePrefix::from_str(code) {
Ok(code) => {
for filename in &self.filenames {
codes.push(StrCheckCodePair {
pattern: filename.clone(),
code: code.clone(),
});
}
}
Err(_) => eprintln!("Skipping unrecognized prefix: {}", code),
}
}
codes
}
}
/// Tokenize the raw 'files-to-codes' mapping.
fn tokenize_files_to_codes_mapping(value: &str) -> Vec<Token> {
let mut tokens = vec![];
let mut i = 0;
while i < value.len() {
for (token_re, token_name) in [
(
Regex::new(r"([A-Z]+[0-9]*)(?:$|\s|,)").unwrap(),
TokenType::Code,
),
(Regex::new(r"([^\s:,]+)").unwrap(), TokenType::File),
(Regex::new(r"(\s*:\s*)").unwrap(), TokenType::Colon),
(Regex::new(r"(\s*,\s*)").unwrap(), TokenType::Comma),
(Regex::new(r"(\s+)").unwrap(), TokenType::Ws),
] {
if let Some(cap) = token_re.captures(&value[i..]) {
let mat = cap.get(1).unwrap();
if mat.start() == 0 {
tokens.push(Token {
token_name,
src: mat.as_str().to_string().trim().to_string(),
});
i += mat.end();
break;
}
}
}
}
tokens.push(Token {
token_name: TokenType::Eof,
src: "".to_string(),
});
tokens
}
/// Parse a 'files-to-codes' mapping, mimicking Flake8's internal logic.
///
/// See: https://github.com/PyCQA/flake8/blob/7dfe99616fc2f07c0017df2ba5fa884158f3ea8a/src/flake8/utils.py#L45
pub fn parse_files_to_codes_mapping(value: &str) -> Result<Vec<StrCheckCodePair>> {
if value.trim().is_empty() {
return Ok(vec![]);
}
let mut codes: Vec<StrCheckCodePair> = vec![];
let mut state = State::new();
for token in tokenize_files_to_codes_mapping(value) {
if matches!(token.token_name, TokenType::Comma | TokenType::Ws) {
state.seen_sep = true;
} else if !state.seen_colon {
if matches!(token.token_name, TokenType::Colon) {
state.seen_colon = true;
state.seen_sep = true;
} else if state.seen_sep && matches!(token.token_name, TokenType::File) {
state.filenames.push(token.src);
state.seen_sep = false;
} else {
return Err(anyhow::anyhow!("Unexpected token: {:?}", token.token_name));
}
} else {
if matches!(token.token_name, TokenType::Eof) {
codes.extend(state.parse());
state = State::new();
} else if state.seen_sep && matches!(token.token_name, TokenType::Code) {
state.codes.push(token.src);
state.seen_sep = false;
} else if state.seen_sep && matches!(token.token_name, TokenType::File) {
codes.extend(state.parse());
state = State::new();
state.filenames.push(token.src);
state.seen_sep = false;
} else {
return Err(anyhow::anyhow!("Unexpected token: {:?}", token.token_name));
}
}
}
Ok(codes)
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_to_ruff::parser::{
parse_files_to_codes_mapping, parse_prefix_codes, parse_strings,
};
use crate::settings::types::StrCheckCodePair;
#[test]
fn it_parses_prefix_codes() {
let actual = parse_prefix_codes("");
let expected: Vec<CheckCodePrefix> = vec![];
assert_eq!(actual, expected);
let actual = parse_prefix_codes(" ");
let expected: Vec<CheckCodePrefix> = vec![];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401");
let expected = vec![CheckCodePrefix::F401];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,");
let expected = vec![CheckCodePrefix::F401];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,E501");
let expected = vec![CheckCodePrefix::F401, CheckCodePrefix::E501];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401, E501");
let expected = vec![CheckCodePrefix::F401, CheckCodePrefix::E501];
assert_eq!(actual, expected);
}
#[test]
fn it_parses_strings() {
let actual = parse_strings("");
let expected: Vec<String> = vec![];
assert_eq!(actual, expected);
let actual = parse_strings(" ");
let expected: Vec<String> = vec![];
assert_eq!(actual, expected);
let actual = parse_strings("__init__.py");
let expected = vec!["__init__.py".to_string()];
assert_eq!(actual, expected);
let actual = parse_strings("__init__.py,");
let expected = vec!["__init__.py".to_string()];
assert_eq!(actual, expected);
let actual = parse_strings("__init__.py,__main__.py");
let expected = vec!["__init__.py".to_string(), "__main__.py".to_string()];
assert_eq!(actual, expected);
let actual = parse_strings("__init__.py, __main__.py");
let expected = vec!["__init__.py".to_string(), "__main__.py".to_string()];
assert_eq!(actual, expected);
}
#[test]
fn it_parse_files_to_codes_mapping() -> Result<()> {
let actual = parse_files_to_codes_mapping("")?;
let expected: Vec<StrCheckCodePair> = vec![];
assert_eq!(actual, expected);
let actual = parse_files_to_codes_mapping(" ")?;
let expected: Vec<StrCheckCodePair> = vec![];
assert_eq!(actual, expected);
// Ex) locust
let actual = parse_files_to_codes_mapping(
"per-file-ignores =
locust/test/*: F841
examples/*: F841
*.pyi: E302,E704"
.strip_prefix("per-file-ignores =")
.unwrap(),
)?;
let expected: Vec<StrCheckCodePair> = vec![
StrCheckCodePair {
pattern: "locust/test/*".to_string(),
code: CheckCodePrefix::F841,
},
StrCheckCodePair {
pattern: "examples/*".to_string(),
code: CheckCodePrefix::F841,
},
];
assert_eq!(actual, expected);
// Ex) celery
let actual = parse_files_to_codes_mapping(
"per-file-ignores =
t/*,setup.py,examples/*,docs/*,extra/*:
D,"
.strip_prefix("per-file-ignores =")
.unwrap(),
)?;
let expected: Vec<StrCheckCodePair> = vec![
StrCheckCodePair {
pattern: "t/*".to_string(),
code: CheckCodePrefix::D,
},
StrCheckCodePair {
pattern: "setup.py".to_string(),
code: CheckCodePrefix::D,
},
StrCheckCodePair {
pattern: "examples/*".to_string(),
code: CheckCodePrefix::D,
},
StrCheckCodePair {
pattern: "docs/*".to_string(),
code: CheckCodePrefix::D,
},
StrCheckCodePair {
pattern: "extra/*".to_string(),
code: CheckCodePrefix::D,
},
];
assert_eq!(actual, expected);
// Ex) scrapy
let actual = parse_files_to_codes_mapping(
"per-file-ignores =
scrapy/__init__.py:E402
scrapy/core/downloader/handlers/http.py:F401
scrapy/http/__init__.py:F401
scrapy/linkextractors/__init__.py:E402,F401
scrapy/selector/__init__.py:F401
scrapy/spiders/__init__.py:E402,F401
scrapy/utils/url.py:F403,F405
tests/test_loader.py:E741"
.strip_prefix("per-file-ignores =")
.unwrap(),
)?;
let expected: Vec<StrCheckCodePair> = vec![
StrCheckCodePair {
pattern: "scrapy/__init__.py".to_string(),
code: CheckCodePrefix::E402,
},
StrCheckCodePair {
pattern: "scrapy/core/downloader/handlers/http.py".to_string(),
code: CheckCodePrefix::F401,
},
StrCheckCodePair {
pattern: "scrapy/http/__init__.py".to_string(),
code: CheckCodePrefix::F401,
},
StrCheckCodePair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
code: CheckCodePrefix::E402,
},
StrCheckCodePair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
code: CheckCodePrefix::F401,
},
StrCheckCodePair {
pattern: "scrapy/selector/__init__.py".to_string(),
code: CheckCodePrefix::F401,
},
StrCheckCodePair {
pattern: "scrapy/spiders/__init__.py".to_string(),
code: CheckCodePrefix::E402,
},
StrCheckCodePair {
pattern: "scrapy/spiders/__init__.py".to_string(),
code: CheckCodePrefix::F401,
},
StrCheckCodePair {
pattern: "scrapy/utils/url.py".to_string(),
code: CheckCodePrefix::F403,
},
StrCheckCodePair {
pattern: "scrapy/utils/url.py".to_string(),
code: CheckCodePrefix::F405,
},
StrCheckCodePair {
pattern: "tests/test_loader.py".to_string(),
code: CheckCodePrefix::E741,
},
];
assert_eq!(actual, expected);
Ok(())
}
}

View File

@ -31,6 +31,7 @@ mod flake8_builtins;
mod flake8_comprehensions; mod flake8_comprehensions;
mod flake8_print; mod flake8_print;
mod flake8_quotes; mod flake8_quotes;
pub mod flake8_to_ruff;
pub mod fs; pub mod fs;
pub mod linter; pub mod linter;
pub mod logging; pub mod logging;

View File

@ -1,6 +1,6 @@
//! Settings for the `pep8-naming` plugin. //! Settings for the `pep8-naming` plugin.
use serde::Deserialize; use serde::{Deserialize, Serialize};
const IGNORE_NAMES: [&str; 12] = [ const IGNORE_NAMES: [&str; 12] = [
"setUp", "setUp",
@ -21,7 +21,7 @@ const CLASSMETHOD_DECORATORS: [&str; 1] = ["classmethod"];
const STATICMETHOD_DECORATORS: [&str; 1] = ["staticmethod"]; const STATICMETHOD_DECORATORS: [&str; 1] = ["staticmethod"];
#[derive(Debug, PartialEq, Eq, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")] #[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options { pub struct Options {
pub ignore_names: Option<Vec<String>>, pub ignore_names: Option<Vec<String>>,

View File

@ -1,12 +1,12 @@
//! Options that the user can provide via pyproject.toml. //! Options that the user can provide via pyproject.toml.
use serde::Deserialize; use serde::{Deserialize, Serialize};
use crate::checks_gen::CheckCodePrefix; use crate::checks_gen::CheckCodePrefix;
use crate::settings::types::{PythonVersion, StrCheckCodePair}; use crate::settings::types::{PythonVersion, StrCheckCodePair};
use crate::{flake8_quotes, pep8_naming}; use crate::{flake8_quotes, pep8_naming};
#[derive(Debug, PartialEq, Eq, Deserialize, Default)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")] #[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options { pub struct Options {
pub line_length: Option<usize>, pub line_length: Option<usize>,

View File

@ -6,21 +6,31 @@ use anyhow::Result;
use common_path::common_path_all; use common_path::common_path_all;
use log::debug; use log::debug;
use path_absolutize::Absolutize; use path_absolutize::Absolutize;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use crate::fs; use crate::fs;
use crate::settings::options::Options; use crate::settings::options::Options;
#[derive(Debug, PartialEq, Eq, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Tools { struct Tools {
ruff: Option<Options>, ruff: Option<Options>,
} }
#[derive(Debug, PartialEq, Eq, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Pyproject { pub struct Pyproject {
tool: Option<Tools>, tool: Option<Tools>,
} }
impl Pyproject {
pub fn new(options: Options) -> Self {
Self {
tool: Some(Tools {
ruff: Some(options),
}),
}
}
}
fn parse_pyproject_toml(path: &Path) -> Result<Pyproject> { fn parse_pyproject_toml(path: &Path) -> Result<Pyproject> {
let contents = fs::read_file(path)?; let contents = fs::read_file(path)?;
toml::from_str(&contents).map_err(|e| e.into()) toml::from_str(&contents).map_err(|e| e.into())

View File

@ -5,7 +5,7 @@ use std::str::FromStr;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use glob::Pattern; use glob::Pattern;
use serde::{de, Deserialize, Deserializer, Serialize}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use crate::checks::CheckCode; use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix; use crate::checks_gen::CheckCodePrefix;
@ -107,6 +107,16 @@ impl<'de> Deserialize<'de> for StrCheckCodePair {
} }
} }
impl Serialize for StrCheckCodePair {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let as_str = format!("{}:{}", self.pattern, self.code.as_ref());
serializer.serialize_str(&as_str)
}
}
impl FromStr for StrCheckCodePair { impl FromStr for StrCheckCodePair {
type Err = anyhow::Error; type Err = anyhow::Error;