mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 04:19:43 +00:00
[isort]
Add --case-sensitive
flag (#5539)
## Summary Adds a `--case-sensitive` setting/flag to isort (default: `false`) which, when set to `true` sorts imports case sensitively instead of case insensitively. Tests and Docs can be improved, can do that if the general idea of the implementation is in order. First `isort` edit so any and all feedback is welcomed even more than usual. ## Test Plan Added a fixture with an assortment of imports in various cases. ## Issue links Closes: https://github.com/astral-sh/ruff/issues/5514
This commit is contained in:
parent
5a74a8e5a1
commit
6f548d9872
8 changed files with 134 additions and 12 deletions
9
crates/ruff/resources/test/fixtures/isort/case_sensitive.py
vendored
Normal file
9
crates/ruff/resources/test/fixtures/isort/case_sensitive.py
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import A
|
||||||
|
import B
|
||||||
|
import b
|
||||||
|
import C
|
||||||
|
import d
|
||||||
|
import E
|
||||||
|
import f
|
||||||
|
from g import a, B, c
|
||||||
|
from h import A, b, C
|
|
@ -74,6 +74,7 @@ pub(crate) fn format_imports(
|
||||||
combine_as_imports: bool,
|
combine_as_imports: bool,
|
||||||
force_single_line: bool,
|
force_single_line: bool,
|
||||||
force_sort_within_sections: bool,
|
force_sort_within_sections: bool,
|
||||||
|
case_sensitive: bool,
|
||||||
force_wrap_aliases: bool,
|
force_wrap_aliases: bool,
|
||||||
force_to_top: &BTreeSet<String>,
|
force_to_top: &BTreeSet<String>,
|
||||||
known_modules: &KnownModules,
|
known_modules: &KnownModules,
|
||||||
|
@ -114,6 +115,7 @@ pub(crate) fn format_imports(
|
||||||
src,
|
src,
|
||||||
package,
|
package,
|
||||||
force_sort_within_sections,
|
force_sort_within_sections,
|
||||||
|
case_sensitive,
|
||||||
force_wrap_aliases,
|
force_wrap_aliases,
|
||||||
force_to_top,
|
force_to_top,
|
||||||
known_modules,
|
known_modules,
|
||||||
|
@ -171,6 +173,7 @@ fn format_import_block(
|
||||||
src: &[PathBuf],
|
src: &[PathBuf],
|
||||||
package: Option<&Path>,
|
package: Option<&Path>,
|
||||||
force_sort_within_sections: bool,
|
force_sort_within_sections: bool,
|
||||||
|
case_sensitive: bool,
|
||||||
force_wrap_aliases: bool,
|
force_wrap_aliases: bool,
|
||||||
force_to_top: &BTreeSet<String>,
|
force_to_top: &BTreeSet<String>,
|
||||||
known_modules: &KnownModules,
|
known_modules: &KnownModules,
|
||||||
|
@ -206,6 +209,7 @@ fn format_import_block(
|
||||||
let imports = order_imports(
|
let imports = order_imports(
|
||||||
import_block,
|
import_block,
|
||||||
order_by_type,
|
order_by_type,
|
||||||
|
case_sensitive,
|
||||||
relative_imports_order,
|
relative_imports_order,
|
||||||
classes,
|
classes,
|
||||||
constants,
|
constants,
|
||||||
|
@ -222,7 +226,13 @@ fn format_import_block(
|
||||||
.collect::<Vec<EitherImport>>();
|
.collect::<Vec<EitherImport>>();
|
||||||
if force_sort_within_sections {
|
if force_sort_within_sections {
|
||||||
imports.sort_by(|import1, import2| {
|
imports.sort_by(|import1, import2| {
|
||||||
cmp_either_import(import1, import2, relative_imports_order, force_to_top)
|
cmp_either_import(
|
||||||
|
import1,
|
||||||
|
import2,
|
||||||
|
relative_imports_order,
|
||||||
|
force_to_top,
|
||||||
|
case_sensitive,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
imports
|
imports
|
||||||
|
@ -449,6 +459,24 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case(Path::new("case_sensitive.py"))]
|
||||||
|
fn case_sensitive(path: &Path) -> Result<()> {
|
||||||
|
let snapshot = format!("case_sensitive_{}", path.to_string_lossy());
|
||||||
|
let diagnostics = test_path(
|
||||||
|
Path::new("isort").join(path).as_path(),
|
||||||
|
&Settings {
|
||||||
|
isort: super::settings::Settings {
|
||||||
|
case_sensitive: true,
|
||||||
|
..super::settings::Settings::default()
|
||||||
|
},
|
||||||
|
src: vec![test_resource_path("fixtures/isort")],
|
||||||
|
..Settings::for_rule(Rule::UnsortedImports)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
assert_messages!(snapshot, diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test_case(Path::new("force_to_top.py"))]
|
#[test_case(Path::new("force_to_top.py"))]
|
||||||
fn force_to_top(path: &Path) -> Result<()> {
|
fn force_to_top(path: &Path) -> Result<()> {
|
||||||
let snapshot = format!("force_to_top_{}", path.to_string_lossy());
|
let snapshot = format!("force_to_top_{}", path.to_string_lossy());
|
||||||
|
|
|
@ -9,9 +9,11 @@ use super::settings::RelativeImportsOrder;
|
||||||
use super::sorting::{cmp_import_from, cmp_members, cmp_modules};
|
use super::sorting::{cmp_import_from, cmp_members, cmp_modules};
|
||||||
use super::types::{AliasData, CommentSet, ImportBlock, OrderedImportBlock};
|
use super::types::{AliasData, CommentSet, ImportBlock, OrderedImportBlock};
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn order_imports<'a>(
|
pub(crate) fn order_imports<'a>(
|
||||||
block: ImportBlock<'a>,
|
block: ImportBlock<'a>,
|
||||||
order_by_type: bool,
|
order_by_type: bool,
|
||||||
|
case_sensitive: bool,
|
||||||
relative_imports_order: RelativeImportsOrder,
|
relative_imports_order: RelativeImportsOrder,
|
||||||
classes: &'a BTreeSet<String>,
|
classes: &'a BTreeSet<String>,
|
||||||
constants: &'a BTreeSet<String>,
|
constants: &'a BTreeSet<String>,
|
||||||
|
@ -25,7 +27,9 @@ pub(crate) fn order_imports<'a>(
|
||||||
block
|
block
|
||||||
.import
|
.import
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by(|(alias1, _), (alias2, _)| cmp_modules(alias1, alias2, force_to_top)),
|
.sorted_by(|(alias1, _), (alias2, _)| {
|
||||||
|
cmp_modules(alias1, alias2, force_to_top, case_sensitive)
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort `Stmt::ImportFrom`.
|
// Sort `Stmt::ImportFrom`.
|
||||||
|
@ -70,6 +74,7 @@ pub(crate) fn order_imports<'a>(
|
||||||
constants,
|
constants,
|
||||||
variables,
|
variables,
|
||||||
force_to_top,
|
force_to_top,
|
||||||
|
case_sensitive,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<(AliasData, CommentSet)>>(),
|
.collect::<Vec<(AliasData, CommentSet)>>(),
|
||||||
|
@ -83,6 +88,7 @@ pub(crate) fn order_imports<'a>(
|
||||||
import_from2,
|
import_from2,
|
||||||
relative_imports_order,
|
relative_imports_order,
|
||||||
force_to_top,
|
force_to_top,
|
||||||
|
case_sensitive,
|
||||||
)
|
)
|
||||||
.then_with(|| match (aliases1.first(), aliases2.first()) {
|
.then_with(|| match (aliases1.first(), aliases2.first()) {
|
||||||
(None, None) => Ordering::Equal,
|
(None, None) => Ordering::Equal,
|
||||||
|
@ -96,6 +102,7 @@ pub(crate) fn order_imports<'a>(
|
||||||
constants,
|
constants,
|
||||||
variables,
|
variables,
|
||||||
force_to_top,
|
force_to_top,
|
||||||
|
case_sensitive,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -127,6 +127,7 @@ pub(crate) fn organize_imports(
|
||||||
settings.isort.combine_as_imports,
|
settings.isort.combine_as_imports,
|
||||||
settings.isort.force_single_line,
|
settings.isort.force_single_line,
|
||||||
settings.isort.force_sort_within_sections,
|
settings.isort.force_sort_within_sections,
|
||||||
|
settings.isort.case_sensitive,
|
||||||
settings.isort.force_wrap_aliases,
|
settings.isort.force_wrap_aliases,
|
||||||
&settings.isort.force_to_top,
|
&settings.isort.force_to_top,
|
||||||
&settings.isort.known_modules,
|
&settings.isort.known_modules,
|
||||||
|
|
|
@ -127,6 +127,15 @@ pub struct Options {
|
||||||
/// imports (like `from itertools import groupby`). Instead, sort the
|
/// imports (like `from itertools import groupby`). Instead, sort the
|
||||||
/// imports by module, independent of import style.
|
/// imports by module, independent of import style.
|
||||||
pub force_sort_within_sections: Option<bool>,
|
pub force_sort_within_sections: Option<bool>,
|
||||||
|
#[option(
|
||||||
|
default = r#"false"#,
|
||||||
|
value_type = "bool",
|
||||||
|
example = r#"
|
||||||
|
case-sensitive = true
|
||||||
|
"#
|
||||||
|
)]
|
||||||
|
/// Sort imports taking into account case sensitivity.
|
||||||
|
pub case_sensitive: Option<bool>,
|
||||||
#[option(
|
#[option(
|
||||||
default = r#"[]"#,
|
default = r#"[]"#,
|
||||||
value_type = "list[str]",
|
value_type = "list[str]",
|
||||||
|
@ -303,6 +312,7 @@ pub struct Settings {
|
||||||
pub combine_as_imports: bool,
|
pub combine_as_imports: bool,
|
||||||
pub force_single_line: bool,
|
pub force_single_line: bool,
|
||||||
pub force_sort_within_sections: bool,
|
pub force_sort_within_sections: bool,
|
||||||
|
pub case_sensitive: bool,
|
||||||
pub force_wrap_aliases: bool,
|
pub force_wrap_aliases: bool,
|
||||||
pub force_to_top: BTreeSet<String>,
|
pub force_to_top: BTreeSet<String>,
|
||||||
pub known_modules: KnownModules,
|
pub known_modules: KnownModules,
|
||||||
|
@ -327,6 +337,7 @@ impl Default for Settings {
|
||||||
combine_as_imports: false,
|
combine_as_imports: false,
|
||||||
force_single_line: false,
|
force_single_line: false,
|
||||||
force_sort_within_sections: false,
|
force_sort_within_sections: false,
|
||||||
|
case_sensitive: false,
|
||||||
force_wrap_aliases: false,
|
force_wrap_aliases: false,
|
||||||
force_to_top: BTreeSet::new(),
|
force_to_top: BTreeSet::new(),
|
||||||
known_modules: KnownModules::default(),
|
known_modules: KnownModules::default(),
|
||||||
|
@ -429,6 +440,7 @@ impl From<Options> for Settings {
|
||||||
combine_as_imports: options.combine_as_imports.unwrap_or(false),
|
combine_as_imports: options.combine_as_imports.unwrap_or(false),
|
||||||
force_single_line: options.force_single_line.unwrap_or(false),
|
force_single_line: options.force_single_line.unwrap_or(false),
|
||||||
force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false),
|
force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false),
|
||||||
|
case_sensitive: options.case_sensitive.unwrap_or(false),
|
||||||
force_wrap_aliases: options.force_wrap_aliases.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()),
|
force_to_top: BTreeSet::from_iter(options.force_to_top.unwrap_or_default()),
|
||||||
known_modules: KnownModules::new(
|
known_modules: KnownModules::new(
|
||||||
|
@ -468,6 +480,7 @@ impl From<Settings> for Options {
|
||||||
),
|
),
|
||||||
force_single_line: Some(settings.force_single_line),
|
force_single_line: Some(settings.force_single_line),
|
||||||
force_sort_within_sections: Some(settings.force_sort_within_sections),
|
force_sort_within_sections: Some(settings.force_sort_within_sections),
|
||||||
|
case_sensitive: Some(settings.case_sensitive),
|
||||||
force_wrap_aliases: Some(settings.force_wrap_aliases),
|
force_wrap_aliases: Some(settings.force_wrap_aliases),
|
||||||
force_to_top: Some(settings.force_to_top.into_iter().collect()),
|
force_to_top: Some(settings.force_to_top.into_iter().collect()),
|
||||||
known_first_party: Some(
|
known_first_party: Some(
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/isort/mod.rs
|
||||||
|
---
|
||||||
|
case_sensitive.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||||
|
|
|
||||||
|
1 | / import A
|
||||||
|
2 | | import B
|
||||||
|
3 | | import b
|
||||||
|
4 | | import C
|
||||||
|
5 | | import d
|
||||||
|
6 | | import E
|
||||||
|
7 | | import f
|
||||||
|
8 | | from g import a, B, c
|
||||||
|
9 | | from h import A, b, C
|
||||||
|
|
|
||||||
|
= help: Organize imports
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
1 1 | import A
|
||||||
|
2 2 | import B
|
||||||
|
3 |+import C
|
||||||
|
4 |+import E
|
||||||
|
3 5 | import b
|
||||||
|
4 |-import C
|
||||||
|
5 6 | import d
|
||||||
|
6 |-import E
|
||||||
|
7 7 | import f
|
||||||
|
8 |-from g import a, B, c
|
||||||
|
9 |-from h import A, b, C
|
||||||
|
8 |+from g import B, a, c
|
||||||
|
9 |+from h import A, C, b
|
||||||
|
|
||||||
|
|
|
@ -56,10 +56,17 @@ pub(crate) fn cmp_modules(
|
||||||
alias1: &AliasData,
|
alias1: &AliasData,
|
||||||
alias2: &AliasData,
|
alias2: &AliasData,
|
||||||
force_to_top: &BTreeSet<String>,
|
force_to_top: &BTreeSet<String>,
|
||||||
|
case_sensitive: bool,
|
||||||
) -> Ordering {
|
) -> Ordering {
|
||||||
cmp_force_to_top(alias1.name, alias2.name, force_to_top)
|
cmp_force_to_top(alias1.name, alias2.name, force_to_top)
|
||||||
.then_with(|| natord::compare_ignore_case(alias1.name, alias2.name))
|
.then_with(|| {
|
||||||
|
if case_sensitive {
|
||||||
|
natord::compare(alias1.name, alias2.name)
|
||||||
|
} else {
|
||||||
|
natord::compare_ignore_case(alias1.name, alias2.name)
|
||||||
.then_with(|| natord::compare(alias1.name, alias2.name))
|
.then_with(|| natord::compare(alias1.name, alias2.name))
|
||||||
|
}
|
||||||
|
})
|
||||||
.then_with(|| match (alias1.asname, alias2.asname) {
|
.then_with(|| match (alias1.asname, alias2.asname) {
|
||||||
(None, None) => Ordering::Equal,
|
(None, None) => Ordering::Equal,
|
||||||
(None, Some(_)) => Ordering::Less,
|
(None, Some(_)) => Ordering::Less,
|
||||||
|
@ -69,6 +76,7 @@ pub(crate) fn cmp_modules(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compare two member imports within `Stmt::ImportFrom` blocks.
|
/// Compare two member imports within `Stmt::ImportFrom` blocks.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn cmp_members(
|
pub(crate) fn cmp_members(
|
||||||
alias1: &AliasData,
|
alias1: &AliasData,
|
||||||
alias2: &AliasData,
|
alias2: &AliasData,
|
||||||
|
@ -77,6 +85,7 @@ pub(crate) fn cmp_members(
|
||||||
constants: &BTreeSet<String>,
|
constants: &BTreeSet<String>,
|
||||||
variables: &BTreeSet<String>,
|
variables: &BTreeSet<String>,
|
||||||
force_to_top: &BTreeSet<String>,
|
force_to_top: &BTreeSet<String>,
|
||||||
|
case_sensitive: bool,
|
||||||
) -> Ordering {
|
) -> Ordering {
|
||||||
match (alias1.name == "*", alias2.name == "*") {
|
match (alias1.name == "*", alias2.name == "*") {
|
||||||
(true, false) => Ordering::Less,
|
(true, false) => Ordering::Less,
|
||||||
|
@ -85,9 +94,9 @@ pub(crate) fn cmp_members(
|
||||||
if order_by_type {
|
if order_by_type {
|
||||||
prefix(alias1.name, classes, constants, variables)
|
prefix(alias1.name, classes, constants, variables)
|
||||||
.cmp(&prefix(alias2.name, classes, constants, variables))
|
.cmp(&prefix(alias2.name, classes, constants, variables))
|
||||||
.then_with(|| cmp_modules(alias1, alias2, force_to_top))
|
.then_with(|| cmp_modules(alias1, alias2, force_to_top, case_sensitive))
|
||||||
} else {
|
} else {
|
||||||
cmp_modules(alias1, alias2, force_to_top)
|
cmp_modules(alias1, alias2, force_to_top, case_sensitive)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,6 +125,7 @@ pub(crate) fn cmp_import_from(
|
||||||
import_from2: &ImportFromData,
|
import_from2: &ImportFromData,
|
||||||
relative_imports_order: RelativeImportsOrder,
|
relative_imports_order: RelativeImportsOrder,
|
||||||
force_to_top: &BTreeSet<String>,
|
force_to_top: &BTreeSet<String>,
|
||||||
|
case_sensitive: bool,
|
||||||
) -> Ordering {
|
) -> Ordering {
|
||||||
cmp_levels(
|
cmp_levels(
|
||||||
import_from1.level,
|
import_from1.level,
|
||||||
|
@ -133,8 +143,13 @@ pub(crate) fn cmp_import_from(
|
||||||
(None, None) => Ordering::Equal,
|
(None, None) => Ordering::Equal,
|
||||||
(None, Some(_)) => Ordering::Less,
|
(None, Some(_)) => Ordering::Less,
|
||||||
(Some(_), None) => Ordering::Greater,
|
(Some(_), None) => Ordering::Greater,
|
||||||
(Some(module1), Some(module2)) => natord::compare_ignore_case(module1, module2)
|
(Some(module1), Some(module2)) => {
|
||||||
.then_with(|| natord::compare(module1, module2)),
|
if case_sensitive {
|
||||||
|
natord::compare(module1, module2)
|
||||||
|
} else {
|
||||||
|
natord::compare_ignore_case(module1, module2)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,9 +158,14 @@ fn cmp_import_import_from(
|
||||||
import: &AliasData,
|
import: &AliasData,
|
||||||
import_from: &ImportFromData,
|
import_from: &ImportFromData,
|
||||||
force_to_top: &BTreeSet<String>,
|
force_to_top: &BTreeSet<String>,
|
||||||
|
case_sensitive: bool,
|
||||||
) -> Ordering {
|
) -> Ordering {
|
||||||
cmp_force_to_top(import.name, &import_from.module_name(), force_to_top).then_with(|| {
|
cmp_force_to_top(import.name, &import_from.module_name(), force_to_top).then_with(|| {
|
||||||
|
if case_sensitive {
|
||||||
|
natord::compare(import.name, import_from.module.unwrap_or_default())
|
||||||
|
} else {
|
||||||
natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default())
|
natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,20 +176,24 @@ pub(crate) fn cmp_either_import(
|
||||||
b: &EitherImport,
|
b: &EitherImport,
|
||||||
relative_imports_order: RelativeImportsOrder,
|
relative_imports_order: RelativeImportsOrder,
|
||||||
force_to_top: &BTreeSet<String>,
|
force_to_top: &BTreeSet<String>,
|
||||||
|
case_sensitive: bool,
|
||||||
) -> Ordering {
|
) -> Ordering {
|
||||||
match (a, b) {
|
match (a, b) {
|
||||||
(Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2, force_to_top),
|
(Import((alias1, _)), Import((alias2, _))) => {
|
||||||
|
cmp_modules(alias1, alias2, force_to_top, case_sensitive)
|
||||||
|
}
|
||||||
(ImportFrom((import_from, ..)), Import((alias, _))) => {
|
(ImportFrom((import_from, ..)), Import((alias, _))) => {
|
||||||
cmp_import_import_from(alias, import_from, force_to_top).reverse()
|
cmp_import_import_from(alias, import_from, force_to_top, case_sensitive).reverse()
|
||||||
}
|
}
|
||||||
(Import((alias, _)), ImportFrom((import_from, ..))) => {
|
(Import((alias, _)), ImportFrom((import_from, ..))) => {
|
||||||
cmp_import_import_from(alias, import_from, force_to_top)
|
cmp_import_import_from(alias, import_from, force_to_top, case_sensitive)
|
||||||
}
|
}
|
||||||
(ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => cmp_import_from(
|
(ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => cmp_import_from(
|
||||||
import_from1,
|
import_from1,
|
||||||
import_from2,
|
import_from2,
|
||||||
relative_imports_order,
|
relative_imports_order,
|
||||||
force_to_top,
|
force_to_top,
|
||||||
|
case_sensitive,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
ruff.schema.json
generated
7
ruff.schema.json
generated
|
@ -1135,6 +1135,13 @@
|
||||||
"IsortOptions": {
|
"IsortOptions": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"case-sensitive": {
|
||||||
|
"description": "Sort imports taking into account case sensitivity.",
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"classes": {
|
"classes": {
|
||||||
"description": "An override list of tokens to always recognize as a Class for `order-by-type` regardless of casing.",
|
"description": "An override list of tokens to always recognize as a Class for `order-by-type` regardless of casing.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue