Move around and rename some of the Settings structs (#496)

This commit is contained in:
Charlie Marsh 2022-10-28 18:46:54 -04:00 committed by GitHub
parent 8fc5e91ec7
commit 7df903dc4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 574 additions and 536 deletions

View file

@ -26,7 +26,8 @@ use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::python::future::ALL_FEATURE_NAMES;
use crate::settings::{PythonVersion, Settings};
use crate::settings::types::PythonVersion;
use crate::settings::Settings;
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{
docstrings, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_print, pep8_naming,

View file

@ -229,7 +229,7 @@ pub fn check_lines(
mod tests {
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::settings;
use crate::settings::Settings;
use super::check_lines;
@ -243,9 +243,9 @@ mod tests {
&mut checks,
line,
&noqa_line_for,
&settings::Settings {
&Settings {
line_length,
..settings::Settings::for_rule(CheckCode::E501)
..Settings::for_rule(CheckCode::E501)
},
&fixer::Mode::Generate,
);

View file

@ -7,9 +7,9 @@ use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::printer::SerializationFormat;
use crate::pyproject::StrCheckCodePair;
use crate::settings::PythonVersion;
use crate::RawSettings;
use crate::settings::configuration::Configuration;
use crate::settings::types::PythonVersion;
use crate::settings::types::StrCheckCodePair;
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
@ -109,7 +109,7 @@ pub fn warn_on(
codes: &[CheckCodePrefix],
cli_ignore: &[CheckCodePrefix],
cli_extend_ignore: &[CheckCodePrefix],
pyproject_settings: &RawSettings,
pyproject_configuration: &Configuration,
pyproject_path: &Option<PathBuf>,
) {
for code in codes {
@ -117,7 +117,7 @@ pub fn warn_on(
if cli_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --ignore")
}
} else if pyproject_settings.ignore.contains(code) {
} else if pyproject_configuration.ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `ignore` field in {}",
@ -131,7 +131,7 @@ pub fn warn_on(
if cli_extend_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --extend-ignore")
}
} else if pyproject_settings.extend_ignore.contains(code) {
} else if pyproject_configuration.extend_ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `extend_ignore` field in {}",

View file

@ -148,15 +148,10 @@ mod tests {
use crate::checks::{Check, CheckCode};
use crate::flake8_quotes::settings::Quote;
use crate::linter::tokenize;
use crate::settings;
use crate::{flake8_quotes, linter};
use crate::{flake8_quotes, linter, Settings};
use crate::{fs, noqa};
fn check_path(
path: &Path,
settings: &settings::Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
@ -174,14 +169,14 @@ mod tests {
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&settings::Settings {
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..settings::Settings::for_rules(vec![
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
@ -206,14 +201,14 @@ mod tests {
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&settings::Settings {
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Double,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..settings::Settings::for_rules(vec![
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
@ -243,14 +238,14 @@ mod tests {
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&settings::Settings {
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..settings::Settings::for_rules(vec![
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
@ -280,14 +275,14 @@ mod tests {
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&settings::Settings {
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..settings::Settings::for_rules(vec![
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,

View file

@ -11,7 +11,7 @@ pub enum Quote {
#[derive(Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub struct Options {
pub inline_quotes: Option<Quote>,
pub multiline_quotes: Option<Quote>,
pub docstring_quotes: Option<Quote>,
@ -27,7 +27,7 @@ pub struct Settings {
}
impl Settings {
pub fn from_config(config: Config) -> Self {
pub fn from_config(config: Options) -> Self {
Self {
inline_quotes: config.inline_quotes.unwrap_or(Quote::Single),
multiline_quotes: config.multiline_quotes.unwrap_or(Quote::Double),

View file

@ -12,7 +12,7 @@ use path_absolutize::Absolutize;
use walkdir::{DirEntry, WalkDir};
use crate::checks::CheckCode;
use crate::settings::{FilePattern, PerFileIgnore};
use crate::settings::types::{FilePattern, PerFileIgnore};
/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
@ -178,7 +178,7 @@ mod tests {
use path_absolutize::Absolutize;
use crate::fs::{extract_path_names, is_excluded, is_included};
use crate::settings::FilePattern;
use crate::settings::types::FilePattern;
#[test]
fn inclusions() {

View file

@ -5,11 +5,13 @@ use std::path::Path;
use anyhow::Result;
use log::debug;
use rustpython_parser::lexer::LexResult;
use settings::pyproject;
use crate::autofix::fixer::Mode;
use crate::linter::{check_path, tokenize};
use crate::message::Message;
use crate::settings::{RawSettings, Settings};
use crate::settings::configuration::Configuration;
use settings::Settings;
mod ast;
mod autofix;
@ -38,7 +40,6 @@ pub mod printer;
mod pycodestyle;
mod pydocstyle;
mod pyflakes;
pub mod pyproject;
mod python;
mod pyupgrade;
pub mod settings;
@ -58,7 +59,7 @@ pub fn check(path: &Path, contents: &str, quiet: bool) -> Result<Vec<Message>> {
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_raw(RawSettings::from_pyproject(
let settings = Settings::from_configuration(Configuration::from_pyproject(
&pyproject,
&project_root,
quiet,

View file

@ -236,16 +236,12 @@ mod tests {
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::linter;
use crate::linter::tokenize;
use crate::settings;
use crate::{fs, noqa};
use crate::{linter, Settings};
fn check_path(
path: &Path,
settings: &settings::Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);

View file

@ -25,10 +25,11 @@ use ruff::linter::{lint_path, lint_stdin};
use ruff::logging::set_up_logging;
use ruff::message::Message;
use ruff::printer::{Printer, SerializationFormat};
use ruff::pyproject::{self};
use ruff::settings::CurrentSettings;
use ruff::settings::RawSettings;
use ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ruff::settings::configuration::Configuration;
use ruff::settings::pyproject;
use ruff::settings::types::{FilePattern, PerFileIgnore};
use ruff::settings::user::UserConfiguration;
use ruff::settings::Settings;
use ruff::tell_user;
#[cfg(feature = "update-informer")]
@ -73,10 +74,14 @@ fn check_for_updates() {
}
}
fn show_settings(settings: RawSettings, project_root: Option<PathBuf>, pyproject: Option<PathBuf>) {
fn show_settings(
configuration: Configuration,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) {
println!(
"{:#?}",
CurrentSettings::from_settings(settings, project_root, pyproject)
UserConfiguration::from_configuration(configuration, project_root, pyproject)
);
}
@ -256,15 +261,15 @@ fn inner_main() -> Result<ExitCode> {
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect();
let mut settings = RawSettings::from_pyproject(&pyproject, &project_root, cli.quiet)?;
let mut configuration = Configuration::from_pyproject(&pyproject, &project_root, cli.quiet)?;
if !exclude.is_empty() {
settings.exclude = exclude;
configuration.exclude = exclude;
}
if !extend_exclude.is_empty() {
settings.extend_exclude = extend_exclude;
configuration.extend_exclude = extend_exclude;
}
if !per_file_ignores.is_empty() {
settings.per_file_ignores = per_file_ignores;
configuration.per_file_ignores = per_file_ignores;
}
if !cli.select.is_empty() {
warn_on(
@ -272,10 +277,10 @@ fn inner_main() -> Result<ExitCode> {
&cli.select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&configuration,
&pyproject,
);
settings.select = cli.select;
configuration.select = cli.select;
}
if !cli.extend_select.is_empty() {
warn_on(
@ -283,22 +288,22 @@ fn inner_main() -> Result<ExitCode> {
&cli.extend_select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&configuration,
&pyproject,
);
settings.extend_select = cli.extend_select;
configuration.extend_select = cli.extend_select;
}
if !cli.ignore.is_empty() {
settings.ignore = cli.ignore;
configuration.ignore = cli.ignore;
}
if !cli.extend_ignore.is_empty() {
settings.extend_ignore = cli.extend_ignore;
configuration.extend_ignore = cli.extend_ignore;
}
if let Some(target_version) = cli.target_version {
settings.target_version = target_version;
configuration.target_version = target_version;
}
if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx {
settings.dummy_variable_rgx = dummy_variable_rgx;
configuration.dummy_variable_rgx = dummy_variable_rgx;
}
if cli.show_settings && cli.show_files {
@ -306,11 +311,11 @@ fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::FAILURE);
}
if cli.show_settings {
show_settings(settings, project_root, pyproject);
show_settings(configuration, project_root, pyproject);
return Ok(ExitCode::SUCCESS);
}
let settings = Settings::from_raw(settings);
let settings = Settings::from_configuration(configuration);
if cli.show_files {
show_files(&cli.files, &settings);

View file

@ -1,367 +0,0 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use glob::Pattern;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::checks::CheckCode;
use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity};
use crate::pyproject::{load_config, StrCheckCodePair};
use crate::{flake8_quotes, fs};
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize)]
pub enum PythonVersion {
Py33,
Py34,
Py35,
Py36,
Py37,
Py38,
Py39,
Py310,
Py311,
}
impl FromStr for PythonVersion {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"py33" => Ok(PythonVersion::Py33),
"py34" => Ok(PythonVersion::Py34),
"py35" => Ok(PythonVersion::Py35),
"py36" => Ok(PythonVersion::Py36),
"py37" => Ok(PythonVersion::Py37),
"py38" => Ok(PythonVersion::Py38),
"py39" => Ok(PythonVersion::Py39),
"py310" => Ok(PythonVersion::Py310),
"py311" => Ok(PythonVersion::Py311),
_ => Err(anyhow!("Unknown version: {}", string)),
}
}
}
#[derive(Debug, Clone, Hash)]
pub enum FilePattern {
Simple(&'static str),
Complex(Pattern, Option<Pattern>),
}
impl FilePattern {
pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
let path = Path::new(pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern.");
let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) {
Some(Pattern::new(pattern).expect("Invalid pattern."))
} else {
None
};
FilePattern::Complex(absolute, basename)
}
}
#[derive(Debug, Clone, Hash)]
pub struct PerFileIgnore {
pub pattern: FilePattern,
pub codes: BTreeSet<CheckCode>,
}
impl PerFileIgnore {
pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self {
let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root);
let codes = BTreeSet::from_iter(user_in.code.codes());
Self { pattern, codes }
}
}
#[derive(Debug)]
pub struct RawSettings {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
FilePattern::Simple(".bzr"),
FilePattern::Simple(".direnv"),
FilePattern::Simple(".eggs"),
FilePattern::Simple(".git"),
FilePattern::Simple(".hg"),
FilePattern::Simple(".mypy_cache"),
FilePattern::Simple(".nox"),
FilePattern::Simple(".pants.d"),
FilePattern::Simple(".ruff_cache"),
FilePattern::Simple(".svn"),
FilePattern::Simple(".tox"),
FilePattern::Simple(".venv"),
FilePattern::Simple("__pypackages__"),
FilePattern::Simple("_build"),
FilePattern::Simple("buck-out"),
FilePattern::Simple("build"),
FilePattern::Simple("dist"),
FilePattern::Simple("node_modules"),
FilePattern::Simple("venv"),
]
});
static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl RawSettings {
pub fn from_pyproject(
pyproject: &Option<PathBuf>,
project_root: &Option<PathBuf>,
quiet: bool,
) -> Result<Self> {
let config = load_config(pyproject, quiet)?;
Ok(RawSettings {
dummy_variable_rgx: match config.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
target_version: config.target_version.unwrap_or(PythonVersion::Py310),
exclude: config
.exclude
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
extend_exclude: config
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect(),
extend_ignore: config.extend_ignore,
select: config
.select
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]),
extend_select: config.extend_select,
ignore: config.ignore,
line_length: config.line_length.unwrap_or(88),
per_file_ignores: config
.per_file_ignores
.into_iter()
.map(|pair| PerFileIgnore::new(pair, project_root))
.collect(),
// Plugins
flake8_quotes: config
.flake8_quotes
.map(flake8_quotes::settings::Settings::from_config)
.unwrap_or_default(),
})
}
}
#[derive(Debug)]
pub struct Settings {
pub dummy_variable_rgx: Regex,
pub enabled: BTreeSet<CheckCode>,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
}
/// Given a set of selected and ignored prefixes, resolve the set of enabled error codes.
fn resolve_codes(
select: &[CheckCodePrefix],
extend_select: &[CheckCodePrefix],
ignore: &[CheckCodePrefix],
extend_ignore: &[CheckCodePrefix],
) -> BTreeSet<CheckCode> {
let mut codes: BTreeSet<CheckCode> = BTreeSet::new();
for specificity in [
PrefixSpecificity::Category,
PrefixSpecificity::Hundreds,
PrefixSpecificity::Tens,
PrefixSpecificity::Explicit,
] {
for prefix in select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
}
}
for prefix in extend_select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
}
}
for prefix in ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
}
}
}
for prefix in extend_ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
}
}
}
}
codes
}
impl Settings {
pub fn from_raw(settings: RawSettings) -> Self {
Self {
dummy_variable_rgx: settings.dummy_variable_rgx,
enabled: resolve_codes(
&settings.select,
&settings.extend_select,
&settings.ignore,
&settings.extend_ignore,
),
exclude: settings.exclude,
extend_exclude: settings.extend_exclude,
flake8_quotes: settings.flake8_quotes,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
target_version: settings.target_version,
}
}
pub fn for_rule(check_code: CheckCode) -> Self {
Self {
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
enabled: BTreeSet::from([check_code]),
exclude: vec![],
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
flake8_quotes: Default::default(),
}
}
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
Self {
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
enabled: BTreeSet::from_iter(check_codes),
exclude: vec![],
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
flake8_quotes: Default::default(),
}
}
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.line_length.hash(state);
self.dummy_variable_rgx.as_str().hash(state);
for value in self.enabled.iter() {
value.hash(state);
}
for value in self.per_file_ignores.iter() {
value.hash(state);
}
}
}
/// Struct to render user-facing exclusion patterns.
#[derive(Debug)]
#[allow(dead_code)]
pub struct Exclusion {
basename: Option<String>,
absolute: Option<String>,
}
impl Exclusion {
pub fn from_file_pattern(file_pattern: FilePattern) -> Self {
match file_pattern {
FilePattern::Simple(basename) => Exclusion {
basename: Some(basename.to_string()),
absolute: None,
},
FilePattern::Complex(absolute, basename) => Exclusion {
basename: basename.map(|pattern| pattern.to_string()),
absolute: Some(absolute.to_string()),
},
}
}
}
/// Struct to render user-facing Settings.
#[derive(Debug)]
pub struct CurrentSettings {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<Exclusion>,
pub extend_exclude: Vec<Exclusion>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
// Non-settings exposed to the user
pub project_root: Option<PathBuf>,
pub pyproject: Option<PathBuf>,
}
impl CurrentSettings {
pub fn from_settings(
settings: RawSettings,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) -> Self {
Self {
dummy_variable_rgx: settings.dummy_variable_rgx,
exclude: settings
.exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_exclude: settings
.extend_exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_ignore: settings.extend_ignore,
extend_select: settings.extend_select,
ignore: settings.ignore,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
select: settings.select,
target_version: settings.target_version,
flake8_quotes: settings.flake8_quotes,
project_root,
pyproject,
}
}
}

View file

@ -0,0 +1,102 @@
use std::path::PathBuf;
use anyhow::{anyhow, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_quotes;
use crate::settings::pyproject::load_options;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
#[derive(Debug)]
pub struct Configuration {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
FilePattern::Simple(".bzr"),
FilePattern::Simple(".direnv"),
FilePattern::Simple(".eggs"),
FilePattern::Simple(".git"),
FilePattern::Simple(".hg"),
FilePattern::Simple(".mypy_cache"),
FilePattern::Simple(".nox"),
FilePattern::Simple(".pants.d"),
FilePattern::Simple(".ruff_cache"),
FilePattern::Simple(".svn"),
FilePattern::Simple(".tox"),
FilePattern::Simple(".venv"),
FilePattern::Simple("__pypackages__"),
FilePattern::Simple("_build"),
FilePattern::Simple("buck-out"),
FilePattern::Simple("build"),
FilePattern::Simple("dist"),
FilePattern::Simple("node_modules"),
FilePattern::Simple("venv"),
]
});
static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl Configuration {
pub fn from_pyproject(
pyproject: &Option<PathBuf>,
project_root: &Option<PathBuf>,
quiet: bool,
) -> Result<Self> {
let options = load_options(pyproject, quiet)?;
Ok(Configuration {
dummy_variable_rgx: match options.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
target_version: options.target_version.unwrap_or(PythonVersion::Py310),
exclude: options
.exclude
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
extend_exclude: options
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect(),
extend_ignore: options.extend_ignore,
select: options
.select
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]),
extend_select: options.extend_select,
ignore: options.ignore,
line_length: options.line_length.unwrap_or(88),
per_file_ignores: options
.per_file_ignores
.into_iter()
.map(|pair| PerFileIgnore::new(pair, project_root))
.collect(),
// Plugins
flake8_quotes: options
.flake8_quotes
.map(flake8_quotes::settings::Settings::from_config)
.unwrap_or_default(),
})
}
}

133
src/settings/mod.rs Normal file
View file

@ -0,0 +1,133 @@
//! Effective program settings, taking into account pyproject.toml and command-line options.
//! Structure is optimized for internal usage, as opposed to external visibility or parsing.
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use regex::Regex;
use crate::checks::CheckCode;
use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity};
use crate::flake8_quotes;
use crate::settings::configuration::Configuration;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
pub mod configuration;
pub mod options;
pub mod pyproject;
pub mod types;
pub mod user;
#[derive(Debug)]
pub struct Settings {
pub dummy_variable_rgx: Regex,
pub enabled: BTreeSet<CheckCode>,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
}
impl Settings {
pub fn from_configuration(config: Configuration) -> Self {
Self {
dummy_variable_rgx: config.dummy_variable_rgx,
enabled: resolve_codes(
&config.select,
&config.extend_select,
&config.ignore,
&config.extend_ignore,
),
exclude: config.exclude,
extend_exclude: config.extend_exclude,
flake8_quotes: config.flake8_quotes,
line_length: config.line_length,
per_file_ignores: config.per_file_ignores,
target_version: config.target_version,
}
}
pub fn for_rule(check_code: CheckCode) -> Self {
Self {
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
enabled: BTreeSet::from([check_code]),
exclude: vec![],
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
flake8_quotes: Default::default(),
}
}
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
Self {
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
enabled: BTreeSet::from_iter(check_codes),
exclude: vec![],
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
flake8_quotes: Default::default(),
}
}
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.line_length.hash(state);
self.dummy_variable_rgx.as_str().hash(state);
for value in self.enabled.iter() {
value.hash(state);
}
for value in self.per_file_ignores.iter() {
value.hash(state);
}
}
}
/// Given a set of selected and ignored prefixes, resolve the set of enabled error codes.
fn resolve_codes(
select: &[CheckCodePrefix],
extend_select: &[CheckCodePrefix],
ignore: &[CheckCodePrefix],
extend_ignore: &[CheckCodePrefix],
) -> BTreeSet<CheckCode> {
let mut codes: BTreeSet<CheckCode> = BTreeSet::new();
for specificity in [
PrefixSpecificity::Category,
PrefixSpecificity::Hundreds,
PrefixSpecificity::Tens,
PrefixSpecificity::Explicit,
] {
for prefix in select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
}
}
for prefix in extend_select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
}
}
for prefix in ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
}
}
}
for prefix in extend_ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
}
}
}
}
codes
}

28
src/settings/options.rs Normal file
View file

@ -0,0 +1,28 @@
//! Options that the user can provide via pyproject.toml.
use serde::Deserialize;
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_quotes;
use crate::settings::types::{PythonVersion, StrCheckCodePair};
#[derive(Debug, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
pub line_length: Option<usize>,
pub exclude: Option<Vec<String>>,
#[serde(default)]
pub extend_exclude: Vec<String>,
pub select: Option<Vec<CheckCodePrefix>>,
#[serde(default)]
pub extend_select: Vec<CheckCodePrefix>,
#[serde(default)]
pub ignore: Vec<CheckCodePrefix>,
#[serde(default)]
pub extend_ignore: Vec<CheckCodePrefix>,
#[serde(default)]
pub per_file_ignores: Vec<StrCheckCodePair>,
pub dummy_variable_rgx: Option<String>,
pub target_version: Option<PythonVersion>,
pub flake8_quotes: Option<flake8_quotes::settings::Options>,
}

