import itertools
+import math
import sys
from dataclasses import dataclass, field
from typing import (
Sequence,
Tuple,
TypeVar,
+ Union,
cast,
)
T = TypeVar("T")
Index = int
LeafID = int
+LN = Union[Leaf, Node]
@dataclass
- it's not a single-element subscript
Additionally, if ensure_removable:
- it's not from square bracket indexing
- (specifically, single-element square bracket indexing with
- Preview.skip_magic_trailing_comma_in_subscript)
+ (specifically, single-element square bracket indexing)
"""
if not (
closing.type in CLOSING_BRACKETS
if closing.type == token.RSQB:
if (
- Preview.one_element_subscript in self.mode
- and closing.parent
+ closing.parent
and closing.parent.type == syms.trailer
and closing.opening_bracket
and is_one_sequence_between(
comma = self.leaves[-1]
if comma.parent is None:
return False
- if Preview.skip_magic_trailing_comma_in_subscript in self.mode:
- 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),
- )
+ 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),
)
- return comma.parent.type == syms.listmaker
+ )
if self.is_import:
return True
and (self.semantic_leading_comment is None or before)
):
self.semantic_leading_comment = block
- elif not current_line.is_decorator:
+ # `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
):
return before, 1
- if (
- Preview.remove_block_trailing_newline in current_line.mode
- and self.previous_line
- and self.previous_line.opens_block
- ):
+ if self.previous_line and self.previous_line.opens_block:
return 0, 0
return before, 0
):
slc = self.semantic_leading_comment
if (
- Preview.empty_lines_before_class_or_def_with_leading_comments
- in current_line.mode
- and slc is not None
+ 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
new_line.append(comment_leaf, preformatted=True)
-def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") -> bool:
- """Return True if `line` is no longer than `line_length`.
-
+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)
- return (
- len(line_str) <= line_length
- and "\n" not in line_str # multiline strings
- and not line.contains_standalone_comments()
- )
+
+ if Preview.multiline_string_handling not in mode:
+ return (
+ len(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 len(line_str) <= mode.line_length
+
+ first, *_, last = line_str.split("\n")
+ if len(first) > mode.line_length or len(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 = 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: