Spaces:
Runtime error
Runtime error
| """ | |
| Generating lines of code. | |
| """ | |
| import sys | |
| from dataclasses import replace | |
| from enum import Enum, auto | |
| from functools import partial, wraps | |
| from typing import Collection, Iterator, List, Optional, Set, Union, cast | |
| from black.brackets import ( | |
| COMMA_PRIORITY, | |
| DOT_PRIORITY, | |
| get_leaves_inside_matching_brackets, | |
| max_delimiter_priority_in_atom, | |
| ) | |
| from black.comments import FMT_OFF, generate_comments, list_comments | |
| from black.lines import ( | |
| Line, | |
| RHSResult, | |
| append_leaves, | |
| can_be_split, | |
| can_omit_invisible_parens, | |
| is_line_short_enough, | |
| line_to_string, | |
| ) | |
| from black.mode import Feature, Mode, Preview | |
| from black.nodes import ( | |
| ASSIGNMENTS, | |
| BRACKETS, | |
| CLOSING_BRACKETS, | |
| OPENING_BRACKETS, | |
| RARROW, | |
| STANDALONE_COMMENT, | |
| STATEMENT, | |
| WHITESPACE, | |
| Visitor, | |
| ensure_visible, | |
| is_arith_like, | |
| is_async_stmt_or_funcdef, | |
| is_atom_with_invisible_parens, | |
| is_docstring, | |
| is_empty_tuple, | |
| is_lpar_token, | |
| is_multiline_string, | |
| is_name_token, | |
| is_one_sequence_between, | |
| is_one_tuple, | |
| is_rpar_token, | |
| is_stub_body, | |
| is_stub_suite, | |
| is_tuple_containing_walrus, | |
| is_type_ignore_comment_string, | |
| is_vararg, | |
| is_walrus_assignment, | |
| is_yield, | |
| syms, | |
| wrap_in_parentheses, | |
| ) | |
| from black.numerics import normalize_numeric_literal | |
| from black.strings import ( | |
| fix_docstring, | |
| get_string_prefix, | |
| normalize_string_prefix, | |
| normalize_string_quotes, | |
| normalize_unicode_escape_sequences, | |
| ) | |
| from black.trans import ( | |
| CannotTransform, | |
| StringMerger, | |
| StringParenStripper, | |
| StringParenWrapper, | |
| StringSplitter, | |
| Transformer, | |
| hug_power_op, | |
| ) | |
| from blib2to3.pgen2 import token | |
| from blib2to3.pytree import Leaf, Node | |
| # types | |
| LeafID = int | |
| LN = Union[Leaf, Node] | |
| class CannotSplit(CannotTransform): | |
| """A readable split that fits the allotted line length is impossible.""" | |
| # This isn't a dataclass because @dataclass + Generic breaks mypyc. | |
| # See also https://github.com/mypyc/mypyc/issues/827. | |
| class LineGenerator(Visitor[Line]): | |
| """Generates reformatted Line objects. Empty lines are not emitted. | |
| Note: destroys the tree it's visiting by mutating prefixes of its leaves | |
| in ways that will no longer stringify to valid Python code on the tree. | |
| """ | |
| def __init__(self, mode: Mode, features: Collection[Feature]) -> None: | |
| self.mode = mode | |
| self.features = features | |
| self.current_line: Line | |
| self.__post_init__() | |
| def line(self, indent: int = 0) -> Iterator[Line]: | |
| """Generate a line. | |
| If the line is empty, only emit if it makes sense. | |
| If the line is too long, split it first and then generate. | |
| If any lines were generated, set up a new current_line. | |
| """ | |
| if not self.current_line: | |
| self.current_line.depth += indent | |
| return # Line is empty, don't emit. Creating a new one unnecessary. | |
| if ( | |
| Preview.improved_async_statements_handling in self.mode | |
| and len(self.current_line.leaves) == 1 | |
| and is_async_stmt_or_funcdef(self.current_line.leaves[0]) | |
| ): | |
| # Special case for async def/for/with statements. `visit_async_stmt` | |
| # adds an `ASYNC` leaf then visits the child def/for/with statement | |
| # nodes. Line yields from those nodes shouldn't treat the former | |
| # `ASYNC` leaf as a complete line. | |
| return | |
| complete_line = self.current_line | |
| self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent) | |
| yield complete_line | |
| def visit_default(self, node: LN) -> Iterator[Line]: | |
| """Default `visit_*()` implementation. Recurses to children of `node`.""" | |
| if isinstance(node, Leaf): | |
| any_open_brackets = self.current_line.bracket_tracker.any_open_brackets() | |
| for comment in generate_comments(node): | |
| if any_open_brackets: | |
| # any comment within brackets is subject to splitting | |
| self.current_line.append(comment) | |
| elif comment.type == token.COMMENT: | |
| # regular trailing comment | |
| self.current_line.append(comment) | |
| yield from self.line() | |
| else: | |
| # regular standalone comment | |
| yield from self.line() | |
| self.current_line.append(comment) | |
| yield from self.line() | |
| normalize_prefix(node, inside_brackets=any_open_brackets) | |
| if self.mode.string_normalization and node.type == token.STRING: | |
| node.value = normalize_string_prefix(node.value) | |
| node.value = normalize_string_quotes(node.value) | |
| if node.type == token.NUMBER: | |
| normalize_numeric_literal(node) | |
| if node.type not in WHITESPACE: | |
| self.current_line.append(node) | |
| yield from super().visit_default(node) | |
| def visit_test(self, node: Node) -> Iterator[Line]: | |
| """Visit an `x if y else z` test""" | |
| if Preview.parenthesize_conditional_expressions in self.mode: | |
| already_parenthesized = ( | |
| node.prev_sibling and node.prev_sibling.type == token.LPAR | |
| ) | |
| if not already_parenthesized: | |
| lpar = Leaf(token.LPAR, "") | |
| rpar = Leaf(token.RPAR, "") | |
| node.insert_child(0, lpar) | |
| node.append_child(rpar) | |
| yield from self.visit_default(node) | |
| def visit_INDENT(self, node: Leaf) -> Iterator[Line]: | |
| """Increase indentation level, maybe yield a line.""" | |
| # In blib2to3 INDENT never holds comments. | |
| yield from self.line(+1) | |
| yield from self.visit_default(node) | |
| def visit_DEDENT(self, node: Leaf) -> Iterator[Line]: | |
| """Decrease indentation level, maybe yield a line.""" | |
| # The current line might still wait for trailing comments. At DEDENT time | |
| # there won't be any (they would be prefixes on the preceding NEWLINE). | |
| # Emit the line then. | |
| yield from self.line() | |
| # While DEDENT has no value, its prefix may contain standalone comments | |
| # that belong to the current indentation level. Get 'em. | |
| yield from self.visit_default(node) | |
| # Finally, emit the dedent. | |
| yield from self.line(-1) | |
| def visit_stmt( | |
| self, node: Node, keywords: Set[str], parens: Set[str] | |
| ) -> Iterator[Line]: | |
| """Visit a statement. | |
| This implementation is shared for `if`, `while`, `for`, `try`, `except`, | |
| `def`, `with`, `class`, `assert`, and assignments. | |
| The relevant Python language `keywords` for a given statement will be | |
| NAME leaves within it. This methods puts those on a separate line. | |
| `parens` holds a set of string leaf values immediately after which | |
| invisible parens should be put. | |
| """ | |
| normalize_invisible_parens( | |
| node, parens_after=parens, mode=self.mode, features=self.features | |
| ) | |
| for child in node.children: | |
| if is_name_token(child) and child.value in keywords: | |
| yield from self.line() | |
| yield from self.visit(child) | |
| def visit_typeparams(self, node: Node) -> Iterator[Line]: | |
| yield from self.visit_default(node) | |
| node.children[0].prefix = "" | |
| def visit_typevartuple(self, node: Node) -> Iterator[Line]: | |
| yield from self.visit_default(node) | |
| node.children[1].prefix = "" | |
| def visit_paramspec(self, node: Node) -> Iterator[Line]: | |
| yield from self.visit_default(node) | |
| node.children[1].prefix = "" | |
| def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: | |
| if Preview.wrap_long_dict_values_in_parens in self.mode: | |
| for i, child in enumerate(node.children): | |
| if i == 0: | |
| continue | |
| if node.children[i - 1].type == token.COLON: | |
| if child.type == syms.atom and child.children[0].type == token.LPAR: | |
| if maybe_make_parens_invisible_in_atom( | |
| child, | |
| parent=node, | |
| remove_brackets_around_comma=False, | |
| ): | |
| wrap_in_parentheses(node, child, visible=False) | |
| else: | |
| wrap_in_parentheses(node, child, visible=False) | |
| yield from self.visit_default(node) | |
| def visit_funcdef(self, node: Node) -> Iterator[Line]: | |
| """Visit function definition.""" | |
| yield from self.line() | |
| # Remove redundant brackets around return type annotation. | |
| is_return_annotation = False | |
| for child in node.children: | |
| if child.type == token.RARROW: | |
| is_return_annotation = True | |
| elif is_return_annotation: | |
| if child.type == syms.atom and child.children[0].type == token.LPAR: | |
| if maybe_make_parens_invisible_in_atom( | |
| child, | |
| parent=node, | |
| remove_brackets_around_comma=False, | |
| ): | |
| wrap_in_parentheses(node, child, visible=False) | |
| else: | |
| wrap_in_parentheses(node, child, visible=False) | |
| is_return_annotation = False | |
| for child in node.children: | |
| yield from self.visit(child) | |
| def visit_match_case(self, node: Node) -> Iterator[Line]: | |
| """Visit either a match or case statement.""" | |
| normalize_invisible_parens( | |
| node, parens_after=set(), mode=self.mode, features=self.features | |
| ) | |
| yield from self.line() | |
| for child in node.children: | |
| yield from self.visit(child) | |
| def visit_suite(self, node: Node) -> Iterator[Line]: | |
| """Visit a suite.""" | |
| if self.mode.is_pyi and is_stub_suite(node): | |
| yield from self.visit(node.children[2]) | |
| else: | |
| yield from self.visit_default(node) | |
| def visit_simple_stmt(self, node: Node) -> Iterator[Line]: | |
| """Visit a statement without nested statements.""" | |
| prev_type: Optional[int] = None | |
| for child in node.children: | |
| if (prev_type is None or prev_type == token.SEMI) and is_arith_like(child): | |
| wrap_in_parentheses(node, child, visible=False) | |
| prev_type = child.type | |
| is_suite_like = node.parent and node.parent.type in STATEMENT | |
| if is_suite_like: | |
| if self.mode.is_pyi and is_stub_body(node): | |
| yield from self.visit_default(node) | |
| else: | |
| yield from self.line(+1) | |
| yield from self.visit_default(node) | |
| yield from self.line(-1) | |
| else: | |
| if ( | |
| not self.mode.is_pyi | |
| or not node.parent | |
| or not is_stub_suite(node.parent) | |
| ): | |
| yield from self.line() | |
| yield from self.visit_default(node) | |
| def visit_async_stmt(self, node: Node) -> Iterator[Line]: | |
| """Visit `async def`, `async for`, `async with`.""" | |
| yield from self.line() | |
| children = iter(node.children) | |
| for child in children: | |
| yield from self.visit(child) | |
| if child.type == token.ASYNC or child.type == STANDALONE_COMMENT: | |
| # STANDALONE_COMMENT happens when `# fmt: skip` is applied on the async | |
| # line. | |
| break | |
| internal_stmt = next(children) | |
| if Preview.improved_async_statements_handling in self.mode: | |
| yield from self.visit(internal_stmt) | |
| else: | |
| for child in internal_stmt.children: | |
| yield from self.visit(child) | |
| def visit_decorators(self, node: Node) -> Iterator[Line]: | |
| """Visit decorators.""" | |
| for child in node.children: | |
| yield from self.line() | |
| yield from self.visit(child) | |
| def visit_power(self, node: Node) -> Iterator[Line]: | |
| for idx, leaf in enumerate(node.children[:-1]): | |
| next_leaf = node.children[idx + 1] | |
| if not isinstance(leaf, Leaf): | |
| continue | |
| value = leaf.value.lower() | |
| if ( | |
| leaf.type == token.NUMBER | |
| and next_leaf.type == syms.trailer | |
| # Ensure that we are in an attribute trailer | |
| and next_leaf.children[0].type == token.DOT | |
| # It shouldn't wrap hexadecimal, binary and octal literals | |
| and not value.startswith(("0x", "0b", "0o")) | |
| # It shouldn't wrap complex literals | |
| and "j" not in value | |
| ): | |
| wrap_in_parentheses(node, leaf) | |
| remove_await_parens(node) | |
| yield from self.visit_default(node) | |
| def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]: | |
| """Remove a semicolon and put the other statement on a separate line.""" | |
| yield from self.line() | |
| def visit_ENDMARKER(self, leaf: Leaf) -> Iterator[Line]: | |
| """End of file. Process outstanding comments and end with a newline.""" | |
| yield from self.visit_default(leaf) | |
| yield from self.line() | |
| def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]: | |
| if not self.current_line.bracket_tracker.any_open_brackets(): | |
| yield from self.line() | |
| yield from self.visit_default(leaf) | |
| def visit_factor(self, node: Node) -> Iterator[Line]: | |
| """Force parentheses between a unary op and a binary power: | |
| -2 ** 8 -> -(2 ** 8) | |
| """ | |
| _operator, operand = node.children | |
| if ( | |
| operand.type == syms.power | |
| and len(operand.children) == 3 | |
| and operand.children[1].type == token.DOUBLESTAR | |
| ): | |
| lpar = Leaf(token.LPAR, "(") | |
| rpar = Leaf(token.RPAR, ")") | |
| index = operand.remove() or 0 | |
| node.insert_child(index, Node(syms.atom, [lpar, operand, rpar])) | |
| yield from self.visit_default(node) | |
| def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: | |
| if Preview.hex_codes_in_unicode_sequences in self.mode: | |
| normalize_unicode_escape_sequences(leaf) | |
| if is_docstring(leaf) and "\\\n" not in leaf.value: | |
| # We're ignoring docstrings with backslash newline escapes because changing | |
| # indentation of those changes the AST representation of the code. | |
| if self.mode.string_normalization: | |
| docstring = normalize_string_prefix(leaf.value) | |
| # visit_default() does handle string normalization for us, but | |
| # since this method acts differently depending on quote style (ex. | |
| # see padding logic below), there's a possibility for unstable | |
| # formatting as visit_default() is called *after*. To avoid a | |
| # situation where this function formats a docstring differently on | |
| # the second pass, normalize it early. | |
| docstring = normalize_string_quotes(docstring) | |
| else: | |
| docstring = leaf.value | |
| prefix = get_string_prefix(docstring) | |
| docstring = docstring[len(prefix) :] # Remove the prefix | |
| quote_char = docstring[0] | |
| # A natural way to remove the outer quotes is to do: | |
| # docstring = docstring.strip(quote_char) | |
| # but that breaks on """""x""" (which is '""x'). | |
| # So we actually need to remove the first character and the next two | |
| # characters but only if they are the same as the first. | |
| quote_len = 1 if docstring[1] != quote_char else 3 | |
| docstring = docstring[quote_len:-quote_len] | |
| docstring_started_empty = not docstring | |
| indent = " " * 4 * self.current_line.depth | |
| if is_multiline_string(leaf): | |
| docstring = fix_docstring(docstring, indent) | |
| else: | |
| docstring = docstring.strip() | |
| has_trailing_backslash = False | |
| if docstring: | |
| # Add some padding if the docstring starts / ends with a quote mark. | |
| if docstring[0] == quote_char: | |
| docstring = " " + docstring | |
| if docstring[-1] == quote_char: | |
| docstring += " " | |
| if docstring[-1] == "\\": | |
| backslash_count = len(docstring) - len(docstring.rstrip("\\")) | |
| if backslash_count % 2: | |
| # Odd number of tailing backslashes, add some padding to | |
| # avoid escaping the closing string quote. | |
| docstring += " " | |
| has_trailing_backslash = True | |
| elif not docstring_started_empty: | |
| docstring = " " | |
| # We could enforce triple quotes at this point. | |
| quote = quote_char * quote_len | |
| # It's invalid to put closing single-character quotes on a new line. | |
| if self.mode and quote_len == 3: | |
| # We need to find the length of the last line of the docstring | |
| # to find if we can add the closing quotes to the line without | |
| # exceeding the maximum line length. | |
| # If docstring is one line, we don't put the closing quotes on a | |
| # separate line because it looks ugly (#3320). | |
| lines = docstring.splitlines() | |
| last_line_length = len(lines[-1]) if docstring else 0 | |
| # If adding closing quotes would cause the last line to exceed | |
| # the maximum line length then put a line break before the | |
| # closing quotes | |
| if ( | |
| len(lines) > 1 | |
| and last_line_length + quote_len > self.mode.line_length | |
| and len(indent) + quote_len <= self.mode.line_length | |
| and not has_trailing_backslash | |
| ): | |
| leaf.value = prefix + quote + docstring + "\n" + indent + quote | |
| else: | |
| leaf.value = prefix + quote + docstring + quote | |
| else: | |
| leaf.value = prefix + quote + docstring + quote | |
| yield from self.visit_default(leaf) | |
| def __post_init__(self) -> None: | |
| """You are in a twisty little maze of passages.""" | |
| self.current_line = Line(mode=self.mode) | |
| v = self.visit_stmt | |
| Ø: Set[str] = set() | |
| self.visit_assert_stmt = partial(v, keywords={"assert"}, parens={"assert", ","}) | |
| self.visit_if_stmt = partial( | |
| v, keywords={"if", "else", "elif"}, parens={"if", "elif"} | |
| ) | |
| self.visit_while_stmt = partial(v, keywords={"while", "else"}, parens={"while"}) | |
| self.visit_for_stmt = partial(v, keywords={"for", "else"}, parens={"for", "in"}) | |
| self.visit_try_stmt = partial( | |
| v, keywords={"try", "except", "else", "finally"}, parens=Ø | |
| ) | |
| self.visit_except_clause = partial(v, keywords={"except"}, parens={"except"}) | |
| self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"}) | |
| self.visit_classdef = partial(v, keywords={"class"}, parens=Ø) | |
| self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS) | |
| self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"}) | |
| self.visit_import_from = partial(v, keywords=Ø, parens={"import"}) | |
| self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"}) | |
| self.visit_async_funcdef = self.visit_async_stmt | |
| self.visit_decorated = self.visit_decorators | |
| # PEP 634 | |
| self.visit_match_stmt = self.visit_match_case | |
| self.visit_case_block = self.visit_match_case | |
| def transform_line( | |
| line: Line, mode: Mode, features: Collection[Feature] = () | |
| ) -> Iterator[Line]: | |
| """Transform a `line`, potentially splitting it into many lines. | |
| They should fit in the allotted `line_length` but might not be able to. | |
| `features` are syntactical features that may be used in the output. | |
| """ | |
| if line.is_comment: | |
| yield line | |
| return | |
| line_str = line_to_string(line) | |
| ll = mode.line_length | |
| sn = mode.string_normalization | |
| string_merge = StringMerger(ll, sn) | |
| string_paren_strip = StringParenStripper(ll, sn) | |
| string_split = StringSplitter(ll, sn) | |
| string_paren_wrap = StringParenWrapper(ll, sn) | |
| transformers: List[Transformer] | |
| if ( | |
| not line.contains_uncollapsable_type_comments() | |
| and not line.should_split_rhs | |
| and not line.magic_trailing_comma | |
| and ( | |
| is_line_short_enough(line, mode=mode, line_str=line_str) | |
| or line.contains_unsplittable_type_ignore() | |
| ) | |
| and not (line.inside_brackets and line.contains_standalone_comments()) | |
| ): | |
| # Only apply basic string preprocessing, since lines shouldn't be split here. | |
| if Preview.string_processing in mode: | |
| transformers = [string_merge, string_paren_strip] | |
| else: | |
| transformers = [] | |
| elif line.is_def: | |
| transformers = [left_hand_split] | |
| else: | |
| def _rhs( | |
| self: object, line: Line, features: Collection[Feature], mode: Mode | |
| ) -> Iterator[Line]: | |
| """Wraps calls to `right_hand_split`. | |
| The calls increasingly `omit` right-hand trailers (bracket pairs with | |
| content), meaning the trailers get glued together to split on another | |
| bracket pair instead. | |
| """ | |
| for omit in generate_trailers_to_omit(line, mode.line_length): | |
| lines = list(right_hand_split(line, mode, features, omit=omit)) | |
| # Note: this check is only able to figure out if the first line of the | |
| # *current* transformation fits in the line length. This is true only | |
| # for simple cases. All others require running more transforms via | |
| # `transform_line()`. This check doesn't know if those would succeed. | |
| if is_line_short_enough(lines[0], mode=mode): | |
| yield from lines | |
| return | |
| # All splits failed, best effort split with no omits. | |
| # This mostly happens to multiline strings that are by definition | |
| # reported as not fitting a single line, as well as lines that contain | |
| # trailing commas (those have to be exploded). | |
| yield from right_hand_split(line, mode, features=features) | |
| # HACK: nested functions (like _rhs) compiled by mypyc don't retain their | |
| # __name__ attribute which is needed in `run_transformer` further down. | |
| # Unfortunately a nested class breaks mypyc too. So a class must be created | |
| # via type ... https://github.com/mypyc/mypyc/issues/884 | |
| rhs = type("rhs", (), {"__call__": _rhs})() | |
| if Preview.string_processing in mode: | |
| if line.inside_brackets: | |
| transformers = [ | |
| string_merge, | |
| string_paren_strip, | |
| string_split, | |
| delimiter_split, | |
| standalone_comment_split, | |
| string_paren_wrap, | |
| rhs, | |
| ] | |
| else: | |
| transformers = [ | |
| string_merge, | |
| string_paren_strip, | |
| string_split, | |
| string_paren_wrap, | |
| rhs, | |
| ] | |
| else: | |
| if line.inside_brackets: | |
| transformers = [delimiter_split, standalone_comment_split, rhs] | |
| else: | |
| transformers = [rhs] | |
| # It's always safe to attempt hugging of power operations and pretty much every line | |
| # could match. | |
| transformers.append(hug_power_op) | |
| for transform in transformers: | |
| # We are accumulating lines in `result` because we might want to abort | |
| # mission and return the original line in the end, or attempt a different | |
| # split altogether. | |
| try: | |
| result = run_transformer(line, transform, mode, features, line_str=line_str) | |
| except CannotTransform: | |
| continue | |
| else: | |
| yield from result | |
| break | |
| else: | |
| yield line | |
| class _BracketSplitComponent(Enum): | |
| head = auto() | |
| body = auto() | |
| tail = auto() | |
| def left_hand_split( | |
| line: Line, _features: Collection[Feature], mode: Mode | |
| ) -> Iterator[Line]: | |
| """Split line into many lines, starting with the first matching bracket pair. | |
| Note: this usually looks weird, only use this for function definitions. | |
| Prefer RHS otherwise. This is why this function is not symmetrical with | |
| :func:`right_hand_split` which also handles optional parentheses. | |
| """ | |
| tail_leaves: List[Leaf] = [] | |
| body_leaves: List[Leaf] = [] | |
| head_leaves: List[Leaf] = [] | |
| current_leaves = head_leaves | |
| matching_bracket: Optional[Leaf] = None | |
| for leaf in line.leaves: | |
| if ( | |
| current_leaves is body_leaves | |
| and leaf.type in CLOSING_BRACKETS | |
| and leaf.opening_bracket is matching_bracket | |
| and isinstance(matching_bracket, Leaf) | |
| ): | |
| ensure_visible(leaf) | |
| ensure_visible(matching_bracket) | |
| current_leaves = tail_leaves if body_leaves else head_leaves | |
| current_leaves.append(leaf) | |
| if current_leaves is head_leaves: | |
| if leaf.type in OPENING_BRACKETS: | |
| matching_bracket = leaf | |
| current_leaves = body_leaves | |
| if not matching_bracket: | |
| raise CannotSplit("No brackets found") | |
| head = bracket_split_build_line( | |
| head_leaves, line, matching_bracket, component=_BracketSplitComponent.head | |
| ) | |
| body = bracket_split_build_line( | |
| body_leaves, line, matching_bracket, component=_BracketSplitComponent.body | |
| ) | |
| tail = bracket_split_build_line( | |
| tail_leaves, line, matching_bracket, component=_BracketSplitComponent.tail | |
| ) | |
| bracket_split_succeeded_or_raise(head, body, tail) | |
| for result in (head, body, tail): | |
| if result: | |
| yield result | |
| def right_hand_split( | |
| line: Line, | |
| mode: Mode, | |
| features: Collection[Feature] = (), | |
| omit: Collection[LeafID] = (), | |
| ) -> Iterator[Line]: | |
| """Split line into many lines, starting with the last matching bracket pair. | |
| If the split was by optional parentheses, attempt splitting without them, too. | |
| `omit` is a collection of closing bracket IDs that shouldn't be considered for | |
| this split. | |
| Note: running this function modifies `bracket_depth` on the leaves of `line`. | |
| """ | |
| rhs_result = _first_right_hand_split(line, omit=omit) | |
| yield from _maybe_split_omitting_optional_parens( | |
| rhs_result, line, mode, features=features, omit=omit | |
| ) | |
| def _first_right_hand_split( | |
| line: Line, | |
| omit: Collection[LeafID] = (), | |
| ) -> RHSResult: | |
| """Split the line into head, body, tail starting with the last bracket pair. | |
| Note: this function should not have side effects. It's relied upon by | |
| _maybe_split_omitting_optional_parens to get an opinion whether to prefer | |
| splitting on the right side of an assignment statement. | |
| """ | |
| tail_leaves: List[Leaf] = [] | |
| body_leaves: List[Leaf] = [] | |
| head_leaves: List[Leaf] = [] | |
| current_leaves = tail_leaves | |
| opening_bracket: Optional[Leaf] = None | |
| closing_bracket: Optional[Leaf] = None | |
| for leaf in reversed(line.leaves): | |
| if current_leaves is body_leaves: | |
| if leaf is opening_bracket: | |
| current_leaves = head_leaves if body_leaves else tail_leaves | |
| current_leaves.append(leaf) | |
| if current_leaves is tail_leaves: | |
| if leaf.type in CLOSING_BRACKETS and id(leaf) not in omit: | |
| opening_bracket = leaf.opening_bracket | |
| closing_bracket = leaf | |
| current_leaves = body_leaves | |
| if not (opening_bracket and closing_bracket and head_leaves): | |
| # If there is no opening or closing_bracket that means the split failed and | |
| # all content is in the tail. Otherwise, if `head_leaves` are empty, it means | |
| # the matching `opening_bracket` wasn't available on `line` anymore. | |
| raise CannotSplit("No brackets found") | |
| tail_leaves.reverse() | |
| body_leaves.reverse() | |
| head_leaves.reverse() | |
| head = bracket_split_build_line( | |
| head_leaves, line, opening_bracket, component=_BracketSplitComponent.head | |
| ) | |
| body = bracket_split_build_line( | |
| body_leaves, line, opening_bracket, component=_BracketSplitComponent.body | |
| ) | |
| tail = bracket_split_build_line( | |
| tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail | |
| ) | |
| bracket_split_succeeded_or_raise(head, body, tail) | |
| return RHSResult(head, body, tail, opening_bracket, closing_bracket) | |
| def _maybe_split_omitting_optional_parens( | |
| rhs: RHSResult, | |
| line: Line, | |
| mode: Mode, | |
| features: Collection[Feature] = (), | |
| omit: Collection[LeafID] = (), | |
| ) -> Iterator[Line]: | |
| if ( | |
| Feature.FORCE_OPTIONAL_PARENTHESES not in features | |
| # the opening bracket is an optional paren | |
| and rhs.opening_bracket.type == token.LPAR | |
| and not rhs.opening_bracket.value | |
| # the closing bracket is an optional paren | |
| and rhs.closing_bracket.type == token.RPAR | |
| and not rhs.closing_bracket.value | |
| # it's not an import (optional parens are the only thing we can split on | |
| # in this case; attempting a split without them is a waste of time) | |
| and not line.is_import | |
| # there are no standalone comments in the body | |
| and not rhs.body.contains_standalone_comments(0) | |
| # and we can actually remove the parens | |
| and can_omit_invisible_parens(rhs, mode.line_length) | |
| ): | |
| omit = {id(rhs.closing_bracket), *omit} | |
| try: | |
| # The RHSResult Omitting Optional Parens. | |
| rhs_oop = _first_right_hand_split(line, omit=omit) | |
| if not ( | |
| Preview.prefer_splitting_right_hand_side_of_assignments in line.mode | |
| # the split is right after `=` | |
| and len(rhs.head.leaves) >= 2 | |
| and rhs.head.leaves[-2].type == token.EQUAL | |
| # the left side of assignment contains brackets | |
| and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1]) | |
| # the left side of assignment is short enough (the -1 is for the ending | |
| # optional paren) | |
| and is_line_short_enough( | |
| rhs.head, mode=replace(mode, line_length=mode.line_length - 1) | |
| ) | |
| # the left side of assignment won't explode further because of magic | |
| # trailing comma | |
| and rhs.head.magic_trailing_comma is None | |
| # the split by omitting optional parens isn't preferred by some other | |
| # reason | |
| and not _prefer_split_rhs_oop(rhs_oop, mode) | |
| ): | |
| yield from _maybe_split_omitting_optional_parens( | |
| rhs_oop, line, mode, features=features, omit=omit | |
| ) | |
| return | |
| except CannotSplit as e: | |
| if not ( | |
| can_be_split(rhs.body) or is_line_short_enough(rhs.body, mode=mode) | |
| ): | |
| raise CannotSplit( | |
| "Splitting failed, body is still too long and can't be split." | |
| ) from e | |
| elif ( | |
| rhs.head.contains_multiline_strings() | |
| or rhs.tail.contains_multiline_strings() | |
| ): | |
| raise CannotSplit( | |
| "The current optional pair of parentheses is bound to fail to" | |
| " satisfy the splitting algorithm because the head or the tail" | |
| " contains multiline strings which by definition never fit one" | |
| " line." | |
| ) from e | |
| ensure_visible(rhs.opening_bracket) | |
| ensure_visible(rhs.closing_bracket) | |
| for result in (rhs.head, rhs.body, rhs.tail): | |
| if result: | |
| yield result | |
| def _prefer_split_rhs_oop(rhs_oop: RHSResult, mode: Mode) -> bool: | |
| """ | |
| Returns whether we should prefer the result from a split omitting optional parens. | |
| """ | |
| has_closing_bracket_after_assign = False | |
| for leaf in reversed(rhs_oop.head.leaves): | |
| if leaf.type == token.EQUAL: | |
| break | |
| if leaf.type in CLOSING_BRACKETS: | |
| has_closing_bracket_after_assign = True | |
| break | |
| return ( | |
| # contains matching brackets after the `=` (done by checking there is a | |
| # closing bracket) | |
| has_closing_bracket_after_assign | |
| or ( | |
| # the split is actually from inside the optional parens (done by checking | |
| # the first line still contains the `=`) | |
| any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves) | |
| # the first line is short enough | |
| and is_line_short_enough(rhs_oop.head, mode=mode) | |
| ) | |
| # contains unsplittable type ignore | |
| or rhs_oop.head.contains_unsplittable_type_ignore() | |
| or rhs_oop.body.contains_unsplittable_type_ignore() | |
| or rhs_oop.tail.contains_unsplittable_type_ignore() | |
| ) | |
| def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None: | |
| """Raise :exc:`CannotSplit` if the last left- or right-hand split failed. | |
| Do nothing otherwise. | |
| A left- or right-hand split is based on a pair of brackets. Content before | |
| (and including) the opening bracket is left on one line, content inside the | |
| brackets is put on a separate line, and finally content starting with and | |
| following the closing bracket is put on a separate line. | |
| Those are called `head`, `body`, and `tail`, respectively. If the split | |
| produced the same line (all content in `head`) or ended up with an empty `body` | |
| and the `tail` is just the closing bracket, then it's considered failed. | |
| """ | |
| tail_len = len(str(tail).strip()) | |
| if not body: | |
| if tail_len == 0: | |
| raise CannotSplit("Splitting brackets produced the same line") | |
| elif tail_len < 3: | |
| raise CannotSplit( | |
| f"Splitting brackets on an empty body to save {tail_len} characters is" | |
| " not worth it" | |
| ) | |
| def bracket_split_build_line( | |
| leaves: List[Leaf], | |
| original: Line, | |
| opening_bracket: Leaf, | |
| *, | |
| component: _BracketSplitComponent, | |
| ) -> Line: | |
| """Return a new line with given `leaves` and respective comments from `original`. | |
| If it's the head component, brackets will be tracked so trailing commas are | |
| respected. | |
| If it's the body component, the result line is one-indented inside brackets and as | |
| such has its first leaf's prefix normalized and a trailing comma added when | |
| expected. | |
| """ | |
| result = Line(mode=original.mode, depth=original.depth) | |
| if component is _BracketSplitComponent.body: | |
| result.inside_brackets = True | |
| result.depth += 1 | |
| if leaves: | |
| # Since body is a new indent level, remove spurious leading whitespace. | |
| normalize_prefix(leaves[0], inside_brackets=True) | |
| # Ensure a trailing comma for imports and standalone function arguments, but | |
| # be careful not to add one after any comments or within type annotations. | |
| no_commas = ( | |
| original.is_def | |
| and opening_bracket.value == "(" | |
| and not any(leaf.type == token.COMMA for leaf in leaves) | |
| # In particular, don't add one within a parenthesized return annotation. | |
| # Unfortunately the indicator we're in a return annotation (RARROW) may | |
| # be defined directly in the parent node, the parent of the parent ... | |
| # and so on depending on how complex the return annotation is. | |
| # This isn't perfect and there's some false negatives but they are in | |
| # contexts were a comma is actually fine. | |
| and not any( | |
| node.prev_sibling.type == RARROW | |
| for node in ( | |
| leaves[0].parent, | |
| getattr(leaves[0].parent, "parent", None), | |
| ) | |
| if isinstance(node, Node) and isinstance(node.prev_sibling, Leaf) | |
| ) | |
| # Except the false negatives above for PEP 604 unions where we | |
| # can't add the comma. | |
| and not ( | |
| leaves[0].parent | |
| and leaves[0].parent.next_sibling | |
| and leaves[0].parent.next_sibling.type == token.VBAR | |
| ) | |
| ) | |
| if original.is_import or no_commas: | |
| for i in range(len(leaves) - 1, -1, -1): | |
| if leaves[i].type == STANDALONE_COMMENT: | |
| continue | |
| if leaves[i].type != token.COMMA: | |
| new_comma = Leaf(token.COMMA, ",") | |
| leaves.insert(i + 1, new_comma) | |
| break | |
| leaves_to_track: Set[LeafID] = set() | |
| if component is _BracketSplitComponent.head: | |
| leaves_to_track = get_leaves_inside_matching_brackets(leaves) | |
| # Populate the line | |
| for leaf in leaves: | |
| result.append( | |
| leaf, | |
| preformatted=True, | |
| track_bracket=id(leaf) in leaves_to_track, | |
| ) | |
| for comment_after in original.comments_after(leaf): | |
| result.append(comment_after, preformatted=True) | |
| if component is _BracketSplitComponent.body and should_split_line( | |
| result, opening_bracket | |
| ): | |
| result.should_split_rhs = True | |
| return result | |
| def dont_increase_indentation(split_func: Transformer) -> Transformer: | |
| """Normalize prefix of the first leaf in every line returned by `split_func`. | |
| This is a decorator over relevant split functions. | |
| """ | |
| def split_wrapper( | |
| line: Line, features: Collection[Feature], mode: Mode | |
| ) -> Iterator[Line]: | |
| for split_line in split_func(line, features, mode): | |
| normalize_prefix(split_line.leaves[0], inside_brackets=True) | |
| yield split_line | |
| return split_wrapper | |
| def _get_last_non_comment_leaf(line: Line) -> Optional[int]: | |
| for leaf_idx in range(len(line.leaves) - 1, 0, -1): | |
| if line.leaves[leaf_idx].type != STANDALONE_COMMENT: | |
| return leaf_idx | |
| return None | |
| def _safe_add_trailing_comma(safe: bool, delimiter_priority: int, line: Line) -> Line: | |
| if ( | |
| safe | |
| and delimiter_priority == COMMA_PRIORITY | |
| and line.leaves[-1].type != token.COMMA | |
| and line.leaves[-1].type != STANDALONE_COMMENT | |
| ): | |
| new_comma = Leaf(token.COMMA, ",") | |
| line.append(new_comma) | |
| return line | |
| def delimiter_split( | |
| line: Line, features: Collection[Feature], mode: Mode | |
| ) -> Iterator[Line]: | |
| """Split according to delimiters of the highest priority. | |
| If the appropriate Features are given, the split will add trailing commas | |
| also in function signatures and calls that contain `*` and `**`. | |
| """ | |
| try: | |
| last_leaf = line.leaves[-1] | |
| except IndexError: | |
| raise CannotSplit("Line empty") from None | |
| bt = line.bracket_tracker | |
| try: | |
| delimiter_priority = bt.max_delimiter_priority(exclude={id(last_leaf)}) | |
| except ValueError: | |
| raise CannotSplit("No delimiters found") from None | |
| if delimiter_priority == DOT_PRIORITY: | |
| if bt.delimiter_count_with_priority(delimiter_priority) == 1: | |
| raise CannotSplit("Splitting a single attribute from its owner looks wrong") | |
| current_line = Line( | |
| mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets | |
| ) | |
| lowest_depth = sys.maxsize | |
| trailing_comma_safe = True | |
| def append_to_line(leaf: Leaf) -> Iterator[Line]: | |
| """Append `leaf` to current line or to new line if appending impossible.""" | |
| nonlocal current_line | |
| try: | |
| current_line.append_safe(leaf, preformatted=True) | |
| except ValueError: | |
| yield current_line | |
| current_line = Line( | |
| mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets | |
| ) | |
| current_line.append(leaf) | |
| last_non_comment_leaf = _get_last_non_comment_leaf(line) | |
| for leaf_idx, leaf in enumerate(line.leaves): | |
| yield from append_to_line(leaf) | |
| for comment_after in line.comments_after(leaf): | |
| yield from append_to_line(comment_after) | |
| lowest_depth = min(lowest_depth, leaf.bracket_depth) | |
| if leaf.bracket_depth == lowest_depth: | |
| if is_vararg(leaf, within={syms.typedargslist}): | |
| trailing_comma_safe = ( | |
| trailing_comma_safe and Feature.TRAILING_COMMA_IN_DEF in features | |
| ) | |
| elif is_vararg(leaf, within={syms.arglist, syms.argument}): | |
| trailing_comma_safe = ( | |
| trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features | |
| ) | |
| if ( | |
| Preview.add_trailing_comma_consistently in mode | |
| and last_leaf.type == STANDALONE_COMMENT | |
| and leaf_idx == last_non_comment_leaf | |
| ): | |
| current_line = _safe_add_trailing_comma( | |
| trailing_comma_safe, delimiter_priority, current_line | |
| ) | |
| leaf_priority = bt.delimiters.get(id(leaf)) | |
| if leaf_priority == delimiter_priority: | |
| yield current_line | |
| current_line = Line( | |
| mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets | |
| ) | |
| if current_line: | |
| current_line = _safe_add_trailing_comma( | |
| trailing_comma_safe, delimiter_priority, current_line | |
| ) | |
| yield current_line | |
| def standalone_comment_split( | |
| line: Line, features: Collection[Feature], mode: Mode | |
| ) -> Iterator[Line]: | |
| """Split standalone comments from the rest of the line.""" | |
| if not line.contains_standalone_comments(0): | |
| raise CannotSplit("Line does not have any standalone comments") | |
| current_line = Line( | |
| mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets | |
| ) | |
| def append_to_line(leaf: Leaf) -> Iterator[Line]: | |
| """Append `leaf` to current line or to new line if appending impossible.""" | |
| nonlocal current_line | |
| try: | |
| current_line.append_safe(leaf, preformatted=True) | |
| except ValueError: | |
| yield current_line | |
| current_line = Line( | |
| line.mode, depth=line.depth, inside_brackets=line.inside_brackets | |
| ) | |
| current_line.append(leaf) | |
| for leaf in line.leaves: | |
| yield from append_to_line(leaf) | |
| for comment_after in line.comments_after(leaf): | |
| yield from append_to_line(comment_after) | |
| if current_line: | |
| yield current_line | |
| def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: | |
| """Leave existing extra newlines if not `inside_brackets`. Remove everything | |
| else. | |
| Note: don't use backslashes for formatting or you'll lose your voting rights. | |
| """ | |
| if not inside_brackets: | |
| spl = leaf.prefix.split("#") | |
| if "\\" not in spl[0]: | |
| nl_count = spl[-1].count("\n") | |
| if len(spl) > 1: | |
| nl_count -= 1 | |
| leaf.prefix = "\n" * nl_count | |
| return | |
| leaf.prefix = "" | |
| def normalize_invisible_parens( | |
| node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature] | |
| ) -> None: | |
| """Make existing optional parentheses invisible or create new ones. | |
| `parens_after` is a set of string leaf values immediately after which parens | |
| should be put. | |
| Standardizes on visible parentheses for single-element tuples, and keeps | |
| existing visible parentheses for other tuples and generator expressions. | |
| """ | |
| for pc in list_comments(node.prefix, is_endmarker=False): | |
| if pc.value in FMT_OFF: | |
| # This `node` has a prefix with `# fmt: off`, don't mess with parens. | |
| return | |
| # The multiple context managers grammar has a different pattern, thus this is | |
| # separate from the for-loop below. This possibly wraps them in invisible parens, | |
| # and later will be removed in remove_with_parens when needed. | |
| if node.type == syms.with_stmt: | |
| _maybe_wrap_cms_in_parens(node, mode, features) | |
| check_lpar = False | |
| for index, child in enumerate(list(node.children)): | |
| # Fixes a bug where invisible parens are not properly stripped from | |
| # assignment statements that contain type annotations. | |
| if isinstance(child, Node) and child.type == syms.annassign: | |
| normalize_invisible_parens( | |
| child, parens_after=parens_after, mode=mode, features=features | |
| ) | |
| # Add parentheses around long tuple unpacking in assignments. | |
| if ( | |
| index == 0 | |
| and isinstance(child, Node) | |
| and child.type == syms.testlist_star_expr | |
| ): | |
| check_lpar = True | |
| if check_lpar: | |
| if ( | |
| child.type == syms.atom | |
| and node.type == syms.for_stmt | |
| and isinstance(child.prev_sibling, Leaf) | |
| and child.prev_sibling.type == token.NAME | |
| and child.prev_sibling.value == "for" | |
| ): | |
| if maybe_make_parens_invisible_in_atom( | |
| child, | |
| parent=node, | |
| remove_brackets_around_comma=True, | |
| ): | |
| wrap_in_parentheses(node, child, visible=False) | |
| elif isinstance(child, Node) and node.type == syms.with_stmt: | |
| remove_with_parens(child, node) | |
| elif child.type == syms.atom: | |
| if maybe_make_parens_invisible_in_atom( | |
| child, | |
| parent=node, | |
| ): | |
| wrap_in_parentheses(node, child, visible=False) | |
| elif is_one_tuple(child): | |
| wrap_in_parentheses(node, child, visible=True) | |
| elif node.type == syms.import_from: | |
| _normalize_import_from(node, child, index) | |
| break | |
| elif ( | |
| index == 1 | |
| and child.type == token.STAR | |
| and node.type == syms.except_clause | |
| ): | |
| # In except* (PEP 654), the star is actually part of | |
| # of the keyword. So we need to skip the insertion of | |
| # invisible parentheses to work more precisely. | |
| continue | |
| elif not (isinstance(child, Leaf) and is_multiline_string(child)): | |
| wrap_in_parentheses(node, child, visible=False) | |
| comma_check = child.type == token.COMMA | |
| check_lpar = isinstance(child, Leaf) and ( | |
| child.value in parens_after or comma_check | |
| ) | |
| def _normalize_import_from(parent: Node, child: LN, index: int) -> None: | |
| # "import from" nodes store parentheses directly as part of | |
| # the statement | |
| if is_lpar_token(child): | |
| assert is_rpar_token(parent.children[-1]) | |
| # make parentheses invisible | |
| child.value = "" | |
| parent.children[-1].value = "" | |
| elif child.type != token.STAR: | |
| # insert invisible parentheses | |
| parent.insert_child(index, Leaf(token.LPAR, "")) | |
| parent.append_child(Leaf(token.RPAR, "")) | |
| def remove_await_parens(node: Node) -> None: | |
| if node.children[0].type == token.AWAIT and len(node.children) > 1: | |
| if ( | |
| node.children[1].type == syms.atom | |
| and node.children[1].children[0].type == token.LPAR | |
| ): | |
| if maybe_make_parens_invisible_in_atom( | |
| node.children[1], | |
| parent=node, | |
| remove_brackets_around_comma=True, | |
| ): | |
| wrap_in_parentheses(node, node.children[1], visible=False) | |
| # Since await is an expression we shouldn't remove | |
| # brackets in cases where this would change | |
| # the AST due to operator precedence. | |
| # Therefore we only aim to remove brackets around | |
| # power nodes that aren't also await expressions themselves. | |
| # https://peps.python.org/pep-0492/#updated-operator-precedence-table | |
| # N.B. We've still removed any redundant nested brackets though :) | |
| opening_bracket = cast(Leaf, node.children[1].children[0]) | |
| closing_bracket = cast(Leaf, node.children[1].children[-1]) | |
| bracket_contents = node.children[1].children[1] | |
| if isinstance(bracket_contents, Node): | |
| if bracket_contents.type != syms.power: | |
| ensure_visible(opening_bracket) | |
| ensure_visible(closing_bracket) | |
| elif ( | |
| bracket_contents.type == syms.power | |
| and bracket_contents.children[0].type == token.AWAIT | |
| ): | |
| ensure_visible(opening_bracket) | |
| ensure_visible(closing_bracket) | |
| # If we are in a nested await then recurse down. | |
| remove_await_parens(bracket_contents) | |
| def _maybe_wrap_cms_in_parens( | |
| node: Node, mode: Mode, features: Collection[Feature] | |
| ) -> None: | |
| """When enabled and safe, wrap the multiple context managers in invisible parens. | |
| It is only safe when `features` contain Feature.PARENTHESIZED_CONTEXT_MANAGERS. | |
| """ | |
| if ( | |
| Feature.PARENTHESIZED_CONTEXT_MANAGERS not in features | |
| or Preview.wrap_multiple_context_managers_in_parens not in mode | |
| or len(node.children) <= 2 | |
| # If it's an atom, it's already wrapped in parens. | |
| or node.children[1].type == syms.atom | |
| ): | |
| return | |
| colon_index: Optional[int] = None | |
| for i in range(2, len(node.children)): | |
| if node.children[i].type == token.COLON: | |
| colon_index = i | |
| break | |
| if colon_index is not None: | |
| lpar = Leaf(token.LPAR, "") | |
| rpar = Leaf(token.RPAR, "") | |
| context_managers = node.children[1:colon_index] | |
| for child in context_managers: | |
| child.remove() | |
| # After wrapping, the with_stmt will look like this: | |
| # with_stmt | |
| # NAME 'with' | |
| # atom | |
| # LPAR '' | |
| # testlist_gexp | |
| # ... <-- context_managers | |
| # /testlist_gexp | |
| # RPAR '' | |
| # /atom | |
| # COLON ':' | |
| new_child = Node( | |
| syms.atom, [lpar, Node(syms.testlist_gexp, context_managers), rpar] | |
| ) | |
| node.insert_child(1, new_child) | |
| def remove_with_parens(node: Node, parent: Node) -> None: | |
| """Recursively hide optional parens in `with` statements.""" | |
| # Removing all unnecessary parentheses in with statements in one pass is a tad | |
| # complex as different variations of bracketed statements result in pretty | |
| # different parse trees: | |
| # | |
| # with (open("file")) as f: # this is an asexpr_test | |
| # ... | |
| # | |
| # with (open("file") as f): # this is an atom containing an | |
| # ... # asexpr_test | |
| # | |
| # with (open("file")) as f, (open("file")) as f: # this is asexpr_test, COMMA, | |
| # ... # asexpr_test | |
| # | |
| # with (open("file") as f, open("file") as f): # an atom containing a | |
| # ... # testlist_gexp which then | |
| # # contains multiple asexpr_test(s) | |
| if node.type == syms.atom: | |
| if maybe_make_parens_invisible_in_atom( | |
| node, | |
| parent=parent, | |
| remove_brackets_around_comma=True, | |
| ): | |
| wrap_in_parentheses(parent, node, visible=False) | |
| if isinstance(node.children[1], Node): | |
| remove_with_parens(node.children[1], node) | |
| elif node.type == syms.testlist_gexp: | |
| for child in node.children: | |
| if isinstance(child, Node): | |
| remove_with_parens(child, node) | |
| elif node.type == syms.asexpr_test and not any( | |
| leaf.type == token.COLONEQUAL for leaf in node.leaves() | |
| ): | |
| if maybe_make_parens_invisible_in_atom( | |
| node.children[0], | |
| parent=node, | |
| remove_brackets_around_comma=True, | |
| ): | |
| wrap_in_parentheses(node, node.children[0], visible=False) | |
| def maybe_make_parens_invisible_in_atom( | |
| node: LN, | |
| parent: LN, | |
| remove_brackets_around_comma: bool = False, | |
| ) -> bool: | |
| """If it's safe, make the parens in the atom `node` invisible, recursively. | |
| Additionally, remove repeated, adjacent invisible parens from the atom `node` | |
| as they are redundant. | |
| Returns whether the node should itself be wrapped in invisible parentheses. | |
| """ | |
| if ( | |
| node.type != syms.atom | |
| or is_empty_tuple(node) | |
| or is_one_tuple(node) | |
| or (is_yield(node) and parent.type != syms.expr_stmt) | |
| or ( | |
| # This condition tries to prevent removing non-optional brackets | |
| # around a tuple, however, can be a bit overzealous so we provide | |
| # and option to skip this check for `for` and `with` statements. | |
| not remove_brackets_around_comma | |
| and max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY | |
| ) | |
| or is_tuple_containing_walrus(node) | |
| ): | |
| return False | |
| if is_walrus_assignment(node): | |
| if parent.type in [ | |
| syms.annassign, | |
| syms.expr_stmt, | |
| syms.assert_stmt, | |
| syms.return_stmt, | |
| syms.except_clause, | |
| syms.funcdef, | |
| syms.with_stmt, | |
| # these ones aren't useful to end users, but they do please fuzzers | |
| syms.for_stmt, | |
| syms.del_stmt, | |
| syms.for_stmt, | |
| ]: | |
| return False | |
| first = node.children[0] | |
| last = node.children[-1] | |
| if is_lpar_token(first) and is_rpar_token(last): | |
| middle = node.children[1] | |
| # make parentheses invisible | |
| if ( | |
| # If the prefix of `middle` includes a type comment with | |
| # ignore annotation, then we do not remove the parentheses | |
| not is_type_ignore_comment_string(middle.prefix.strip()) | |
| ): | |
| first.value = "" | |
| last.value = "" | |
| maybe_make_parens_invisible_in_atom( | |
| middle, | |
| parent=parent, | |
| remove_brackets_around_comma=remove_brackets_around_comma, | |
| ) | |
| if is_atom_with_invisible_parens(middle): | |
| # Strip the invisible parens from `middle` by replacing | |
| # it with the child in-between the invisible parens | |
| middle.replace(middle.children[1]) | |
| return False | |
| return True | |
| def should_split_line(line: Line, opening_bracket: Leaf) -> bool: | |
| """Should `line` be immediately split with `delimiter_split()` after RHS?""" | |
| if not (opening_bracket.parent and opening_bracket.value in "[{("): | |
| return False | |
| # We're essentially checking if the body is delimited by commas and there's more | |
| # than one of them (we're excluding the trailing comma and if the delimiter priority | |
| # is still commas, that means there's more). | |
| exclude = set() | |
| trailing_comma = False | |
| try: | |
| last_leaf = line.leaves[-1] | |
| if last_leaf.type == token.COMMA: | |
| trailing_comma = True | |
| exclude.add(id(last_leaf)) | |
| max_priority = line.bracket_tracker.max_delimiter_priority(exclude=exclude) | |
| except (IndexError, ValueError): | |
| return False | |
| return max_priority == COMMA_PRIORITY and ( | |
| (line.mode.magic_trailing_comma and trailing_comma) | |
| # always explode imports | |
| or opening_bracket.parent.type in {syms.atom, syms.import_from} | |
| ) | |
| def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[LeafID]]: | |
| """Generate sets of closing bracket IDs that should be omitted in a RHS. | |
| Brackets can be omitted if the entire trailer up to and including | |
| a preceding closing bracket fits in one line. | |
| Yielded sets are cumulative (contain results of previous yields, too). First | |
| set is empty, unless the line should explode, in which case bracket pairs until | |
| the one that needs to explode are omitted. | |
| """ | |
| omit: Set[LeafID] = set() | |
| if not line.magic_trailing_comma: | |
| yield omit | |
| length = 4 * line.depth | |
| opening_bracket: Optional[Leaf] = None | |
| closing_bracket: Optional[Leaf] = None | |
| inner_brackets: Set[LeafID] = set() | |
| for index, leaf, leaf_length in line.enumerate_with_length(reversed=True): | |
| length += leaf_length | |
| if length > line_length: | |
| break | |
| has_inline_comment = leaf_length > len(leaf.value) + len(leaf.prefix) | |
| if leaf.type == STANDALONE_COMMENT or has_inline_comment: | |
| break | |
| if opening_bracket: | |
| if leaf is opening_bracket: | |
| opening_bracket = None | |
| elif leaf.type in CLOSING_BRACKETS: | |
| prev = line.leaves[index - 1] if index > 0 else None | |
| if ( | |
| prev | |
| and prev.type == token.COMMA | |
| and leaf.opening_bracket is not None | |
| and not is_one_sequence_between( | |
| leaf.opening_bracket, leaf, line.leaves | |
| ) | |
| ): | |
| # Never omit bracket pairs with trailing commas. | |
| # We need to explode on those. | |
| break | |
| inner_brackets.add(id(leaf)) | |
| elif leaf.type in CLOSING_BRACKETS: | |
| prev = line.leaves[index - 1] if index > 0 else None | |
| if prev and prev.type in OPENING_BRACKETS: | |
| # Empty brackets would fail a split so treat them as "inner" | |
| # brackets (e.g. only add them to the `omit` set if another | |
| # pair of brackets was good enough. | |
| inner_brackets.add(id(leaf)) | |
| continue | |
| if closing_bracket: | |
| omit.add(id(closing_bracket)) | |
| omit.update(inner_brackets) | |
| inner_brackets.clear() | |
| yield omit | |
| if ( | |
| prev | |
| and prev.type == token.COMMA | |
| and leaf.opening_bracket is not None | |
| and not is_one_sequence_between(leaf.opening_bracket, leaf, line.leaves) | |
| ): | |
| # Never omit bracket pairs with trailing commas. | |
| # We need to explode on those. | |
| break | |
| if leaf.value: | |
| opening_bracket = leaf.opening_bracket | |
| closing_bracket = leaf | |
| def run_transformer( | |
| line: Line, | |
| transform: Transformer, | |
| mode: Mode, | |
| features: Collection[Feature], | |
| *, | |
| line_str: str = "", | |
| ) -> List[Line]: | |
| if not line_str: | |
| line_str = line_to_string(line) | |
| result: List[Line] = [] | |
| for transformed_line in transform(line, features, mode): | |
| if str(transformed_line).strip("\n") == line_str: | |
| raise CannotTransform("Line transformer returned an unchanged result") | |
| result.extend(transform_line(transformed_line, mode=mode, features=features)) | |
| features_set = set(features) | |
| if ( | |
| Feature.FORCE_OPTIONAL_PARENTHESES in features_set | |
| or transform.__class__.__name__ != "rhs" | |
| or not line.bracket_tracker.invisible | |
| or any(bracket.value for bracket in line.bracket_tracker.invisible) | |
| or line.contains_multiline_strings() | |
| or result[0].contains_uncollapsable_type_comments() | |
| or result[0].contains_unsplittable_type_ignore() | |
| or is_line_short_enough(result[0], mode=mode) | |
| # If any leaves have no parents (which _can_ occur since | |
| # `transform(line)` potentially destroys the line's underlying node | |
| # structure), then we can't proceed. Doing so would cause the below | |
| # call to `append_leaves()` to fail. | |
| or any(leaf.parent is None for leaf in line.leaves) | |
| ): | |
| return result | |
| line_copy = line.clone() | |
| append_leaves(line_copy, line, line.leaves) | |
| features_fop = features_set | {Feature.FORCE_OPTIONAL_PARENTHESES} | |
| second_opinion = run_transformer( | |
| line_copy, transform, mode, features_fop, line_str=line_str | |
| ) | |
| if all(is_line_short_enough(ln, mode=mode) for ln in second_opinion): | |
| result = second_opinion | |
| return result | |