feat: Add isort option lines-after-imports (#2440)

Fixes https://github.com/charliermarsh/ruff/issues/2243

Adds support for the isort option [lines_after_imports](https://pycqa.github.io/isort/docs/configuration/options.html#lines-after-imports) to insert blank lines between imports and the follow up code.
This commit is contained in:
Reid Swan 2023-02-02 04:39:45 +02:00 committed by GitHub
parent 68422d4ff2
commit ec7b25290b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 232 additions and 6 deletions

View File

@ -3500,6 +3500,25 @@ known-third-party = ["src"]
--- ---
#### [`lines-after-imports`](#lines-after-imports)
The number of blank lines to place after imports.
-1 for automatic determination.
**Default value**: `-1`
**Type**: `int`
**Example usage**:
```toml
[tool.ruff.isort]
# Use a single line after each import block.
lines-after-imports = 1
```
---
#### [`no-lines-before`](#no-lines-before) #### [`no-lines-before`](#no-lines-before)
A list of sections that should _not_ be delineated from the previous A list of sections that should _not_ be delineated from the previous

View File

@ -0,0 +1,13 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object
class Thing(object):
name: str
def __init__(self, name: str):
self.name = name

View File

@ -0,0 +1,22 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object
def main():
my_local_folder_object.get()

View File

@ -0,0 +1,9 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object

View File

@ -1,2 +1,5 @@
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 88
[tool.ruff.isort]
lines-after-imports = 3

View File

@ -957,6 +957,14 @@
"type": "string" "type": "string"
} }
}, },
"lines-after-imports": {
"description": "The number of blank lines to place after imports. -1 for automatic determination.",
"type": [
"integer",
"null"
],
"format": "int"
},
"no-lines-before": { "no-lines-before": {
"description": "A list of sections that should _not_ be delineated from the previous section via empty lines.", "description": "A list of sections that should _not_ be delineated from the previous section via empty lines.",
"type": [ "type": [

View File

@ -3,11 +3,12 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use itertools::Either::{Left, Right};
use annotate::annotate_imports; use annotate::annotate_imports;
use categorize::categorize_imports; use categorize::categorize_imports;
pub use categorize::{categorize, ImportType}; pub use categorize::{categorize, ImportType};
use comments::Comment; use comments::Comment;
use itertools::Either::{Left, Right};
use normalize::normalize_imports; use normalize::normalize_imports;
use order::order_imports; use order::order_imports;
use settings::RelativeImportsOrder; use settings::RelativeImportsOrder;
@ -127,6 +128,7 @@ pub fn format_imports(
constants: &BTreeSet<String>, constants: &BTreeSet<String>,
variables: &BTreeSet<String>, variables: &BTreeSet<String>,
no_lines_before: &BTreeSet<ImportType>, no_lines_before: &BTreeSet<ImportType>,
lines_after_imports: isize,
) -> String { ) -> String {
let trailer = &block.trailer; let trailer = &block.trailer;
let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma); let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma);
@ -214,11 +216,23 @@ pub fn format_imports(
match trailer { match trailer {
None => {} None => {}
Some(Trailer::Sibling) => { Some(Trailer::Sibling) => {
output.push_str(stylist.line_ending()); if lines_after_imports >= 0 {
for _ in 0..lines_after_imports {
output.push_str(stylist.line_ending());
}
} else {
output.push_str(stylist.line_ending());
}
} }
Some(Trailer::FunctionDef | Trailer::ClassDef) => { Some(Trailer::FunctionDef | Trailer::ClassDef) => {
output.push_str(stylist.line_ending()); if lines_after_imports >= 0 {
output.push_str(stylist.line_ending()); for _ in 0..lines_after_imports {
output.push_str(stylist.line_ending());
}
} else {
output.push_str(stylist.line_ending());
output.push_str(stylist.line_ending());
}
} }
} }
output output
@ -232,13 +246,14 @@ mod tests {
use anyhow::Result; use anyhow::Result;
use test_case::test_case; use test_case::test_case;
use super::categorize::ImportType;
use super::settings::RelativeImportsOrder;
use crate::assert_yaml_snapshot; use crate::assert_yaml_snapshot;
use crate::registry::Rule; use crate::registry::Rule;
use crate::settings::Settings; use crate::settings::Settings;
use crate::test::{test_path, test_resource_path}; use crate::test::{test_path, test_resource_path};
use super::categorize::ImportType;
use super::settings::RelativeImportsOrder;
#[test_case(Path::new("add_newline_before_comments.py"))] #[test_case(Path::new("add_newline_before_comments.py"))]
#[test_case(Path::new("combine_as_imports.py"))] #[test_case(Path::new("combine_as_imports.py"))]
#[test_case(Path::new("combine_import_from.py"))] #[test_case(Path::new("combine_import_from.py"))]
@ -641,4 +656,25 @@ mod tests {
assert_yaml_snapshot!(snapshot, diagnostics); assert_yaml_snapshot!(snapshot, diagnostics);
Ok(()) Ok(())
} }
#[test_case(Path::new("lines_after_imports_nothing_after.py"))]
#[test_case(Path::new("lines_after_imports_func_after.py"))]
#[test_case(Path::new("lines_after_imports_class_after.py"))]
fn lines_after_imports(path: &Path) -> Result<()> {
let snapshot = format!("lines_after_imports_{}", path.to_string_lossy());
let mut diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&Settings {
isort: super::settings::Settings {
lines_after_imports: 3,
..super::settings::Settings::default()
},
src: vec![test_resource_path("fixtures/isort")],
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
diagnostics.sort_by_key(|diagnostic| diagnostic.location);
assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
} }

View File

@ -55,6 +55,7 @@ fn matches_ignoring_indentation(val1: &str, val2: &str) -> bool {
}) })
} }
#[allow(clippy::cast_sign_loss)]
/// I001 /// I001
pub fn organize_imports( pub fn organize_imports(
block: &Block, block: &Block,
@ -117,6 +118,7 @@ pub fn organize_imports(
&settings.isort.constants, &settings.isort.constants,
&settings.isort.variables, &settings.isort.variables,
&settings.isort.no_lines_before, &settings.isort.no_lines_before,
settings.isort.lines_after_imports,
); );
// Expand the span the entire range, including leading and trailing space. // Expand the span the entire range, including leading and trailing space.

View File

@ -213,6 +213,17 @@ pub struct Options {
/// A list of sections that should _not_ be delineated from the previous /// A list of sections that should _not_ be delineated from the previous
/// section via empty lines. /// section via empty lines.
pub no_lines_before: Option<Vec<ImportType>>, pub no_lines_before: Option<Vec<ImportType>>,
#[option(
default = r#"-1"#,
value_type = "int",
example = r#"
# Use a single line after each import block.
lines-after-imports = 1
"#
)]
/// The number of blank lines to place after imports.
/// -1 for automatic determination.
pub lines_after_imports: Option<isize>,
} }
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -234,6 +245,7 @@ pub struct Settings {
pub constants: BTreeSet<String>, pub constants: BTreeSet<String>,
pub variables: BTreeSet<String>, pub variables: BTreeSet<String>,
pub no_lines_before: BTreeSet<ImportType>, pub no_lines_before: BTreeSet<ImportType>,
pub lines_after_imports: isize,
} }
impl Default for Settings { impl Default for Settings {
@ -255,6 +267,7 @@ impl Default for Settings {
constants: BTreeSet::new(), constants: BTreeSet::new(),
variables: BTreeSet::new(), variables: BTreeSet::new(),
no_lines_before: BTreeSet::new(), no_lines_before: BTreeSet::new(),
lines_after_imports: -1,
} }
} }
} }
@ -282,6 +295,7 @@ impl From<Options> for Settings {
constants: BTreeSet::from_iter(options.constants.unwrap_or_default()), constants: BTreeSet::from_iter(options.constants.unwrap_or_default()),
variables: BTreeSet::from_iter(options.variables.unwrap_or_default()), variables: BTreeSet::from_iter(options.variables.unwrap_or_default()),
no_lines_before: BTreeSet::from_iter(options.no_lines_before.unwrap_or_default()), no_lines_before: BTreeSet::from_iter(options.no_lines_before.unwrap_or_default()),
lines_after_imports: options.lines_after_imports.unwrap_or(-1),
} }
} }
} }
@ -305,6 +319,7 @@ impl From<Settings> for Options {
constants: Some(settings.constants.into_iter().collect()), constants: Some(settings.constants.into_iter().collect()),
variables: Some(settings.variables.into_iter().collect()), variables: Some(settings.variables.into_iter().collect()),
no_lines_before: Some(settings.no_lines_before.into_iter().collect()), no_lines_before: Some(settings.no_lines_before.into_iter().collect()),
lines_after_imports: Some(settings.lines_after_imports),
} }
} }
} }

