From 77099dcd4dfba32fc3555c944292c4208f895467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mr=C3=B3wka?= Date: Sat, 11 Feb 2023 18:17:37 +0100 Subject: [PATCH] implemented option lines-between-types for isort (#2762) Fixes #2585 Add support for the isort option [lines_between_types](https://pycqa.github.io/isort/docs/configuration/options.html#lines-between-types) --- README.md | 23 +++++++++-- .../fixtures/isort/lines_between_types.py | 16 ++++++++ .../test/fixtures/isort/pyproject.toml | 1 + crates/ruff/src/rules/isort/mod.rs | 36 +++++++++++++++++ .../src/rules/isort/rules/organize_imports.rs | 1 + crates/ruff/src/rules/isort/settings.rs | 19 +++++++-- ...s_between_typeslines_between_types.py.snap | 39 +++++++++++++++++++ ruff.schema.json | 13 ++++++- 8 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/isort/lines_between_types.py create mode 100644 crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__lines_between_typeslines_between_types.py.snap diff --git a/README.md b/README.md index fe294955ea861..e55bca4ee9b52 100644 --- a/README.md +++ b/README.md @@ -3480,8 +3480,7 @@ known-first-party = ["src"] #### [`known-local-folder`](#known-local-folder) A list of modules to consider being a local folder. -Generally, this is reserved for relative -imports (from . import module). +Generally, this is reserved for relative imports (`from . import module`). **Default value**: `[]` @@ -3517,7 +3516,7 @@ known-third-party = ["src"] #### [`lines-after-imports`](#lines-after-imports) The number of blank lines to place after imports. --1 for automatic determination. +Use `-1` for automatic determination. **Default value**: `-1` @@ -3533,6 +3532,24 @@ lines-after-imports = 1 --- +#### [`lines-between-types`](#lines-between-types) + +The number of lines to place between "direct" and `import from` imports. + +**Default value**: `0` + +**Type**: `int` + +**Example usage**: + +```toml +[tool.ruff.isort] +# Use a single line between direct and from import +lines-between-types = 1 +``` + +--- + #### [`no-lines-before`](#no-lines-before) A list of sections that should _not_ be delineated from the previous diff --git a/crates/ruff/resources/test/fixtures/isort/lines_between_types.py b/crates/ruff/resources/test/fixtures/isort/lines_between_types.py new file mode 100644 index 0000000000000..630d28ed45d35 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/isort/lines_between_types.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import datetime +import json + + +from binascii import hexlify + +import requests + + +from sanic import Sanic +from loguru import Logger + +from . import config +from .data import Data diff --git a/crates/ruff/resources/test/fixtures/isort/pyproject.toml b/crates/ruff/resources/test/fixtures/isort/pyproject.toml index 1387db1ad660b..a4d2f7e73ff89 100644 --- a/crates/ruff/resources/test/fixtures/isort/pyproject.toml +++ b/crates/ruff/resources/test/fixtures/isort/pyproject.toml @@ -3,4 +3,5 @@ line-length = 88 [tool.ruff.isort] lines-after-imports = 3 +lines-between-types = 2 known-local-folder = ["ruff"] diff --git a/crates/ruff/src/rules/isort/mod.rs b/crates/ruff/src/rules/isort/mod.rs index 54b445c889df5..ba6463c708d6f 100644 --- a/crates/ruff/src/rules/isort/mod.rs +++ b/crates/ruff/src/rules/isort/mod.rs @@ -132,6 +132,7 @@ pub fn format_imports( variables: &BTreeSet, no_lines_before: &BTreeSet, lines_after_imports: isize, + lines_between_types: usize, forced_separate: &[String], target_version: PythonVersion, ) -> String { @@ -165,6 +166,7 @@ pub fn format_imports( constants, variables, no_lines_before, + lines_between_types, target_version, ); @@ -223,6 +225,7 @@ fn format_import_block( constants: &BTreeSet, variables: &BTreeSet, no_lines_before: &BTreeSet, + lines_between_types: usize, target_version: PythonVersion, ) -> String { // Categorize by type (e.g., first-party vs. third-party). @@ -277,6 +280,8 @@ fn format_import_block( output.push_str(stylist.line_ending()); } + let mut lines_inserted = false; + let mut has_direct_import = false; let mut is_first_statement = true; for import in imports { match import { @@ -287,8 +292,20 @@ fn format_import_block( is_first_statement, stylist, )); + + has_direct_import = true; } + ImportFrom((import_from, comments, trailing_comma, aliases)) => { + // Add a blank lines between direct and from imports + if lines_between_types > 0 && has_direct_import && !lines_inserted { + for _ in 0..lines_between_types { + output.push_str(stylist.line_ending()); + } + + lines_inserted = true; + } + output.push_str(&format::format_import_from( &import_from, &comments, @@ -752,6 +769,25 @@ mod tests { Ok(()) } + #[test_case(Path::new("lines_between_types.py"))] + fn lines_between_types(path: &Path) -> Result<()> { + let snapshot = format!("lines_between_types{}", path.to_string_lossy()); + let mut diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &Settings { + isort: super::settings::Settings { + lines_between_types: 2, + ..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(()) + } + #[test_case(Path::new("forced_separate.py"))] fn forced_separate(path: &Path) -> Result<()> { let snapshot = format!("{}", path.to_string_lossy()); diff --git a/crates/ruff/src/rules/isort/rules/organize_imports.rs b/crates/ruff/src/rules/isort/rules/organize_imports.rs index 10f775ed7f090..9f8d040c9fbc8 100644 --- a/crates/ruff/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff/src/rules/isort/rules/organize_imports.rs @@ -137,6 +137,7 @@ pub fn organize_imports( &settings.isort.variables, &settings.isort.no_lines_before, settings.isort.lines_after_imports, + settings.isort.lines_between_types, &settings.isort.forced_separate, settings.target_version, ); diff --git a/crates/ruff/src/rules/isort/settings.rs b/crates/ruff/src/rules/isort/settings.rs index 794f51e71ce8a..c9c6f975fb3d4 100644 --- a/crates/ruff/src/rules/isort/settings.rs +++ b/crates/ruff/src/rules/isort/settings.rs @@ -146,8 +146,7 @@ pub struct Options { "# )] /// A list of modules to consider being a local folder. - /// Generally, this is reserved for relative - /// imports (from . import module). + /// Generally, this is reserved for relative imports (`from . import module`). pub known_local_folder: Option>, #[option( default = r#"[]"#, @@ -233,8 +232,18 @@ pub struct Options { "# )] /// The number of blank lines to place after imports. - /// -1 for automatic determination. + /// Use `-1` for automatic determination. pub lines_after_imports: Option, + #[option( + default = r#"0"#, + value_type = "int", + example = r#" + # Use a single line between direct and from import + lines-between-types = 1 + "# + )] + /// The number of lines to place between "direct" and `import from` imports. + pub lines_between_types: Option, #[option( default = r#"[]"#, value_type = "Vec", @@ -268,6 +277,7 @@ pub struct Settings { pub variables: BTreeSet, pub no_lines_before: BTreeSet, pub lines_after_imports: isize, + pub lines_between_types: usize, pub forced_separate: Vec, } @@ -292,6 +302,7 @@ impl Default for Settings { variables: BTreeSet::new(), no_lines_before: BTreeSet::new(), lines_after_imports: -1, + lines_between_types: 0, forced_separate: Vec::new(), } } @@ -322,6 +333,7 @@ impl From for Settings { variables: BTreeSet::from_iter(options.variables.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), + lines_between_types: options.lines_between_types.unwrap_or_default(), forced_separate: Vec::from_iter(options.forced_separate.unwrap_or_default()), } } @@ -348,6 +360,7 @@ impl From for Options { variables: Some(settings.variables.into_iter().collect()), no_lines_before: Some(settings.no_lines_before.into_iter().collect()), lines_after_imports: Some(settings.lines_after_imports), + lines_between_types: Some(settings.lines_between_types), forced_separate: Some(settings.forced_separate.into_iter().collect()), } } diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__lines_between_typeslines_between_types.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__lines_between_typeslines_between_types.py.snap new file mode 100644 index 0000000000000..be0f623d9b952 --- /dev/null +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__lines_between_typeslines_between_types.py.snap @@ -0,0 +1,39 @@ +--- +source: crates/ruff/src/rules/isort/mod.rs +expression: diagnostics +--- +- kind: + UnsortedImports: ~ + location: + row: 1 + column: 0 + end_location: + row: 17 + column: 0 + fix: + content: + - from __future__ import annotations + - "" + - import datetime + - import json + - "" + - "" + - from binascii import hexlify + - "" + - import requests + - "" + - "" + - from loguru import Logger + - from sanic import Sanic + - "" + - from . import config + - from .data import Data + - "" + location: + row: 1 + column: 0 + end_location: + row: 17 + column: 0 + parent: ~ + diff --git a/ruff.schema.json b/ruff.schema.json index 53afb04fb57a2..c6cfbd44fc66b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -965,7 +965,7 @@ } }, "known-local-folder": { - "description": "A list of modules to consider being a local folder. Generally, this is reserved for relative imports (from . import module).", + "description": "A list of modules to consider being a local folder. Generally, this is reserved for relative imports (`from . import module`).", "type": [ "array", "null" @@ -985,13 +985,22 @@ } }, "lines-after-imports": { - "description": "The number of blank lines to place after imports. -1 for automatic determination.", + "description": "The number of blank lines to place after imports. Use `-1` for automatic determination.", "type": [ "integer", "null" ], "format": "int" }, + "lines-between-types": { + "description": "The number of lines to place between \"direct\" and `import from` imports.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, "no-lines-before": { "description": "A list of sections that should _not_ be delineated from the previous section via empty lines.", "type": [