View file

@ -1,106 +1,26 @@
use std::path::{Path, PathBuf};
use std::str::FromStr;
//! Utilities for locating (and extracting configuration from) a pyproject.toml.
use anyhow::{anyhow, Result};
use std::path::{Path, PathBuf};
use anyhow::Result;
use common_path::common_path_all;
use path_absolutize::Absolutize;
use serde::de;
use serde::{Deserialize, Deserializer};
use serde::Deserialize;
use crate::checks_gen::CheckCodePrefix;
use crate::settings::PythonVersion;
use crate::{flake8_quotes, fs};
pub fn load_config(pyproject: &Option<PathBuf>, quiet: bool) -> Result<Config> {
match pyproject {
Some(pyproject) => Ok(parse_pyproject_toml(pyproject)?
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default()),
None => {
if !quiet {
eprintln!("No pyproject.toml found.");
eprintln!("Falling back to default configuration...");
}
Ok(Default::default())
}
}
}
#[derive(Debug, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub line_length: Option<usize>,
pub exclude: Option<Vec<String>>,
#[serde(default)]
pub extend_exclude: Vec<String>,
pub select: Option<Vec<CheckCodePrefix>>,
#[serde(default)]
pub extend_select: Vec<CheckCodePrefix>,
#[serde(default)]
pub ignore: Vec<CheckCodePrefix>,
#[serde(default)]
pub extend_ignore: Vec<CheckCodePrefix>,
#[serde(default)]
pub per_file_ignores: Vec<StrCheckCodePair>,
pub dummy_variable_rgx: Option<String>,
pub target_version: Option<PythonVersion>,
pub flake8_quotes: Option<flake8_quotes::settings::Config>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrCheckCodePair {
pub pattern: String,
pub code: CheckCodePrefix,
}
impl StrCheckCodePair {
const EXPECTED_PATTERN: &'static str = "<FilePattern>:<CheckCode> pattern";
}
impl<'de> Deserialize<'de> for StrCheckCodePair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str_result = String::deserialize(deserializer)?;
Self::from_str(str_result.as_str()).map_err(|_| {
de::Error::invalid_value(
de::Unexpected::Str(str_result.as_str()),
&Self::EXPECTED_PATTERN,
)
})
}
}
impl FromStr for StrCheckCodePair {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
let (pattern_str, code_string) = {
let tokens = string.split(':').collect::<Vec<_>>();
if tokens.len() != 2 {
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
}
(tokens[0].trim(), tokens[1].trim())
};
let code = CheckCodePrefix::from_str(code_string)?;
let pattern = pattern_str.into();
Ok(Self { pattern, code })
}
}
use crate::fs;
use crate::settings::options::Options;
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct Tools {
ruff: Option<Config>,
ruff: Option<Options>,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct PyProject {
struct Pyproject {
tool: Option<Tools>,
}
fn parse_pyproject_toml(path: &Path) -> Result<PyProject> {
fn parse_pyproject_toml(path: &Path) -> Result<Pyproject> {
let contents = fs::read_file(path)?;
toml::from_str(&contents).map_err(|e| e.into())
}
@ -149,6 +69,22 @@ pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
None
}
pub fn load_options(pyproject: &Option<PathBuf>, quiet: bool) -> Result<Options> {
match pyproject {
Some(pyproject) => Ok(parse_pyproject_toml(pyproject)?
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default()),
None => {
if !quiet {
eprintln!("No pyproject.toml found.");
eprintln!("Falling back to default configuration...");
}
Ok(Default::default())
}
}
}
#[cfg(test)]
mod tests {
use std::env::current_dir;
@ -160,25 +96,24 @@ mod tests {
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_quotes;
use crate::flake8_quotes::settings::Quote;
use crate::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools,
use crate::settings::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools,
};
use super::StrCheckCodePair;
use crate::settings::types::StrCheckCodePair;
#[test]
fn deserialize() -> Result<()> {
let pyproject: PyProject = toml::from_str(r#""#)?;
let pyproject: Pyproject = toml::from_str(r#""#)?;
assert_eq!(pyproject.tool, None);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
"#,
)?;
assert_eq!(pyproject.tool, Some(Tools { ruff: None }));
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@ -187,7 +122,7 @@ mod tests {
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: None,
exclude: None,
extend_exclude: vec![],
@ -203,7 +138,7 @@ mod tests {
})
);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@ -213,7 +148,7 @@ line-length = 79
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: Some(79),
exclude: None,
extend_exclude: vec![],
@ -229,7 +164,7 @@ line-length = 79
})
);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@ -239,7 +174,7 @@ exclude = ["foo.py"]
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: None,
exclude: Some(vec!["foo.py".to_string()]),
extend_exclude: vec![],
@ -255,7 +190,7 @@ exclude = ["foo.py"]
})
);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@ -265,7 +200,7 @@ select = ["E501"]
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: None,
exclude: None,
extend_exclude: vec![],
@ -281,7 +216,7 @@ select = ["E501"]
})
);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@ -292,7 +227,7 @@ ignore = ["E501"]
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: None,
exclude: None,
extend_exclude: vec![],
@ -308,7 +243,7 @@ ignore = ["E501"]
})
);
assert!(toml::from_str::<PyProject>(
assert!(toml::from_str::<Pyproject>(
r#"
[tool.black]
[tool.ruff]
@ -317,7 +252,7 @@ line_length = 79
)
.is_err());
assert!(toml::from_str::<PyProject>(
assert!(toml::from_str::<Pyproject>(
r#"
[tool.black]
[tool.ruff]
@ -326,7 +261,7 @@ select = ["E123"]
)
.is_err());
assert!(toml::from_str::<PyProject>(
assert!(toml::from_str::<Pyproject>(
r#"
[tool.black]
[tool.ruff]
@ -358,7 +293,7 @@ other-attribute = 1
.expect("Unable to find tool.ruff.");
assert_eq!(
config,
Config {
Options {
line_length: Some(88),
exclude: None,
extend_exclude: vec![
@ -376,7 +311,7 @@ other-attribute = 1
}],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: Some(flake8_quotes::settings::Config {
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(Quote::Single),
multiline_quotes: Some(Quote::Double),
docstring_quotes: Some(Quote::Double),

125
src/settings/types.rs Normal file
View file

@ -0,0 +1,125 @@
use std::collections::BTreeSet;
use std::hash::Hash;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use glob::Pattern;
use serde::{de, Deserialize, Deserializer, Serialize};
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::fs;
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize)]
pub enum PythonVersion {
Py33,
Py34,
Py35,
Py36,
Py37,
Py38,
Py39,
Py310,
Py311,
}
impl FromStr for PythonVersion {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"py33" => Ok(PythonVersion::Py33),
"py34" => Ok(PythonVersion::Py34),
"py35" => Ok(PythonVersion::Py35),
"py36" => Ok(PythonVersion::Py36),
"py37" => Ok(PythonVersion::Py37),
"py38" => Ok(PythonVersion::Py38),
"py39" => Ok(PythonVersion::Py39),
"py310" => Ok(PythonVersion::Py310),
"py311" => Ok(PythonVersion::Py311),
_ => Err(anyhow!("Unknown version: {}", string)),
}
}
}
#[derive(Debug, Clone, Hash)]
pub enum FilePattern {
Simple(&'static str),
Complex(Pattern, Option<Pattern>),
}
impl FilePattern {
pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
let path = Path::new(pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern.");
let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) {
Some(Pattern::new(pattern).expect("Invalid pattern."))
} else {
None
};
FilePattern::Complex(absolute, basename)
}
}
#[derive(Debug, Clone, Hash)]
pub struct PerFileIgnore {
pub pattern: FilePattern,
pub codes: BTreeSet<CheckCode>,
}
impl PerFileIgnore {
pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self {
let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root);
let codes = BTreeSet::from_iter(user_in.code.codes());
Self { pattern, codes }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrCheckCodePair {
pub pattern: String,
pub code: CheckCodePrefix,
}
impl StrCheckCodePair {
const EXPECTED_PATTERN: &'static str = "<FilePattern>:<CheckCode> pattern";
}
impl<'de> Deserialize<'de> for StrCheckCodePair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str_result = String::deserialize(deserializer)?;
Self::from_str(str_result.as_str()).map_err(|_| {
de::Error::invalid_value(
de::Unexpected::Str(str_result.as_str()),
&Self::EXPECTED_PATTERN,
)
})
}
}
impl FromStr for StrCheckCodePair {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
let (pattern_str, code_string) = {
let tokens = string.split(':').collect::<Vec<_>>();
if tokens.len() != 2 {
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
}
(tokens[0].trim(), tokens[1].trim())
};
let code = CheckCodePrefix::from_str(code_string)?;
let pattern = pattern_str.into();
Ok(Self { pattern, code })
}
}

84
src/settings/user.rs Normal file
View file

@ -0,0 +1,84 @@
//! Structs to render user-facing settings.
use std::path::PathBuf;
use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
use crate::{flake8_quotes, Configuration};
/// Struct to render user-facing exclusion patterns.
#[derive(Debug)]
#[allow(dead_code)]
pub struct Exclusion {
basename: Option<String>,
absolute: Option<String>,
}
impl Exclusion {
pub fn from_file_pattern(file_pattern: FilePattern) -> Self {
match file_pattern {
FilePattern::Simple(basename) => Exclusion {
basename: Some(basename.to_string()),
absolute: None,
},
FilePattern::Complex(absolute, basename) => Exclusion {
basename: basename.map(|pattern| pattern.to_string()),
absolute: Some(absolute.to_string()),
},
}
}
}
/// Struct to render user-facing configuration.
#[derive(Debug)]
pub struct UserConfiguration {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<Exclusion>,
pub extend_exclude: Vec<Exclusion>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
// Non-settings exposed to the user
pub project_root: Option<PathBuf>,
pub pyproject: Option<PathBuf>,
}
impl UserConfiguration {
pub fn from_configuration(
settings: Configuration,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) -> Self {
Self {
dummy_variable_rgx: settings.dummy_variable_rgx,
exclude: settings
.exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_exclude: settings
.extend_exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_ignore: settings.extend_ignore,
extend_select: settings.extend_select,
ignore: settings.ignore,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
select: settings.select,
target_version: settings.target_version,
flake8_quotes: settings.flake8_quotes,
project_root,
pyproject,
}
}
}