kaiserpister's picture
Upload folder using huggingface_hub
6eefbd7
import itertools
import math
import sys
from dataclasses import dataclass, field
from typing import (
Callable,
Dict,
Iterator,
List,
Optional,
Sequence,
Tuple,
TypeVar,
Union,
cast,
)
from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, BracketTracker
from black.mode import Mode, Preview
from black.nodes import (
BRACKETS,
CLOSING_BRACKETS,
OPENING_BRACKETS,
STANDALONE_COMMENT,
TEST_DESCENDANTS,
child_towards,
is_import,
is_multiline_string,
is_one_sequence_between,
is_type_comment,
is_type_ignore_comment,
is_with_or_async_with_stmt,
replace_child,
syms,
whitespace,
)
from black.strings import str_width
from blib2to3.pgen2 import token
from blib2to3.pytree import Leaf, Node
# types
T = TypeVar("T")
Index = int
LeafID = int
LN = Union[Leaf, Node]
@dataclass
class Line:
"""Holds leaves and comments. Can be printed with `str(line)`."""
mode: Mode
depth: int = 0
leaves: List[Leaf] = field(default_factory=list)
# keys ordered like `leaves`
comments: Dict[LeafID, List[Leaf]] = field(default_factory=dict)
bracket_tracker: BracketTracker = field(default_factory=BracketTracker)
inside_brackets: bool = False
should_split_rhs: bool = False
magic_trailing_comma: Optional[Leaf] = None
def append(
self, leaf: Leaf, preformatted: bool = False, track_bracket: bool = False
) -> None:
"""Add a new `leaf` to the end of the line.
Unless `preformatted` is True, the `leaf` will receive a new consistent
whitespace prefix and metadata applied by :class:`BracketTracker`.
Trailing commas are maybe removed, unpacked for loop variables are
demoted from being delimiters.
Inline comments are put aside.
"""
has_value = leaf.type in BRACKETS or bool(leaf.value.strip())
if not has_value:
return
if token.COLON == leaf.type and self.is_class_paren_empty:
del self.leaves[-2:]
if self.leaves and not preformatted:
# Note: at this point leaf.prefix should be empty except for
# imports, for which we only preserve newlines.
leaf.prefix += whitespace(
leaf, complex_subscript=self.is_complex_subscript(leaf)
)
if self.inside_brackets or not preformatted or track_bracket:
self.bracket_tracker.mark(leaf)
if self.mode.magic_trailing_comma:
if self.has_magic_trailing_comma(leaf):
self.magic_trailing_comma = leaf
elif self.has_magic_trailing_comma(leaf, ensure_removable=True):
self.remove_trailing_comma()
if not self.append_comment(leaf):
self.leaves.append(leaf)
def append_safe(self, leaf: Leaf, preformatted: bool = False) -> None:
"""Like :func:`append()` but disallow invalid standalone comment structure.
Raises ValueError when any `leaf` is appended after a standalone comment
or when a standalone comment is not the first leaf on the line.
"""
if self.bracket_tracker.depth == 0:
if self.is_comment:
raise ValueError("cannot append to standalone comments")
if self.leaves and leaf.type == STANDALONE_COMMENT:
raise ValueError(
"cannot append standalone comments to a populated line"
)
self.append(leaf, preformatted=preformatted)
@property
def is_comment(self) -> bool:
"""Is this line a standalone comment?"""
return len(self.leaves) == 1 and self.leaves[0].type == STANDALONE_COMMENT
@property
def is_decorator(self) -> bool:
"""Is this line a decorator?"""
return bool(self) and self.leaves[0].type == token.AT
@property
def is_import(self) -> bool:
"""Is this an import line?"""
return bool(self) and is_import(self.leaves[0])
@property
def is_with_or_async_with_stmt(self) -> bool:
"""Is this a with_stmt line?"""
return bool(self) and is_with_or_async_with_stmt(self.leaves[0])
@property
def is_class(self) -> bool:
"""Is this line a class definition?"""
return (
bool(self)
and self.leaves[0].type == token.NAME
and self.leaves[0].value == "class"
)
@property
def is_stub_class(self) -> bool:
"""Is this line a class definition with a body consisting only of "..."?"""
return self.is_class and self.leaves[-3:] == [
Leaf(token.DOT, ".") for _ in range(3)
]
@property
def is_def(self) -> bool:
"""Is this a function definition? (Also returns True for async defs.)"""
try:
first_leaf = self.leaves[0]
except IndexError:
return False
try:
second_leaf: Optional[Leaf] = self.leaves[1]
except IndexError:
second_leaf = None
return (first_leaf.type == token.NAME and first_leaf.value == "def") or (
first_leaf.type == token.ASYNC
and second_leaf is not None
and second_leaf.type == token.NAME
and second_leaf.value == "def"
)
@property
def is_class_paren_empty(self) -> bool:
"""Is this a class with no base classes but using parentheses?
Those are unnecessary and should be removed.
"""
return (
bool(self)
and len(self.leaves) == 4
and self.is_class
and self.leaves[2].type == token.LPAR
and self.leaves[2].value == "("
and self.leaves[3].type == token.RPAR
and self.leaves[3].value == ")"
)
@property
def is_triple_quoted_string(self) -> bool:
"""Is the line a triple quoted string?"""
return (
bool(self)
and self.leaves[0].type == token.STRING
and self.leaves[0].value.startswith(('"""', "'''"))
)
@property
def opens_block(self) -> bool:
"""Does this line open a new level of indentation."""
if len(self.leaves) == 0:
return False
return self.leaves[-1].type == token.COLON
def is_fmt_pass_converted(
self, *, first_leaf_matches: Optional[Callable[[Leaf], bool]] = None
) -> bool:
"""Is this line converted from fmt off/skip code?
If first_leaf_matches is not None, it only returns True if the first
leaf of converted code matches.
"""
if len(self.leaves) != 1:
return False
leaf = self.leaves[0]
if (
leaf.type != STANDALONE_COMMENT
or leaf.fmt_pass_converted_first_leaf is None
):
return False
return first_leaf_matches is None or first_leaf_matches(
leaf.fmt_pass_converted_first_leaf
)
def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool:
"""If so, needs to be split before emitting."""
for leaf in self.leaves:
if leaf.type == STANDALONE_COMMENT and leaf.bracket_depth <= depth_limit:
return True
return False
def contains_uncollapsable_type_comments(self) -> bool:
ignored_ids = set()
try:
last_leaf = self.leaves[-1]
ignored_ids.add(id(last_leaf))
if last_leaf.type == token.COMMA or (
last_leaf.type == token.RPAR and not last_leaf.value
):
# When trailing commas or optional parens are inserted by Black for
# consistency, comments after the previous last element are not moved
# (they don't have to, rendering will still be correct). So we ignore
# trailing commas and invisible.
last_leaf = self.leaves[-2]
ignored_ids.add(id(last_leaf))
except IndexError:
return False
# A type comment is uncollapsable if it is attached to a leaf
# that isn't at the end of the line (since that could cause it
# to get associated to a different argument) or if there are
# comments before it (since that could cause it to get hidden
# behind a comment.
comment_seen = False
for leaf_id, comments in self.comments.items():
for comment in comments:
if is_type_comment(comment):
if comment_seen or (
not is_type_ignore_comment(comment)
and leaf_id not in ignored_ids
):
return True
comment_seen = True
return False
def contains_unsplittable_type_ignore(self) -> bool:
if not self.leaves:
return False
# If a 'type: ignore' is attached to the end of a line, we
# can't split the line, because we can't know which of the
# subexpressions the ignore was meant to apply to.
#
# We only want this to apply to actual physical lines from the
# original source, though: we don't want the presence of a
# 'type: ignore' at the end of a multiline expression to
# justify pushing it all onto one line. Thus we
# (unfortunately) need to check the actual source lines and
# only report an unsplittable 'type: ignore' if this line was
# one line in the original code.
# Grab the first and last line numbers, skipping generated leaves
first_line = next((leaf.lineno for leaf in self.leaves if leaf.lineno != 0), 0)
last_line = next(
(leaf.lineno for leaf in reversed(self.leaves) if leaf.lineno != 0), 0
)
if first_line == last_line:
# We look at the last two leaves since a comma or an
# invisible paren could have been added at the end of the
# line.
for node in self.leaves[-2:]:
for comment in self.comments.get(id(node), []):
if is_type_ignore_comment(comment):
return True
return False
def contains_multiline_strings(self) -> bool:
return any(is_multiline_string(leaf) for leaf in self.leaves)
def has_magic_trailing_comma(
self, closing: Leaf, ensure_removable: bool = False
) -> bool:
"""Return True if we have a magic trailing comma, that is when:
- there's a trailing comma here
- it's not a one-tuple
- it's not a single-element subscript
Additionally, if ensure_removable:
- it's not from square bracket indexing
(specifically, single-element square bracket indexing)
"""
if not (
closing.type in CLOSING_BRACKETS
and self.leaves
and self.leaves[-1].type == token.COMMA
):
return False
if closing.type == token.RBRACE:
return True
if closing.type == token.RSQB:
if (
closing.parent
and closing.parent.type == syms.trailer
and closing.opening_bracket
and is_one_sequence_between(
closing.opening_bracket,
closing,
self.leaves,
brackets=(token.LSQB, token.RSQB),
)
):
return False
if not ensure_removable:
return True
comma = self.leaves[-1]
if comma.parent is None:
return False
return (
comma.parent.type != syms.subscriptlist
or closing.opening_bracket is None
or not is_one_sequence_between(
closing.opening_bracket,
closing,
self.leaves,
brackets=(token.LSQB, token.RSQB),
)
)
if self.is_import:
return True
if closing.opening_bracket is not None and not is_one_sequence_between(
closing.opening_bracket, closing, self.leaves
):
return True
return False
def append_comment(self, comment: Leaf) -> bool:
"""Add an inline or standalone comment to the line."""
if (
comment.type == STANDALONE_COMMENT
and self.bracket_tracker.any_open_brackets()
):
comment.prefix = ""
return False
if comment.type != token.COMMENT:
return False
if not self.leaves:
comment.type = STANDALONE_COMMENT
comment.prefix = ""
return False
last_leaf = self.leaves[-1]
if (
last_leaf.type == token.RPAR
and not last_leaf.value
and last_leaf.parent
and len(list(last_leaf.parent.leaves())) <= 3
and not is_type_comment(comment)
):
# Comments on an optional parens wrapping a single leaf should belong to
# the wrapped node except if it's a type comment. Pinning the comment like
# this avoids unstable formatting caused by comment migration.
if len(self.leaves) < 2:
comment.type = STANDALONE_COMMENT
comment.prefix = ""
return False
last_leaf = self.leaves[-2]
self.comments.setdefault(id(last_leaf), []).append(comment)
return True
def comments_after(self, leaf: Leaf) -> List[Leaf]:
"""Generate comments that should appear directly after `leaf`."""
return self.comments.get(id(leaf), [])
def remove_trailing_comma(self) -> None:
"""Remove the trailing comma and moves the comments attached to it."""
trailing_comma = self.leaves.pop()
trailing_comma_comments = self.comments.pop(id(trailing_comma), [])
self.comments.setdefault(id(self.leaves[-1]), []).extend(
trailing_comma_comments
)
def is_complex_subscript(self, leaf: Leaf) -> bool:
"""Return True iff `leaf` is part of a slice with non-trivial exprs."""
open_lsqb = self.bracket_tracker.get_open_lsqb()
if open_lsqb is None:
return False
subscript_start = open_lsqb.next_sibling
if isinstance(subscript_start, Node):
if subscript_start.type == syms.listmaker:
return False
if subscript_start.type == syms.subscriptlist:
subscript_start = child_towards(subscript_start, leaf)
return subscript_start is not None and any(
n.type in TEST_DESCENDANTS for n in subscript_start.pre_order()
)
def enumerate_with_length(
self, reversed: bool = False
) -> Iterator[Tuple[Index, Leaf, int]]:
"""Return an enumeration of leaves with their length.
Stops prematurely on multiline strings and standalone comments.
"""
op = cast(
Callable[[Sequence[Leaf]], Iterator[Tuple[Index, Leaf]]],
enumerate_reversed if reversed else enumerate,
)
for index, leaf in op(self.leaves):
length = len(leaf.prefix) + len(leaf.value)
if "\n" in leaf.value:
return # Multiline strings, we can't continue.
for comment in self.comments_after(leaf):
length += len(comment.value)
yield index, leaf, length
def clone(self) -> "Line":
return Line(
mode=self.mode,
depth=self.depth,
inside_brackets=self.inside_brackets,
should_split_rhs=self.should_split_rhs,
magic_trailing_comma=self.magic_trailing_comma,
)
def __str__(self) -> str:
"""Render the line."""
if not self:
return "\n"
indent = " " * self.depth
leaves = iter(self.leaves)
first = next(leaves)
res = f"{first.prefix}{indent}{first.value}"
for leaf in leaves:
res += str(leaf)
for comment in itertools.chain.from_iterable(self.comments.values()):
res += str(comment)
return res + "\n"
def __bool__(self) -> bool:
"""Return True if the line has leaves or comments."""
return bool(self.leaves or self.comments)
@dataclass
class RHSResult:
"""Intermediate split result from a right hand split."""
head: Line
body: Line
tail: Line
opening_bracket: Leaf
closing_bracket: Leaf
@dataclass
class LinesBlock:
"""Class that holds information about a block of formatted lines.
This is introduced so that the EmptyLineTracker can look behind the standalone
comments and adjust their empty lines for class or def lines.
"""
mode: Mode
previous_block: Optional["LinesBlock"]
original_line: Line
before: int = 0
content_lines: List[str] = field(default_factory=list)
after: int = 0
def all_lines(self) -> List[str]:
empty_line = str(Line(mode=self.mode))
return (
[empty_line * self.before] + self.content_lines + [empty_line * self.after]
)
@dataclass
class EmptyLineTracker:
"""Provides a stateful method that returns the number of potential extra
empty lines needed before and after the currently processed line.
Note: this tracker works on lines that haven't been split yet. It assumes
the prefix of the first leaf consists of optional newlines. Those newlines
are consumed by `maybe_empty_lines()` and included in the computation.
"""
mode: Mode
previous_line: Optional[Line] = None
previous_block: Optional[LinesBlock] = None
previous_defs: List[Line] = field(default_factory=list)
semantic_leading_comment: Optional[LinesBlock] = None
def maybe_empty_lines(self, current_line: Line) -> LinesBlock:
"""Return the number of extra empty lines before and after the `current_line`.
This is for separating `def`, `async def` and `class` with extra empty
lines (two on module-level).
"""
before, after = self._maybe_empty_lines(current_line)
previous_after = self.previous_block.after if self.previous_block else 0
before = (
# Black should not insert empty lines at the beginning
# of the file
0
if self.previous_line is None
else before - previous_after
)
block = LinesBlock(
mode=self.mode,
previous_block=self.previous_block,
original_line=current_line,
before=before,
after=after,
)
# Maintain the semantic_leading_comment state.
if current_line.is_comment:
if self.previous_line is None or (
not self.previous_line.is_decorator
# `or before` means this comment already has an empty line before
and (not self.previous_line.is_comment or before)
and (self.semantic_leading_comment is None or before)
):
self.semantic_leading_comment = block
# `or before` means this decorator already has an empty line before
elif not current_line.is_decorator or before:
self.semantic_leading_comment = None
self.previous_line = current_line
self.previous_block = block
return block
def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
max_allowed = 1
if current_line.depth == 0:
max_allowed = 1 if self.mode.is_pyi else 2
if current_line.leaves:
# Consume the first leaf's extra newlines.
first_leaf = current_line.leaves[0]
before = first_leaf.prefix.count("\n")
before = min(before, max_allowed)
first_leaf.prefix = ""
else:
before = 0
depth = current_line.depth
while self.previous_defs and self.previous_defs[-1].depth >= depth:
if self.mode.is_pyi:
assert self.previous_line is not None
if depth and not current_line.is_def and self.previous_line.is_def:
# Empty lines between attributes and methods should be preserved.
before = min(1, before)
elif (
Preview.blank_line_after_nested_stub_class in self.mode
and self.previous_defs[-1].is_class
and not self.previous_defs[-1].is_stub_class
):
before = 1
elif depth:
before = 0
else:
before = 1
else:
if depth:
before = 1
elif (
not depth
and self.previous_defs[-1].depth
and current_line.leaves[-1].type == token.COLON
and (
current_line.leaves[0].value
not in ("with", "try", "for", "while", "if", "match")
)
):
# We shouldn't add two newlines between an indented function and
# a dependent non-indented clause. This is to avoid issues with
# conditional function definitions that are technically top-level
# and therefore get two trailing newlines, but look weird and
# inconsistent when they're followed by elif, else, etc. This is
# worse because these functions only get *one* preceding newline
# already.
before = 1
else:
before = 2
self.previous_defs.pop()
if current_line.is_decorator or current_line.is_def or current_line.is_class:
return self._maybe_empty_lines_for_class_or_def(current_line, before)
if (
self.previous_line
and self.previous_line.is_import
and not current_line.is_import
and not current_line.is_fmt_pass_converted(first_leaf_matches=is_import)
and depth == self.previous_line.depth
):
return (before or 1), 0
if (
self.previous_line
and self.previous_line.is_class
and current_line.is_triple_quoted_string
):
if Preview.no_blank_line_before_class_docstring in current_line.mode:
return 0, 1
return before, 1
if self.previous_line and self.previous_line.opens_block:
return 0, 0
return before, 0
def _maybe_empty_lines_for_class_or_def(
self, current_line: Line, before: int
) -> Tuple[int, int]:
if not current_line.is_decorator:
self.previous_defs.append(current_line)
if self.previous_line is None:
# Don't insert empty lines before the first line in the file.
return 0, 0
if self.previous_line.is_decorator:
if self.mode.is_pyi and current_line.is_stub_class:
# Insert an empty line after a decorated stub class
return 0, 1
return 0, 0
if self.previous_line.depth < current_line.depth and (
self.previous_line.is_class or self.previous_line.is_def
):
return 0, 0
comment_to_add_newlines: Optional[LinesBlock] = None
if (
self.previous_line.is_comment
and self.previous_line.depth == current_line.depth
and before == 0
):
slc = self.semantic_leading_comment
if (
slc is not None
and slc.previous_block is not None
and not slc.previous_block.original_line.is_class
and not slc.previous_block.original_line.opens_block
and slc.before <= 1
):
comment_to_add_newlines = slc
else:
return 0, 0
if self.mode.is_pyi:
if current_line.is_class or self.previous_line.is_class:
if self.previous_line.depth < current_line.depth:
newlines = 0
elif self.previous_line.depth > current_line.depth:
newlines = 1
elif current_line.is_stub_class and self.previous_line.is_stub_class:
# No blank line between classes with an empty body
newlines = 0
else:
newlines = 1
elif (
current_line.is_def or current_line.is_decorator
) and not self.previous_line.is_def:
if current_line.depth:
# In classes empty lines between attributes and methods should
# be preserved.
newlines = min(1, before)
else:
# Blank line between a block of functions (maybe with preceding
# decorators) and a block of non-functions
newlines = 1
elif self.previous_line.depth > current_line.depth:
newlines = 1
else:
newlines = 0
else:
newlines = 1 if current_line.depth else 2
if comment_to_add_newlines is not None:
previous_block = comment_to_add_newlines.previous_block
if previous_block is not None:
comment_to_add_newlines.before = (
max(comment_to_add_newlines.before, newlines) - previous_block.after
)
newlines = 0
return newlines, 0
def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]:
"""Like `reversed(enumerate(sequence))` if that were possible."""
index = len(sequence) - 1
for element in reversed(sequence):
yield (index, element)
index -= 1
def append_leaves(
new_line: Line, old_line: Line, leaves: List[Leaf], preformatted: bool = False
) -> None:
"""
Append leaves (taken from @old_line) to @new_line, making sure to fix the
underlying Node structure where appropriate.
All of the leaves in @leaves are duplicated. The duplicates are then
appended to @new_line and used to replace their originals in the underlying
Node structure. Any comments attached to the old leaves are reattached to
the new leaves.
Pre-conditions:
set(@leaves) is a subset of set(@old_line.leaves).
"""
for old_leaf in leaves:
new_leaf = Leaf(old_leaf.type, old_leaf.value)
replace_child(old_leaf, new_leaf)
new_line.append(new_leaf, preformatted=preformatted)
for comment_leaf in old_line.comments_after(old_leaf):
new_line.append(comment_leaf, preformatted=True)
def is_line_short_enough( # noqa: C901
line: Line, *, mode: Mode, line_str: str = ""
) -> bool:
"""For non-multiline strings, return True if `line` is no longer than `line_length`.
For multiline strings, looks at the context around `line` to determine
if it should be inlined or split up.
Uses the provided `line_str` rendering, if any, otherwise computes a new one.
"""
if not line_str:
line_str = line_to_string(line)
width = str_width if mode.preview else len
if Preview.multiline_string_handling not in mode:
return (
width(line_str) <= mode.line_length
and "\n" not in line_str # multiline strings
and not line.contains_standalone_comments()
)
if line.contains_standalone_comments():
return False
if "\n" not in line_str:
# No multiline strings (MLS) present
return width(line_str) <= mode.line_length
first, *_, last = line_str.split("\n")
if width(first) > mode.line_length or width(last) > mode.line_length:
return False
# Traverse the AST to examine the context of the multiline string (MLS),
# tracking aspects such as depth and comma existence,
# to determine whether to split the MLS or keep it together.
# Depth (which is based on the existing bracket_depth concept)
# is needed to determine nesting level of the MLS.
# Includes special case for trailing commas.
commas: List[int] = [] # tracks number of commas per depth level
multiline_string: Optional[Leaf] = None
# store the leaves that contain parts of the MLS
multiline_string_contexts: List[LN] = []
max_level_to_update: Union[int, float] = math.inf # track the depth of the MLS
for i, leaf in enumerate(line.leaves):
if max_level_to_update == math.inf:
had_comma: Optional[int] = None
if leaf.bracket_depth + 1 > len(commas):
commas.append(0)
elif leaf.bracket_depth + 1 < len(commas):
had_comma = commas.pop()
if (
had_comma is not None
and multiline_string is not None
and multiline_string.bracket_depth == leaf.bracket_depth + 1
):
# Have left the level with the MLS, stop tracking commas
max_level_to_update = leaf.bracket_depth
if had_comma > 0:
# MLS was in parens with at least one comma - force split
return False
if leaf.bracket_depth <= max_level_to_update and leaf.type == token.COMMA:
# Ignore non-nested trailing comma
# directly after MLS/MLS-containing expression
ignore_ctxs: List[Optional[LN]] = [None]
ignore_ctxs += multiline_string_contexts
if not (leaf.prev_sibling in ignore_ctxs and i == len(line.leaves) - 1):
commas[leaf.bracket_depth] += 1
if max_level_to_update != math.inf:
max_level_to_update = min(max_level_to_update, leaf.bracket_depth)
if is_multiline_string(leaf):
if len(multiline_string_contexts) > 0:
# >1 multiline string cannot fit on a single line - force split
return False
multiline_string = leaf
ctx: LN = leaf
# fetch the leaf components of the MLS in the AST
while str(ctx) in line_str:
multiline_string_contexts.append(ctx)
if ctx.parent is None:
break
ctx = ctx.parent
# May not have a triple-quoted multiline string at all,
# in case of a regular string with embedded newlines and line continuations
if len(multiline_string_contexts) == 0:
return True
return all(val == 0 for val in commas)
def can_be_split(line: Line) -> bool:
"""Return False if the line cannot be split *for sure*.
This is not an exhaustive search but a cheap heuristic that we can use to
avoid some unfortunate formattings (mostly around wrapping unsplittable code
in unnecessary parentheses).
"""
leaves = line.leaves
if len(leaves) < 2:
return False
if leaves[0].type == token.STRING and leaves[1].type == token.DOT:
call_count = 0
dot_count = 0
next = leaves[-1]
for leaf in leaves[-2::-1]:
if leaf.type in OPENING_BRACKETS:
if next.type not in CLOSING_BRACKETS:
return False
call_count += 1
elif leaf.type == token.DOT:
dot_count += 1
elif leaf.type == token.NAME:
if not (next.type == token.DOT or next.type in OPENING_BRACKETS):
return False
elif leaf.type not in CLOSING_BRACKETS:
return False
if dot_count > 1 and call_count > 1:
return False
return True
def can_omit_invisible_parens(
rhs: RHSResult,
line_length: int,
) -> bool:
"""Does `rhs.body` have a shape safe to reformat without optional parens around it?
Returns True for only a subset of potentially nice looking formattings but
the point is to not return false positives that end up producing lines that
are too long.
"""
line = rhs.body
bt = line.bracket_tracker
if not bt.delimiters:
# Without delimiters the optional parentheses are useless.
return True
max_priority = bt.max_delimiter_priority()
delimiter_count = bt.delimiter_count_with_priority(max_priority)
if delimiter_count > 1:
# With more than one delimiter of a kind the optional parentheses read better.
return False
if delimiter_count == 1:
if (
Preview.wrap_multiple_context_managers_in_parens in line.mode
and max_priority == COMMA_PRIORITY
and rhs.head.is_with_or_async_with_stmt
):
# For two context manager with statements, the optional parentheses read
# better. In this case, `rhs.body` is the context managers part of
# the with statement. `rhs.head` is the `with (` part on the previous
# line.
return False
# Otherwise it may also read better, but we don't do it today and requires
# careful considerations for all possible cases. See
# https://github.com/psf/black/issues/2156.
if max_priority == DOT_PRIORITY:
# A single stranded method call doesn't require optional parentheses.
return True
assert len(line.leaves) >= 2, "Stranded delimiter"
# With a single delimiter, omit if the expression starts or ends with
# a bracket.
first = line.leaves[0]
second = line.leaves[1]
if first.type in OPENING_BRACKETS and second.type not in CLOSING_BRACKETS:
if _can_omit_opening_paren(line, first=first, line_length=line_length):
return True
# Note: we are not returning False here because a line might have *both*
# a leading opening bracket and a trailing closing bracket. If the
# opening bracket doesn't match our rule, maybe the closing will.
penultimate = line.leaves[-2]
last = line.leaves[-1]
if (
last.type == token.RPAR
or last.type == token.RBRACE
or (
# don't use indexing for omitting optional parentheses;
# it looks weird
last.type == token.RSQB
and last.parent
and last.parent.type != syms.trailer
)
):
if penultimate.type in OPENING_BRACKETS:
# Empty brackets don't help.
return False
if is_multiline_string(first):
# Additional wrapping of a multiline string in this situation is
# unnecessary.
return True
if _can_omit_closing_paren(line, last=last, line_length=line_length):
return True
return False
def _can_omit_opening_paren(line: Line, *, first: Leaf, line_length: int) -> bool:
"""See `can_omit_invisible_parens`."""
remainder = False
length = 4 * line.depth
_index = -1
for _index, leaf, leaf_length in line.enumerate_with_length():
if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first:
remainder = True
if remainder:
length += leaf_length
if length > line_length:
break
if leaf.type in OPENING_BRACKETS:
# There are brackets we can further split on.
remainder = False
else:
# checked the entire string and line length wasn't exceeded
if len(line.leaves) == _index + 1:
return True
return False
def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool:
"""See `can_omit_invisible_parens`."""
length = 4 * line.depth
seen_other_brackets = False
for _index, leaf, leaf_length in line.enumerate_with_length():
length += leaf_length
if leaf is last.opening_bracket:
if seen_other_brackets or length <= line_length:
return True
elif leaf.type in OPENING_BRACKETS:
# There are brackets we can further split on.
seen_other_brackets = True
return False
def line_to_string(line: Line) -> str:
"""Returns the string representation of @line.
WARNING: This is known to be computationally expensive.
"""
return str(line).strip("\n")