diff --git a/resources/test/fixtures/isort/skip_file.py b/resources/test/fixtures/isort/skip_file.py new file mode 100644 index 0000000000..522504fe27 --- /dev/null +++ b/resources/test/fixtures/isort/skip_file.py @@ -0,0 +1,10 @@ +# isort: skip_file +import e +import f + +# isort: split + +import a +import b +import c +import d diff --git a/resources/test/fixtures/isort/split.py b/resources/test/fixtures/isort/split.py new file mode 100644 index 0000000000..7554a90b8d --- /dev/null +++ b/resources/test/fixtures/isort/split.py @@ -0,0 +1,9 @@ +import e +import f + +# isort: split + +import a +import b +import c +import d diff --git a/src/check_imports.rs b/src/check_imports.rs index f0c7a8ae24..f6f531b9c0 100644 --- a/src/check_imports.rs +++ b/src/check_imports.rs @@ -1,10 +1,10 @@ //! Lint rules based on import analysis. -use nohash_hasher::IntSet; use rustpython_parser::ast::Suite; use crate::ast::visitor::Visitor; use crate::checks::Check; +use crate::directives::IsortDirectives; use crate::isort; use crate::isort::track::ImportTracker; use crate::settings::Settings; @@ -30,11 +30,11 @@ fn check_import_blocks( pub fn check_imports( python_ast: &Suite, locator: &SourceCodeLocator, - exclusions: &IntSet, + directives: &IsortDirectives, settings: &Settings, autofix: bool, ) -> Vec { - let mut tracker = ImportTracker::new(exclusions); + let mut tracker = ImportTracker::new(directives); for stmt in python_ast { tracker.visit_stmt(stmt); } diff --git a/src/directives.rs b/src/directives.rs index 6744df4c18..0a3055e8ff 100644 --- a/src/directives.rs +++ b/src/directives.rs @@ -30,9 +30,15 @@ impl Flags { } } +#[derive(Default)] +pub struct IsortDirectives { + pub exclusions: IntSet, + pub splits: Vec, +} + pub struct Directives { pub noqa_line_for: IntMap, - pub isort_exclusions: IntSet, + pub isort: IsortDirectives, } pub fn extract_directives( @@ -46,10 +52,10 @@ pub fn extract_directives( } else { IntMap::default() }, - isort_exclusions: if flags.contains(Flags::ISORT) { - extract_isort_exclusions(lxr, locator) + isort: if flags.contains(Flags::ISORT) { + extract_isort_directives(lxr, locator) } else { - IntSet::default() + IsortDirectives::default() }, } } @@ -73,8 +79,9 @@ pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap { } /// Extract a set of lines over which to disable isort. -pub fn extract_isort_exclusions(lxr: &[LexResult], locator: &SourceCodeLocator) -> IntSet { +pub fn extract_isort_directives(lxr: &[LexResult], locator: &SourceCodeLocator) -> IsortDirectives { let mut exclusions: IntSet = IntSet::default(); + let mut splits: Vec = Vec::default(); let mut skip_file: bool = false; let mut off: Option = None; let mut last: Option = None; @@ -92,7 +99,10 @@ pub fn extract_isort_exclusions(lxr: &[LexResult], locator: &SourceCodeLocator) location: start, end_location: end, }); - if comment_text == "# isort: skip_file" { + + if comment_text == "# isort: split" { + splits.push(start.row()); + } else if comment_text == "# isort: skip_file" { skip_file = true; } else if off.is_some() { if comment_text == "# isort: on" { @@ -127,7 +137,7 @@ pub fn extract_isort_exclusions(lxr: &[LexResult], locator: &SourceCodeLocator) } } } - exclusions + IsortDirectives { exclusions, splits } } #[cfg(test)] @@ -136,7 +146,7 @@ mod tests { use rustpython_parser::lexer; use rustpython_parser::lexer::LexResult; - use crate::directives::{extract_isort_exclusions, extract_noqa_line_for}; + use crate::directives::{extract_isort_directives, extract_noqa_line_for}; use crate::SourceCodeLocator; #[test] @@ -220,13 +230,16 @@ z = x + 1", } #[test] - fn isort_extraction() { + fn isort_exclusions() { let contents = "x = 1 y = 2 z = x + 1"; let lxr: Vec = lexer::make_tokenizer(contents).collect(); let locator = SourceCodeLocator::new(contents); - assert_eq!(extract_isort_exclusions(&lxr, &locator), IntSet::default()); + assert_eq!( + extract_isort_directives(&lxr, &locator).exclusions, + IntSet::default() + ); let contents = "# isort: off x = 1 @@ -236,7 +249,7 @@ z = x + 1"; let lxr: Vec = lexer::make_tokenizer(contents).collect(); let locator = SourceCodeLocator::new(contents); assert_eq!( - extract_isort_exclusions(&lxr, &locator), + extract_isort_directives(&lxr, &locator).exclusions, IntSet::from_iter([2, 3, 4]) ); @@ -250,7 +263,7 @@ z = x + 1 let lxr: Vec = lexer::make_tokenizer(contents).collect(); let locator = SourceCodeLocator::new(contents); assert_eq!( - extract_isort_exclusions(&lxr, &locator), + extract_isort_directives(&lxr, &locator).exclusions, IntSet::from_iter([2, 3, 4, 5]) ); @@ -261,7 +274,7 @@ z = x + 1"; let lxr: Vec = lexer::make_tokenizer(contents).collect(); let locator = SourceCodeLocator::new(contents); assert_eq!( - extract_isort_exclusions(&lxr, &locator), + extract_isort_directives(&lxr, &locator).exclusions, IntSet::from_iter([2, 3, 4]) ); @@ -272,7 +285,7 @@ z = x + 1"; let lxr: Vec = lexer::make_tokenizer(contents).collect(); let locator = SourceCodeLocator::new(contents); assert_eq!( - extract_isort_exclusions(&lxr, &locator), + extract_isort_directives(&lxr, &locator).exclusions, IntSet::from_iter([1, 2, 3, 4]) ); @@ -285,8 +298,36 @@ z = x + 1"; let lxr: Vec = lexer::make_tokenizer(contents).collect(); let locator = SourceCodeLocator::new(contents); assert_eq!( - extract_isort_exclusions(&lxr, &locator), + extract_isort_directives(&lxr, &locator).exclusions, IntSet::from_iter([1, 2, 3, 4, 5, 6]) ); } + + #[test] + fn isort_splits() { + let contents = "x = 1 +y = 2 +z = x + 1"; + let lxr: Vec = lexer::make_tokenizer(contents).collect(); + let locator = SourceCodeLocator::new(contents); + assert_eq!( + extract_isort_directives(&lxr, &locator).splits, + Vec::::new() + ); + + let contents = "x = 1 +y = 2 +# isort: split +z = x + 1"; + let lxr: Vec = lexer::make_tokenizer(contents).collect(); + let locator = SourceCodeLocator::new(contents); + assert_eq!(extract_isort_directives(&lxr, &locator).splits, vec![3]); + + let contents = "x = 1 +y = 2 # isort: split +z = x + 1"; + let lxr: Vec = lexer::make_tokenizer(contents).collect(); + let locator = SourceCodeLocator::new(contents); + assert_eq!(extract_isort_directives(&lxr, &locator).splits, vec![2]); + } } diff --git a/src/isort/mod.rs b/src/isort/mod.rs index dfcebb61c7..aa9607c74c 100644 --- a/src/isort/mod.rs +++ b/src/isort/mod.rs @@ -572,7 +572,9 @@ mod tests { #[test_case(Path::new("separate_local_folder_imports.py"))] #[test_case(Path::new("separate_third_party_imports.py"))] #[test_case(Path::new("skip.py"))] + #[test_case(Path::new("skip_file.py"))] #[test_case(Path::new("sort_similar_imports.py"))] + #[test_case(Path::new("split.py"))] #[test_case(Path::new("trailing_suffix.py"))] #[test_case(Path::new("type_comments.py"))] fn default(path: &Path) -> Result<()> { diff --git a/src/isort/snapshots/ruff__isort__tests__skip_file.py.snap b/src/isort/snapshots/ruff__isort__tests__skip_file.py.snap new file mode 100644 index 0000000000..6b7d975e4e --- /dev/null +++ b/src/isort/snapshots/ruff__isort__tests__skip_file.py.snap @@ -0,0 +1,6 @@ +--- +source: src/isort/mod.rs +expression: checks +--- +[] + diff --git a/src/isort/snapshots/ruff__isort__tests__split.py.snap b/src/isort/snapshots/ruff__isort__tests__split.py.snap new file mode 100644 index 0000000000..6b7d975e4e --- /dev/null +++ b/src/isort/snapshots/ruff__isort__tests__split.py.snap @@ -0,0 +1,6 @@ +--- +source: src/isort/mod.rs +expression: checks +--- +[] + diff --git a/src/isort/track.rs b/src/isort/track.rs index a92077d399..c034ae22d2 100644 --- a/src/isort/track.rs +++ b/src/isort/track.rs @@ -1,4 +1,3 @@ -use nohash_hasher::IntSet; use rustpython_ast::{ Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, StmtKind, @@ -6,31 +5,32 @@ use rustpython_ast::{ }; use crate::ast::visitor::Visitor; +use crate::directives::IsortDirectives; -#[derive(Debug)] pub enum Trailer { Sibling, ClassDef, FunctionDef, } -#[derive(Debug, Default)] +#[derive(Default)] pub struct Block<'a> { pub imports: Vec<&'a Stmt>, pub trailer: Option, } -#[derive(Debug)] pub struct ImportTracker<'a> { - exclusions: &'a IntSet, blocks: Vec>, + directives: &'a IsortDirectives, + split_index: usize, } impl<'a> ImportTracker<'a> { - pub fn new(exclusions: &'a IntSet) -> Self { + pub fn new(directives: &'a IsortDirectives) -> Self { Self { - exclusions, + directives, blocks: vec![Block::default()], + split_index: 0, } } @@ -57,11 +57,27 @@ where 'b: 'a, { fn visit_stmt(&mut self, stmt: &'b Stmt) { + // Track manual splits. + while self.split_index < self.directives.splits.len() { + if stmt.location.row() >= self.directives.splits[self.split_index] { + self.finalize(Some(match &stmt.node { + StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => { + Trailer::FunctionDef + } + StmtKind::ClassDef { .. } => Trailer::ClassDef, + _ => Trailer::Sibling, + })); + self.split_index += 1; + } else { + break; + } + } + // Track imports. if matches!( stmt.node, StmtKind::Import { .. } | StmtKind::ImportFrom { .. } - ) && !self.exclusions.contains(&stmt.location.row()) + ) && !self.directives.exclusions.contains(&stmt.location.row()) { self.track_import(stmt); } else { diff --git a/src/linter.rs b/src/linter.rs index 6da9bd2ab2..2c4527e543 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -89,7 +89,7 @@ pub(crate) fn check_path( checks.extend(check_imports( &python_ast, locator, - &directives.isort_exclusions, + &directives.isort, settings, autofix, )); @@ -193,7 +193,7 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result { &locator, &Directives { noqa_line_for: IntMap::default(), - isort_exclusions: directives.isort_exclusions, + isort: directives.isort, }, settings, false,