feat(isort): Implement isort.force_to_top (#2877)

This commit is contained in:
Florian Best 2023-02-16 20:01:59 +01:00 committed by GitHub
parent 059601d968
commit a919041dda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 298 additions and 39 deletions

View File

@ -3517,6 +3517,23 @@ force-sort-within-sections = true
---
#### [`force-to-top`](#force-to-top)
Force specific imports to the top of their appropriate section.
**Default value**: `[]`
**Type**: `list[str]`
**Example usage**:
```toml
[tool.ruff.isort]
force-to-top = ["src"]
```
---
#### [`force-wrap-aliases`](#force-wrap-aliases)
Force `import from` statements with multiple members and at least one

View File

@ -0,0 +1,23 @@
import lib6
import lib2
import lib5
import lib1
import lib3
import lib4
import foo
import z
from foo import bar
from lib1 import foo
from lib2 import foo
from lib1.lib2 import foo
from foo.lib1.bar import baz
from lib4 import lib1
from lib5 import lib2
from lib4 import lib2
from lib5 import lib1
import lib3.lib4
import lib3.lib4.lib5
from lib3.lib4 import foo
from lib3.lib4.lib5 import foo

View File

@ -5,3 +5,4 @@ line-length = 88
lines-after-imports = 3
lines-between-types = 2
known-local-folder = ["ruff"]
force-to-top = ["lib1", "lib3", "lib5", "lib3.lib4", "z"]

View File

@ -120,6 +120,7 @@ pub fn format_imports(
force_single_line: bool,
force_sort_within_sections: bool,
force_wrap_aliases: bool,
force_to_top: &BTreeSet<String>,
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
known_local_folder: &BTreeSet<String>,
@ -155,6 +156,7 @@ pub fn format_imports(
force_single_line,
force_sort_within_sections,
force_wrap_aliases,
force_to_top,
known_first_party,
known_third_party,
known_local_folder,
@ -214,6 +216,7 @@ fn format_import_block(
force_single_line: bool,
force_sort_within_sections: bool,
force_wrap_aliases: bool,
force_to_top: &BTreeSet<String>,
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
known_local_folder: &BTreeSet<String>,
@ -252,6 +255,7 @@ fn format_import_block(
classes,
constants,
variables,
force_to_top,
);
if force_single_line {
@ -267,7 +271,7 @@ fn format_import_block(
.collect::<Vec<EitherImport>>();
if force_sort_within_sections {
imports.sort_by(|import1, import2| {
cmp_either_import(import1, import2, relative_imports_order)
cmp_either_import(import1, import2, relative_imports_order, force_to_top)
});
};
imports
@ -347,6 +351,7 @@ mod tests {
#[test_case(Path::new("fit_line_length.py"))]
#[test_case(Path::new("fit_line_length_comment.py"))]
#[test_case(Path::new("force_sort_within_sections.py"))]
#[test_case(Path::new("force_to_top.py"))]
#[test_case(Path::new("force_wrap_aliases.py"))]
#[test_case(Path::new("import_from_after_import.py"))]
#[test_case(Path::new("inline_comments.py"))]
@ -387,12 +392,6 @@ mod tests {
Path::new("isort").join(path).as_path(),
&Settings {
src: vec![test_resource_path("fixtures/isort")],
isort: super::settings::Settings {
known_local_folder: vec!["ruff".to_string()]
.into_iter()
.collect::<BTreeSet<_>>(),
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
@ -418,6 +417,48 @@ mod tests {
// Ok(())
// }
#[test_case(Path::new("separate_local_folder_imports.py"))]
fn known_local_folder(path: &Path) -> Result<()> {
let snapshot = format!("known_local_folder_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&Settings {
isort: super::settings::Settings {
known_local_folder: BTreeSet::from(["ruff".to_string()]),
..super::settings::Settings::default()
},
src: vec![test_resource_path("fixtures/isort")],
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("force_to_top.py"))]
fn force_to_top(path: &Path) -> Result<()> {
let snapshot = format!("force_to_top_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&Settings {
isort: super::settings::Settings {
force_to_top: BTreeSet::from([
"z".to_string(),
"lib1".to_string(),
"lib3".to_string(),
"lib5".to_string(),
"lib3.lib4".to_string(),
]),
..super::settings::Settings::default()
},
src: vec![test_resource_path("fixtures/isort")],
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("combine_as_imports.py"))]
fn combine_as_imports(path: &Path) -> Result<()> {
let snapshot = format!("combine_as_imports_{}", path.to_string_lossy());

View File

@ -15,6 +15,7 @@ pub fn order_imports<'a>(
classes: &'a BTreeSet<String>,
constants: &'a BTreeSet<String>,
variables: &'a BTreeSet<String>,
force_to_top: &'a BTreeSet<String>,
) -> OrderedImportBlock<'a> {
let mut ordered = OrderedImportBlock::default();
@ -23,7 +24,7 @@ pub fn order_imports<'a>(
block
.import
.into_iter()
.sorted_by(|(alias1, _), (alias2, _)| cmp_modules(alias1, alias2)),
.sorted_by(|(alias1, _), (alias2, _)| cmp_modules(alias1, alias2, force_to_top)),
);
// Sort `StmtKind::ImportFrom`.
@ -101,6 +102,7 @@ pub fn order_imports<'a>(
classes,
constants,
variables,
force_to_top,
)
})
.collect::<Vec<(AliasData, CommentSet)>>(),
@ -108,8 +110,13 @@ pub fn order_imports<'a>(
})
.sorted_by(
|(import_from1, _, _, aliases1), (import_from2, _, _, aliases2)| {
cmp_import_from(import_from1, import_from2, relative_imports_order).then_with(
|| match (aliases1.first(), aliases2.first()) {
cmp_import_from(
import_from1,
import_from2,
relative_imports_order,
force_to_top,
)
.then_with(|| match (aliases1.first(), aliases2.first()) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
@ -120,9 +127,9 @@ pub fn order_imports<'a>(
classes,
constants,
variables,
force_to_top,
),
},
)
})
},
),
);

View File

@ -125,6 +125,7 @@ pub fn organize_imports(
settings.isort.force_single_line,
settings.isort.force_sort_within_sections,
settings.isort.force_wrap_aliases,
&settings.isort.force_to_top,
&settings.isort.known_first_party,
&settings.isort.known_third_party,
&settings.isort.known_local_folder,

View File

@ -118,6 +118,15 @@ pub struct Options {
/// imports (like `from itertools import groupby`). Instead, sort the
/// imports by module, independent of import style.
pub force_sort_within_sections: Option<bool>,
#[option(
default = r#"[]"#,
value_type = "list[str]",
example = r#"
force-to-top = ["src"]
"#
)]
/// Force specific imports to the top of their appropriate section.
pub force_to_top: Option<Vec<String>>,
#[option(
default = r#"[]"#,
value_type = "list[str]",
@ -265,6 +274,7 @@ pub struct Settings {
pub force_single_line: bool,
pub force_sort_within_sections: bool,
pub force_wrap_aliases: bool,
pub force_to_top: BTreeSet<String>,
pub known_first_party: BTreeSet<String>,
pub known_third_party: BTreeSet<String>,
pub known_local_folder: BTreeSet<String>,
@ -290,6 +300,7 @@ impl Default for Settings {
force_single_line: false,
force_sort_within_sections: false,
force_wrap_aliases: false,
force_to_top: BTreeSet::new(),
known_first_party: BTreeSet::new(),
known_third_party: BTreeSet::new(),
known_local_folder: BTreeSet::new(),
@ -319,6 +330,7 @@ impl From<Options> for Settings {
force_single_line: options.force_single_line.unwrap_or(false),
force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false),
force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false),
force_to_top: BTreeSet::from_iter(options.force_to_top.unwrap_or_default()),
known_first_party: BTreeSet::from_iter(options.known_first_party.unwrap_or_default()),
known_third_party: BTreeSet::from_iter(options.known_third_party.unwrap_or_default()),
known_local_folder: BTreeSet::from_iter(options.known_local_folder.unwrap_or_default()),
@ -348,6 +360,7 @@ impl From<Settings> for Options {
force_single_line: Some(settings.force_single_line),
force_sort_within_sections: Some(settings.force_sort_within_sections),
force_wrap_aliases: Some(settings.force_wrap_aliases),
force_to_top: Some(settings.force_to_top.into_iter().collect()),
known_first_party: Some(settings.known_first_party.into_iter().collect()),
known_third_party: Some(settings.known_third_party.into_iter().collect()),
known_local_folder: Some(settings.known_local_folder.into_iter().collect()),

View File

@ -1,5 +1,5 @@
---
source: src/rules/isort/mod.rs
source: crates/ruff/src/rules/isort/mod.rs
expression: diagnostics
---
- kind:

View File

@ -0,0 +1,42 @@
---
source: crates/ruff/src/rules/isort/mod.rs
expression: diagnostics
---
- kind:
UnsortedImports: ~
location:
row: 1
column: 0
end_location:
row: 24
column: 0
fix:
content:
- import foo
- import lib1
- import lib2
- import lib3
- import lib3.lib4
- import lib3.lib4.lib5
- import lib4
- import lib5
- import lib6
- import z
- from foo import bar
- from foo.lib1.bar import baz
- from lib1 import foo
- from lib1.lib2 import foo
- from lib2 import foo
- from lib3.lib4 import foo
- from lib3.lib4.lib5 import foo
- "from lib4 import lib1, lib2"
- "from lib5 import lib1, lib2"
- ""
location:
row: 1
column: 0
end_location:
row: 24
column: 0
parent: ~

View File

@ -0,0 +1,42 @@
---
source: crates/ruff/src/rules/isort/mod.rs
expression: diagnostics
---
- kind:
UnsortedImports: ~
location:
row: 1
column: 0
end_location:
row: 24
column: 0
fix:
content:
- import lib1
- import lib3
- import lib3.lib4
- import lib5
- import z
- import foo
- import lib2
- import lib3.lib4.lib5
- import lib4
- import lib6
- from lib1 import foo
- from lib3.lib4 import foo
- "from lib5 import lib1, lib2"
- from foo import bar
- from foo.lib1.bar import baz
- from lib1.lib2 import foo
- from lib2 import foo
- from lib3.lib4.lib5 import foo
- "from lib4 import lib1, lib2"
- ""
location:
row: 1
column: 0
end_location:
row: 24
column: 0
parent: ~

View File

@ -0,0 +1,30 @@
---
source: crates/ruff/src/rules/isort/mod.rs
expression: diagnostics
---
- kind:
UnsortedImports: ~
location:
row: 1
column: 0
end_location:
row: 6
column: 0
fix:
content:
- import os
- import sys
- ""
- import leading_prefix
- ""
- import ruff
- from . import leading_prefix
- ""
location:
row: 1
column: 0
end_location:
row: 6
column: 0
parent: ~

View File

@ -15,9 +15,10 @@ expression: diagnostics
- import os
- import sys
- ""
- import ruff
- ""
- import leading_prefix
- ""
- import ruff
- from . import leading_prefix
- ""
location:

View File

@ -2,6 +2,7 @@
use std::cmp::Ordering;
use std::collections::BTreeSet;
use crate::rules::isort::types::Importable;
use ruff_python::string;
use super::settings::RelativeImportsOrder;
@ -43,8 +44,21 @@ fn prefix(
}
/// Compare two top-level modules.
pub fn cmp_modules(alias1: &AliasData, alias2: &AliasData) -> Ordering {
natord::compare_ignore_case(alias1.name, alias2.name)
pub fn cmp_modules(
alias1: &AliasData,
alias2: &AliasData,
force_to_top: &BTreeSet<String>,
) -> Ordering {
(match (
force_to_top.contains(alias1.name),
force_to_top.contains(alias2.name),
) {
(true, true) => Ordering::Equal,
(false, false) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
})
.then_with(|| natord::compare_ignore_case(alias1.name, alias2.name))
.then_with(|| natord::compare(alias1.name, alias2.name))
.then_with(|| match (alias1.asname, alias2.asname) {
(None, None) => Ordering::Equal,
@ -62,6 +76,7 @@ pub fn cmp_members(
classes: &BTreeSet<String>,
constants: &BTreeSet<String>,
variables: &BTreeSet<String>,
force_to_top: &BTreeSet<String>,
) -> Ordering {
match (alias1.name == "*", alias2.name == "*") {
(true, false) => Ordering::Less,
@ -70,9 +85,9 @@ pub fn cmp_members(
if order_by_type {
prefix(alias1.name, classes, constants, variables)
.cmp(&prefix(alias2.name, classes, constants, variables))
.then_with(|| cmp_modules(alias1, alias2))
.then_with(|| cmp_modules(alias1, alias2, force_to_top))
} else {
cmp_modules(alias1, alias2)
cmp_modules(alias1, alias2, force_to_top)
}
}
}
@ -100,12 +115,24 @@ pub fn cmp_import_from(
import_from1: &ImportFromData,
import_from2: &ImportFromData,
relative_imports_order: RelativeImportsOrder,
force_to_top: &BTreeSet<String>,
) -> Ordering {
cmp_levels(
import_from1.level,
import_from2.level,
relative_imports_order,
)
.then_with(|| {
match (
force_to_top.contains(&import_from1.module_name()),
force_to_top.contains(&import_from2.module_name()),
) {
(true, true) => Ordering::Equal,
(false, false) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
}
})
.then_with(|| match (&import_from1.module, import_from2.module) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
@ -121,17 +148,21 @@ pub fn cmp_either_import(
a: &EitherImport,
b: &EitherImport,
relative_imports_order: RelativeImportsOrder,
force_to_top: &BTreeSet<String>,
) -> Ordering {
match (a, b) {
(Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2),
(Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2, force_to_top),
(ImportFrom((import_from, ..)), Import((alias, _))) => {
natord::compare_ignore_case(import_from.module.unwrap_or_default(), alias.name)
}
(Import((alias, _)), ImportFrom((import_from, ..))) => {
natord::compare_ignore_case(alias.name, import_from.module.unwrap_or_default())
}
(ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => {
cmp_import_from(import_from1, import_from2, relative_imports_order)
}
(ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => cmp_import_from(
import_from1,
import_from2,
relative_imports_order,
force_to_top,
),
}
}

View File

@ -944,6 +944,16 @@
"null"
]
},
"force-to-top": {
"description": "Force specific imports to the top of their appropriate section.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"force-wrap-aliases": {
"description": "Force `import from` statements with multiple members and at least one alias (e.g., `import A as B`) to wrap such that every line contains exactly one member. For example, this formatting would be retained, rather than condensing to a single line:\n\n```python from .utils import ( test_directory as test_directory, test_id as test_id ) ```\n\nNote that this setting is only effective when combined with `combine-as-imports = true`. When `combine-as-imports` isn't enabled, every aliased `import from` will be given its own line, in which case, wrapping is not necessary.",
"type": [