View File

@ -0,0 +1,34 @@
---
source: src/rules/isort/mod.rs
expression: diagnostics
---
- kind:
UnsortedImports: ~
location:
row: 1
column: 0
end_location:
row: 10
column: 0
fix:
content:
- from __future__ import annotations
- ""
- from typing import Any
- ""
- from my_first_party import my_first_party_object
- from requests import Session
- ""
- from . import my_local_folder_object
- ""
- ""
- ""
- ""
location:
row: 1
column: 0
end_location:
row: 10
column: 0
parent: ~

View File

@ -0,0 +1,34 @@
---
source: src/rules/isort/mod.rs
expression: diagnostics
---
- kind:
UnsortedImports: ~
location:
row: 1
column: 0
end_location:
row: 21
column: 0
fix:
content:
- from __future__ import annotations
- ""
- from typing import Any
- ""
- from my_first_party import my_first_party_object
- from requests import Session
- ""
- from . import my_local_folder_object
- ""
- ""
- ""
- ""
location:
row: 1
column: 0
end_location:
row: 21
column: 0
parent: ~

View File

@ -0,0 +1,31 @@
---
source: src/rules/isort/mod.rs
expression: diagnostics
---
- kind:
UnsortedImports: ~
location:
row: 1
column: 0
end_location:
row: 10
column: 0
fix:
content:
- from __future__ import annotations
- ""
- from typing import Any
- ""
- from my_first_party import my_first_party_object
- from requests import Session
- ""
- from . import my_local_folder_object
- ""
location:
row: 1
column: 0
end_location:
row: 10
column: 0
parent: ~