From 3aae16f1bdef51e2f64ea4e9e64f9201ad4d9020 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 16 Jan 2024 14:42:47 +0000 Subject: [PATCH] Add rule and autofix to sort the contents of `__all__` (#9474) ## Summary This implements the rule proposed in #1198 (though it doesn't close the issue, as there are some open questions about configuration that might merit some further discussion). ## Test Plan `cargo test` / `cargo insta review`. I also ran this PR branch on the CPython codebase with `--fix --select=RUF022 --preview `, and the results looked pretty good to me. --------- Co-authored-by: Micha Reiser Co-authored-by: Andrew Gallant --- .../resources/test/fixtures/ruff/RUF022.py | 308 +++++ .../src/checkers/ast/analyze/expression.rs | 3 + .../src/checkers/ast/analyze/statement.rs | 11 +- crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/ruff/mod.rs | 1 + .../ruff_linter/src/rules/ruff/rules/mod.rs | 2 + .../src/rules/ruff/rules/sort_dunder_all.rs | 1019 +++++++++++++++++ ..._rules__ruff__tests__RUF022_RUF022.py.snap | 903 +++++++++++++++ ruff.schema.json | 1 + 9 files changed, 2248 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py create mode 100644 crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py new file mode 100644 index 0000000000000..5a749d3ed4dd1 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -0,0 +1,308 @@ +################################################## +# Single-line __all__ definitions (nice 'n' easy!) +################################################## + +__all__ = ["d", "c", "b", "a"] # a comment that is untouched +__all__ += ["foo", "bar", "antipasti"] +__all__ = ("d", "c", "b", "a") + +# Quoting style is retained, +# but unnecessary parens are not +__all__: list = ['b', "c", ((('a')))] +# Trailing commas are also not retained in single-line `__all__` definitions +# (but they are in multiline `__all__` definitions) +__all__: tuple = ("b", "c", "a",) + +if bool(): + __all__ += ("x", "m", "a", "s") +else: + __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + +__all__: list[str] = ["the", "three", "little", "pigs"] + +__all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +__all__.extend(["foo", "bar"]) +__all__.extend(("foo", "bar")) +__all__.extend((((["foo", "bar"])))) + +#################################### +# Neat multiline __all__ definitions +#################################### + +__all__ = ( + "d0", + "c0", # a comment regarding 'c0' + "b0", + # a comment regarding 'a0': + "a0" +) + +__all__ = [ + "d", + "c", # a comment regarding 'c' + "b", + # a comment regarding 'a': + "a" +] + +# we implement an "isort-style sort": +# SCEAMING_CASE constants first, +# then CamelCase classes, +# then anything thats lowercase_snake_case. +# This (which is currently alphabetically sorted) +# should get reordered accordingly: +__all__ = [ + "APRIL", + "AUGUST", + "Calendar", + "DECEMBER", + "Day", + "FEBRUARY", + "FRIDAY", + "HTMLCalendar", + "IllegalMonthError", + "JANUARY", + "JULY", + "JUNE", + "LocaleHTMLCalendar", + "MARCH", + "MAY", + "MONDAY", + "Month", + "NOVEMBER", + "OCTOBER", + "SATURDAY", + "SEPTEMBER", + "SUNDAY", + "THURSDAY", + "TUESDAY", + "TextCalendar", + "WEDNESDAY", + "calendar", + "timegm", + "weekday", + "weekheader"] + +########################################## +# Messier multiline __all__ definitions... +########################################## + +# comment0 +__all__ = ("d", "a", # comment1 + # comment2 + "f", "b", + "strangely", # comment3 + # comment4 + "formatted", + # comment5 +) # comment6 +# comment7 + +__all__ = [ # comment0 + # comment1 + # comment2 + "dx", "cx", "bx", "ax" # comment3 + # comment4 + # comment5 + # comment6 +] # comment7 + +__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", + "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", + "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", + "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", + "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", + "StreamReader", "StreamWriter", + "StreamReaderWriter", "StreamRecoder", + "getencoder", "getdecoder", "getincrementalencoder", + "getincrementaldecoder", "getreader", "getwriter", + "encode", "decode", "iterencode", "iterdecode", + "strict_errors", "ignore_errors", "replace_errors", + "xmlcharrefreplace_errors", + "backslashreplace_errors", "namereplace_errors", + "register_error", "lookup_error"] + +__all__: tuple[str, ...] = ( # a comment about the opening paren + # multiline comment about "bbb" part 1 + # multiline comment about "bbb" part 2 + "bbb", + # multiline comment about "aaa" part 1 + # multiline comment about "aaa" part 2 + "aaa", +) + +# we use natural sort for `__all__`, +# not alphabetical sort. +# Also, this doesn't end with a trailing comma, +# so the autofix shouldn't introduce one: +__all__ = ( + "aadvark237", + "aadvark10092", + "aadvark174", # the very long whitespace span before this comment is retained + "aadvark532" # the even longer whitespace span before this comment is retained +) + +__all__.extend(( # comment0 + # comment about foo + "foo", # comment about foo + # comment about bar + "bar" # comment about bar + # comment1 +)) # comment2 + +__all__.extend( # comment0 + # comment1 + ( # comment2 + # comment about foo + "foo", # comment about foo + # comment about bar + "bar" # comment about bar + # comment3 + ) # comment4 +) # comment2 + +__all__.extend([ # comment0 + # comment about foo + "foo", # comment about foo + # comment about bar + "bar" # comment about bar + # comment1 +]) # comment2 + +__all__.extend( # comment0 + # comment1 + [ # comment2 + # comment about foo + "foo", # comment about foo + # comment about bar + "bar" # comment about bar + # comment3 + ] # comment4 +) # comment2 + +__all__ = ["Style", "Treeview", + # Extensions + "LabeledScale", "OptionMenu", +] + +__all__ = ["Awaitable", "Coroutine", + "AsyncIterable", "AsyncIterator", "AsyncGenerator", + ] + +__all__ = [ + "foo", + "bar", + "baz", + ] + +######################################################################### +# These should be flagged, but not fixed: +# - Parenthesized items in multiline definitions are out of scope +# - The same goes for any `__all__` definitions with concatenated strings +######################################################################### + +__all__ = ( + "look", + ( + "a_veeeeeeeeeeeeeeeeeeery_long_parenthesized_item" + ), +) + +__all__ = ( + "b", + (( + "c" + )), + "a" +) + +__all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") + +################################### +# These should all not get flagged: +################################### + +__all__ = () +__all__ = [] +__all__ = ("single_item",) +__all__ = ( + "single_item_multiline", +) +__all__ = ["single_item",] +__all__ = ["single_item_no_trailing_comma"] +__all__ = [ + "single_item_multiline_no_trailing_comma" +] +__all__ = ("not_a_tuple_just_a_string") +__all__ = ["a", "b", "c", "d"] +__all__ += ["e", "f", "g"] +__all__ = ("a", "b", "c", "d") + +if bool(): + __all__ += ("e", "f", "g") +else: + __all__ += ["alpha", "omega"] + +class IntroducesNonModuleScope: + __all__ = ("b", "a", "e", "d") + __all__ = ["b", "a", "e", "d"] + __all__ += ["foo", "bar", "antipasti"] + __all__.extend(["zebra", "giraffe", "antelope"]) + +__all__ = {"look", "a", "set"} +__all__ = {"very": "strange", "not": "sorted", "we don't": "care"} +["not", "an", "assignment", "just", "an", "expression"] +__all__ = (9, 8, 7) +__all__ = ( # This is just an empty tuple, + # but, + # it's very well +) # documented + +__all__.append("foo") +__all__.extend(["bar", "foo"]) +__all__.extend([ + "bar", # comment0 + "foo" # comment1 +]) +__all__.extend(("bar", "foo")) +__all__.extend( + ( + "bar", + "foo" + ) +) + +# We don't deduplicate elements (yet); +# this just ensures that duplicate elements aren't unnecessarily +# reordered by an autofix: +__all__ = ( + "duplicate_element", # comment1 + "duplicate_element", # comment3 + "duplicate_element", # comment2 + "duplicate_element", # comment0 +) + +__all__ =[ + [] +] +__all__ [ + () +] +__all__ = ( + () +) +__all__ = ( + [] +) +__all__ = ( + (), +) +__all__ = ( + [], +) +__all__ = ( + "foo", [], "bar" +) +__all__ = [ + "foo", (), "bar" +] diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index f8214c6c8924d..7896a2f4f7c82 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -974,6 +974,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::SslInsecureVersion) { flake8_bandit::rules::ssl_insecure_version(checker, call); } + if checker.enabled(Rule::UnsortedDunderAll) { + ruff::rules::sort_dunder_all_extend_call(checker, call); + } } Expr::Dict(dict) => { if checker.any_enabled(&[ diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index c9bbe4539a401..c31f147997663 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1059,12 +1059,15 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pylint::rules::misplaced_bare_raise(checker, raise); } } - Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => { + Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => { if checker.enabled(Rule::GlobalStatement) { if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { pylint::rules::global_statement(checker, id); } } + if checker.enabled(Rule::UnsortedDunderAll) { + ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign); + } } Stmt::If( if_ @ ast::StmtIf { @@ -1452,6 +1455,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.settings.rules.enabled(Rule::TypeBivariance) { pylint::rules::type_bivariance(checker, value); } + if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { + ruff::rules::sort_dunder_all_assign(checker, assign); + } if checker.source_type.is_stub() { if checker.any_enabled(&[ Rule::UnprefixedTypeParam, @@ -1522,6 +1528,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::NonPEP695TypeAlias) { pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt); } + if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { + ruff::rules::sort_dunder_all_ann_assign(checker, assign_stmt); + } if checker.source_type.is_stub() { if let Some(value) = value { if checker.enabled(Rule::AssignmentDefaultInStub) { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 6812870a13a32..013b5cf3a8fb7 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -923,6 +923,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck), (Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion), (Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators), + (Ruff, "022") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderAll), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 5e5731ca9a4e9..d4eddf142ed95 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -42,6 +42,7 @@ mod tests { #[test_case(Rule::UnnecessaryKeyCheck, Path::new("RUF019.py"))] #[test_case(Rule::NeverUnion, Path::new("RUF020.py"))] #[test_case(Rule::ParenthesizeChainedOperators, Path::new("RUF021.py"))] + #[test_case(Rule::UnsortedDunderAll, Path::new("RUF022.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 1a89cb6a71fac..07f55e184b1ad 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -13,6 +13,7 @@ pub(crate) use never_union::*; pub(crate) use pairwise_over_zipped::*; pub(crate) use parenthesize_logical_operators::*; pub(crate) use quadratic_list_summation::*; +pub(crate) use sort_dunder_all::*; pub(crate) use static_key_dict_comprehension::*; pub(crate) use unnecessary_iterable_allocation_for_first_element::*; pub(crate) use unnecessary_key_check::*; @@ -34,6 +35,7 @@ mod mutable_dataclass_default; mod never_union; mod pairwise_over_zipped; mod parenthesize_logical_operators; +mod sort_dunder_all; mod static_key_dict_comprehension; mod unnecessary_iterable_allocation_for_first_element; mod unnecessary_key_check; diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs new file mode 100644 index 0000000000000..788b300dd56cc --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -0,0 +1,1019 @@ +use std::borrow::Cow; +use std::cmp::Ordering; + +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast as ast; +use ruff_python_codegen::Stylist; +use ruff_python_parser::{lexer, Mode, Tok}; +use ruff_python_stdlib::str::is_cased_uppercase; +use ruff_python_trivia::leading_indentation; +use ruff_source_file::Locator; +use ruff_text_size::{Ranged, TextRange, TextSize}; + +use crate::checkers::ast::Checker; + +use is_macro; +use itertools::Itertools; +use natord; + +/// ## What it does +/// Checks for `__all__` definitions that are not ordered +/// according to an "isort-style" sort. +/// +/// An isort-style sort sorts items first according to their casing: +/// SCREAMING_SNAKE_CASE names (conventionally used for global constants) +/// come first, followed by CamelCase names (conventionally used for +/// classes), followed by anything else. Within each category, +/// a [natural sort](https://en.wikipedia.org/wiki/Natural_sort_order) +/// is used to order the elements. +/// +/// ## Why is this bad? +/// Consistency is good. Use a common convention for `__all__` to make your +/// code more readable and idiomatic. +/// +/// ## Example +/// ```python +/// import sys +/// +/// __all__ = [ +/// "b", +/// "c", +/// "a", +/// ] +/// +/// if sys.platform == "win32": +/// __all__ += ["z", "y"] +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// __all__ = [ +/// "a", +/// "b", +/// "c", +/// ] +/// +/// if sys.platform == "win32": +/// __all__ += ["y", "z"] +/// ``` +/// +/// ## Fix safety +/// This rule's fix is marked as always being safe, in that +/// it should never alter the semantics of any Python code. +/// However, note that for multiline `__all__` definitions +/// that include comments on their own line, it can be hard +/// to tell where the comments should be moved to when sorting +/// the contents of `__all__`. While this rule's fix will +/// never delete a comment, it might *sometimes* move a +/// comment to an unexpected location. +#[violation] +pub struct UnsortedDunderAll; + +impl Violation for UnsortedDunderAll { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + format!("`__all__` is not sorted") + } + + fn fix_title(&self) -> Option { + Some("Apply an isort-style sorting to `__all__`".to_string()) + } +} + +/// Sort an `__all__` definition represented by a `StmtAssign` AST node. +/// For example: `__all__ = ["b", "c", "a"]`. +pub(crate) fn sort_dunder_all_assign( + checker: &mut Checker, + ast::StmtAssign { value, targets, .. }: &ast::StmtAssign, +) { + if let [expr] = targets.as_slice() { + sort_dunder_all(checker, expr, value); + } +} + +/// Sort an `__all__` mutation represented by a `StmtAugAssign` AST node. +/// For example: `__all__ += ["b", "c", "a"]`. +pub(crate) fn sort_dunder_all_aug_assign(checker: &mut Checker, node: &ast::StmtAugAssign) { + if node.op.is_add() { + sort_dunder_all(checker, &node.target, &node.value); + } +} + +/// Sort a tuple or list passed to `__all__.extend()`. +pub(crate) fn sort_dunder_all_extend_call( + checker: &mut Checker, + ast::ExprCall { + func, + arguments: ast::Arguments { args, keywords, .. }, + .. + }: &ast::ExprCall, +) { + let ([value_passed], []) = (args.as_slice(), keywords.as_slice()) else { + return; + }; + let ast::Expr::Attribute(ast::ExprAttribute { + ref value, + ref attr, + .. + }) = **func + else { + return; + }; + if attr == "extend" { + sort_dunder_all(checker, value, value_passed); + } +} + +/// Sort an `__all__` definition represented by a `StmtAnnAssign` AST node. +/// For example: `__all__: list[str] = ["b", "c", "a"]`. +pub(crate) fn sort_dunder_all_ann_assign(checker: &mut Checker, node: &ast::StmtAnnAssign) { + if let Some(value) = &node.value { + sort_dunder_all(checker, &node.target, value); + } +} + +/// Sort a tuple or list that defines or mutates the global variable `__all__`. +/// +/// This routine checks whether the tuple or list is sorted, and emits a +/// violation if it is not sorted. If the tuple/list was not sorted, +/// it attempts to set a `Fix` on the violation. +fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) { + let ast::Expr::Name(ast::ExprName { id, .. }) = target else { + return; + }; + + if id != "__all__" { + return; + } + + // We're only interested in `__all__` in the global scope + if !checker.semantic().current_scope().kind.is_module() { + return; + } + + let (elts, range, kind) = match node { + ast::Expr::List(ast::ExprList { elts, range, .. }) => (elts, *range, DunderAllKind::List), + ast::Expr::Tuple(tuple_node @ ast::ExprTuple { elts, range, .. }) => { + (elts, *range, DunderAllKind::Tuple(tuple_node)) + } + _ => return, + }; + + let elts_analysis = DunderAllSortClassification::from_elements(elts); + if elts_analysis.is_not_a_list_of_string_literals() || elts_analysis.is_sorted() { + return; + } + + let mut diagnostic = Diagnostic::new(UnsortedDunderAll, range); + + if let DunderAllSortClassification::UnsortedAndMaybeFixable { items } = elts_analysis { + if let Some(fix) = create_fix(range, elts, &items, &kind, checker) { + diagnostic.set_fix(fix); + } + } + + checker.diagnostics.push(diagnostic); +} + +/// An enumeration of the two valid ways of defining +/// `__all__`: as a list, or as a tuple. +/// +/// Whereas lists are always parenthesized +/// (they always start with `[` and end with `]`), +/// single-line tuples *can* be unparenthesized. +/// We keep the original AST node around for the +/// Tuple variant so that this can be queried later. +#[derive(Debug)] +enum DunderAllKind<'a> { + List, + Tuple(&'a ast::ExprTuple), +} + +impl DunderAllKind<'_> { + fn is_parenthesized(&self, source: &str) -> bool { + match self { + Self::List => true, + Self::Tuple(ast_node) => ast_node.is_parenthesized(source), + } + } + + fn opening_token_for_multiline_definition(&self) -> Tok { + match self { + Self::List => Tok::Lsqb, + Self::Tuple(_) => Tok::Lpar, + } + } + + fn closing_token_for_multiline_definition(&self) -> Tok { + match self { + Self::List => Tok::Rsqb, + Self::Tuple(_) => Tok::Rpar, + } + } +} + +/// An enumeration of the possible conclusions we could come to +/// regarding the ordering of the elements in an `__all__` definition: +/// +/// 1. `__all__` is a list of string literals that is already sorted +/// 2. `__all__` is an unsorted list of string literals, +/// but we wouldn't be able to autofix it +/// 3. `__all__` is an unsorted list of string literals, +/// and it's possible we could generate a fix for it +/// 4. `__all__` contains one or more items that are not string +/// literals. +/// +/// ("Sorted" here means "ordered according to an isort-style sort". +/// See the module-level docs for a definition of "isort-style sort.") +#[derive(Debug, is_macro::Is)] +enum DunderAllSortClassification<'a> { + Sorted, + UnsortedButUnfixable, + UnsortedAndMaybeFixable { items: Vec<&'a str> }, + NotAListOfStringLiterals, +} + +impl<'a> DunderAllSortClassification<'a> { + fn from_elements(elements: &'a [ast::Expr]) -> Self { + let Some((first, rest @ [_, ..])) = elements.split_first() else { + return Self::Sorted; + }; + let Some(string_node) = first.as_string_literal_expr() else { + return Self::NotAListOfStringLiterals; + }; + let mut this = string_node.value.to_str(); + + for expr in rest { + let Some(string_node) = expr.as_string_literal_expr() else { + return Self::NotAListOfStringLiterals; + }; + let next = string_node.value.to_str(); + if AllItemSortKey::from(next) < AllItemSortKey::from(this) { + let mut items = Vec::with_capacity(elements.len()); + for expr in elements { + let Some(string_node) = expr.as_string_literal_expr() else { + return Self::NotAListOfStringLiterals; + }; + if string_node.value.is_implicit_concatenated() { + return Self::UnsortedButUnfixable; + } + items.push(string_node.value.to_str()); + } + return Self::UnsortedAndMaybeFixable { items }; + } + this = next; + } + Self::Sorted + } +} + +/// A struct to implement logic necessary to achieve +/// an "isort-style sort". +/// +/// See the docs for this module as a whole for the +/// definition we use here of an "isort-style sort". +struct AllItemSortKey<'a> { + category: InferredMemberType, + value: &'a str, +} + +impl Ord for AllItemSortKey<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.category + .cmp(&other.category) + .then_with(|| natord::compare(self.value, other.value)) + } +} + +impl PartialOrd for AllItemSortKey<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for AllItemSortKey<'_> { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl Eq for AllItemSortKey<'_> {} + +impl<'a> From<&'a str> for AllItemSortKey<'a> { + fn from(value: &'a str) -> Self { + Self { + category: InferredMemberType::of(value), + value, + } + } +} + +impl<'a> From<&'a DunderAllItem> for AllItemSortKey<'a> { + fn from(item: &'a DunderAllItem) -> Self { + Self::from(item.value.as_str()) + } +} + +/// Classification for an element in `__all__`. +/// +/// This is necessary to achieve an "isort-style" sort, +/// where elements are sorted first by category, +/// then, within categories, are sorted according +/// to a natural sort. +/// +/// You'll notice that a very similar enum exists +/// in ruff's reimplementation of isort. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)] +enum InferredMemberType { + Constant, + Class, + Other, +} + +impl InferredMemberType { + fn of(value: &str) -> Self { + // E.g. `CONSTANT` + if value.len() > 1 && is_cased_uppercase(value) { + Self::Constant + // E.g. `Class` + } else if value.starts_with(char::is_uppercase) { + Self::Class + // E.g. `some_variable` or `some_function` + } else { + Self::Other + } + } +} + +/// Attempt to return `Some(fix)`, where `fix` is a `Fix` +/// that can be set on the diagnostic to sort the user's +/// `__all__` definition +/// +/// Return `None` if it's a multiline `__all__` definition +/// and the token-based analysis in +/// `MultilineDunderAllValue::from_source_range()` encounters +/// something it doesn't expect, meaning the violation +/// is unfixable in this instance. +fn create_fix( + range: TextRange, + elts: &[ast::Expr], + string_items: &[&str], + kind: &DunderAllKind, + checker: &Checker, +) -> Option { + let locator = checker.locator(); + let is_multiline = locator.contains_line_break(range); + + let sorted_source_code = { + // The machinery in the `MultilineDunderAllValue` is actually + // sophisticated enough that it would work just as well for + // single-line `__all__` definitions, and we could reduce + // the number of lines of code in this file by doing that. + // Unfortunately, however, `MultilineDunderAllValue::from_source_range()` + // must process every token in an `__all__` definition as + // part of its analysis, and this is quite slow. For + // single-line `__all__` definitions, it's also unnecessary, + // as it's impossible to have comments in between the + // `__all__` elements if the `__all__` definition is all on + // a single line. Therefore, as an optimisation, we do the + // bare minimum of token-processing for single-line `__all__` + // definitions: + if is_multiline { + let value = MultilineDunderAllValue::from_source_range(range, kind, locator)?; + assert_eq!(value.items.len(), elts.len()); + value.into_sorted_source_code(locator, checker.stylist()) + } else { + sort_single_line_dunder_all(elts, string_items, kind, locator) + } + }; + + Some(Fix::safe_edit(Edit::range_replacement( + sorted_source_code, + range, + ))) +} + +/// An instance of this struct encapsulates an analysis +/// of a multiline Python tuple/list that represents an +/// `__all__` definition or augmentation. +struct MultilineDunderAllValue { + items: Vec, + range: TextRange, + ends_with_trailing_comma: bool, +} + +impl MultilineDunderAllValue { + /// Analyse the source range for a multiline Python tuple/list that + /// represents an `__all__` definition or augmentation. Return `None` + /// if the analysis fails for whatever reason. + fn from_source_range( + range: TextRange, + kind: &DunderAllKind, + locator: &Locator, + ) -> Option { + // Parse the multiline `__all__` definition using the raw tokens. + // See the docs for `collect_dunder_all_lines()` for why we have to + // use the raw tokens, rather than just the AST, to do this parsing. + // + // Step (1). Start by collecting information on each line individually: + let (lines, ends_with_trailing_comma) = collect_dunder_all_lines(range, kind, locator)?; + + // Step (2). Group lines together into sortable "items": + // - Any "item" contains a single element of the `__all__` list/tuple + // - Assume that any comments on their own line are meant to be grouped + // with the element immediately below them: if the element moves, + // the comments above the element move with it. + // - The same goes for any comments on the same line as an element: + // if the element moves, the comment moves with it. + let items = collect_dunder_all_items(lines, range, locator); + + Some(MultilineDunderAllValue { + items, + range, + ends_with_trailing_comma, + }) + } + + /// Sort a multiline `__all__` definition + /// that is known to be unsorted. + /// + /// Panics if this is called and `self.items` + /// has length < 2. It's redundant to call this method in this case, + /// since lists with < 2 items cannot be unsorted, + /// so this is a logic error. + fn into_sorted_source_code(mut self, locator: &Locator, stylist: &Stylist) -> String { + let (first_item_start, last_item_end) = match self.items.as_slice() { + [first_item, .., last_item] => (first_item.start(), last_item.end()), + _ => panic!( + "We shouldn't be attempting an autofix if `__all__` has < 2 elements; + an `__all__` definition with 1 or 0 elements cannot be unsorted." + ), + }; + + // As well as the "items" in the `__all__` definition, + // there is also a "prelude" and a "postlude": + // - Prelude == the region of source code from the opening parenthesis, + // up to the start of the first item in `__all__`. + // - Postlude == the region of source code from the end of the last + // item in `__all__` up to and including the closing parenthesis. + // + // For example: + // + // ```python + // __all__ = [ # comment0 + // # comment1 + // "first item", + // "last item" # comment2 + // # comment3 + // ] # comment4 + // ``` + // + // - The prelude in the above example is the source code region + // starting just before the opening `[` and ending just after `# comment0`. + // `comment0` here counts as part of the prelude because it is on + // the same line as the opening paren, and because we haven't encountered + // any elements of `__all__` yet, but `comment1` counts as part of the first item, + // as it's on its own line, and all comments on their own line are grouped + // with the next element below them to make "items", + // (an "item" being a region of source code that all moves as one unit + // when `__all__` is sorted). + // - The postlude in the above example is the source code region starting + // just after `# comment2` and ending just after the closing paren. + // `# comment2` is part of the last item, as it's an inline comment on the + // same line as an element, but `# comment3` becomes part of the postlude + // because there are no items below it. `# comment4` is not part of the + // postlude: it's outside of the source-code range considered by this rule, + // and should therefore be untouched. + // + let newline = stylist.line_ending().as_str(); + let start_offset = self.start(); + let leading_indent = leading_indentation(locator.full_line(start_offset)); + let item_indent = format!("{}{}", leading_indent, stylist.indentation().as_str()); + + let prelude = + multiline_dunder_all_prelude(first_item_start, newline, start_offset, locator); + let postlude = multiline_dunder_all_postlude( + last_item_end, + newline, + leading_indent, + &item_indent, + self.end(), + locator, + ); + + self.items + .sort_by(|this, next| AllItemSortKey::from(this).cmp(&AllItemSortKey::from(next))); + let joined_items = join_multiline_dunder_all_items( + &self.items, + locator, + &item_indent, + newline, + self.ends_with_trailing_comma, + ); + + format!("{prelude}{joined_items}{postlude}") + } +} + +impl Ranged for MultilineDunderAllValue { + fn range(&self) -> TextRange { + self.range + } +} + +/// Collect data on each line of a multiline `__all__` definition. +/// Return `None` if `__all__` appears to be invalid, +/// or if it's an edge case we don't support. +/// +/// Why do we need to do this using the raw tokens, +/// when we already have the AST? The AST strips out +/// crucial information that we need to track here for +/// a multiline `__all__` definition, such as: +/// - The value of comments +/// - The amount of whitespace between the end of a line +/// and an inline comment +/// - Whether or not the final item in the tuple/list has a +/// trailing comma +/// +/// All of this information is necessary to have at a later +/// stage if we're to sort items without doing unnecessary +/// brutality to the comments and pre-existing style choices +/// in the original source code. +fn collect_dunder_all_lines( + range: TextRange, + kind: &DunderAllKind, + locator: &Locator, +) -> Option<(Vec, bool)> { + // These first two variables are used for keeping track of state + // regarding the entirety of the `__all__` definition... + let mut ends_with_trailing_comma = false; + let mut lines = vec![]; + // ... all state regarding a single line of an `__all__` definition + // is encapsulated in this variable + let mut line_state = LineState::default(); + + // `lex_starts_at()` gives us absolute ranges rather than relative ranges, + // but (surprisingly) we still need to pass in the slice of code we want it to lex, + // rather than the whole source file: + let mut token_iter = + lexer::lex_starts_at(locator.slice(range), Mode::Expression, range.start()); + let (first_tok, _) = token_iter.next()?.ok()?; + if first_tok != kind.opening_token_for_multiline_definition() { + return None; + } + let expected_final_token = kind.closing_token_for_multiline_definition(); + + for pair in token_iter { + let (tok, subrange) = pair.ok()?; + match tok { + Tok::NonLogicalNewline => { + lines.push(line_state.into_dunder_all_line()); + line_state = LineState::default(); + } + Tok::Comment(_) => { + line_state.visit_comment_token(subrange); + } + Tok::String { value, .. } => { + line_state.visit_string_token(value, subrange); + ends_with_trailing_comma = false; + } + Tok::Comma => { + line_state.visit_comma_token(subrange); + ends_with_trailing_comma = true; + } + tok if tok == expected_final_token => { + lines.push(line_state.into_dunder_all_line()); + break; + } + _ => return None, + } + } + Some((lines, ends_with_trailing_comma)) +} + +/// This struct is for keeping track of state +/// regarding a single line in a multiline `__all__` definition. +/// It is purely internal to `collect_dunder_all_lines()`, +/// and should not be used outside that function. +/// +/// There are three possible kinds of line in a multiline +/// `__all__` definition, and we don't know what kind of a line +/// we're in until all tokens in that line have been processed: +/// +/// - A line with just a comment (`DunderAllLine::JustAComment)`) +/// - A line with one or more string items in it (`DunderAllLine::OneOrMoreItems`) +/// - An empty line (`DunderAllLine::Empty`) +/// +/// As we process the tokens in a single line, +/// this struct accumulates the necessary state for us +/// to be able to determine what kind of a line we're in. +/// Once the entire line has been processed, `into_dunder_all_line()` +/// is called, which consumes `self` and produces the +/// classification for the line. +#[derive(Debug, Default)] +struct LineState { + first_item_in_line: Option<(String, TextRange)>, + following_items_in_line: Vec<(String, TextRange)>, + comment_range_start: Option, + comment_in_line: Option, +} + +impl LineState { + fn visit_string_token(&mut self, token_value: String, token_range: TextRange) { + if self.first_item_in_line.is_none() { + self.first_item_in_line = Some((token_value, token_range)); + } else { + self.following_items_in_line + .push((token_value, token_range)); + } + self.comment_range_start = Some(token_range.end()); + } + + fn visit_comma_token(&mut self, token_range: TextRange) { + self.comment_range_start = Some(token_range.end()); + } + + /// If this is a comment on its own line, + /// record the range of that comment. + /// + /// *If*, however, we've already seen a comma + /// or a string in this line, that means that we're + /// in a line with items. In that case, we want to + /// record the range of the comment, *plus* the whitespace + /// (if any) preceding the comment. This is so that we don't + /// unnecessarily apply opinionated formatting changes + /// where they might not be welcome. + fn visit_comment_token(&mut self, token_range: TextRange) { + self.comment_in_line = { + if let Some(comment_range_start) = self.comment_range_start { + Some(TextRange::new(comment_range_start, token_range.end())) + } else { + Some(token_range) + } + } + } + + fn into_dunder_all_line(self) -> DunderAllLine { + if let Some(first_item) = self.first_item_in_line { + DunderAllLine::OneOrMoreItems(LineWithItems { + first_item, + following_items: self.following_items_in_line, + trailing_comment_range: self.comment_in_line, + }) + } else { + self.comment_in_line + .map_or(DunderAllLine::Empty, |comment_range| { + DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) + }) + } + } +} + +/// Instances of this struct represent source-code lines in the middle +/// of multiline `__all__` tuples/lists where the line contains +/// 0 elements of the tuple/list, but the line does have a comment in it. +#[derive(Debug)] +struct LineWithJustAComment(TextRange); + +/// Instances of this struct represent source-code lines in single-line +/// or multiline `__all__` tuples/lists where the line contains at least +/// 1 element of the tuple/list. The line may contain > 1 element of the +/// tuple/list, and may also have a trailing comment after the element(s). +#[derive(Debug)] +struct LineWithItems { + // For elements in the list, we keep track of the value of the + // value of the element as well as the source-code range of the element. + // (We need to know the actual value so that we can sort the items.) + first_item: (String, TextRange), + following_items: Vec<(String, TextRange)>, + // For comments, we only need to keep track of the source-code range. + trailing_comment_range: Option, +} + +impl LineWithItems { + fn num_items(&self) -> usize { + self.following_items.len() + 1 + } +} + +/// An enumeration of the possible kinds of source-code lines +/// that can exist in a multiline `__all__` tuple or list: +/// +/// - A line that has no string elements, but does have a comment. +/// - A line that has one or more string elements, +/// and may also have a trailing comment. +/// - An entirely empty line. +#[derive(Debug)] +enum DunderAllLine { + JustAComment(LineWithJustAComment), + OneOrMoreItems(LineWithItems), + Empty, +} + +/// Given data on each line in a multiline `__all__` definition, +/// group lines together into "items". +/// +/// Each item contains exactly one string element, +/// but might contain multiple comments attached to that element +/// that must move with the element when `__all__` is sorted. +/// +/// Note that any comments following the last item are discarded here, +/// but that doesn't matter: we add them back in `into_sorted_source_code()` +/// as part of the `postlude` (see comments in that function) +fn collect_dunder_all_items( + lines: Vec, + dunder_all_range: TextRange, + locator: &Locator, +) -> Vec { + let mut all_items = Vec::with_capacity(match lines.as_slice() { + [DunderAllLine::OneOrMoreItems(single)] => single.num_items(), + _ => lines.len(), + }); + let mut first_item_encountered = false; + let mut preceding_comment_ranges = vec![]; + for line in lines { + match line { + DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) => { + // Comments on the same line as the opening paren and before any elements + // count as part of the "prelude"; these are not grouped into any item... + if first_item_encountered + || locator.line_start(comment_range.start()) + != locator.line_start(dunder_all_range.start()) + { + // ...but for all other comments that precede an element, + // group the comment with the element following that comment + // into an "item", so that the comment moves as one with the element + // when the `__all__` list/tuple is sorted + preceding_comment_ranges.push(comment_range); + } + } + DunderAllLine::OneOrMoreItems(LineWithItems { + first_item: (first_val, first_range), + following_items, + trailing_comment_range: comment_range, + }) => { + first_item_encountered = true; + all_items.push(DunderAllItem::new( + first_val, + std::mem::take(&mut preceding_comment_ranges), + first_range, + comment_range, + )); + for (value, range) in following_items { + all_items.push(DunderAllItem::with_no_comments(value, range)); + } + } + DunderAllLine::Empty => continue, // discard empty lines + } + } + all_items +} + +/// An instance of this struct represents a single element +/// from a multiline `__all__` tuple/list, *and* any comments that +/// are "attached" to it. The comments "attached" to the element +/// will move with the element when the `__all__` tuple/list is sorted. +/// +/// Comments on their own line immediately preceding the element will +/// always form a contiguous range with the range of the element itself; +/// however, inline comments won't necessary form a contiguous range. +/// Consider the following scenario, where both `# comment0` and `# comment1` +/// will move with the "a" element when the list is sorted: +/// +/// ```python +/// __all__ = [ +/// "b", +/// # comment0 +/// "a", "c", # comment1 +/// ] +/// ``` +/// +/// The desired outcome here is: +/// +/// ```python +/// __all__ = [ +/// # comment0 +/// "a", # comment1 +/// "b", +/// "c", +/// ] +/// ``` +/// +/// To achieve this, both `# comment0` and `# comment1` +/// are grouped into the `DunderAllItem` instance +/// where the value is `"a"`, even though the source-code range +/// of `# comment1` does not form a contiguous range with the +/// source-code range of `"a"`. +#[derive(Debug)] +struct DunderAllItem { + value: String, + preceding_comment_ranges: Vec, + element_range: TextRange, + // total_range incorporates the ranges of preceding comments + // (which must be contiguous with the element), + // but doesn't incorporate any trailing comments + // (which might be contiguous, but also might not be) + total_range: TextRange, + end_of_line_comments: Option, +} + +impl DunderAllItem { + fn new( + value: String, + preceding_comment_ranges: Vec, + element_range: TextRange, + end_of_line_comments: Option, + ) -> Self { + let total_range = { + if let Some(first_comment_range) = preceding_comment_ranges.first() { + TextRange::new(first_comment_range.start(), element_range.end()) + } else { + element_range + } + }; + Self { + value, + preceding_comment_ranges, + element_range, + total_range, + end_of_line_comments, + } + } + + fn with_no_comments(value: String, element_range: TextRange) -> Self { + Self::new(value, vec![], element_range, None) + } +} + +impl Ranged for DunderAllItem { + fn range(&self) -> TextRange { + self.total_range + } +} + +/// Return a string representing the "prelude" for a +/// multiline `__all__` definition. +/// +/// See inline comments in +/// `MultilineDunderAllValue::into_sorted_source_code()` +/// for a definition of the term "prelude" in this context. +fn multiline_dunder_all_prelude<'a>( + first_item_start_offset: TextSize, + newline: &str, + dunder_all_offset: TextSize, + locator: &'a Locator, +) -> Cow<'a, str> { + let prelude_end = { + let first_item_line_offset = locator.line_start(first_item_start_offset); + if first_item_line_offset == locator.line_start(dunder_all_offset) { + first_item_start_offset + } else { + first_item_line_offset + } + }; + let prelude = locator.slice(TextRange::new(dunder_all_offset, prelude_end)); + if prelude.ends_with(['\r', '\n']) { + Cow::Borrowed(prelude) + } else { + Cow::Owned(format!("{}{}", prelude.trim_end(), newline)) + } +} + +/// Join the elements and comments of a multiline `__all__` +/// definition into a single string. +/// +/// The resulting string does not include the "prelude" or +/// "postlude" of the `__all__` definition. +/// (See inline comments in `MultilineDunderAllValue::into_sorted_source_code()` +/// for definitions of the terms "prelude" and "postlude" +/// in this context.) +fn join_multiline_dunder_all_items( + sorted_items: &[DunderAllItem], + locator: &Locator, + item_indent: &str, + newline: &str, + needs_trailing_comma: bool, +) -> String { + let last_item_index = sorted_items.len() - 1; + + let mut new_dunder_all = String::new(); + for (i, item) in sorted_items.iter().enumerate() { + let is_final_item = i == last_item_index; + for comment_range in &item.preceding_comment_ranges { + new_dunder_all.push_str(item_indent); + new_dunder_all.push_str(locator.slice(comment_range)); + new_dunder_all.push_str(newline); + } + new_dunder_all.push_str(item_indent); + new_dunder_all.push_str(locator.slice(item.element_range)); + if !is_final_item || needs_trailing_comma { + new_dunder_all.push(','); + } + if let Some(trailing_comments) = item.end_of_line_comments { + new_dunder_all.push_str(locator.slice(trailing_comments)); + } + if !is_final_item { + new_dunder_all.push_str(newline); + } + } + new_dunder_all +} + +/// Return a string representing the "postlude" for a +/// multiline `__all__` definition. +/// +/// See inline comments in +/// `MultilineDunderAllValue::into_sorted_source_code()` +/// for a definition of the term "postlude" in this context. +fn multiline_dunder_all_postlude<'a>( + last_item_end_offset: TextSize, + newline: &str, + leading_indent: &str, + item_indent: &str, + dunder_all_range_end: TextSize, + locator: &'a Locator, +) -> Cow<'a, str> { + let postlude_start = { + let last_item_line_offset = locator.line_end(last_item_end_offset); + if last_item_line_offset == locator.line_end(dunder_all_range_end) { + last_item_end_offset + } else { + last_item_line_offset + } + }; + let postlude = locator.slice(TextRange::new(postlude_start, dunder_all_range_end)); + + // The rest of this function uses heuristics to + // avoid very long indents for the closing paren + // that don't match the style for the rest of the + // new `__all__` definition. + // + // For example, we want to avoid something like this + // (not uncommon in code that hasn't been + // autoformatted)... + // + // ```python + // __all__ = ["xxxxxx", "yyyyyy", + // "aaaaaa", "bbbbbb", + // ] + // ``` + // + // ...getting autofixed to this: + // + // ```python + // __all__ = [ + // "a", + // "b", + // "x", + // "y", + // ] + // ``` + let newline_chars = ['\r', '\n']; + if !postlude.starts_with(newline_chars) { + return Cow::Borrowed(postlude); + } + if TextSize::of(leading_indentation( + postlude.trim_start_matches(newline_chars), + )) <= TextSize::of(item_indent) + { + return Cow::Borrowed(postlude); + } + let trimmed_postlude = postlude.trim_start(); + if trimmed_postlude.starts_with([']', ')']) { + return Cow::Owned(format!("{newline}{leading_indent}{trimmed_postlude}")); + } + Cow::Borrowed(postlude) +} + +/// Create a string representing a fixed-up single-line +/// `__all__` definition, that can be inserted into the +/// source code as a `range_replacement` autofix. +fn sort_single_line_dunder_all( + elts: &[ast::Expr], + elements: &[&str], + kind: &DunderAllKind, + locator: &Locator, +) -> String { + // We grab the original source-code ranges using `locator.slice()` + // rather than using the expression generator, as this approach allows + // us to easily preserve stylistic choices in the original source code + // such as whether double or single quotes were used. + let mut element_pairs = elts.iter().zip(elements).collect_vec(); + element_pairs.sort_by_key(|(_, elem)| AllItemSortKey::from(**elem)); + let joined_items = element_pairs + .iter() + .map(|(elt, _)| locator.slice(elt)) + .join(", "); + match kind { + DunderAllKind::List => format!("[{joined_items}]"), + DunderAllKind::Tuple(_) if kind.is_parenthesized(locator.contents()) => { + format!("({joined_items})") + } + DunderAllKind::Tuple(_) => joined_items, + } +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap new file mode 100644 index 0000000000000..f1ff9af1d6b67 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -0,0 +1,903 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF022.py:5:11: RUF022 [*] `__all__` is not sorted + | +3 | ################################################## +4 | +5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched + | ^^^^^^^^^^^^^^^^^^^^ RUF022 +6 | __all__ += ["foo", "bar", "antipasti"] +7 | __all__ = ("d", "c", "b", "a") + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +2 2 | # Single-line __all__ definitions (nice 'n' easy!) +3 3 | ################################################## +4 4 | +5 |-__all__ = ["d", "c", "b", "a"] # a comment that is untouched + 5 |+__all__ = ["a", "b", "c", "d"] # a comment that is untouched +6 6 | __all__ += ["foo", "bar", "antipasti"] +7 7 | __all__ = ("d", "c", "b", "a") +8 8 | + +RUF022.py:6:12: RUF022 [*] `__all__` is not sorted + | +5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +6 | __all__ += ["foo", "bar", "antipasti"] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 +7 | __all__ = ("d", "c", "b", "a") + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +3 3 | ################################################## +4 4 | +5 5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +6 |-__all__ += ["foo", "bar", "antipasti"] + 6 |+__all__ += ["antipasti", "bar", "foo"] +7 7 | __all__ = ("d", "c", "b", "a") +8 8 | +9 9 | # Quoting style is retained, + +RUF022.py:7:11: RUF022 [*] `__all__` is not sorted + | +5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +6 | __all__ += ["foo", "bar", "antipasti"] +7 | __all__ = ("d", "c", "b", "a") + | ^^^^^^^^^^^^^^^^^^^^ RUF022 +8 | +9 | # Quoting style is retained, + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +4 4 | +5 5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +6 6 | __all__ += ["foo", "bar", "antipasti"] +7 |-__all__ = ("d", "c", "b", "a") + 7 |+__all__ = ("a", "b", "c", "d") +8 8 | +9 9 | # Quoting style is retained, +10 10 | # but unnecessary parens are not + +RUF022.py:11:17: RUF022 [*] `__all__` is not sorted + | + 9 | # Quoting style is retained, +10 | # but unnecessary parens are not +11 | __all__: list = ['b', "c", ((('a')))] + | ^^^^^^^^^^^^^^^^^^^^^ RUF022 +12 | # Trailing commas are also not retained in single-line `__all__` definitions +13 | # (but they are in multiline `__all__` definitions) + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +8 8 | +9 9 | # Quoting style is retained, +10 10 | # but unnecessary parens are not +11 |-__all__: list = ['b', "c", ((('a')))] + 11 |+__all__: list = ['a', 'b', "c"] +12 12 | # Trailing commas are also not retained in single-line `__all__` definitions +13 13 | # (but they are in multiline `__all__` definitions) +14 14 | __all__: tuple = ("b", "c", "a",) + +RUF022.py:14:18: RUF022 [*] `__all__` is not sorted + | +12 | # Trailing commas are also not retained in single-line `__all__` definitions +13 | # (but they are in multiline `__all__` definitions) +14 | __all__: tuple = ("b", "c", "a",) + | ^^^^^^^^^^^^^^^^ RUF022 +15 | +16 | if bool(): + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +11 11 | __all__: list = ['b', "c", ((('a')))] +12 12 | # Trailing commas are also not retained in single-line `__all__` definitions +13 13 | # (but they are in multiline `__all__` definitions) +14 |-__all__: tuple = ("b", "c", "a",) + 14 |+__all__: tuple = ("a", "b", "c") +15 15 | +16 16 | if bool(): +17 17 | __all__ += ("x", "m", "a", "s") + +RUF022.py:17:16: RUF022 [*] `__all__` is not sorted + | +16 | if bool(): +17 | __all__ += ("x", "m", "a", "s") + | ^^^^^^^^^^^^^^^^^^^^ RUF022 +18 | else: +19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +14 14 | __all__: tuple = ("b", "c", "a",) +15 15 | +16 16 | if bool(): +17 |- __all__ += ("x", "m", "a", "s") + 17 |+ __all__ += ("a", "m", "s", "x") +18 18 | else: +19 19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +20 20 | + +RUF022.py:19:16: RUF022 [*] `__all__` is not sorted + | +17 | __all__ += ("x", "m", "a", "s") +18 | else: +19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + | ^^^^^^^^^^^^^^^^^^^^^^ RUF022 +20 | +21 | __all__: list[str] = ["the", "three", "little", "pigs"] + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +16 16 | if bool(): +17 17 | __all__ += ("x", "m", "a", "s") +18 18 | else: +19 |- __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + 19 |+ __all__ += "foo1", "foo2", "foo3" # NB: an implicit tuple (without parens) +20 20 | +21 21 | __all__: list[str] = ["the", "three", "little", "pigs"] +22 22 | + +RUF022.py:21:22: RUF022 [*] `__all__` is not sorted + | +19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +20 | +21 | __all__: list[str] = ["the", "three", "little", "pigs"] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 +22 | +23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +18 18 | else: +19 19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +20 20 | +21 |-__all__: list[str] = ["the", "three", "little", "pigs"] + 21 |+__all__: list[str] = ["little", "pigs", "the", "three"] +22 22 | +23 23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 24 | __all__.extend(["foo", "bar"]) + +RUF022.py:23:11: RUF022 [*] `__all__` is not sorted + | +21 | __all__: list[str] = ["the", "three", "little", "pigs"] +22 | +23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 +24 | __all__.extend(["foo", "bar"]) +25 | __all__.extend(("foo", "bar")) + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +20 20 | +21 21 | __all__: list[str] = ["the", "three", "little", "pigs"] +22 22 | +23 |-__all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") + 23 |+__all__ = "an_unparenthesized_tuple", "in", "parenthesized_item" +24 24 | __all__.extend(["foo", "bar"]) +25 25 | __all__.extend(("foo", "bar")) +26 26 | __all__.extend((((["foo", "bar"])))) + +RUF022.py:24:16: RUF022 [*] `__all__` is not sorted + | +23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 | __all__.extend(["foo", "bar"]) + | ^^^^^^^^^^^^^^ RUF022 +25 | __all__.extend(("foo", "bar")) +26 | __all__.extend((((["foo", "bar"])))) + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +21 21 | __all__: list[str] = ["the", "three", "little", "pigs"] +22 22 | +23 23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 |-__all__.extend(["foo", "bar"]) + 24 |+__all__.extend(["bar", "foo"]) +25 25 | __all__.extend(("foo", "bar")) +26 26 | __all__.extend((((["foo", "bar"])))) +27 27 | + +RUF022.py:25:16: RUF022 [*] `__all__` is not sorted + | +23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 | __all__.extend(["foo", "bar"]) +25 | __all__.extend(("foo", "bar")) + | ^^^^^^^^^^^^^^ RUF022 +26 | __all__.extend((((["foo", "bar"])))) + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +22 22 | +23 23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 24 | __all__.extend(["foo", "bar"]) +25 |-__all__.extend(("foo", "bar")) + 25 |+__all__.extend(("bar", "foo")) +26 26 | __all__.extend((((["foo", "bar"])))) +27 27 | +28 28 | #################################### + +RUF022.py:26:19: RUF022 [*] `__all__` is not sorted + | +24 | __all__.extend(["foo", "bar"]) +25 | __all__.extend(("foo", "bar")) +26 | __all__.extend((((["foo", "bar"])))) + | ^^^^^^^^^^^^^^ RUF022 +27 | +28 | #################################### + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +23 23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 24 | __all__.extend(["foo", "bar"]) +25 25 | __all__.extend(("foo", "bar")) +26 |-__all__.extend((((["foo", "bar"])))) + 26 |+__all__.extend((((["bar", "foo"])))) +27 27 | +28 28 | #################################### +29 29 | # Neat multiline __all__ definitions + +RUF022.py:32:11: RUF022 [*] `__all__` is not sorted + | +30 | #################################### +31 | +32 | __all__ = ( + | ___________^ +33 | | "d0", +34 | | "c0", # a comment regarding 'c0' +35 | | "b0", +36 | | # a comment regarding 'a0': +37 | | "a0" +38 | | ) + | |_^ RUF022 +39 | +40 | __all__ = [ + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +30 30 | #################################### +31 31 | +32 32 | __all__ = ( +33 |- "d0", + 33 |+ # a comment regarding 'a0': + 34 |+ "a0", + 35 |+ "b0", +34 36 | "c0", # a comment regarding 'c0' +35 |- "b0", +36 |- # a comment regarding 'a0': +37 |- "a0" + 37 |+ "d0" +38 38 | ) +39 39 | +40 40 | __all__ = [ + +RUF022.py:40:11: RUF022 [*] `__all__` is not sorted + | +38 | ) +39 | +40 | __all__ = [ + | ___________^ +41 | | "d", +42 | | "c", # a comment regarding 'c' +43 | | "b", +44 | | # a comment regarding 'a': +45 | | "a" +46 | | ] + | |_^ RUF022 +47 | +48 | # we implement an "isort-style sort": + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +38 38 | ) +39 39 | +40 40 | __all__ = [ +41 |- "d", + 41 |+ # a comment regarding 'a': + 42 |+ "a", + 43 |+ "b", +42 44 | "c", # a comment regarding 'c' +43 |- "b", +44 |- # a comment regarding 'a': +45 |- "a" + 45 |+ "d" +46 46 | ] +47 47 | +48 48 | # we implement an "isort-style sort": + +RUF022.py:54:11: RUF022 [*] `__all__` is not sorted + | +52 | # This (which is currently alphabetically sorted) +53 | # should get reordered accordingly: +54 | __all__ = [ + | ___________^ +55 | | "APRIL", +56 | | "AUGUST", +57 | | "Calendar", +58 | | "DECEMBER", +59 | | "Day", +60 | | "FEBRUARY", +61 | | "FRIDAY", +62 | | "HTMLCalendar", +63 | | "IllegalMonthError", +64 | | "JANUARY", +65 | | "JULY", +66 | | "JUNE", +67 | | "LocaleHTMLCalendar", +68 | | "MARCH", +69 | | "MAY", +70 | | "MONDAY", +71 | | "Month", +72 | | "NOVEMBER", +73 | | "OCTOBER", +74 | | "SATURDAY", +75 | | "SEPTEMBER", +76 | | "SUNDAY", +77 | | "THURSDAY", +78 | | "TUESDAY", +79 | | "TextCalendar", +80 | | "WEDNESDAY", +81 | | "calendar", +82 | | "timegm", +83 | | "weekday", +84 | | "weekheader"] + | |_________________^ RUF022 +85 | +86 | ########################################## + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +54 54 | __all__ = [ +55 55 | "APRIL", +56 56 | "AUGUST", +57 |- "Calendar", +58 57 | "DECEMBER", +59 |- "Day", +60 58 | "FEBRUARY", +61 59 | "FRIDAY", +62 |- "HTMLCalendar", +63 |- "IllegalMonthError", +64 60 | "JANUARY", +65 61 | "JULY", +66 62 | "JUNE", +67 |- "LocaleHTMLCalendar", +68 63 | "MARCH", +69 64 | "MAY", +70 65 | "MONDAY", +71 |- "Month", +72 66 | "NOVEMBER", +73 67 | "OCTOBER", +74 68 | "SATURDAY", +-------------------------------------------------------------------------------- +76 70 | "SUNDAY", +77 71 | "THURSDAY", +78 72 | "TUESDAY", + 73 |+ "WEDNESDAY", + 74 |+ "Calendar", + 75 |+ "Day", + 76 |+ "HTMLCalendar", + 77 |+ "IllegalMonthError", + 78 |+ "LocaleHTMLCalendar", + 79 |+ "Month", +79 80 | "TextCalendar", +80 |- "WEDNESDAY", +81 81 | "calendar", +82 82 | "timegm", +83 83 | "weekday", + +RUF022.py:91:11: RUF022 [*] `__all__` is not sorted + | +90 | # comment0 +91 | __all__ = ("d", "a", # comment1 + | ___________^ +92 | | # comment2 +93 | | "f", "b", +94 | | "strangely", # comment3 +95 | | # comment4 +96 | | "formatted", +97 | | # comment5 +98 | | ) # comment6 + | |_^ RUF022 +99 | # comment7 + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +88 88 | ########################################## +89 89 | +90 90 | # comment0 +91 |-__all__ = ("d", "a", # comment1 +92 |- # comment2 +93 |- "f", "b", +94 |- "strangely", # comment3 +95 |- # comment4 + 91 |+__all__ = ( + 92 |+ "a", + 93 |+ "b", + 94 |+ "d", # comment1 + 95 |+ # comment2 + 96 |+ "f", + 97 |+ # comment4 +96 98 | "formatted", + 99 |+ "strangely", # comment3 +97 100 | # comment5 +98 101 | ) # comment6 +99 102 | # comment7 + +RUF022.py:101:11: RUF022 [*] `__all__` is not sorted + | + 99 | # comment7 +100 | +101 | __all__ = [ # comment0 + | ___________^ +102 | | # comment1 +103 | | # comment2 +104 | | "dx", "cx", "bx", "ax" # comment3 +105 | | # comment4 +106 | | # comment5 +107 | | # comment6 +108 | | ] # comment7 + | |_^ RUF022 +109 | +110 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +99 99 | # comment7 +100 100 | +101 101 | __all__ = [ # comment0 + 102 |+ "ax", + 103 |+ "bx", + 104 |+ "cx", +102 105 | # comment1 +103 106 | # comment2 +104 |- "dx", "cx", "bx", "ax" # comment3 + 107 |+ "dx" # comment3 +105 108 | # comment4 +106 109 | # comment5 +107 110 | # comment6 + +RUF022.py:110:11: RUF022 [*] `__all__` is not sorted + | +108 | ] # comment7 +109 | +110 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", + | ___________^ +111 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +112 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +113 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +114 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +115 | | "StreamReader", "StreamWriter", +116 | | "StreamReaderWriter", "StreamRecoder", +117 | | "getencoder", "getdecoder", "getincrementalencoder", +118 | | "getincrementaldecoder", "getreader", "getwriter", +119 | | "encode", "decode", "iterencode", "iterdecode", +120 | | "strict_errors", "ignore_errors", "replace_errors", +121 | | "xmlcharrefreplace_errors", +122 | | "backslashreplace_errors", "namereplace_errors", +123 | | "register_error", "lookup_error"] + | |____________________________________________^ RUF022 +124 | +125 | __all__: tuple[str, ...] = ( # a comment about the opening paren + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +107 107 | # comment6 +108 108 | ] # comment7 +109 109 | +110 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +111 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +112 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +113 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +114 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +115 |- "StreamReader", "StreamWriter", +116 |- "StreamReaderWriter", "StreamRecoder", +117 |- "getencoder", "getdecoder", "getincrementalencoder", +118 |- "getincrementaldecoder", "getreader", "getwriter", +119 |- "encode", "decode", "iterencode", "iterdecode", +120 |- "strict_errors", "ignore_errors", "replace_errors", +121 |- "xmlcharrefreplace_errors", +122 |- "backslashreplace_errors", "namereplace_errors", +123 |- "register_error", "lookup_error"] + 110 |+__all__ = [ + 111 |+ "BOM", + 112 |+ "BOM32_BE", + 113 |+ "BOM32_LE", + 114 |+ "BOM64_BE", + 115 |+ "BOM64_LE", + 116 |+ "BOM_BE", + 117 |+ "BOM_LE", + 118 |+ "BOM_UTF8", + 119 |+ "BOM_UTF16", + 120 |+ "BOM_UTF16_BE", + 121 |+ "BOM_UTF16_LE", + 122 |+ "BOM_UTF32", + 123 |+ "BOM_UTF32_BE", + 124 |+ "BOM_UTF32_LE", + 125 |+ "Codec", + 126 |+ "CodecInfo", + 127 |+ "EncodedFile", + 128 |+ "IncrementalDecoder", + 129 |+ "IncrementalEncoder", + 130 |+ "StreamReader", + 131 |+ "StreamReaderWriter", + 132 |+ "StreamRecoder", + 133 |+ "StreamWriter", + 134 |+ "backslashreplace_errors", + 135 |+ "decode", + 136 |+ "encode", + 137 |+ "getdecoder", + 138 |+ "getencoder", + 139 |+ "getincrementaldecoder", + 140 |+ "getincrementalencoder", + 141 |+ "getreader", + 142 |+ "getwriter", + 143 |+ "ignore_errors", + 144 |+ "iterdecode", + 145 |+ "iterencode", + 146 |+ "lookup", + 147 |+ "lookup_error", + 148 |+ "namereplace_errors", + 149 |+ "open", + 150 |+ "register", + 151 |+ "register_error", + 152 |+ "replace_errors", + 153 |+ "strict_errors", + 154 |+ "xmlcharrefreplace_errors"] +124 155 | +125 156 | __all__: tuple[str, ...] = ( # a comment about the opening paren +126 157 | # multiline comment about "bbb" part 1 + +RUF022.py:125:28: RUF022 [*] `__all__` is not sorted + | +123 | "register_error", "lookup_error"] +124 | +125 | __all__: tuple[str, ...] = ( # a comment about the opening paren + | ____________________________^ +126 | | # multiline comment about "bbb" part 1 +127 | | # multiline comment about "bbb" part 2 +128 | | "bbb", +129 | | # multiline comment about "aaa" part 1 +130 | | # multiline comment about "aaa" part 2 +131 | | "aaa", +132 | | ) + | |_^ RUF022 +133 | +134 | # we use natural sort for `__all__`, + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +123 123 | "register_error", "lookup_error"] +124 124 | +125 125 | __all__: tuple[str, ...] = ( # a comment about the opening paren + 126 |+ # multiline comment about "aaa" part 1 + 127 |+ # multiline comment about "aaa" part 2 + 128 |+ "aaa", +126 129 | # multiline comment about "bbb" part 1 +127 130 | # multiline comment about "bbb" part 2 +128 131 | "bbb", +129 |- # multiline comment about "aaa" part 1 +130 |- # multiline comment about "aaa" part 2 +131 |- "aaa", +132 132 | ) +133 133 | +134 134 | # we use natural sort for `__all__`, + +RUF022.py:138:11: RUF022 [*] `__all__` is not sorted + | +136 | # Also, this doesn't end with a trailing comma, +137 | # so the autofix shouldn't introduce one: +138 | __all__ = ( + | ___________^ +139 | | "aadvark237", +140 | | "aadvark10092", +141 | | "aadvark174", # the very long whitespace span before this comment is retained +142 | | "aadvark532" # the even longer whitespace span before this comment is retained +143 | | ) + | |_^ RUF022 +144 | +145 | __all__.extend(( # comment0 + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +136 136 | # Also, this doesn't end with a trailing comma, +137 137 | # so the autofix shouldn't introduce one: +138 138 | __all__ = ( + 139 |+ "aadvark174", # the very long whitespace span before this comment is retained +139 140 | "aadvark237", +140 |- "aadvark10092", +141 |- "aadvark174", # the very long whitespace span before this comment is retained +142 |- "aadvark532" # the even longer whitespace span before this comment is retained + 141 |+ "aadvark532", # the even longer whitespace span before this comment is retained + 142 |+ "aadvark10092" +143 143 | ) +144 144 | +145 145 | __all__.extend(( # comment0 + +RUF022.py:145:16: RUF022 [*] `__all__` is not sorted + | +143 | ) +144 | +145 | __all__.extend(( # comment0 + | ________________^ +146 | | # comment about foo +147 | | "foo", # comment about foo +148 | | # comment about bar +149 | | "bar" # comment about bar +150 | | # comment1 +151 | | )) # comment2 + | |_^ RUF022 +152 | +153 | __all__.extend( # comment0 + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +143 143 | ) +144 144 | +145 145 | __all__.extend(( # comment0 + 146 |+ # comment about bar + 147 |+ "bar", # comment about bar +146 148 | # comment about foo +147 |- "foo", # comment about foo +148 |- # comment about bar +149 |- "bar" # comment about bar + 149 |+ "foo" # comment about foo +150 150 | # comment1 +151 151 | )) # comment2 +152 152 | + +RUF022.py:155:5: RUF022 [*] `__all__` is not sorted + | +153 | __all__.extend( # comment0 +154 | # comment1 +155 | ( # comment2 + | _____^ +156 | | # comment about foo +157 | | "foo", # comment about foo +158 | | # comment about bar +159 | | "bar" # comment about bar +160 | | # comment3 +161 | | ) # comment4 + | |_____^ RUF022 +162 | ) # comment2 + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +153 153 | __all__.extend( # comment0 +154 154 | # comment1 +155 155 | ( # comment2 + 156 |+ # comment about bar + 157 |+ "bar", # comment about bar +156 158 | # comment about foo +157 |- "foo", # comment about foo +158 |- # comment about bar +159 |- "bar" # comment about bar + 159 |+ "foo" # comment about foo +160 160 | # comment3 +161 161 | ) # comment4 +162 162 | ) # comment2 + +RUF022.py:164:16: RUF022 [*] `__all__` is not sorted + | +162 | ) # comment2 +163 | +164 | __all__.extend([ # comment0 + | ________________^ +165 | | # comment about foo +166 | | "foo", # comment about foo +167 | | # comment about bar +168 | | "bar" # comment about bar +169 | | # comment1 +170 | | ]) # comment2 + | |_^ RUF022 +171 | +172 | __all__.extend( # comment0 + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +162 162 | ) # comment2 +163 163 | +164 164 | __all__.extend([ # comment0 + 165 |+ # comment about bar + 166 |+ "bar", # comment about bar +165 167 | # comment about foo +166 |- "foo", # comment about foo +167 |- # comment about bar +168 |- "bar" # comment about bar + 168 |+ "foo" # comment about foo +169 169 | # comment1 +170 170 | ]) # comment2 +171 171 | + +RUF022.py:174:5: RUF022 [*] `__all__` is not sorted + | +172 | __all__.extend( # comment0 +173 | # comment1 +174 | [ # comment2 + | _____^ +175 | | # comment about foo +176 | | "foo", # comment about foo +177 | | # comment about bar +178 | | "bar" # comment about bar +179 | | # comment3 +180 | | ] # comment4 + | |_____^ RUF022 +181 | ) # comment2 + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +172 172 | __all__.extend( # comment0 +173 173 | # comment1 +174 174 | [ # comment2 + 175 |+ # comment about bar + 176 |+ "bar", # comment about bar +175 177 | # comment about foo +176 |- "foo", # comment about foo +177 |- # comment about bar +178 |- "bar" # comment about bar + 178 |+ "foo" # comment about foo +179 179 | # comment3 +180 180 | ] # comment4 +181 181 | ) # comment2 + +RUF022.py:183:11: RUF022 [*] `__all__` is not sorted + | +181 | ) # comment2 +182 | +183 | __all__ = ["Style", "Treeview", + | ___________^ +184 | | # Extensions +185 | | "LabeledScale", "OptionMenu", +186 | | ] + | |_^ RUF022 +187 | +188 | __all__ = ["Awaitable", "Coroutine", + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +180 180 | ] # comment4 +181 181 | ) # comment2 +182 182 | +183 |-__all__ = ["Style", "Treeview", +184 |- # Extensions +185 |- "LabeledScale", "OptionMenu", + 183 |+__all__ = [ + 184 |+ # Extensions + 185 |+ "LabeledScale", + 186 |+ "OptionMenu", + 187 |+ "Style", + 188 |+ "Treeview", +186 189 | ] +187 190 | +188 191 | __all__ = ["Awaitable", "Coroutine", + +RUF022.py:188:11: RUF022 [*] `__all__` is not sorted + | +186 | ] +187 | +188 | __all__ = ["Awaitable", "Coroutine", + | ___________^ +189 | | "AsyncIterable", "AsyncIterator", "AsyncGenerator", +190 | | ] + | |____________^ RUF022 +191 | +192 | __all__ = [ + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +185 185 | "LabeledScale", "OptionMenu", +186 186 | ] +187 187 | +188 |-__all__ = ["Awaitable", "Coroutine", +189 |- "AsyncIterable", "AsyncIterator", "AsyncGenerator", +190 |- ] + 188 |+__all__ = [ + 189 |+ "AsyncGenerator", + 190 |+ "AsyncIterable", + 191 |+ "AsyncIterator", + 192 |+ "Awaitable", + 193 |+ "Coroutine", + 194 |+] +191 195 | +192 196 | __all__ = [ +193 197 | "foo", + +RUF022.py:192:11: RUF022 [*] `__all__` is not sorted + | +190 | ] +191 | +192 | __all__ = [ + | ___________^ +193 | | "foo", +194 | | "bar", +195 | | "baz", +196 | | ] + | |_____^ RUF022 +197 | +198 | ######################################################################### + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +190 190 | ] +191 191 | +192 192 | __all__ = [ +193 |- "foo", +194 193 | "bar", +195 194 | "baz", + 195 |+ "foo", +196 196 | ] +197 197 | +198 198 | ######################################################################### + +RUF022.py:204:11: RUF022 `__all__` is not sorted + | +202 | ######################################################################### +203 | +204 | __all__ = ( + | ___________^ +205 | | "look", +206 | | ( +207 | | "a_veeeeeeeeeeeeeeeeeeery_long_parenthesized_item" +208 | | ), +209 | | ) + | |_^ RUF022 +210 | +211 | __all__ = ( + | + = help: Apply an isort-style sorting to `__all__` + +RUF022.py:211:11: RUF022 `__all__` is not sorted + | +209 | ) +210 | +211 | __all__ = ( + | ___________^ +212 | | "b", +213 | | (( +214 | | "c" +215 | | )), +216 | | "a" +217 | | ) + | |_^ RUF022 +218 | +219 | __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") + | + = help: Apply an isort-style sorting to `__all__` + +RUF022.py:219:11: RUF022 `__all__` is not sorted + | +217 | ) +218 | +219 | __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 +220 | +221 | ################################### + | + = help: Apply an isort-style sorting to `__all__` + + diff --git a/ruff.schema.json b/ruff.schema.json index c85044300ba12..e58cd3cccf087 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3443,6 +3443,7 @@ "RUF02", "RUF020", "RUF021", + "RUF022", "RUF1", "RUF10", "RUF100",