mirror of https://github.com/astral-sh/ruff
feat(isort): Implement isort.force_to_top (#2877)
This commit is contained in:
parent
059601d968
commit
a919041dda
17
README.md
17
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
},
|
||||
)
|
||||
})
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
source: src/rules/isort/mod.rs
|
||||
source: crates/ruff/src/rules/isort/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
|
|
|
|||
|
|
@ -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: ~
|
||||
|
||||
|
|
@ -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: ~
|
||||
|
||||
|
|
@ -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: ~
|
||||
|
||||
|
|
@ -15,9 +15,10 @@ expression: diagnostics
|
|||
- import os
|
||||
- import sys
|
||||
- ""
|
||||
- import ruff
|
||||
- ""
|
||||
- import leading_prefix
|
||||
- ""
|
||||
- import ruff
|
||||
- from . import leading_prefix
|
||||
- ""
|
||||
location:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
Loading…
Reference in New Issue