From 0de5bcbcc7bb659679a9829d53a629fec56cedab Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 10 Dec 2022 16:25:58 -0500 Subject: [PATCH] Support hierarchical settings for nested directories --- src/commands.rs | 22 ++- src/fs.rs | 76 +++++++---- src/iterators.rs | 16 +++ src/lib.rs | 6 +- src/main.rs | 128 +++++++++++------- src/resolver.rs | 62 +++++++++ src/settings/configuration.rs | 103 +++++++------- src/settings/mod.rs | 23 ++-- src/settings/pyproject.rs | 19 +-- src/settings/types.rs | 8 +- .../docs_snippets/docs_snippets/__init__.py | 0 .../docs_snippets/docs_snippets/celery_job.py | 5 + .../docs_snippets/concepts/__init__.py | 0 .../docs_snippets/concepts/ignore.py | 5 + .../examples/docs_snippets/pyproject.toml | 4 + test_project/pyproject.toml | 2 + test_project/src/__init__.py | 0 test_project/src/core.py | 5 + 18 files changed, 323 insertions(+), 161 deletions(-) create mode 100644 src/iterators.rs create mode 100644 src/resolver.rs create mode 100644 test_project/examples/docs_snippets/docs_snippets/__init__.py create mode 100755 test_project/examples/docs_snippets/docs_snippets/celery_job.py create mode 100644 test_project/examples/docs_snippets/docs_snippets/concepts/__init__.py create mode 100644 test_project/examples/docs_snippets/docs_snippets/concepts/ignore.py create mode 100644 test_project/examples/docs_snippets/pyproject.toml create mode 100644 test_project/pyproject.toml create mode 100644 test_project/src/__init__.py create mode 100644 test_project/src/core.py diff --git a/src/commands.rs b/src/commands.rs index 645358c52dd0f2..7134fd0661fce7 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,19 +1,21 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::{bail, Result}; use serde::Serialize; use walkdir::DirEntry; use crate::checks::CheckCode; +use crate::cli::Overrides; use crate::fs::iter_python_files; +use crate::resolver::{discover_settings, Resolver}; use crate::settings::types::SerializationFormat; use crate::{Configuration, Settings}; /// Print the user-facing configuration settings. pub fn show_settings( configuration: &Configuration, - project_root: Option<&PathBuf>, - pyproject: Option<&PathBuf>, + project_root: Option<&Path>, + pyproject: Option<&Path>, ) { println!("Resolved configuration: {configuration:#?}"); 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. -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 = files .iter() - .flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) + .flat_map(|path| iter_python_files(path, &resolver)) .flatten() .collect(); entries.sort_by(|a, b| a.path().cmp(b.path())); + + // Print the list of files. for entry in entries { println!("{}", entry.path().to_string_lossy()); } diff --git a/src/fs.rs b/src/fs.rs index ae5cf63d8e5b95..285c4ea499d0ab 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -11,6 +11,7 @@ use rustc_hash::FxHashSet; use walkdir::{DirEntry, WalkDir}; use crate::checks::CheckCode; +use crate::resolver::Resolver; /// Extract the absolute path and basename (as strings) from a Path. 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 { - let file_name = path.to_string_lossy(); - file_name.ends_with(".py") || file_name.ends_with(".pyi") + path.extension() + .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 { + 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>( path: &'a Path, - exclude: &'a globset::GlobSet, - extend_exclude: &'a globset::GlobSet, + resolver: &'a Resolver<'a>, ) -> impl Iterator> + '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)) .into_iter() .filter_entry(move |entry| { - if !has_exclude && !has_extend_exclude { - return true; - } - let path = entry.path(); + let settings = resolver.resolve(path); + let exclude = &settings.exclude; + let extend_exclude = &settings.extend_exclude; + match extract_path_names(path) { 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); false - } else if has_extend_exclude + } else if !extend_exclude.is_empty() && is_excluded(file_path, file_basename, extend_exclude) { debug!("Ignored path via `extend-exclude`: {:?}", path); @@ -131,7 +161,7 @@ pub(crate) fn read_file(path: &Path) -> Result { #[cfg(test)] mod tests { - use std::path::{Path, PathBuf}; + use std::path::Path; use anyhow::Result; use globset::GlobSet; @@ -155,7 +185,7 @@ mod tests { 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(); file_pattern.add_to(&mut builder, project_root).unwrap(); builder.build().unwrap() @@ -171,7 +201,7 @@ mod tests { assert!(is_excluded( file_path, 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(); @@ -180,7 +210,7 @@ mod tests { assert!(is_excluded( file_path, 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") @@ -191,7 +221,7 @@ mod tests { assert!(is_excluded( file_path, 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(); @@ -200,7 +230,7 @@ mod tests { assert!(is_excluded( file_path, 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") @@ -211,7 +241,7 @@ mod tests { assert!(is_excluded( file_path, 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") @@ -222,7 +252,7 @@ mod tests { assert!(is_excluded( file_path, 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") @@ -233,7 +263,7 @@ mod tests { assert!(!is_excluded( file_path, file_basename, - &make_exclusion(exclude, Some(&project_root.to_path_buf())) + &make_exclusion(exclude, Some(project_root)) )); Ok(()) diff --git a/src/iterators.rs b/src/iterators.rs new file mode 100644 index 00000000000000..80603b1637f933 --- /dev/null +++ b/src/iterators.rs @@ -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(iterable: &[T]) -> impl ParallelIterator { + iterable.par_iter() +} + +#[cfg(target_family = "wasm")] +pub fn par_iter(iterable: &[T]) -> impl Iterator { + iterable.iter() +} diff --git a/src/lib.rs b/src/lib.rs index b9d8e1b5844198..248ac546392d04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,7 @@ pub mod flake8_tidy_imports; mod flake8_unused_arguments; pub mod fs; mod isort; +pub mod iterators; mod lex; pub mod linter; pub mod logging; @@ -73,6 +74,7 @@ mod pygrep_hooks; mod pylint; mod python; mod pyupgrade; +pub mod resolver; mod ruff; mod rustpython_helpers; pub mod settings; @@ -97,8 +99,8 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result> { }; let settings = Settings::from_configuration( - Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?, - project_root.as_ref(), + Configuration::from_pyproject(pyproject.as_ref())?, + project_root.as_deref(), )?; // Tokenize once. diff --git a/src/main.rs b/src/main.rs index ad4b6d2b05c06c..6c4abf1024bea6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,12 +19,14 @@ use std::time::Instant; use ::ruff::autofix::fixer; 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::iterators::par_iter; use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics}; use ::ruff::logging::{set_up_logging, LogLevel}; use ::ruff::message::Message; use ::ruff::printer::Printer; +use ::ruff::resolver::Resolver; use ::ruff::settings::configuration::Configuration; use ::ruff::settings::types::SerializationFormat; use ::ruff::settings::{pyproject, Settings}; @@ -38,23 +40,10 @@ use log::{debug, error}; use notify::{recommended_watcher, RecursiveMode, Watcher}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; +use ruff::resolver::discover_settings; use rustpython_ast::Location; 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(iterable: &Vec) -> impl ParallelIterator { - iterable.par_iter() -} - -#[cfg(target_family = "wasm")] -fn par_iter(iterable: &Vec) -> impl Iterator { - iterable.iter() -} - fn read_from_stdin() -> Result { let mut buffer = String::new(); io::stdin().lock().read_to_string(&mut buffer)?; @@ -74,15 +63,23 @@ fn run_once_stdin( fn run_once( files: &[PathBuf], - settings: &Settings, + default: &Settings, + overrides: &Overrides, cache: bool, autofix: &fixer::Mode, ) -> 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. let start = Instant::now(); let paths: Vec> = files .iter() - .flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) + .flat_map(|path| iter_python_files(path, &resolver)) .collect(); let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); @@ -93,6 +90,7 @@ fn run_once( match entry { Ok(entry) => { let path = entry.path(); + let settings = resolver.resolve(path); lint_path(path, settings, &cache.into(), autofix) .map_err(|e| (Some(path.to_owned()), e.to_string())) } @@ -104,6 +102,7 @@ fn run_once( } .unwrap_or_else(|(path, message)| { if let Some(path) = path { + let settings = resolver.resolve(&path); if settings.enabled.contains(&CheckCode::E902) { Diagnostics::new(vec![Message { kind: CheckKind::IOError(message), @@ -135,12 +134,19 @@ fn run_once( 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. let start = Instant::now(); let paths: Vec = files .iter() - .flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) + .flat_map(|path| iter_python_files(path, &resolver)) .flatten() .collect(); let duration = start.elapsed(); @@ -150,6 +156,7 @@ fn add_noqa(files: &[PathBuf], settings: &Settings) -> usize { let modifications: usize = par_iter(&paths) .filter_map(|entry| { let path = entry.path(); + let settings = resolver.resolve(path); match add_noqa_to_path(path, settings) { Ok(count) => Some(count), Err(e) => { @@ -166,12 +173,19 @@ fn add_noqa(files: &[PathBuf], settings: &Settings) -> usize { 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. let start = Instant::now(); let paths: Vec = files .iter() - .flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) + .flat_map(|path| iter_python_files(path, &resolver)) .flatten() .collect(); let duration = start.elapsed(); @@ -204,29 +218,45 @@ fn inner_main() -> Result { set_up_logging(&log_level)?; 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); } // Find the project root and pyproject.toml. - let config: Option = cli.config; - let project_root = config.as_ref().map_or_else( + // TODO(charlie): look in the current directory, but respect `--config`. + let project_root = cli.config.as_ref().map_or_else( || pyproject::find_project_root(&cli.files), |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. - let mut configuration = - Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?; - configuration.merge(overrides); + let mut configuration = Configuration::from_pyproject(pyproject.as_ref())?; + configuration.merge(&overrides); if cli.show_settings && cli.show_files { eprintln!("Error: specify --show-settings or show-files (not both)."); return Ok(ExitCode::FAILURE); } + 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); } @@ -240,18 +270,7 @@ fn inner_main() -> Result { }; let format = configuration.format; - let settings = Settings::from_configuration(configuration, project_root.as_ref())?; - - // 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..."), - }; + let settings = Settings::from_configuration(configuration, project_root.as_deref())?; if let Some(code) = cli.explain { commands::explain(&code, format)?; @@ -259,7 +278,7 @@ fn inner_main() -> Result { } if cli.show_files { - commands::show_files(&cli.files, &settings); + commands::show_files(&cli.files, &settings, &overrides); return Ok(ExitCode::SUCCESS); } @@ -289,7 +308,13 @@ fn inner_main() -> Result { printer.clear_screen()?; 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)?; // Configure the file watcher. @@ -305,15 +330,20 @@ fn inner_main() -> Result { let paths = e?.paths; let py_changed = paths.iter().any(|p| { p.extension() - .map(|ext| ext.eq_ignore_ascii_case("py")) + .map(|ext| ext == "py" || ext == "pyi") .unwrap_or_default() }); if py_changed { printer.clear_screen()?; printer.write_to_user("File change detected...\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)?; } } @@ -321,12 +351,12 @@ fn inner_main() -> Result { } } } 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 { println!("Added {modifications} noqa directives."); } } else if cli.autoformat { - let modifications = autoformat(&cli.files, &settings); + let modifications = autoformat(&cli.files, &settings, &overrides); if modifications > 0 && log_level >= LogLevel::Default { println!("Formatted {modifications} files."); } @@ -339,7 +369,7 @@ fn inner_main() -> Result { let path = Path::new(&filename); run_once_stdin(&settings, path, &fix)? } 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), diff --git a/src/resolver.rs b/src/resolver.rs new file mode 100644 index 00000000000000..feac81003c97e0 --- /dev/null +++ b/src/resolver.rs @@ -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 = 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::>() +} diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 66ba7b3cce39b1..ec7792f036e4f1 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -5,18 +5,19 @@ use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; +use log::debug; use once_cell::sync::Lazy; -use path_absolutize::path_dedot; use regex::Regex; use rustc_hash::FxHashSet; use crate::checks_gen::{CheckCodePrefix, CATEGORIES}; use crate::cli::{collect_per_file_ignores, Overrides}; +use crate::settings::options::Options; use crate::settings::pyproject::load_options; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat}; use crate::{ 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)] @@ -80,11 +81,18 @@ static DEFAULT_DUMMY_VARIABLE_RGX: Lazy = 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>, - ) -> Result { - let options = load_options(pyproject)?; + pub fn from_pyproject(pyproject: Option<&PathBuf>) -> Result { + Self::from_options(pyproject.map_or_else( + || { + debug!("No pyproject.toml found."); + debug!("Falling back to default configuration..."); + Ok(Options::default()) + }, + |path| load_options(path), + )?) + } + + pub fn from_options(options: Options) -> Result { Ok(Configuration { allowed_confusables: FxHashSet::from_iter( options.allowed_confusables.unwrap_or_default(), @@ -95,21 +103,10 @@ impl Configuration { None => DEFAULT_DUMMY_VARIABLE_RGX.clone(), }, src: options.src.map_or_else( - || { - vec![match project_root { - Some(project_root) => project_root.clone(), - None => path_dedot::CWD.clone(), - }] - }, + || vec![Path::new(".").to_path_buf()], |src| { src.iter() - .map(|path| { - let path = Path::new(path); - match project_root { - Some(project_root) => fs::normalize_path_to(path, project_root), - None => fs::normalize_path(path), - } - }) + .map(|path| Path::new(path).to_path_buf()) .collect() }, ), @@ -187,54 +184,54 @@ impl Configuration { }) } - pub fn merge(&mut self, overrides: Overrides) { - if let Some(dummy_variable_rgx) = overrides.dummy_variable_rgx { - self.dummy_variable_rgx = dummy_variable_rgx; + pub fn merge(&mut self, overrides: &Overrides) { + if let Some(dummy_variable_rgx) = &overrides.dummy_variable_rgx { + self.dummy_variable_rgx = dummy_variable_rgx.clone(); } - if let Some(exclude) = overrides.exclude { - self.exclude = exclude; + if let Some(exclude) = &overrides.exclude { + self.exclude = exclude.clone(); } - if let Some(extend_exclude) = overrides.extend_exclude { - self.extend_exclude = extend_exclude; + if let Some(extend_exclude) = &overrides.extend_exclude { + self.extend_exclude = extend_exclude.clone(); } - if let Some(extend_ignore) = overrides.extend_ignore { - self.extend_ignore = extend_ignore; + if let Some(extend_ignore) = &overrides.extend_ignore { + self.extend_ignore = extend_ignore.clone(); } - if let Some(extend_select) = overrides.extend_select { - self.extend_select = extend_select; + if let Some(extend_select) = &overrides.extend_select { + self.extend_select = extend_select.clone(); } - if let Some(fix) = overrides.fix { - self.fix = fix; + if let Some(fix) = &overrides.fix { + self.fix = *fix; } - if let Some(fixable) = overrides.fixable { - self.fixable = fixable; + if let Some(fixable) = &overrides.fixable { + self.fixable = fixable.clone(); } - if let Some(format) = overrides.format { - self.format = format; + if let Some(format) = &overrides.format { + self.format = *format; } - if let Some(ignore) = overrides.ignore { - self.ignore = ignore; + if let Some(ignore) = &overrides.ignore { + self.ignore = ignore.clone(); } - if let Some(line_length) = overrides.line_length { - self.line_length = line_length; + if let Some(line_length) = &overrides.line_length { + self.line_length = *line_length; } - if let Some(max_complexity) = overrides.max_complexity { - self.mccabe.max_complexity = max_complexity; + if let Some(max_complexity) = &overrides.max_complexity { + self.mccabe.max_complexity = *max_complexity; } - if let Some(per_file_ignores) = overrides.per_file_ignores { - self.per_file_ignores = collect_per_file_ignores(per_file_ignores); + if let Some(per_file_ignores) = &overrides.per_file_ignores { + self.per_file_ignores = collect_per_file_ignores(per_file_ignores.clone()); } - if let Some(select) = overrides.select { - self.select = select; + if let Some(select) = &overrides.select { + self.select = select.clone(); } - if let Some(show_source) = overrides.show_source { - self.show_source = show_source; + if let Some(show_source) = &overrides.show_source { + self.show_source = *show_source; } - if let Some(target_version) = overrides.target_version { - self.target_version = target_version; + if let Some(target_version) = &overrides.target_version { + self.target_version = *target_version; } - if let Some(unfixable) = overrides.unfixable { - self.unfixable = unfixable; + if let Some(unfixable) = &overrides.unfixable { + self.unfixable = unfixable.clone(); } } } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 3b9c9b2361d00f..2eac73fe51f88d 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -55,10 +55,7 @@ pub struct Settings { } impl Settings { - pub fn from_configuration( - config: Configuration, - project_root: Option<&PathBuf>, - ) -> Result { + pub fn from_configuration(config: Configuration, project_root: Option<&Path>) -> Result { Ok(Self { allowed_confusables: config.allowed_confusables, dummy_variable_rgx: config.dummy_variable_rgx, @@ -90,7 +87,7 @@ impl Settings { pep8_naming: config.pep8_naming, pyupgrade: config.pyupgrade, 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, show_source: config.show_source, }) @@ -192,10 +189,7 @@ impl Hash for Settings { } /// Given a list of patterns, create a `GlobSet`. -pub fn resolve_globset( - patterns: Vec, - project_root: Option<&PathBuf>, -) -> Result { +pub fn resolve_globset(patterns: Vec, project_root: Option<&Path>) -> Result { let mut builder = globset::GlobSetBuilder::new(); for pattern in patterns { pattern.add_to(&mut builder, project_root)?; @@ -206,7 +200,7 @@ pub fn resolve_globset( /// Given a list of patterns, create a `GlobSet`. pub fn resolve_per_file_ignores( per_file_ignores: Vec, - project_root: Option<&PathBuf>, + project_root: Option<&Path>, ) -> Result)>> { per_file_ignores .into_iter() @@ -227,6 +221,15 @@ pub fn resolve_per_file_ignores( .collect() } +pub fn resolve_src(src: Vec, project_root: Option<&Path>) -> Vec { + 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 /// error codes. fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet { diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index 2afef701380edc..18523d0b8bb70e 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -4,7 +4,6 @@ use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; use common_path::common_path_all; -use log::debug; use path_absolutize::Absolutize; use serde::{Deserialize, Serialize}; @@ -80,18 +79,12 @@ pub fn find_project_root(sources: &[PathBuf]) -> Option { None } -pub fn load_options(pyproject: Option<&PathBuf>) -> Result { - if let Some(pyproject) = pyproject { - Ok(parse_pyproject_toml(pyproject) - .map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))? - .tool - .and_then(|tool| tool.ruff) - .unwrap_or_default()) - } else { - debug!("No pyproject.toml found."); - debug!("Falling back to default configuration..."); - Ok(Options::default()) - } +pub fn load_options(pyproject: &Path) -> Result { + Ok(parse_pyproject_toml(pyproject) + .map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))? + .tool + .and_then(|tool| tool.ruff) + .unwrap_or_default()) } #[cfg(test)] diff --git a/src/settings/types.rs b/src/settings/types.rs index 3021cc01111803..a99a9774a5e0db 100644 --- a/src/settings/types.rs +++ b/src/settings/types.rs @@ -1,6 +1,6 @@ use std::env; use std::hash::Hash; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::str::FromStr; use anyhow::{anyhow, bail, Result}; @@ -53,11 +53,7 @@ pub enum FilePattern { } impl FilePattern { - pub fn add_to( - self, - builder: &mut GlobSetBuilder, - project_root: Option<&PathBuf>, - ) -> Result<()> { + pub fn add_to(self, builder: &mut GlobSetBuilder, project_root: Option<&Path>) -> Result<()> { match self { FilePattern::Builtin(pattern) => { builder.add(Glob::from_str(pattern)?); diff --git a/test_project/examples/docs_snippets/docs_snippets/__init__.py b/test_project/examples/docs_snippets/docs_snippets/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test_project/examples/docs_snippets/docs_snippets/celery_job.py b/test_project/examples/docs_snippets/docs_snippets/celery_job.py new file mode 100755 index 00000000000000..4a49d9883f684d --- /dev/null +++ b/test_project/examples/docs_snippets/docs_snippets/celery_job.py @@ -0,0 +1,5 @@ +import os + + +def f(): + x = 1 diff --git a/test_project/examples/docs_snippets/docs_snippets/concepts/__init__.py b/test_project/examples/docs_snippets/docs_snippets/concepts/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test_project/examples/docs_snippets/docs_snippets/concepts/ignore.py b/test_project/examples/docs_snippets/docs_snippets/concepts/ignore.py new file mode 100644 index 00000000000000..4a49d9883f684d --- /dev/null +++ b/test_project/examples/docs_snippets/docs_snippets/concepts/ignore.py @@ -0,0 +1,5 @@ +import os + + +def f(): + x = 1 diff --git a/test_project/examples/docs_snippets/pyproject.toml b/test_project/examples/docs_snippets/pyproject.toml new file mode 100644 index 00000000000000..9eb281d69c4e50 --- /dev/null +++ b/test_project/examples/docs_snippets/pyproject.toml @@ -0,0 +1,4 @@ +[tool.ruff] +src = ["docs_snippets"] +extend-ignore = ["F401"] +extend-exclude = ["./docs_snippets/concepts/ignore.py"] diff --git a/test_project/pyproject.toml b/test_project/pyproject.toml new file mode 100644 index 00000000000000..85d4d5352beb45 --- /dev/null +++ b/test_project/pyproject.toml @@ -0,0 +1,2 @@ +[tool.ruff] +src = ["src"] diff --git a/test_project/src/__init__.py b/test_project/src/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test_project/src/core.py b/test_project/src/core.py new file mode 100644 index 00000000000000..4a49d9883f684d --- /dev/null +++ b/test_project/src/core.py @@ -0,0 +1,5 @@ +import os + + +def f(): + x = 1