+++ /dev/null
-"""
-Generating lines of code.
-"""
-from functools import partial, wraps
-import sys
-from typing import Collection, Iterator, List, Optional, Set, Union
-
-from dataclasses import dataclass, field
-
-from black.nodes import WHITESPACE, RARROW, STATEMENT, STANDALONE_COMMENT
-from black.nodes import ASSIGNMENTS, OPENING_BRACKETS, CLOSING_BRACKETS
-from black.nodes import Visitor, syms, first_child_is_arith, ensure_visible
-from black.nodes import is_docstring, is_empty_tuple, is_one_tuple, is_one_tuple_between
-from black.nodes import is_walrus_assignment, is_yield, is_vararg, is_multiline_string
-from black.nodes import is_stub_suite, is_stub_body, is_atom_with_invisible_parens
-from black.nodes import wrap_in_parentheses
-from black.brackets import max_delimiter_priority_in_atom
-from black.brackets import DOT_PRIORITY, COMMA_PRIORITY
-from black.lines import Line, line_to_string, is_line_short_enough
-from black.lines import can_omit_invisible_parens, can_be_split, append_leaves
-from black.comments import generate_comments, list_comments, FMT_OFF
-from black.numerics import normalize_numeric_literal
-from black.strings import get_string_prefix, fix_docstring
-from black.strings import normalize_string_prefix, normalize_string_quotes
-from black.trans import Transformer, CannotTransform, StringMerger
-from black.trans import StringSplitter, StringParenWrapper, StringParenStripper
-from black.mode import Mode
-from black.mode import Feature
-
-from blib2to3.pytree import Node, Leaf
-from blib2to3.pgen2 import token
-
-
-# types
-LeafID = int
-LN = Union[Leaf, Node]
-
-
-class CannotSplit(CannotTransform):
- """A readable split that fits the allotted line length is impossible."""
-
-
-@dataclass
-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.
- """
-
- mode: Mode
- remove_u_prefix: bool = False
- current_line: Line = field(init=False)
-
- 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.
-
- 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, remove_u_prefix=self.remove_u_prefix
- )
- 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_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)
- for child in node.children:
- if child.type == token.NAME and child.value in keywords: # type: ignore
- yield from self.line()
-
- 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."""
- if first_child_is_arith(node):
- wrap_in_parentheses(node, node.children[0], visible=False)
- 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:
- break
-
- internal_stmt = next(children)
- 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_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 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.
- docstring = normalize_string_prefix(leaf.value, self.remove_u_prefix)
- 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
-
- if is_multiline_string(leaf):
- indent = " " * 4 * self.current_line.depth
- docstring = fix_docstring(docstring, indent)
- else:
- docstring = docstring.strip()
-
- 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 += " "
- elif not docstring_started_empty:
- docstring = " "
-
- # We could enforce triple quotes at this point.
- quote = quote_char * quote_len
- 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=Ø)
- self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø)
- self.visit_funcdef = partial(v, keywords={"def"}, parens=Ø)
- 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
-
-
-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, line_length=mode.line_length, 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 mode.experimental_string_processing:
- transformers = [string_merge, string_paren_strip]
- else:
- transformers = []
- elif line.is_def:
- transformers = [left_hand_split]
- else:
-
- def rhs(line: Line, features: Collection[Feature]) -> 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.line_length, 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], line_length=mode.line_length):
- 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, line_length=mode.line_length, features=features
- )
-
- if mode.experimental_string_processing:
- 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]
-
- 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
-
-
-def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> 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
- ):
- 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)
- body = bracket_split_build_line(body_leaves, line, matching_bracket, is_body=True)
- tail = bracket_split_build_line(tail_leaves, line, matching_bracket)
- bracket_split_succeeded_or_raise(head, body, tail)
- for result in (head, body, tail):
- if result:
- yield result
-
-
-def right_hand_split(
- line: Line,
- line_length: int,
- 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`.
- """
- 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)
- body = bracket_split_build_line(body_leaves, line, opening_bracket, is_body=True)
- tail = bracket_split_build_line(tail_leaves, line, opening_bracket)
- bracket_split_succeeded_or_raise(head, body, tail)
- if (
- Feature.FORCE_OPTIONAL_PARENTHESES not in features
- # the opening bracket is an optional paren
- and opening_bracket.type == token.LPAR
- and not opening_bracket.value
- # the closing bracket is an optional paren
- and closing_bracket.type == token.RPAR
- and not 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 body.contains_standalone_comments(0)
- # and we can actually remove the parens
- and can_omit_invisible_parens(body, line_length, omit_on_explode=omit)
- ):
- omit = {id(closing_bracket), *omit}
- try:
- yield from right_hand_split(line, line_length, features=features, omit=omit)
- return
-
- except CannotSplit as e:
- if not (
- can_be_split(body)
- or is_line_short_enough(body, line_length=line_length)
- ):
- raise CannotSplit(
- "Splitting failed, body is still too long and can't be split."
- ) from e
-
- elif head.contains_multiline_strings() or 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(opening_bracket)
- ensure_visible(closing_bracket)
- for result in (head, body, tail):
- if result:
- yield result
-
-
-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, *, is_body: bool = False
-) -> Line:
- """Return a new line with given `leaves` and respective comments from `original`.
-
- If `is_body` is True, 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 is_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)
- )
- )
-
- 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
-
- # Populate the line
- for leaf in leaves:
- result.append(leaf, preformatted=True)
- for comment_after in original.comments_after(leaf):
- result.append(comment_after, preformatted=True)
- if is_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.
- """
-
- @wraps(split_func)
- def split_wrapper(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]:
- for line in split_func(line, features):
- normalize_prefix(line.leaves[0], inside_brackets=True)
- yield line
-
- return split_wrapper
-
-
-@dont_increase_indentation
-def delimiter_split(line: Line, features: Collection[Feature] = ()) -> 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)
-
- 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)
-
- 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
- )
-
- 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:
- if (
- trailing_comma_safe
- and delimiter_priority == COMMA_PRIORITY
- and current_line.leaves[-1].type != token.COMMA
- and current_line.leaves[-1].type != STANDALONE_COMMENT
- ):
- new_comma = Leaf(token.COMMA, ",")
- current_line.append(new_comma)
- yield current_line
-
-
-@dont_increase_indentation
-def standalone_comment_split(
- line: Line, features: Collection[Feature] = ()
-) -> 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]) -> 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
- 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)
-
- # 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:
- 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:
- # "import from" nodes store parentheses directly as part of
- # the statement
- if child.type == token.LPAR:
- # make parentheses invisible
- child.value = "" # type: ignore
- node.children[-1].value = "" # type: ignore
- elif child.type != token.STAR:
- # insert invisible parentheses
- node.insert_child(index, Leaf(token.LPAR, ""))
- node.append_child(Leaf(token.RPAR, ""))
- break
-
- elif not (isinstance(child, Leaf) and is_multiline_string(child)):
- wrap_in_parentheses(node, child, visible=False)
-
- check_lpar = isinstance(child, Leaf) and child.value in parens_after
-
-
-def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> 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 max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY
- ):
- return False
-
- if is_walrus_assignment(node):
- if parent.type in [
- syms.annassign,
- syms.expr_stmt,
- syms.assert_stmt,
- syms.return_stmt,
- # these ones aren't useful to end users, but they do please fuzzers
- syms.for_stmt,
- syms.del_stmt,
- ]:
- return False
-
- first = node.children[0]
- last = node.children[-1]
- if first.type == token.LPAR and last.type == token.RPAR:
- middle = node.children[1]
- # make parentheses invisible
- first.value = "" # type: ignore
- last.value = "" # type: ignore
- maybe_make_parens_invisible_in_atom(middle, parent=parent)
-
- 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 not is_one_tuple_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 not is_one_tuple_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):
- 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))
-
- if (
- transform.__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], line_length=mode.line_length)
- # 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 = set(features) | {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, line_length=mode.line_length) for ln in second_opinion
- ):
- result = second_opinion
- return result