diff --git a/flake8_to_ruff/src/converter.rs b/flake8_to_ruff/src/converter.rs index 6919ee6371..c79676d94f 100644 --- a/flake8_to_ruff/src/converter.rs +++ b/flake8_to_ruff/src/converter.rs @@ -268,6 +268,7 @@ mod tests { fix: None, fixable: None, format: None, + force_exclude: None, ignore: Some(vec![]), ignore_init_module_imports: None, line_length: None, @@ -317,6 +318,7 @@ mod tests { fix: None, fixable: None, format: None, + force_exclude: None, ignore: Some(vec![]), ignore_init_module_imports: None, line_length: Some(100), @@ -366,6 +368,7 @@ mod tests { fix: None, fixable: None, format: None, + force_exclude: None, ignore: Some(vec![]), ignore_init_module_imports: None, line_length: Some(100), @@ -415,6 +418,7 @@ mod tests { fix: None, fixable: None, format: None, + force_exclude: None, ignore: Some(vec![]), ignore_init_module_imports: None, line_length: None, @@ -464,6 +468,7 @@ mod tests { fix: None, fixable: None, format: None, + force_exclude: None, ignore: Some(vec![]), ignore_init_module_imports: None, line_length: None, @@ -521,6 +526,7 @@ mod tests { fix: None, fixable: None, format: None, + force_exclude: None, ignore: Some(vec![]), ignore_init_module_imports: None, line_length: None, @@ -606,6 +612,7 @@ mod tests { fix: None, fixable: None, format: None, + force_exclude: None, ignore: Some(vec![]), ignore_init_module_imports: None, line_length: None, diff --git a/pyproject.toml b/pyproject.toml index 551b8d646a..0c60e2d2b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,10 @@ build-backend = "maturin" bindings = "bin" strip = true +[tool.ruff] +force-exclude = true +exclude = ["setup.py"] + [tool.ruff.isort] force-wrap-aliases = true combine-as-imports = true diff --git a/setup.py b/setup.py index f9734c974a..ae851fbadf 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import sys +import os from setuptools import setup sys.stderr.write( diff --git a/src/cli.rs b/src/cli.rs index af81b54ec0..f48a121208 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -92,6 +92,12 @@ pub struct Cli { respect_gitignore: bool, #[clap(long, overrides_with("respect_gitignore"), hide = true)] no_respect_gitignore: bool, + /// Enforce exclusions, even for paths passed to Ruff directly on the + /// command-line. + #[arg(long, overrides_with("no_show_source"))] + force_exclude: bool, + #[clap(long, overrides_with("force_exclude"), hide = true)] + no_force_exclude: bool, /// See the files Ruff will be run against with the current settings. #[arg(long)] pub show_files: bool, @@ -173,6 +179,7 @@ impl Cli { // TODO(charlie): Included in `pyproject.toml`, but not inherited. fix: resolve_bool_arg(self.fix, self.no_fix), format: self.format, + force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude), }, ) } @@ -230,6 +237,7 @@ pub struct Overrides { // TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`. pub fix: Option, pub format: Option, + pub force_exclude: Option, } /// Map the CLI settings to a `LogLevel`. diff --git a/src/main.rs b/src/main.rs index ebb7f3707e..cb75132031 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,6 +103,10 @@ fn inner_main() -> Result { // Extract options that are included in `Settings`, but only apply at the top // level. let file_strategy = FileDiscovery { + force_exclude: match &pyproject_strategy { + PyprojectDiscovery::Fixed(settings) => settings.force_exclude, + PyprojectDiscovery::Hierarchical(settings) => settings.force_exclude, + }, respect_gitignore: match &pyproject_strategy { PyprojectDiscovery::Fixed(settings) => settings.respect_gitignore, PyprojectDiscovery::Hierarchical(settings) => settings.respect_gitignore, diff --git a/src/resolver.rs b/src/resolver.rs index 3d8f890867..a9eb336b2a 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -20,6 +20,7 @@ use crate::settings::{pyproject, Settings}; /// The strategy used to discover Python files in the filesystem.. #[derive(Debug)] pub struct FileDiscovery { + pub force_exclude: bool, pub respect_gitignore: bool, } @@ -263,7 +264,7 @@ pub fn python_files_in_path( // Respect our own exclusion behavior. if let Ok(entry) = &result { - if entry.depth() > 0 { + if file_strategy.force_exclude || entry.depth() > 0 { let path = entry.path(); let resolver = resolver.read().unwrap(); let settings = resolver.resolve(path, pyproject_strategy); diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 4b991c1e3e..e517b7c490 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -31,6 +31,7 @@ pub struct Configuration { pub fix: Option, pub fixable: Option>, pub format: Option, + pub force_exclude: Option, pub ignore: Option>, pub ignore_init_module_imports: Option, pub line_length: Option, @@ -96,6 +97,7 @@ impl Configuration { fix: options.fix, fixable: options.fixable, format: options.format, + force_exclude: options.force_exclude, ignore: options.ignore, ignore_init_module_imports: options.ignore_init_module_imports, line_length: options.line_length, @@ -159,6 +161,7 @@ impl Configuration { fix: self.fix.or(config.fix), fixable: self.fixable.or(config.fixable), format: self.format.or(config.format), + force_exclude: self.force_exclude.or(config.force_exclude), ignore: self.ignore.or(config.ignore), ignore_init_module_imports: self .ignore_init_module_imports @@ -208,6 +211,9 @@ impl Configuration { if let Some(format) = overrides.format { self.format = Some(format); } + if let Some(force_exclude) = overrides.force_exclude { + self.force_exclude = Some(force_exclude); + } if let Some(ignore) = overrides.ignore { self.ignore = Some(ignore); } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 784306be61..a045d04eeb 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -41,6 +41,7 @@ pub struct Settings { pub fix: bool, pub fixable: FxHashSet, pub format: SerializationFormat, + pub force_exclude: bool, pub ignore_init_module_imports: bool, pub line_length: usize, pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet)>, @@ -127,6 +128,7 @@ impl Settings { .into_iter(), ), format: config.format.unwrap_or(SerializationFormat::Text), + force_exclude: config.force_exclude.unwrap_or(false), ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(), line_length: config.line_length.unwrap_or(88), per_file_ignores: resolve_per_file_ignores( @@ -199,6 +201,7 @@ impl Settings { fix: false, fixable: FxHashSet::from_iter([check_code]), format: SerializationFormat::Text, + force_exclude: false, ignore_init_module_imports: false, line_length: 88, per_file_ignores: vec![], @@ -231,6 +234,7 @@ impl Settings { fix: false, fixable: FxHashSet::from_iter(check_codes), format: SerializationFormat::Text, + force_exclude: false, ignore_init_module_imports: false, line_length: 88, per_file_ignores: vec![], diff --git a/src/settings/options.rs b/src/settings/options.rs index 1a30784181..fe31f2e957 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -165,6 +165,24 @@ pub struct Options { "# )] pub format: Option, + #[option( + doc = r#" + Whether to enforce `exclude` and `extend-exclude` patterns, even for paths that are + passed to Ruff explicitly. Typically, Ruff will lint any paths passed in directly, even + if they would typically be excluded. Setting `force-exclude = true` will cause Ruff to + respect these exclusions unequivocally. + + This is useful for [`pre-commit`](https://pre-commit.com/), which explicitly passes all + changed files to the [`ruff-pre-commit`](https://github.com/charliermarsh/ruff-pre-commit) + plugin, regardless of whether they're marked as excluded by Ruff's own settings. + "#, + default = r#"false"#, + value_type = "bool", + example = r#" + force-exclude = true + "# + )] + pub force_exclude: Option, #[option( doc = r" A list of check code prefixes to ignore. Prefixes can specify exact checks (like diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index 66704f4f6c..e7719077ec 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -129,6 +129,8 @@ mod tests { external: None, fix: None, fixable: None, + format: None, + force_exclude: None, ignore: None, ignore_init_module_imports: None, line_length: None, @@ -138,7 +140,6 @@ mod tests { show_source: None, src: None, target_version: None, - format: None, unfixable: None, flake8_annotations: None, flake8_bugbear: None, @@ -176,6 +177,8 @@ line-length = 79 external: None, fix: None, fixable: None, + force_exclude: None, + format: None, ignore: None, ignore_init_module_imports: None, line_length: Some(79), @@ -185,7 +188,6 @@ line-length = 79 show_source: None, src: None, target_version: None, - format: None, unfixable: None, flake8_annotations: None, flake8_bugbear: None, @@ -214,26 +216,27 @@ exclude = ["foo.py"] Some(Tools { ruff: Some(Options { allowed_confusables: None, - line_length: None, - fix: None, - extend: None, + dummy_variable_rgx: None, exclude: Some(vec!["foo.py".to_string()]), + extend: None, extend_exclude: None, - select: None, + extend_ignore: None, extend_select: None, external: None, + fix: None, + fixable: None, + force_exclude: None, + format: None, ignore: None, ignore_init_module_imports: None, - extend_ignore: None, - fixable: None, - format: None, - unfixable: None, + line_length: None, per_file_ignores: None, respect_gitignore: None, - dummy_variable_rgx: None, + select: None, + show_source: None, src: None, target_version: None, - show_source: None, + unfixable: None, flake8_annotations: None, flake8_errmsg: None, flake8_bugbear: None, @@ -270,6 +273,8 @@ select = ["E501"] external: None, fix: None, fixable: None, + force_exclude: None, + format: None, ignore: None, ignore_init_module_imports: None, line_length: None, @@ -279,7 +284,6 @@ select = ["E501"] show_source: None, src: None, target_version: None, - format: None, unfixable: None, flake8_annotations: None, flake8_bugbear: None, @@ -318,6 +322,8 @@ ignore = ["E501"] external: None, fix: None, fixable: None, + force_exclude: None, + format: None, ignore: Some(vec![CheckCodePrefix::E501]), ignore_init_module_imports: None, line_length: None, @@ -327,7 +333,6 @@ ignore = ["E501"] show_source: None, src: None, target_version: None, - format: None, unfixable: None, flake8_annotations: None, flake8_bugbear: None, @@ -408,6 +413,7 @@ other-attribute = 1 extend_ignore: None, fixable: None, format: None, + force_exclude: None, unfixable: None, per_file_ignores: Some(FxHashMap::from_iter([( "__init__.py".to_string(),