-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sort by key #7963
Merged
Merged
Sort by key #7963
Changes from 12 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
8661004
Re-implement `module_key` function + sort with it
bluthej 4050471
Re-implement `member_key` function + use it
bluthej e6ef94c
Rename `prefix` related things to `member type`
bluthej 8b82da0
Unify module key between straight and from imports
bluthej a902a90
Replace last call to `cmp_either_import`
bluthej 452d7ef
Make meaning of two `bool`s clearer
bluthej 5a4b9dd
Remove useless `Display` impl
bluthej 087bd57
Add `NatOrdStr` to remove some allocations
bluthej e01bead
Remove remaining unnecessary allocations
bluthej 56b462f
Use `Cow` to avoid unnecessary allocations
bluthej 97914c4
Merge branch 'main' into sort-by-key
charliermarsh 8163c04
Add docstring
charliermarsh c2a15e3
Remove i64
charliermarsh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,171 +1,147 @@ | ||
/// See: <https://github.com/PyCQA/isort/blob/12cc5fbd67eebf92eb2213b03c07b138ae1fb448/isort/sorting.py#L13> | ||
use std::cmp::Ordering; | ||
use std::collections::BTreeSet; | ||
use natord; | ||
use std::{borrow::Cow, cmp::Ordering}; | ||
|
||
use ruff_python_stdlib::str; | ||
|
||
use super::settings::{RelativeImportsOrder, Settings}; | ||
use super::types::EitherImport::{Import, ImportFrom}; | ||
use super::types::{AliasData, EitherImport, ImportFromData, Importable}; | ||
|
||
#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone)] | ||
pub(crate) enum Prefix { | ||
Constants, | ||
Classes, | ||
Variables, | ||
|
||
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Copy, Clone)] | ||
pub(crate) enum MemberType { | ||
Constant, | ||
Class, | ||
Variable, | ||
} | ||
|
||
fn prefix(name: &str, settings: &Settings) -> Prefix { | ||
fn member_type(name: &str, settings: &Settings) -> MemberType { | ||
if settings.constants.contains(name) { | ||
// Ex) `CONSTANT` | ||
Prefix::Constants | ||
MemberType::Constant | ||
} else if settings.classes.contains(name) { | ||
// Ex) `CLASS` | ||
Prefix::Classes | ||
MemberType::Class | ||
} else if settings.variables.contains(name) { | ||
// Ex) `variable` | ||
Prefix::Variables | ||
MemberType::Variable | ||
} else if name.len() > 1 && str::is_cased_uppercase(name) { | ||
// Ex) `CONSTANT` | ||
Prefix::Constants | ||
MemberType::Constant | ||
} else if name.chars().next().is_some_and(char::is_uppercase) { | ||
// Ex) `Class` | ||
Prefix::Classes | ||
MemberType::Class | ||
} else { | ||
// Ex) `variable` | ||
Prefix::Variables | ||
MemberType::Variable | ||
} | ||
} | ||
|
||
/// Compare two module names' by their `force-to-top`ness. | ||
fn cmp_force_to_top(name1: &str, name2: &str, force_to_top: &BTreeSet<String>) -> Ordering { | ||
let force_to_top1 = force_to_top.contains(name1); | ||
let force_to_top2 = force_to_top.contains(name2); | ||
force_to_top1.cmp(&force_to_top2).reverse() | ||
#[derive(Eq, PartialEq, Debug)] | ||
pub(crate) struct NatOrdStr<'a>(Cow<'a, str>); | ||
|
||
impl Ord for NatOrdStr<'_> { | ||
fn cmp(&self, other: &Self) -> Ordering { | ||
natord::compare(&self.0, &other.0) | ||
} | ||
} | ||
|
||
/// Compare two top-level modules. | ||
pub(crate) fn cmp_modules(alias1: &AliasData, alias2: &AliasData, settings: &Settings) -> Ordering { | ||
cmp_force_to_top(alias1.name, alias2.name, &settings.force_to_top) | ||
.then_with(|| { | ||
if settings.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, | ||
(Some(_), None) => Ordering::Greater, | ||
(Some(asname1), Some(asname2)) => natord::compare(asname1, asname2), | ||
}) | ||
impl PartialOrd for NatOrdStr<'_> { | ||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||
Some(self.cmp(other)) | ||
} | ||
} | ||
|
||
/// Compare two member imports within `Stmt::ImportFrom` blocks. | ||
pub(crate) fn cmp_members(alias1: &AliasData, alias2: &AliasData, settings: &Settings) -> Ordering { | ||
match (alias1.name == "*", alias2.name == "*") { | ||
(true, false) => Ordering::Less, | ||
(false, true) => Ordering::Greater, | ||
_ => { | ||
if settings.order_by_type { | ||
prefix(alias1.name, settings) | ||
.cmp(&prefix(alias2.name, settings)) | ||
.then_with(|| cmp_modules(alias1, alias2, settings)) | ||
} else { | ||
cmp_modules(alias1, alias2, settings) | ||
} | ||
} | ||
impl<'a> From<&'a str> for NatOrdStr<'a> { | ||
fn from(s: &'a str) -> Self { | ||
NatOrdStr(Cow::Borrowed(s)) | ||
} | ||
} | ||
|
||
/// Compare two relative import levels. | ||
pub(crate) fn cmp_levels( | ||
level1: Option<u32>, | ||
level2: Option<u32>, | ||
relative_imports_order: RelativeImportsOrder, | ||
) -> Ordering { | ||
match (level1, level2) { | ||
(None, None) => Ordering::Equal, | ||
(None, Some(_)) => Ordering::Less, | ||
(Some(_), None) => Ordering::Greater, | ||
(Some(level1), Some(level2)) => match relative_imports_order { | ||
RelativeImportsOrder::ClosestToFurthest => level1.cmp(&level2), | ||
RelativeImportsOrder::FurthestToClosest => level2.cmp(&level1), | ||
}, | ||
impl<'a> From<String> for NatOrdStr<'a> { | ||
fn from(s: String) -> Self { | ||
NatOrdStr(Cow::Owned(s)) | ||
} | ||
} | ||
|
||
/// Compare two `Stmt::ImportFrom` blocks. | ||
pub(crate) fn cmp_import_from( | ||
import_from1: &ImportFromData, | ||
import_from2: &ImportFromData, | ||
type ModuleKey<'a> = ( | ||
i64, | ||
Option<bool>, | ||
Option<NatOrdStr<'a>>, | ||
Option<NatOrdStr<'a>>, | ||
Option<NatOrdStr<'a>>, | ||
Option<MemberKey<'a>>, | ||
); | ||
|
||
/// Returns a comparable key to capture the desired sorting order for an imported module (e.g., | ||
/// `foo` in `from foo import bar`). | ||
pub(crate) fn module_key<'a>( | ||
name: Option<&'a str>, | ||
asname: Option<&'a str>, | ||
level: Option<u32>, | ||
first_alias: Option<(&'a str, Option<&'a str>)>, | ||
settings: &Settings, | ||
) -> Ordering { | ||
cmp_levels( | ||
import_from1.level, | ||
import_from2.level, | ||
settings.relative_imports_order, | ||
) -> ModuleKey<'a> { | ||
let level = level | ||
.map(i64::from) | ||
.map(|l| match settings.relative_imports_order { | ||
RelativeImportsOrder::ClosestToFurthest => l, | ||
RelativeImportsOrder::FurthestToClosest => -l, | ||
}) | ||
.unwrap_or_default(); | ||
let force_to_top = name.map(|name| !settings.force_to_top.contains(name)); // `false` < `true` so we get forced to top first | ||
let maybe_lowercase_name = name | ||
.and_then(|name| (!settings.case_sensitive).then_some(NatOrdStr(maybe_lowercase(name)))); | ||
let module_name = name.map(NatOrdStr::from); | ||
let asname = asname.map(NatOrdStr::from); | ||
let first_alias = first_alias.map(|(name, asname)| member_key(name, asname, settings)); | ||
|
||
( | ||
level, | ||
force_to_top, | ||
maybe_lowercase_name, | ||
module_name, | ||
asname, | ||
first_alias, | ||
) | ||
.then_with(|| { | ||
cmp_force_to_top( | ||
&import_from1.module_name(), | ||
&import_from2.module_name(), | ||
&settings.force_to_top, | ||
) | ||
}) | ||
.then_with(|| match (&import_from1.module, import_from2.module) { | ||
(None, None) => Ordering::Equal, | ||
(None, Some(_)) => Ordering::Less, | ||
(Some(_), None) => Ordering::Greater, | ||
(Some(module1), Some(module2)) => { | ||
if settings.case_sensitive { | ||
natord::compare(module1, module2) | ||
} else { | ||
natord::compare_ignore_case(module1, module2) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
/// Compare an import to an import-from. | ||
fn cmp_import_import_from( | ||
import: &AliasData, | ||
import_from: &ImportFromData, | ||
type MemberKey<'a> = ( | ||
bool, | ||
Option<MemberType>, | ||
Option<NatOrdStr<'a>>, | ||
NatOrdStr<'a>, | ||
Option<NatOrdStr<'a>>, | ||
); | ||
|
||
/// Returns a comparable key to capture the desired sorting order for an imported member (e.g., | ||
/// `bar` in `from foo import bar`). | ||
pub(crate) fn member_key<'a>( | ||
name: &'a str, | ||
asname: Option<&'a str>, | ||
settings: &Settings, | ||
) -> Ordering { | ||
cmp_force_to_top( | ||
import.name, | ||
&import_from.module_name(), | ||
&settings.force_to_top, | ||
) -> MemberKey<'a> { | ||
let not_star_import = name != "*"; // `false` < `true` so we get star imports first | ||
let member_type = settings | ||
.order_by_type | ||
.then_some(member_type(name, settings)); | ||
let maybe_lowercase_name = | ||
(!settings.case_sensitive).then_some(NatOrdStr(maybe_lowercase(name))); | ||
let module_name = NatOrdStr::from(name); | ||
let asname = asname.map(NatOrdStr::from); | ||
|
||
( | ||
not_star_import, | ||
member_type, | ||
maybe_lowercase_name, | ||
module_name, | ||
asname, | ||
) | ||
.then_with(|| { | ||
if settings.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()) | ||
} | ||
}) | ||
} | ||
|
||
/// Compare two [`EitherImport`] enums which may be [`Import`] or [`ImportFrom`] | ||
/// structs. | ||
pub(crate) fn cmp_either_import( | ||
a: &EitherImport, | ||
b: &EitherImport, | ||
settings: &Settings, | ||
) -> Ordering { | ||
match (a, b) { | ||
(Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2, settings), | ||
(ImportFrom((import_from, ..)), Import((alias, _))) => { | ||
cmp_import_import_from(alias, import_from, settings).reverse() | ||
} | ||
(Import((alias, _)), ImportFrom((import_from, ..))) => { | ||
cmp_import_import_from(alias, import_from, settings) | ||
} | ||
(ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => { | ||
cmp_import_from(import_from1, import_from2, settings) | ||
} | ||
/// Lowercase the given string, if it contains any uppercase characters. | ||
fn maybe_lowercase(name: &str) -> Cow<'_, str> { | ||
if name.chars().all(char::is_lowercase) { | ||
Cow::Borrowed(name) | ||
} else { | ||
Cow::Owned(name.to_lowercase()) | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this logic still being captured in the refactor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be because both
module_key
andmember_key
return a tuple with firstmaybe_lower_case_name
(which isSome(name.to_lowercase())
ifsettings.case_sensitive
is false, otherwiseNone
) and then the module/member name.So if
settings.case_sensitive
is true, then we just compare the names usingnatord::compare
, and if it's false we first compare the lowercase names withnatord::compare
, and then the actual names.I guess the question is, is comparing two strings that have been turned into lowercase with
natord::compare
the same as comparing them directly withnatord::compare_ignore_case
? I looked at the source code fornatord::compare_ignore_case
and they're just callingto_lowercase
on thechar
s individually, but apart from that it seems like what I did should be equivalent.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is slightly different, because they also map integers to avoid sorting them lexicographically, right? For example, to avoid putting
module10
beforemodule2
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I thought you were talking about the fact that there are two function calls (one case sensitive and one case insensitive).
The correct natrural ordering (typically
module2
comes beforemodule10
) is indeed correctly captured in my refactor because I'm relying onnatord::compare
to implement theOrd
trait for my types. Was that the question?