from black.mode import Feature
from black.nodes import syms, replace_child, parent_type
from black.nodes import is_empty_par, is_empty_lpar, is_empty_rpar
-from black.nodes import CLOSING_BRACKETS, STANDALONE_COMMENT
+from black.nodes import OPENING_BRACKETS, CLOSING_BRACKETS, STANDALONE_COMMENT
from black.lines import Line, append_leaves
from black.brackets import BracketMatchError
from black.comments import contains_pragma_comment
lines by themselves).
Requirements:
- * The line consists ONLY of a single string (with the exception of a
- '+' symbol which MAY exist at the start of the line), MAYBE a string
- trailer, and MAYBE a trailing comma.
+ * The line consists ONLY of a single string (possibly prefixed by a
+ string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE
+ a trailing comma.
AND
* All of the requirements listed in BaseStringSplitter's docstring.
CustomSplit objects and add them to the custom split map.
"""
+ STRING_OPERATORS = [
+ token.PLUS,
+ token.STAR,
+ token.EQEQUAL,
+ token.NOTEQUAL,
+ token.LESS,
+ token.LESSEQUAL,
+ token.GREATER,
+ token.GREATEREQUAL,
+ ]
MIN_SUBSTR_SIZE = 6
# Matches an "f-expression" (e.g. {var}) that might be found in an f-string.
RE_FEXPR = r"""
| \{\{
| \}\}
| (?R)
- )+?
- (?<!\}) \} (?:\}\})* (?!\})
+ )+
+ \}
"""
def do_splitter_match(self, line: Line) -> TMatchResult:
idx = 0
- # The first leaf MAY be a '+' symbol...
- if is_valid_index(idx) and LL[idx].type == token.PLUS:
+ # The first two leaves MAY be the 'not in' keywords...
+ if (
+ is_valid_index(idx)
+ and is_valid_index(idx + 1)
+ and [LL[idx].type, LL[idx + 1].type] == [token.NAME, token.NAME]
+ and str(LL[idx]) + str(LL[idx + 1]) == "not in"
+ ):
+ idx += 2
+ # Else the first leaf MAY be a string operator symbol or the 'in' keyword...
+ elif is_valid_index(idx) and (
+ LL[idx].type in self.STRING_OPERATORS
+ or LL[idx].type == token.NAME
+ and str(LL[idx]) == "in"
+ ):
idx += 1
# The next/first leaf MAY be an empty LPAR...
)
first_string_line = True
- starts_with_plus = LL[0].type == token.PLUS
- def line_needs_plus() -> bool:
- return first_string_line and starts_with_plus
+ string_op_leaves = self._get_string_operator_leaves(LL)
+ string_op_leaves_length = (
+ sum([len(str(prefix_leaf)) for prefix_leaf in string_op_leaves]) + 1
+ if string_op_leaves
+ else 0
+ )
- def maybe_append_plus(new_line: Line) -> None:
+ def maybe_append_string_operators(new_line: Line) -> None:
"""
Side Effects:
- If @line starts with a plus and this is the first line we are
- constructing, this function appends a PLUS leaf to @new_line
- and replaces the old PLUS leaf in the node structure. Otherwise
- this function does nothing.
+ If @line starts with a string operator and this is the first
+ line we are constructing, this function appends the string
+ operator to @new_line and replaces the old string operator leaf
+ in the node structure. Otherwise this function does nothing.
"""
- if line_needs_plus():
- plus_leaf = Leaf(token.PLUS, "+")
- replace_child(LL[0], plus_leaf)
- new_line.append(plus_leaf)
+ maybe_prefix_leaves = string_op_leaves if first_string_line else []
+ for i, prefix_leaf in enumerate(maybe_prefix_leaves):
+ replace_child(LL[i], prefix_leaf)
+ new_line.append(prefix_leaf)
ends_with_comma = (
is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA
result = self.line_length
result -= line.depth * 4
result -= 1 if ends_with_comma else 0
- result -= 2 if line_needs_plus() else 0
+ result -= string_op_leaves_length
return result
# --- Calculate Max Break Index (for string value)
break_idx = csplit.break_idx
else:
# Algorithmic Split (automatic)
- max_bidx = max_break_idx - 2 if line_needs_plus() else max_break_idx
+ max_bidx = max_break_idx - string_op_leaves_length
maybe_break_idx = self._get_break_idx(rest_value, max_bidx)
if maybe_break_idx is None:
# If we are unable to algorithmically determine a good split
# --- Construct `next_line`
next_line = line.clone()
- maybe_append_plus(next_line)
+ maybe_append_string_operators(next_line)
next_line.append(next_leaf)
string_line_results.append(Ok(next_line))
self._maybe_normalize_string_quotes(rest_leaf)
last_line = line.clone()
- maybe_append_plus(last_line)
+ maybe_append_string_operators(last_line)
# If there are any leaves to the right of the target string...
if is_valid_index(string_idx + 1):
else:
return string
+ def _get_string_operator_leaves(self, leaves: Iterable[Leaf]) -> List[Leaf]:
+ LL = list(leaves)
+
+ string_op_leaves = []
+ i = 0
+ while LL[i].type in self.STRING_OPERATORS + [token.NAME]:
+ prefix_leaf = Leaf(LL[i].type, str(LL[i]).strip())
+ string_op_leaves.append(prefix_leaf)
+ i += 1
+ return string_op_leaves
+
class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
"""
def do_splitter_match(self, line: Line) -> TMatchResult:
LL = line.leaves
+ if line.leaves[-1].type in OPENING_BRACKETS:
+ return TErr(
+ "Cannot wrap parens around a line that ends in an opening bracket."
+ )
+
string_idx = (
self._return_match(LL)
or self._else_match(LL)
right_leaves.pop()
if old_parens_exist:
- assert (
- right_leaves and right_leaves[-1].type == token.RPAR
- ), "Apparently, old parentheses do NOT exist?!"
+ assert right_leaves and right_leaves[-1].type == token.RPAR, (
+ "Apparently, old parentheses do NOT exist?!"
+ f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
+ )
old_rpar_leaf = right_leaves.pop()
append_leaves(string_line, line, right_leaves)