Skip to content

Commit a885bb4

Browse files
charliermarshpull[bot]
authored andcommitted
Respect natural ordering for imports (#1374)
1 parent 2e3e5a8 commit a885bb4

File tree

6 files changed

+112
-40
lines changed

6 files changed

+112
-40
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ ignore = { version = "0.4.18" }
3434
itertools = { version = "0.10.5" }
3535
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
3636
log = { version = "0.4.17" }
37+
natord = { version = "1.0.9" }
3738
nohash-hasher = { version = "0.2.0" }
3839
notify = { version = "5.0.0" }
3940
num-bigint = { version = "0.4.3" }
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import numpy1
2+
import numpy10
3+
import numpy2
4+
from numpy import (
5+
cos,
6+
int8,
7+
sin,
8+
int32,
9+
int64,
10+
tan,
11+
uint8,
12+
uint16,
13+
int16,
14+
uint32,
15+
uint64,
16+
)

src/isort/mod.rs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::cmp::Reverse;
1+
use std::cmp::Ordering;
22
use std::collections::{BTreeMap, BTreeSet};
33
use std::path::{Path, PathBuf};
44

@@ -9,7 +9,7 @@ use rustpython_ast::{Stmt, StmtKind};
99

1010
use crate::isort::categorize::{categorize, ImportType};
1111
use crate::isort::comments::Comment;
12-
use crate::isort::sorting::{member_key, module_key};
12+
use crate::isort::sorting::{cmp_import_froms, cmp_members, cmp_modules};
1313
use crate::isort::track::{Block, Trailer};
1414
use crate::isort::types::{
1515
AliasData, CommentSet, ImportBlock, ImportFromData, Importable, OrderedImportBlock,
@@ -379,7 +379,7 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
379379
block
380380
.import
381381
.into_iter()
382-
.sorted_by_cached_key(|(alias, _)| module_key(alias.name, alias.asname)),
382+
.sorted_by(|(alias1, _), (alias2, _)| cmp_modules(alias1, alias2)),
383383
);
384384

385385
// Sort `StmtKind::ImportFrom`.
@@ -446,23 +446,19 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
446446
comments,
447447
aliases
448448
.into_iter()
449-
.sorted_by_cached_key(|(alias, _)| member_key(alias.name, alias.asname))
449+
.sorted_by(|(alias1, _), (alias2, _)| cmp_members(alias1, alias2))
450450
.collect::<Vec<(AliasData, CommentSet)>>(),
451451
)
452452
})
453-
.sorted_by_cached_key(|(import_from, _, aliases)| {
454-
// Sort each `StmtKind::ImportFrom` by module key, breaking ties based on
455-
// members.
456-
(
457-
Reverse(import_from.level),
458-
import_from
459-
.module
460-
.as_ref()
461-
.map(|module| module_key(module, None)),
462-
aliases
463-
.first()
464-
.map(|(alias, _)| member_key(alias.name, alias.asname)),
465-
)
453+
.sorted_by(|(import_from1, _, aliases1), (import_from2, _, aliases2)| {
454+
cmp_import_froms(import_from1, import_from2).then_with(|| {
455+
match (aliases1.first(), aliases2.first()) {
456+
(None, None) => Ordering::Equal,
457+
(None, Some(_)) => Ordering::Less,
458+
(Some(_), None) => Ordering::Greater,
459+
(Some((alias1, _)), Some((alias2, _))) => cmp_members(alias1, alias2),
460+
}
461+
})
466462
}),
467463
);
468464

@@ -569,6 +565,7 @@ mod tests {
569565
#[test_case(Path::new("insert_empty_lines.py"))]
570566
#[test_case(Path::new("insert_empty_lines.pyi"))]
571567
#[test_case(Path::new("leading_prefix.py"))]
568+
#[test_case(Path::new("natural_order.py"))]
572569
#[test_case(Path::new("no_reorder_within_section.py"))]
573570
#[test_case(Path::new("no_wrap_star.py"))]
574571
#[test_case(Path::new("order_by_type.py"))]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
source: src/isort/mod.rs
3+
expression: checks
4+
---
5+
- kind: UnsortedImports
6+
location:
7+
row: 1
8+
column: 0
9+
end_location:
10+
row: 17
11+
column: 0
12+
fix:
13+
content: "import numpy1\nimport numpy2\nimport numpy10\nfrom numpy import (\n cos,\n int8,\n int16,\n int32,\n int64,\n sin,\n tan,\n uint8,\n uint16,\n uint32,\n uint64,\n)\n"
14+
location:
15+
row: 1
16+
column: 0
17+
end_location:
18+
row: 17
19+
column: 0
20+

src/isort/sorting.rs

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
/// See: <https://github.com/PyCQA/isort/blob/12cc5fbd67eebf92eb2213b03c07b138ae1fb448/isort/sorting.py#L13>
2+
use std::cmp::Ordering;
3+
4+
use crate::isort::types::{AliasData, ImportFromData};
25
use crate::python::string;
36

47
#[derive(PartialOrd, Ord, PartialEq, Eq)]
@@ -8,29 +11,57 @@ pub enum Prefix {
811
Variables,
912
}
1013

11-
pub fn module_key<'a>(
12-
name: &'a str,
13-
asname: Option<&'a String>,
14-
) -> (String, &'a str, Option<&'a String>) {
15-
(name.to_lowercase(), name, asname)
14+
fn prefix(name: &str) -> Prefix {
15+
if name.len() > 1 && string::is_upper(name) {
16+
// Ex) `CONSTANT`
17+
Prefix::Constants
18+
} else if name.chars().next().map_or(false, char::is_uppercase) {
19+
// Ex) `Class`
20+
Prefix::Classes
21+
} else {
22+
// Ex) `variable`
23+
Prefix::Variables
24+
}
25+
}
26+
27+
/// Compare two top-level modules.
28+
pub fn cmp_modules(alias1: &AliasData, alias2: &AliasData) -> Ordering {
29+
natord::compare_ignore_case(alias1.name, alias2.name)
30+
.then_with(|| natord::compare(alias1.name, alias2.name))
31+
.then_with(|| match (alias1.asname, alias2.asname) {
32+
(None, None) => Ordering::Equal,
33+
(None, Some(_)) => Ordering::Less,
34+
(Some(_), None) => Ordering::Greater,
35+
(Some(asname1), Some(asname2)) => natord::compare(asname1, asname2),
36+
})
37+
}
38+
39+
/// Compare two member imports within `StmtKind::ImportFrom` blocks.
40+
pub fn cmp_members(alias1: &AliasData, alias2: &AliasData) -> Ordering {
41+
prefix(alias1.name)
42+
.cmp(&prefix(alias2.name))
43+
.then_with(|| cmp_modules(alias1, alias2))
44+
}
45+
46+
/// Compare two relative import levels.
47+
pub fn cmp_levels(level1: Option<&usize>, level2: Option<&usize>) -> Ordering {
48+
match (level1, level2) {
49+
(None, None) => Ordering::Equal,
50+
(None, Some(_)) => Ordering::Less,
51+
(Some(_), None) => Ordering::Greater,
52+
(Some(level1), Some(level2)) => level2.cmp(level1),
53+
}
1654
}
1755

18-
pub fn member_key<'a>(
19-
name: &'a str,
20-
asname: Option<&'a String>,
21-
) -> (Prefix, String, Option<&'a String>) {
22-
(
23-
if name.len() > 1 && string::is_upper(name) {
24-
// Ex) `CONSTANT`
25-
Prefix::Constants
26-
} else if name.chars().next().map_or(false, char::is_uppercase) {
27-
// Ex) `Class`
28-
Prefix::Classes
29-
} else {
30-
// Ex) `variable`
31-
Prefix::Variables
32-
},
33-
name.to_lowercase(),
34-
asname,
35-
)
56+
/// Compare two `StmtKind::ImportFrom` blocks.
57+
pub fn cmp_import_froms(import_from1: &ImportFromData, import_from2: &ImportFromData) -> Ordering {
58+
cmp_levels(import_from1.level, import_from2.level).then_with(|| {
59+
match (&import_from1.module, import_from2.module) {
60+
(None, None) => Ordering::Equal,
61+
(None, Some(_)) => Ordering::Less,
62+
(Some(_), None) => Ordering::Greater,
63+
(Some(module1), Some(module2)) => natord::compare_ignore_case(module1, module2)
64+
.then_with(|| natord::compare(module1, module2)),
65+
}
66+
})
3667
}

0 commit comments

Comments
 (0)