Support hierarchical settings for nested directories (#1190)

This commit is contained in:
Charlie Marsh 2022-12-12 10:12:23 -05:00 committed by GitHub
parent 19e9eb1af8
commit 0adc9ed259
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 323 additions and 161 deletions

View file

@ -1,19 +1,21 @@
use std::path::PathBuf; use std::path::{Path, PathBuf};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use serde::Serialize; use serde::Serialize;
use walkdir::DirEntry; use walkdir::DirEntry;
use crate::checks::CheckCode; use crate::checks::CheckCode;
use crate::cli::Overrides;
use crate::fs::iter_python_files; use crate::fs::iter_python_files;
use crate::resolver::{discover_settings, Resolver};
use crate::settings::types::SerializationFormat; use crate::settings::types::SerializationFormat;
use crate::{Configuration, Settings}; use crate::{Configuration, Settings};
/// Print the user-facing configuration settings. /// Print the user-facing configuration settings.
pub fn show_settings( pub fn show_settings(
configuration: &Configuration, configuration: &Configuration,
project_root: Option<&PathBuf>, project_root: Option<&Path>,
pyproject: Option<&PathBuf>, pyproject: Option<&Path>,
) { ) {
println!("Resolved configuration: {configuration:#?}"); println!("Resolved configuration: {configuration:#?}");
println!("Found project root at: {project_root:?}"); println!("Found project root at: {project_root:?}");
@ -21,13 +23,23 @@ pub fn show_settings(
} }
/// Show the list of files to be checked based on current settings. /// Show the list of files to be checked based on current settings.
pub fn show_files(files: &[PathBuf], settings: &Settings) { pub fn show_files(files: &[PathBuf], default: &Settings, overrides: &Overrides) {
// Discover the settings for the filesystem hierarchy.
let settings = discover_settings(files, overrides);
let resolver = Resolver {
default,
settings: &settings,
};
// Collect all files in the hierarchy.
let mut entries: Vec<DirEntry> = files let mut entries: Vec<DirEntry> = files
.iter() .iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) .flat_map(|path| iter_python_files(path, &resolver))
.flatten() .flatten()
.collect(); .collect();
entries.sort_by(|a, b| a.path().cmp(b.path())); entries.sort_by(|a, b| a.path().cmp(b.path()));
// Print the list of files.
for entry in entries { for entry in entries {
println!("{}", entry.path().to_string_lossy()); println!("{}", entry.path().to_string_lossy());
} }

View file

@ -11,6 +11,7 @@ use rustc_hash::FxHashSet;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
use crate::checks::CheckCode; use crate::checks::CheckCode;
use crate::resolver::Resolver;
/// Extract the absolute path and basename (as strings) from a Path. /// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> { fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
@ -30,33 +31,62 @@ fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet)
} }
fn is_included(path: &Path) -> bool { fn is_included(path: &Path) -> bool {
let file_name = path.to_string_lossy(); path.extension()
file_name.ends_with(".py") || file_name.ends_with(".pyi") .map_or(false, |ext| ext == "py" || ext == "pyi")
} }
/// Find all `pyproject.toml` files for a given `Path`. Both parents and
/// children will be included in the resulting `Vec`.
pub fn iter_pyproject_files(path: &Path) -> Vec<PathBuf> {
let mut paths = Vec::new();
// Search for `pyproject.toml` files in all parent directories.
let path = normalize_path(path);
for path in path.ancestors() {
if path.is_dir() {
let toml_path = path.join("pyproject.toml");
if toml_path.exists() {
paths.push(toml_path);
}
}
}
// Search for `pyproject.toml` files in all child directories.
for path in WalkDir::new(path)
.into_iter()
.filter_entry(|entry| {
entry.file_name().to_str().map_or(false, |file_name| {
entry.depth() == 0 || !file_name.starts_with('.')
})
})
.filter_map(std::result::Result::ok)
.filter(|entry| entry.path().ends_with("pyproject.toml"))
{
paths.push(path.into_path());
}
paths
}
/// Find all Python (`.py` and `.pyi` files) in a given `Path`.
pub fn iter_python_files<'a>( pub fn iter_python_files<'a>(
path: &'a Path, path: &'a Path,
exclude: &'a globset::GlobSet, resolver: &'a Resolver<'a>,
extend_exclude: &'a globset::GlobSet,
) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a { ) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a {
// Run some checks over the provided patterns, to enable optimizations below.
let has_exclude = !exclude.is_empty();
let has_extend_exclude = !extend_exclude.is_empty();
WalkDir::new(normalize_path(path)) WalkDir::new(normalize_path(path))
.into_iter() .into_iter()
.filter_entry(move |entry| { .filter_entry(move |entry| {
if !has_exclude && !has_extend_exclude {
return true;
}
let path = entry.path(); let path = entry.path();
let settings = resolver.resolve(path);
let exclude = &settings.exclude;
let extend_exclude = &settings.extend_exclude;
match extract_path_names(path) { match extract_path_names(path) {
Ok((file_path, file_basename)) => { Ok((file_path, file_basename)) => {
if has_exclude && is_excluded(file_path, file_basename, exclude) { if !exclude.is_empty() && is_excluded(file_path, file_basename, exclude) {
debug!("Ignored path via `exclude`: {:?}", path); debug!("Ignored path via `exclude`: {:?}", path);
false false
} else if has_extend_exclude } else if !extend_exclude.is_empty()
&& is_excluded(file_path, file_basename, extend_exclude) && is_excluded(file_path, file_basename, extend_exclude)
{ {
debug!("Ignored path via `extend-exclude`: {:?}", path); debug!("Ignored path via `extend-exclude`: {:?}", path);
@ -131,7 +161,7 @@ pub(crate) fn read_file(path: &Path) -> Result<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::{Path, PathBuf}; use std::path::Path;
use anyhow::Result; use anyhow::Result;
use globset::GlobSet; use globset::GlobSet;
@ -155,7 +185,7 @@ mod tests {
assert!(!is_included(&path)); assert!(!is_included(&path));
} }
fn make_exclusion(file_pattern: FilePattern, project_root: Option<&PathBuf>) -> GlobSet { fn make_exclusion(file_pattern: FilePattern, project_root: Option<&Path>) -> GlobSet {
let mut builder = globset::GlobSetBuilder::new(); let mut builder = globset::GlobSetBuilder::new();
file_pattern.add_to(&mut builder, project_root).unwrap(); file_pattern.add_to(&mut builder, project_root).unwrap();
builder.build().unwrap() builder.build().unwrap()
@ -171,7 +201,7 @@ mod tests {
assert!(is_excluded( assert!(is_excluded(
file_path, file_path,
file_basename, file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf())) &make_exclusion(exclude, Some(project_root))
)); ));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
@ -180,7 +210,7 @@ mod tests {
assert!(is_excluded( assert!(is_excluded(
file_path, file_path,
file_basename, file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf())) &make_exclusion(exclude, Some(project_root))
)); ));
let path = Path::new("foo/bar/baz.py") let path = Path::new("foo/bar/baz.py")
@ -191,7 +221,7 @@ mod tests {
assert!(is_excluded( assert!(is_excluded(
file_path, file_path,
file_basename, file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf())) &make_exclusion(exclude, Some(project_root))
)); ));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
@ -200,7 +230,7 @@ mod tests {
assert!(is_excluded( assert!(is_excluded(
file_path, file_path,
file_basename, file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf())) &make_exclusion(exclude, Some(project_root))
)); ));
let path = Path::new("foo/bar/baz.py") let path = Path::new("foo/bar/baz.py")
@ -211,7 +241,7 @@ mod tests {
assert!(is_excluded( assert!(is_excluded(
file_path, file_path,
file_basename, file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf())) &make_exclusion(exclude, Some(project_root))
)); ));
let path = Path::new("foo/bar/baz.py") let path = Path::new("foo/bar/baz.py")
@ -222,7 +252,7 @@ mod tests {
assert!(is_excluded( assert!(is_excluded(
file_path, file_path,
file_basename, file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf())) &make_exclusion(exclude, Some(project_root))
)); ));
let path = Path::new("foo/bar/baz.py") let path = Path::new("foo/bar/baz.py")
@ -233,7 +263,7 @@ mod tests {
assert!(!is_excluded( assert!(!is_excluded(
file_path, file_path,
file_basename, file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf())) &make_exclusion(exclude, Some(project_root))
)); ));
Ok(()) Ok(())

16
src/iterators.rs Normal file
View file

@ -0,0 +1,16 @@
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
/// Shim that calls `par_iter` except for wasm because there's no wasm support
/// in rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, `ParallelIterator` does not implement `Iterator`
/// so the signatures diverge
#[cfg(not(target_family = "wasm"))]
pub fn par_iter<T: Sync>(iterable: &[T]) -> impl ParallelIterator<Item = &T> {
iterable.par_iter()
}
#[cfg(target_family = "wasm")]
pub fn par_iter<T: Sync>(iterable: &[T]) -> impl Iterator<Item = &T> {
iterable.iter()
}

View file

@ -58,6 +58,7 @@ pub mod flake8_tidy_imports;
mod flake8_unused_arguments; mod flake8_unused_arguments;
pub mod fs; pub mod fs;
mod isort; mod isort;
pub mod iterators;
mod lex; mod lex;
pub mod linter; pub mod linter;
pub mod logging; pub mod logging;
@ -73,6 +74,7 @@ mod pygrep_hooks;
mod pylint; mod pylint;
mod python; mod python;
mod pyupgrade; mod pyupgrade;
pub mod resolver;
mod ruff; mod ruff;
mod rustpython_helpers; mod rustpython_helpers;
pub mod settings; pub mod settings;
@ -97,8 +99,8 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
}; };
let settings = Settings::from_configuration( let settings = Settings::from_configuration(
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?, Configuration::from_pyproject(pyproject.as_ref())?,
project_root.as_ref(), project_root.as_deref(),
)?; )?;
// Tokenize once. // Tokenize once.

View file

@ -19,12 +19,14 @@ use std::time::Instant;
use ::ruff::autofix::fixer; use ::ruff::autofix::fixer;
use ::ruff::checks::{CheckCode, CheckKind}; use ::ruff::checks::{CheckCode, CheckKind};
use ::ruff::cli::{extract_log_level, Cli}; use ::ruff::cli::{extract_log_level, Cli, Overrides};
use ::ruff::fs::iter_python_files; use ::ruff::fs::iter_python_files;
use ::ruff::iterators::par_iter;
use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics}; use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics};
use ::ruff::logging::{set_up_logging, LogLevel}; use ::ruff::logging::{set_up_logging, LogLevel};
use ::ruff::message::Message; use ::ruff::message::Message;
use ::ruff::printer::Printer; use ::ruff::printer::Printer;
use ::ruff::resolver::Resolver;
use ::ruff::settings::configuration::Configuration; use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::types::SerializationFormat; use ::ruff::settings::types::SerializationFormat;
use ::ruff::settings::{pyproject, Settings}; use ::ruff::settings::{pyproject, Settings};
@ -38,23 +40,10 @@ use log::{debug, error};
use notify::{recommended_watcher, RecursiveMode, Watcher}; use notify::{recommended_watcher, RecursiveMode, Watcher};
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
use rayon::prelude::*; use rayon::prelude::*;
use ruff::resolver::discover_settings;
use rustpython_ast::Location; use rustpython_ast::Location;
use walkdir::DirEntry; use walkdir::DirEntry;
/// Shim that calls `par_iter` except for wasm because there's no wasm support
/// in rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, `ParallelIterator` does not implement `Iterator`
/// so the signatures diverge
#[cfg(not(target_family = "wasm"))]
fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl ParallelIterator<Item = &T> {
iterable.par_iter()
}
#[cfg(target_family = "wasm")]
fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl Iterator<Item = &T> {
iterable.iter()
}
fn read_from_stdin() -> Result<String> { fn read_from_stdin() -> Result<String> {
let mut buffer = String::new(); let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?; io::stdin().lock().read_to_string(&mut buffer)?;
@ -74,15 +63,23 @@ fn run_once_stdin(
fn run_once( fn run_once(
files: &[PathBuf], files: &[PathBuf],
settings: &Settings, default: &Settings,
overrides: &Overrides,
cache: bool, cache: bool,
autofix: &fixer::Mode, autofix: &fixer::Mode,
) -> Diagnostics { ) -> Diagnostics {
// Discover the settings for the filesystem hierarchy.
let settings = discover_settings(files, overrides);
let resolver = Resolver {
default,
settings: &settings,
};
// Collect all the files to check. // Collect all the files to check.
let start = Instant::now(); let start = Instant::now();
let paths: Vec<Result<DirEntry, walkdir::Error>> = files let paths: Vec<Result<DirEntry, walkdir::Error>> = files
.iter() .iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) .flat_map(|path| iter_python_files(path, &resolver))
.collect(); .collect();
let duration = start.elapsed(); let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration); debug!("Identified files to lint in: {:?}", duration);
@ -93,6 +90,7 @@ fn run_once(
match entry { match entry {
Ok(entry) => { Ok(entry) => {
let path = entry.path(); let path = entry.path();
let settings = resolver.resolve(path);
lint_path(path, settings, &cache.into(), autofix) lint_path(path, settings, &cache.into(), autofix)
.map_err(|e| (Some(path.to_owned()), e.to_string())) .map_err(|e| (Some(path.to_owned()), e.to_string()))
} }
@ -104,6 +102,7 @@ fn run_once(
} }
.unwrap_or_else(|(path, message)| { .unwrap_or_else(|(path, message)| {
if let Some(path) = path { if let Some(path) = path {
let settings = resolver.resolve(&path);
if settings.enabled.contains(&CheckCode::E902) { if settings.enabled.contains(&CheckCode::E902) {
Diagnostics::new(vec![Message { Diagnostics::new(vec![Message {
kind: CheckKind::IOError(message), kind: CheckKind::IOError(message),
@ -135,12 +134,19 @@ fn run_once(
diagnostics diagnostics
} }
fn add_noqa(files: &[PathBuf], settings: &Settings) -> usize { fn add_noqa(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> usize {
// Discover the settings for the filesystem hierarchy.
let settings = discover_settings(files, overrides);
let resolver = Resolver {
default,
settings: &settings,
};
// Collect all the files to check. // Collect all the files to check.
let start = Instant::now(); let start = Instant::now();
let paths: Vec<DirEntry> = files let paths: Vec<DirEntry> = files
.iter() .iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) .flat_map(|path| iter_python_files(path, &resolver))
.flatten() .flatten()
.collect(); .collect();
let duration = start.elapsed(); let duration = start.elapsed();
@ -150,6 +156,7 @@ fn add_noqa(files: &[PathBuf], settings: &Settings) -> usize {
let modifications: usize = par_iter(&paths) let modifications: usize = par_iter(&paths)
.filter_map(|entry| { .filter_map(|entry| {
let path = entry.path(); let path = entry.path();
let settings = resolver.resolve(path);
match add_noqa_to_path(path, settings) { match add_noqa_to_path(path, settings) {
Ok(count) => Some(count), Ok(count) => Some(count),
Err(e) => { Err(e) => {
@ -166,12 +173,19 @@ fn add_noqa(files: &[PathBuf], settings: &Settings) -> usize {
modifications modifications
} }
fn autoformat(files: &[PathBuf], settings: &Settings) -> usize { fn autoformat(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> usize {
// Discover the settings for the filesystem hierarchy.
let settings = discover_settings(files, overrides);
let resolver = Resolver {
default,
settings: &settings,
};
// Collect all the files to format. // Collect all the files to format.
let start = Instant::now(); let start = Instant::now();
let paths: Vec<DirEntry> = files let paths: Vec<DirEntry> = files
.iter() .iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) .flat_map(|path| iter_python_files(path, &resolver))
.flatten() .flatten()
.collect(); .collect();
let duration = start.elapsed(); let duration = start.elapsed();
@ -204,29 +218,45 @@ fn inner_main() -> Result<ExitCode> {
set_up_logging(&log_level)?; set_up_logging(&log_level)?;
if let Some(shell) = cli.generate_shell_completion { if let Some(shell) = cli.generate_shell_completion {
shell.generate(&mut Cli::command(), &mut std::io::stdout()); shell.generate(&mut Cli::command(), &mut io::stdout());
return Ok(ExitCode::SUCCESS); return Ok(ExitCode::SUCCESS);
} }
// Find the project root and pyproject.toml. // Find the project root and pyproject.toml.
let config: Option<PathBuf> = cli.config; // TODO(charlie): look in the current directory, but respect `--config`.
let project_root = config.as_ref().map_or_else( let project_root = cli.config.as_ref().map_or_else(
|| pyproject::find_project_root(&cli.files), || pyproject::find_project_root(&cli.files),
|config| config.parent().map(fs::normalize_path), |config| config.parent().map(fs::normalize_path),
); );
let pyproject = config.or_else(|| pyproject::find_pyproject_toml(project_root.as_ref())); let pyproject = cli
.config
.or_else(|| pyproject::find_pyproject_toml(project_root.as_ref()));
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
// Reconcile configuration from pyproject.toml and command-line arguments. // Reconcile configuration from pyproject.toml and command-line arguments.
let mut configuration = let mut configuration = Configuration::from_pyproject(pyproject.as_ref())?;
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?; configuration.merge(&overrides);
configuration.merge(overrides);
if cli.show_settings && cli.show_files { if cli.show_settings && cli.show_files {
eprintln!("Error: specify --show-settings or show-files (not both)."); eprintln!("Error: specify --show-settings or show-files (not both).");
return Ok(ExitCode::FAILURE); return Ok(ExitCode::FAILURE);
} }
if cli.show_settings { if cli.show_settings {
commands::show_settings(&configuration, project_root.as_ref(), pyproject.as_ref()); // TODO(charlie): This would be more useful if required a single file, and told
// you the settings used to lint that file.
commands::show_settings(
&configuration,
project_root.as_deref(),
pyproject.as_deref(),
);
return Ok(ExitCode::SUCCESS); return Ok(ExitCode::SUCCESS);
} }
@ -240,18 +270,7 @@ fn inner_main() -> Result<ExitCode> {
}; };
let format = configuration.format; let format = configuration.format;
let settings = Settings::from_configuration(configuration, project_root.as_ref())?; let settings = Settings::from_configuration(configuration, project_root.as_deref())?;
// Now that we've inferred the appropriate log level, add some debug
// information.
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
if let Some(code) = cli.explain { if let Some(code) = cli.explain {
commands::explain(&code, format)?; commands::explain(&code, format)?;
@ -259,7 +278,7 @@ fn inner_main() -> Result<ExitCode> {
} }
if cli.show_files { if cli.show_files {
commands::show_files(&cli.files, &settings); commands::show_files(&cli.files, &settings, &overrides);
return Ok(ExitCode::SUCCESS); return Ok(ExitCode::SUCCESS);
} }
@ -289,7 +308,13 @@ fn inner_main() -> Result<ExitCode> {
printer.clear_screen()?; printer.clear_screen()?;
printer.write_to_user("Starting linter in watch mode...\n"); printer.write_to_user("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None); let messages = run_once(
&cli.files,
&settings,
&overrides,
cache_enabled,
&fixer::Mode::None,
);
printer.write_continuously(&messages)?; printer.write_continuously(&messages)?;
// Configure the file watcher. // Configure the file watcher.
@ -305,15 +330,20 @@ fn inner_main() -> Result<ExitCode> {
let paths = e?.paths; let paths = e?.paths;
let py_changed = paths.iter().any(|p| { let py_changed = paths.iter().any(|p| {
p.extension() p.extension()
.map(|ext| ext.eq_ignore_ascii_case("py")) .map(|ext| ext == "py" || ext == "pyi")
.unwrap_or_default() .unwrap_or_default()
}); });
if py_changed { if py_changed {
printer.clear_screen()?; printer.clear_screen()?;
printer.write_to_user("File change detected...\n"); printer.write_to_user("File change detected...\n");
let messages = let messages = run_once(
run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None); &cli.files,
&settings,
&overrides,
cache_enabled,
&fixer::Mode::None,
);
printer.write_continuously(&messages)?; printer.write_continuously(&messages)?;
} }
} }
@ -321,12 +351,12 @@ fn inner_main() -> Result<ExitCode> {
} }
} }
} else if cli.add_noqa { } else if cli.add_noqa {
let modifications = add_noqa(&cli.files, &settings); let modifications = add_noqa(&cli.files, &settings, &overrides);
if modifications > 0 && log_level >= LogLevel::Default { if modifications > 0 && log_level >= LogLevel::Default {
println!("Added {modifications} noqa directives."); println!("Added {modifications} noqa directives.");
} }
} else if cli.autoformat { } else if cli.autoformat {
let modifications = autoformat(&cli.files, &settings); let modifications = autoformat(&cli.files, &settings, &overrides);
if modifications > 0 && log_level >= LogLevel::Default { if modifications > 0 && log_level >= LogLevel::Default {
println!("Formatted {modifications} files."); println!("Formatted {modifications} files.");
} }
@ -339,7 +369,7 @@ fn inner_main() -> Result<ExitCode> {
let path = Path::new(&filename); let path = Path::new(&filename);
run_once_stdin(&settings, path, &fix)? run_once_stdin(&settings, path, &fix)?
} else { } else {
run_once(&cli.files, &settings, cache_enabled, &fix) run_once(&cli.files, &settings, &overrides, cache_enabled, &fix)
}; };
// Always try to print violations (the printer itself may suppress output), // Always try to print violations (the printer itself may suppress output),

62
src/resolver.rs Normal file
View file

@ -0,0 +1,62 @@
//! Discover and resolve `Settings` from the filesystem hierarchy.
use std::cmp::Reverse;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use log::error;
use crate::cli::Overrides;
use crate::fs::iter_pyproject_files;
use crate::settings::configuration::Configuration;
use crate::settings::{pyproject, Settings};
pub struct Resolver<'a> {
pub settings: &'a [(PathBuf, Settings)],
pub default: &'a Settings,
}
impl<'a> Resolver<'a> {
pub fn resolve(&'a self, path: &Path) -> &'a Settings {
self.settings
.iter()
.find(|(root, _)| path.starts_with(root))
.map_or(self.default, |(_, settings)| settings)
}
}
/// Extract the `Settings` from a given `pyproject.toml`.
pub fn settings_for_path(pyproject: &Path, overrides: &Overrides) -> Result<(PathBuf, Settings)> {
let project_root = pyproject
.parent()
.ok_or_else(|| anyhow!("Expected pyproject.toml to be in a directory"))?
.to_path_buf();
let options = pyproject::load_options(pyproject)?;
let mut configuration = Configuration::from_options(options)?;
configuration.merge(overrides);
let settings = Settings::from_configuration(configuration, Some(&project_root))?;
Ok((project_root, settings))
}
/// Discover all `Settings` objects within the relevant filesystem hierarchy.
pub fn discover_settings(files: &[PathBuf], overrides: &Overrides) -> Vec<(PathBuf, Settings)> {
// Collect all `pyproject.toml` files.
let mut pyprojects: Vec<PathBuf> = files
.iter()
.flat_map(|path| iter_pyproject_files(path))
.collect();
pyprojects.sort_unstable_by_key(|path| Reverse(path.to_string_lossy().len()));
pyprojects.dedup();
// Read every `pyproject.toml`.
pyprojects
.into_iter()
.filter_map(|pyproject| match settings_for_path(&pyproject, overrides) {
Ok((project_root, settings)) => Some((project_root, settings)),
Err(error) => {
error!("Failed to read settings: {error}");
None
}
})
.collect::<Vec<_>>()
}

View file

@ -5,18 +5,19 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use log::debug;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use path_absolutize::path_dedot;
use regex::Regex; use regex::Regex;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use crate::checks_gen::{CheckCodePrefix, CATEGORIES}; use crate::checks_gen::{CheckCodePrefix, CATEGORIES};
use crate::cli::{collect_per_file_ignores, Overrides}; use crate::cli::{collect_per_file_ignores, Overrides};
use crate::settings::options::Options;
use crate::settings::pyproject::load_options; use crate::settings::pyproject::load_options;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat}; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat};
use crate::{ use crate::{
flake8_annotations, flake8_bugbear, flake8_import_conventions, flake8_quotes, flake8_annotations, flake8_bugbear, flake8_import_conventions, flake8_quotes,
flake8_tidy_imports, fs, isort, mccabe, pep8_naming, pyupgrade, flake8_tidy_imports, isort, mccabe, pep8_naming, pyupgrade,
}; };
#[derive(Debug)] #[derive(Debug)]
@ -80,11 +81,18 @@ static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap()); Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl Configuration { impl Configuration {
pub fn from_pyproject( pub fn from_pyproject(pyproject: Option<&PathBuf>) -> Result<Self> {
pyproject: Option<&PathBuf>, Self::from_options(pyproject.map_or_else(
project_root: Option<&PathBuf>, || {
) -> Result<Self> { debug!("No pyproject.toml found.");
let options = load_options(pyproject)?; debug!("Falling back to default configuration...");
Ok(Options::default())
},
|path| load_options(path),
)?)
}
pub fn from_options(options: Options) -> Result<Self> {
Ok(Configuration { Ok(Configuration {
allowed_confusables: FxHashSet::from_iter( allowed_confusables: FxHashSet::from_iter(
options.allowed_confusables.unwrap_or_default(), options.allowed_confusables.unwrap_or_default(),
@ -95,21 +103,10 @@ impl Configuration {
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(), None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
}, },
src: options.src.map_or_else( src: options.src.map_or_else(
|| { || vec![Path::new(".").to_path_buf()],
vec![match project_root {
Some(project_root) => project_root.clone(),
None => path_dedot::CWD.clone(),
}]
},
|src| { |src| {
src.iter() src.iter()
.map(|path| { .map(|path| Path::new(path).to_path_buf())
let path = Path::new(path);
match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
}
})
.collect() .collect()
}, },
), ),
@ -187,54 +184,54 @@ impl Configuration {
}) })
} }
pub fn merge(&mut self, overrides: Overrides) { pub fn merge(&mut self, overrides: &Overrides) {
if let Some(dummy_variable_rgx) = overrides.dummy_variable_rgx { if let Some(dummy_variable_rgx) = &overrides.dummy_variable_rgx {
self.dummy_variable_rgx = dummy_variable_rgx; self.dummy_variable_rgx = dummy_variable_rgx.clone();
} }
if let Some(exclude) = overrides.exclude { if let Some(exclude) = &overrides.exclude {
self.exclude = exclude; self.exclude = exclude.clone();
} }
if let Some(extend_exclude) = overrides.extend_exclude { if let Some(extend_exclude) = &overrides.extend_exclude {
self.extend_exclude = extend_exclude; self.extend_exclude = extend_exclude.clone();
} }
if let Some(extend_ignore) = overrides.extend_ignore { if let Some(extend_ignore) = &overrides.extend_ignore {
self.extend_ignore = extend_ignore; self.extend_ignore = extend_ignore.clone();
} }
if let Some(extend_select) = overrides.extend_select { if let Some(extend_select) = &overrides.extend_select {
self.extend_select = extend_select; self.extend_select = extend_select.clone();
} }
if let Some(fix) = overrides.fix { if let Some(fix) = &overrides.fix {
self.fix = fix; self.fix = *fix;
} }
if let Some(fixable) = overrides.fixable { if let Some(fixable) = &overrides.fixable {
self.fixable = fixable; self.fixable = fixable.clone();
} }
if let Some(format) = overrides.format { if let Some(format) = &overrides.format {
self.format = format; self.format = *format;
} }
if let Some(ignore) = overrides.ignore { if let Some(ignore) = &overrides.ignore {
self.ignore = ignore; self.ignore = ignore.clone();
} }
if let Some(line_length) = overrides.line_length { if let Some(line_length) = &overrides.line_length {
self.line_length = line_length; self.line_length = *line_length;
} }
if let Some(max_complexity) = overrides.max_complexity { if let Some(max_complexity) = &overrides.max_complexity {
self.mccabe.max_complexity = max_complexity; self.mccabe.max_complexity = *max_complexity;
} }
if let Some(per_file_ignores) = overrides.per_file_ignores { if let Some(per_file_ignores) = &overrides.per_file_ignores {
self.per_file_ignores = collect_per_file_ignores(per_file_ignores); self.per_file_ignores = collect_per_file_ignores(per_file_ignores.clone());
} }
if let Some(select) = overrides.select { if let Some(select) = &overrides.select {
self.select = select; self.select = select.clone();
} }
if let Some(show_source) = overrides.show_source { if let Some(show_source) = &overrides.show_source {
self.show_source = show_source; self.show_source = *show_source;
} }
if let Some(target_version) = overrides.target_version { if let Some(target_version) = &overrides.target_version {
self.target_version = target_version; self.target_version = *target_version;
} }
if let Some(unfixable) = overrides.unfixable { if let Some(unfixable) = &overrides.unfixable {
self.unfixable = unfixable; self.unfixable = unfixable.clone();
} }
} }
} }

View file

@ -55,10 +55,7 @@ pub struct Settings {
} }
impl Settings { impl Settings {
pub fn from_configuration( pub fn from_configuration(config: Configuration, project_root: Option<&Path>) -> Result<Self> {
config: Configuration,
project_root: Option<&PathBuf>,
) -> Result<Self> {
Ok(Self { Ok(Self {
allowed_confusables: config.allowed_confusables, allowed_confusables: config.allowed_confusables,
dummy_variable_rgx: config.dummy_variable_rgx, dummy_variable_rgx: config.dummy_variable_rgx,
@ -90,7 +87,7 @@ impl Settings {
pep8_naming: config.pep8_naming, pep8_naming: config.pep8_naming,
pyupgrade: config.pyupgrade, pyupgrade: config.pyupgrade,
per_file_ignores: resolve_per_file_ignores(config.per_file_ignores, project_root)?, per_file_ignores: resolve_per_file_ignores(config.per_file_ignores, project_root)?,
src: config.src, src: resolve_src(config.src, project_root),
target_version: config.target_version, target_version: config.target_version,
show_source: config.show_source, show_source: config.show_source,
}) })
@ -192,10 +189,7 @@ impl Hash for Settings {
} }
/// Given a list of patterns, create a `GlobSet`. /// Given a list of patterns, create a `GlobSet`.
pub fn resolve_globset( pub fn resolve_globset(patterns: Vec<FilePattern>, project_root: Option<&Path>) -> Result<GlobSet> {
patterns: Vec<FilePattern>,
project_root: Option<&PathBuf>,
) -> Result<GlobSet> {
let mut builder = globset::GlobSetBuilder::new(); let mut builder = globset::GlobSetBuilder::new();
for pattern in patterns { for pattern in patterns {
pattern.add_to(&mut builder, project_root)?; pattern.add_to(&mut builder, project_root)?;
@ -206,7 +200,7 @@ pub fn resolve_globset(
/// Given a list of patterns, create a `GlobSet`. /// Given a list of patterns, create a `GlobSet`.
pub fn resolve_per_file_ignores( pub fn resolve_per_file_ignores(
per_file_ignores: Vec<PerFileIgnore>, per_file_ignores: Vec<PerFileIgnore>,
project_root: Option<&PathBuf>, project_root: Option<&Path>,
) -> Result<Vec<(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)>> { ) -> Result<Vec<(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)>> {
per_file_ignores per_file_ignores
.into_iter() .into_iter()
@ -227,6 +221,15 @@ pub fn resolve_per_file_ignores(
.collect() .collect()
} }
pub fn resolve_src(src: Vec<PathBuf>, project_root: Option<&Path>) -> Vec<PathBuf> {
src.into_iter()
.map(|path| match project_root {
Some(project_root) => fs::normalize_path_to(&path, project_root),
None => fs::normalize_path(&path),
})
.collect()
}
/// Given a set of selected and ignored prefixes, resolve the set of enabled /// Given a set of selected and ignored prefixes, resolve the set of enabled
/// error codes. /// error codes.
fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet<CheckCode> { fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet<CheckCode> {

View file

@ -4,7 +4,6 @@ use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use common_path::common_path_all; use common_path::common_path_all;
use log::debug;
use path_absolutize::Absolutize; use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -80,18 +79,12 @@ pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
None None
} }
pub fn load_options(pyproject: Option<&PathBuf>) -> Result<Options> { pub fn load_options(pyproject: &Path) -> Result<Options> {
if let Some(pyproject) = pyproject {
Ok(parse_pyproject_toml(pyproject) Ok(parse_pyproject_toml(pyproject)
.map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))? .map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))?
.tool .tool
.and_then(|tool| tool.ruff) .and_then(|tool| tool.ruff)
.unwrap_or_default()) .unwrap_or_default())
} else {
debug!("No pyproject.toml found.");
debug!("Falling back to default configuration...");
Ok(Options::default())
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,6 +1,6 @@
use std::env; use std::env;
use std::hash::Hash; use std::hash::Hash;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
@ -53,11 +53,7 @@ pub enum FilePattern {
} }
impl FilePattern { impl FilePattern {
pub fn add_to( pub fn add_to(self, builder: &mut GlobSetBuilder, project_root: Option<&Path>) -> Result<()> {
self,
builder: &mut GlobSetBuilder,
project_root: Option<&PathBuf>,
) -> Result<()> {
match self { match self {
FilePattern::Builtin(pattern) => { FilePattern::Builtin(pattern) => {
builder.add(Glob::from_str(pattern)?); builder.add(Glob::from_str(pattern)?);

View file

@ -0,0 +1,5 @@
import os
def f():
x = 1

View file

@ -0,0 +1,5 @@
import os
def f():
x = 1

View file

@ -0,0 +1,4 @@
[tool.ruff]
src = ["docs_snippets"]
extend-ignore = ["F401"]
extend-exclude = ["./docs_snippets/concepts/ignore.py"]

View file

@ -0,0 +1,2 @@
[tool.ruff]
src = ["src"]

View file

5
test_project/src/core.py Normal file
View file

@ -0,0 +1,5 @@
import os
def f():
x = 1