[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:
qdegraaf 2023-07-05 22:10:53 +02:00 committed by GitHub
parent 5a74a8e5a1
commit 6f548d9872
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 134 additions and 12 deletions

View 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

View file

@ -74,6 +74,7 @@ pub(crate) fn format_imports(
combine_as_imports: bool,
force_single_line: bool,
force_sort_within_sections: bool,
case_sensitive: bool,
force_wrap_aliases: bool,
force_to_top: &BTreeSet<String>,
known_modules: &KnownModules,
@ -114,6 +115,7 @@ pub(crate) fn format_imports(
src,
package,
force_sort_within_sections,
case_sensitive,
force_wrap_aliases,
force_to_top,
known_modules,
@ -171,6 +173,7 @@ fn format_import_block(
src: &[PathBuf],
package: Option<&Path>,
force_sort_within_sections: bool,
case_sensitive: bool,
force_wrap_aliases: bool,
force_to_top: &BTreeSet<String>,
known_modules: &KnownModules,
@ -206,6 +209,7 @@ fn format_import_block(
let imports = order_imports(
import_block,
order_by_type,
case_sensitive,
relative_imports_order,
classes,
constants,
@ -222,7 +226,13 @@ 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, force_to_top)
cmp_either_import(
import1,
import2,
relative_imports_order,
force_to_top,
case_sensitive,
)
});
};
imports
@ -449,6 +459,24 @@ mod tests {
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"))]
fn force_to_top(path: &Path) -> Result<()> {
let snapshot = format!("force_to_top_{}", path.to_string_lossy());

View file

@ -9,9 +9,11 @@ use super::settings::RelativeImportsOrder;
use super::sorting::{cmp_import_from, cmp_members, cmp_modules};
use super::types::{AliasData, CommentSet, ImportBlock, OrderedImportBlock};
#[allow(clippy::too_many_arguments)]
pub(crate) fn order_imports<'a>(
block: ImportBlock<'a>,
order_by_type: bool,
case_sensitive: bool,
relative_imports_order: RelativeImportsOrder,
classes: &'a BTreeSet<String>,
constants: &'a BTreeSet<String>,
@ -25,7 +27,9 @@ pub(crate) fn order_imports<'a>(
block
.import
.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`.
@ -70,6 +74,7 @@ pub(crate) fn order_imports<'a>(
constants,
variables,
force_to_top,
case_sensitive,
)
})
.collect::<Vec<(AliasData, CommentSet)>>(),
@ -83,6 +88,7 @@ pub(crate) fn order_imports<'a>(
import_from2,
relative_imports_order,
force_to_top,
case_sensitive,
)
.then_with(|| match (aliases1.first(), aliases2.first()) {
(None, None) => Ordering::Equal,
@ -96,6 +102,7 @@ pub(crate) fn order_imports<'a>(
constants,
variables,
force_to_top,
case_sensitive,
),
})
},

View file

@ -127,6 +127,7 @@ pub(crate) fn organize_imports(
settings.isort.combine_as_imports,
settings.isort.force_single_line,
settings.isort.force_sort_within_sections,
settings.isort.case_sensitive,
settings.isort.force_wrap_aliases,
&settings.isort.force_to_top,
&settings.isort.known_modules,

View file

@ -127,6 +127,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#"false"#,
value_type = "bool",
example = r#"
case-sensitive = true
"#
)]
/// Sort imports taking into account case sensitivity.
pub case_sensitive: Option<bool>,
#[option(
default = r#"[]"#,
value_type = "list[str]",
@ -303,6 +312,7 @@ pub struct Settings {
pub combine_as_imports: bool,
pub force_single_line: bool,
pub force_sort_within_sections: bool,
pub case_sensitive: bool,
pub force_wrap_aliases: bool,
pub force_to_top: BTreeSet<String>,
pub known_modules: KnownModules,
@ -327,6 +337,7 @@ impl Default for Settings {
combine_as_imports: false,
force_single_line: false,
force_sort_within_sections: false,
case_sensitive: false,
force_wrap_aliases: false,
force_to_top: BTreeSet::new(),
known_modules: KnownModules::default(),
@ -429,6 +440,7 @@ impl From<Options> for Settings {
combine_as_imports: options.combine_as_imports.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),
case_sensitive: options.case_sensitive.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_modules: KnownModules::new(
@ -468,6 +480,7 @@ impl From<Settings> for Options {
),
force_single_line: Some(settings.force_single_line),
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_to_top: Some(settings.force_to_top.into_iter().collect()),
known_first_party: Some(

View file

@ -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

View file

@ -56,10 +56,17 @@ pub(crate) fn cmp_modules(
alias1: &AliasData,
alias2: &AliasData,
force_to_top: &BTreeSet<String>,
case_sensitive: bool,
) -> Ordering {
cmp_force_to_top(alias1.name, alias2.name, force_to_top)
.then_with(|| natord::compare_ignore_case(alias1.name, alias2.name))
.then_with(|| natord::compare(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(|| match (alias1.asname, alias2.asname) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
@ -69,6 +76,7 @@ pub(crate) fn cmp_modules(
}
/// Compare two member imports within `Stmt::ImportFrom` blocks.
#[allow(clippy::too_many_arguments)]
pub(crate) fn cmp_members(
alias1: &AliasData,
alias2: &AliasData,
@ -77,6 +85,7 @@ pub(crate) fn cmp_members(
constants: &BTreeSet<String>,
variables: &BTreeSet<String>,
force_to_top: &BTreeSet<String>,
case_sensitive: bool,
) -> Ordering {
match (alias1.name == "*", alias2.name == "*") {
(true, false) => Ordering::Less,
@ -85,9 +94,9 @@ pub(crate) 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, force_to_top))
.then_with(|| cmp_modules(alias1, alias2, force_to_top, case_sensitive))
} 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,
relative_imports_order: RelativeImportsOrder,
force_to_top: &BTreeSet<String>,
case_sensitive: bool,
) -> Ordering {
cmp_levels(
import_from1.level,
@ -133,8 +143,13 @@ pub(crate) fn cmp_import_from(
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(Some(module1), Some(module2)) => natord::compare_ignore_case(module1, module2)
.then_with(|| natord::compare(module1, module2)),
(Some(module1), Some(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_from: &ImportFromData,
force_to_top: &BTreeSet<String>,
case_sensitive: bool,
) -> Ordering {
cmp_force_to_top(import.name, &import_from.module_name(), force_to_top).then_with(|| {
natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default())
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())
}
})
}
@ -156,20 +176,24 @@ pub(crate) fn cmp_either_import(
b: &EitherImport,
relative_imports_order: RelativeImportsOrder,
force_to_top: &BTreeSet<String>,
case_sensitive: bool,
) -> Ordering {
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, _))) => {
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, ..))) => {
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(
import_from1,
import_from2,
relative_imports_order,
force_to_top,
case_sensitive,
),
}
}

7
ruff.schema.json generated
View file

@ -1135,6 +1135,13 @@
"IsortOptions": {
"type": "object",
"properties": {
"case-sensitive": {
"description": "Sort imports taking into account case sensitivity.",
"type": [
"boolean",
"null"
]
},
"classes": {
"description": "An override list of tokens to always recognize as a Class for `order-by-type` regardless of casing.",
"type": [