diff --git a/crates/ruff_linter/resources/test/fixtures/isort/no_sections.py b/crates/ruff_linter/resources/test/fixtures/isort/no_sections.py new file mode 100644 index 0000000000000..c9f590d80fad2 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/no_sections.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +import django.settings +import os +import pytz +import sys +from . import local +from library import foo diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 2b9c39f8cb0c6..1ce6336d3b158 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -285,6 +285,7 @@ pub(crate) fn typing_only_runtime_import( checker.settings.isort.detect_same_package, &checker.settings.isort.known_modules, checker.settings.target_version, + checker.settings.isort.no_sections, ) { ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => { ImportType::FirstParty diff --git a/crates/ruff_linter/src/rules/isort/categorize.rs b/crates/ruff_linter/src/rules/isort/categorize.rs index f7e6a9ddf2739..7fa6def9534df 100644 --- a/crates/ruff_linter/src/rules/isort/categorize.rs +++ b/crates/ruff_linter/src/rules/isort/categorize.rs @@ -61,6 +61,7 @@ enum Reason<'a> { SourceMatch(&'a Path), NoMatch, UserDefinedSection, + NoSections, } #[allow(clippy::too_many_arguments)] @@ -72,16 +73,22 @@ pub(crate) fn categorize<'a>( detect_same_package: bool, known_modules: &'a KnownModules, target_version: PythonVersion, + no_sections: bool, ) -> &'a ImportSection { let module_base = module_name.split('.').next().unwrap(); let (import_type, reason) = { - if level.is_some_and(|level| level > 0) { + if !no_sections && level.is_some_and(|level| level > 0) { ( &ImportSection::Known(ImportType::LocalFolder), Reason::NonZeroLevel, ) } else if module_base == "__future__" { (&ImportSection::Known(ImportType::Future), Reason::Future) + } else if no_sections { + ( + &ImportSection::Known(ImportType::FirstParty), + Reason::NoSections, + ) } else if let Some((import_type, reason)) = known_modules.categorize(module_name) { (import_type, reason) } else if is_known_standard_library(target_version.minor(), module_base) { @@ -141,6 +148,7 @@ pub(crate) fn categorize_imports<'a>( detect_same_package: bool, known_modules: &'a KnownModules, target_version: PythonVersion, + no_sections: bool, ) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> { let mut block_by_type: BTreeMap<&ImportSection, ImportBlock> = BTreeMap::default(); // Categorize `Stmt::Import`. @@ -153,6 +161,7 @@ pub(crate) fn categorize_imports<'a>( detect_same_package, known_modules, target_version, + no_sections, ); block_by_type .entry(import_type) @@ -170,6 +179,7 @@ pub(crate) fn categorize_imports<'a>( detect_same_package, known_modules, target_version, + no_sections, ); block_by_type .entry(classification) @@ -187,6 +197,7 @@ pub(crate) fn categorize_imports<'a>( detect_same_package, known_modules, target_version, + no_sections, ); block_by_type .entry(classification) @@ -204,6 +215,7 @@ pub(crate) fn categorize_imports<'a>( detect_same_package, known_modules, target_version, + no_sections, ); block_by_type .entry(classification) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index e82700398f810..a281edbd50777 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -156,6 +156,7 @@ fn format_import_block( settings.detect_same_package, &settings.known_modules, target_version, + settings.no_sections, ); let mut output = String::new(); @@ -862,6 +863,24 @@ mod tests { Ok(()) } + #[test_case(Path::new("no_sections.py"))] + fn no_sections(path: &Path) -> Result<()> { + let snapshot = format!("no_sections_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + no_sections: true, + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Path::new("no_lines_before.py"))] fn no_lines_before(path: &Path) -> Result<()> { let snapshot = format!("no_lines_before.py_{}", path.to_string_lossy()); diff --git a/crates/ruff_linter/src/rules/isort/settings.rs b/crates/ruff_linter/src/rules/isort/settings.rs index 89eea0c960c66..6e27a2debf4ee 100644 --- a/crates/ruff_linter/src/rules/isort/settings.rs +++ b/crates/ruff_linter/src/rules/isort/settings.rs @@ -56,6 +56,7 @@ pub struct Settings { pub lines_between_types: usize, pub forced_separate: Vec, pub section_order: Vec, + pub no_sections: bool, } impl Default for Settings { @@ -82,6 +83,7 @@ impl Default for Settings { lines_between_types: 0, forced_separate: Vec::new(), section_order: ImportType::iter().map(ImportSection::Known).collect(), + no_sections: false, } } } diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_sections_no_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_sections_no_sections.py.snap new file mode 100644 index 0000000000000..ed369f0fd61f0 --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__no_sections_no_sections.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- + diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 25a76de206c98..b6da89d9ff556 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -1978,6 +1978,16 @@ pub struct IsortOptions { )] pub section_order: Option>, + /// Put all imports into the same section bucket + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + no-sections = true + "# + )] + pub no_sections: Option, + /// Whether to automatically mark imports from within the same package as first-party. /// For example, when `detect-same-package = true`, then when analyzing files within the /// `foo` package, any imports from within the `foo` package will be considered first-party. @@ -2107,6 +2117,9 @@ impl IsortOptions { } } + let no_sections = self.no_sections.unwrap_or_default(); + // TODO: Warn if `no_sections` is true and `sections` or `section-order` is non-empty? + // Verify that all sections listed in `no_lines_before` are defined in `sections`. for section in &no_lines_before { if let ImportSection::UserDefined(section_name) = section { @@ -2169,6 +2182,7 @@ impl IsortOptions { lines_between_types: self.lines_between_types.unwrap_or_default(), forced_separate: Vec::from_iter(self.forced_separate.unwrap_or_default()), section_order, + no_sections, }) } } diff --git a/ruff.schema.json b/ruff.schema.json index 989b21078fcab..b993ae1d6191a 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1497,6 +1497,13 @@ "$ref": "#/definitions/ImportSection" } }, + "no-sections": { + "description": "Put all imports into the same section bucket", + "type": [ + "boolean", + "null" + ] + }, "order-by-type": { "description": "Order imports by type, which is determined by case, in addition to alphabetically.", "type": [