Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/black/trans.py: 12%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2String transformers that can split and merge strings.
3"""
5import re
6from abc import ABC, abstractmethod
7from collections import defaultdict
8from collections.abc import Callable, Collection, Iterable, Iterator, Sequence
9from dataclasses import dataclass
10from typing import Any, ClassVar, Final, Literal, TypeVar, Union
12from mypy_extensions import trait
14from black.comments import contains_pragma_comment
15from black.lines import Line, append_leaves, is_line_short_enough, line_to_string
16from black.mode import Feature, Mode
17from black.nodes import (
18 ASSIGNMENTS,
19 CLOSING_BRACKETS,
20 COMPARATORS,
21 MATH_OPERATORS,
22 OPENING_BRACKETS,
23 STANDALONE_COMMENT,
24 is_empty_lpar,
25 is_empty_par,
26 is_empty_rpar,
27 is_part_of_annotation,
28 parent_type,
29 replace_child,
30 syms,
31)
32from black.rusty import Err, Ok, Result
33from black.strings import (
34 assert_is_leaf_string,
35 count_chars_in_width,
36 get_string_prefix,
37 has_triple_quotes,
38 normalize_string_quotes,
39 str_width,
40)
41from blib2to3.pgen2 import token
42from blib2to3.pytree import Leaf, Node
45class CannotTransform(Exception):
46 """Base class for errors raised by Transformers."""
49# types
50T = TypeVar("T")
51LN = Union[Leaf, Node]
52Transformer = Callable[[Line, Collection[Feature], Mode], Iterator[Line]]
53Index = int
54NodeType = int
55ParserState = int
56StringID = int
57TResult = Result[T, CannotTransform] # (T)ransform Result
58TMatchResult = TResult[list[Index]]
60SPLIT_SAFE_CHARS = frozenset(["\u3001", "\u3002", "\uff0c"]) # East Asian stops
63def TErr(err_msg: str) -> Err[CannotTransform]:
64 """(T)ransform Err
66 Convenience function used when working with the TResult type.
67 """
68 cant_transform = CannotTransform(err_msg)
69 return Err(cant_transform)
72# Remove when `simplify_power_operator_hugging` becomes stable.
73def hug_power_op(
74 line: Line, features: Collection[Feature], mode: Mode
75) -> Iterator[Line]:
76 """A transformer which normalizes spacing around power operators."""
78 # Performance optimization to avoid unnecessary Leaf clones and other ops.
79 for leaf in line.leaves:
80 if leaf.type == token.DOUBLESTAR:
81 break
82 else:
83 raise CannotTransform("No doublestar token was found in the line.")
85 def is_simple_lookup(index: int, kind: Literal[1, -1]) -> bool:
86 # Brackets and parentheses indicate calls, subscripts, etc. ...
87 # basically stuff that doesn't count as "simple". Only a NAME lookup
88 # or dotted lookup (eg. NAME.NAME) is OK.
89 if kind == -1:
90 return handle_is_simple_look_up_prev(line, index, {token.RPAR, token.RSQB})
91 else:
92 return handle_is_simple_lookup_forward(
93 line, index, {token.LPAR, token.LSQB}
94 )
96 def is_simple_operand(index: int, kind: Literal[1, -1]) -> bool:
97 # An operand is considered "simple" if's a NAME, a numeric CONSTANT, a simple
98 # lookup (see above), with or without a preceding unary operator.
99 start = line.leaves[index]
100 if start.type in {token.NAME, token.NUMBER}:
101 return is_simple_lookup(index, kind)
103 if start.type in {token.PLUS, token.MINUS, token.TILDE}:
104 if line.leaves[index + 1].type in {token.NAME, token.NUMBER}:
105 # kind is always one as bases with a preceding unary op will be checked
106 # for simplicity starting from the next token (so it'll hit the check
107 # above).
108 return is_simple_lookup(index + 1, kind=1)
110 return False
112 new_line = line.clone()
113 should_hug = False
114 for idx, leaf in enumerate(line.leaves):
115 hug_this_leaf = should_hug
116 should_hug = (
117 (0 < idx < len(line.leaves) - 1)
118 and leaf.type == token.DOUBLESTAR
119 and is_simple_operand(idx - 1, kind=-1)
120 and line.leaves[idx - 1].value != "lambda"
121 and is_simple_operand(idx + 1, kind=1)
122 )
124 if hug_this_leaf or should_hug:
125 new_leaf = leaf.clone()
126 new_leaf.prefix = ""
127 else:
128 # Only the operands around a hugged `**` need a copy. Reuse the leaf
129 # otherwise: a clone has no parent, and the trailing-comma guards in
130 # `Line.append` read the tree to tell a syntactically required comma
131 # (a one-tuple, or a one-element subscript like `a[x,]`) from a magic
132 # one. Without a parent they can't, so the comma was being dropped
133 # under --skip-magic-trailing-comma.
134 new_leaf = leaf
136 # We have to be careful to make a new line properly:
137 # - bracket related metadata must be maintained (handled by Line.append)
138 # - comments need to copied over, updating the leaf IDs they're attached to
139 new_line.append(new_leaf, preformatted=True)
140 for comment_leaf in line.comments_after(leaf):
141 new_line.append(comment_leaf, preformatted=True)
143 yield new_line
146# Remove when `simplify_power_operator_hugging` becomes stable.
147def handle_is_simple_look_up_prev(line: Line, index: int, disallowed: set[int]) -> bool:
148 """
149 Handling the determination of is_simple_lookup for the lines prior to the doublestar
150 token. This is required because of the need to isolate the chained expression
151 to determine the bracket or parenthesis belong to the single expression.
152 """
153 contains_disallowed = False
154 chain = []
156 while 0 <= index < len(line.leaves):
157 current = line.leaves[index]
158 chain.append(current)
159 if not contains_disallowed and current.type in disallowed:
160 contains_disallowed = True
161 if not is_expression_chained(chain):
162 return not contains_disallowed
164 index -= 1
166 return True
169# Remove when `simplify_power_operator_hugging` becomes stable.
170def handle_is_simple_lookup_forward(
171 line: Line, index: int, disallowed: set[int]
172) -> bool:
173 """
174 Handling decision is_simple_lookup for the lines behind the doublestar token.
175 This function is simplified to keep consistent with the prior logic and the forward
176 case are more straightforward and do not need to care about chained expressions.
177 """
178 while 0 <= index < len(line.leaves):
179 current = line.leaves[index]
180 if current.type in disallowed:
181 return False
182 if current.type not in {token.NAME, token.DOT} or (
183 current.type == token.NAME and current.value == "for"
184 ):
185 # If the current token isn't disallowed, we'll assume this is simple as
186 # only the disallowed tokens are semantically attached to this lookup
187 # expression we're checking. Also, stop early if we hit the 'for' bit
188 # of a comprehension.
189 return True
191 index += 1
193 return True
196# Remove when `simplify_power_operator_hugging` becomes stable.
197def is_expression_chained(chained_leaves: list[Leaf]) -> bool:
198 """
199 Function to determine if the variable is a chained call.
200 (e.g., foo.lookup, foo().lookup, (foo.lookup())) will be recognized as chained call)
201 """
202 if len(chained_leaves) < 2:
203 return True
205 current_leaf = chained_leaves[-1]
206 past_leaf = chained_leaves[-2]
208 if past_leaf.type == token.NAME:
209 return current_leaf.type in {token.DOT}
210 elif past_leaf.type in {token.RPAR, token.RSQB}:
211 return current_leaf.type in {token.RSQB, token.RPAR}
212 elif past_leaf.type in {token.LPAR, token.LSQB}:
213 return current_leaf.type in {token.NAME, token.LPAR, token.LSQB}
214 else:
215 return False
218class StringTransformer(ABC):
219 """
220 An implementation of the Transformer protocol that relies on its
221 subclasses overriding the template methods `do_match(...)` and
222 `do_transform(...)`.
224 This Transformer works exclusively on strings (for example, by merging
225 or splitting them).
227 The following sections can be found among the docstrings of each concrete
228 StringTransformer subclass.
230 Requirements:
231 Which requirements must be met of the given Line for this
232 StringTransformer to be applied?
234 Transformations:
235 If the given Line meets all of the above requirements, which string
236 transformations can you expect to be applied to it by this
237 StringTransformer?
239 Collaborations:
240 What contractual agreements does this StringTransformer have with other
241 StringTransformers? Such collaborations should be eliminated/minimized
242 as much as possible.
243 """
245 __name__: Final = "StringTransformer"
247 # Ideally this would be a dataclass, but unfortunately mypyc breaks when used with
248 # `abc.ABC`.
249 def __init__(self, line_length: int, normalize_strings: bool) -> None:
250 self.line_length = line_length
251 self.normalize_strings = normalize_strings
253 @abstractmethod
254 def do_match(self, line: Line) -> TMatchResult:
255 """
256 Returns:
257 * Ok(string_indices) such that for each index, `line.leaves[index]`
258 is our target string if a match was able to be made. For
259 transformers that don't result in more lines (e.g. StringMerger,
260 StringParenStripper), multiple matches and transforms are done at
261 once to reduce the complexity.
262 OR
263 * Err(CannotTransform), if no match could be made.
264 """
266 @abstractmethod
267 def do_transform(
268 self, line: Line, string_indices: list[int]
269 ) -> Iterator[TResult[Line]]:
270 """
271 Yields:
272 * Ok(new_line) where new_line is the new transformed line.
273 OR
274 * Err(CannotTransform) if the transformation failed for some reason. The
275 `do_match(...)` template method should usually be used to reject
276 the form of the given Line, but in some cases it is difficult to
277 know whether or not a Line meets the StringTransformer's
278 requirements until the transformation is already midway.
280 Side Effects:
281 This method should NOT mutate @line directly, but it MAY mutate the
282 Line's underlying Node structure. (WARNING: If the underlying Node
283 structure IS altered, then this method should NOT be allowed to
284 yield an CannotTransform after that point.)
285 """
287 def __call__(
288 self, line: Line, _features: Collection[Feature], _mode: Mode
289 ) -> Iterator[Line]:
290 """
291 StringTransformer instances have a call signature that mirrors that of
292 the Transformer type.
294 Raises:
295 CannotTransform(...) if the concrete StringTransformer class is unable
296 to transform @line.
297 """
298 # Optimization to avoid calling `self.do_match(...)` when the line does
299 # not contain any string.
300 if not any(leaf.type == token.STRING for leaf in line.leaves):
301 raise CannotTransform("There are no strings in this line.")
303 match_result = self.do_match(line)
305 if isinstance(match_result, Err):
306 cant_transform = match_result.err()
307 raise CannotTransform(
308 f"The string transformer {self.__class__.__name__} does not recognize"
309 " this line as one that it can transform."
310 ) from cant_transform
312 string_indices = match_result.ok()
314 for line_result in self.do_transform(line, string_indices):
315 if isinstance(line_result, Err):
316 cant_transform = line_result.err()
317 raise CannotTransform(
318 "StringTransformer failed while attempting to transform string."
319 ) from cant_transform
320 line = line_result.ok()
321 yield line
324@dataclass
325class CustomSplit:
326 """A custom (i.e. manual) string split.
328 A single CustomSplit instance represents a single substring.
330 Examples:
331 Consider the following string:
332 ```
333 "Hi there friend."
334 " This is a custom"
335 f" string {split}."
336 ```
338 This string will correspond to the following three CustomSplit instances:
339 ```
340 CustomSplit(False, 16)
341 CustomSplit(False, 17)
342 CustomSplit(True, 16)
343 ```
344 """
346 has_prefix: bool
347 break_idx: int
350CustomSplitMapKey = tuple[StringID, str]
353@trait
354class CustomSplitMapMixin:
355 """
356 This mixin class is used to map merged strings to a sequence of
357 CustomSplits, which will then be used to re-split the strings iff none of
358 the resultant substrings go over the configured max line length.
359 """
361 _CUSTOM_SPLIT_MAP: ClassVar[dict[CustomSplitMapKey, tuple[CustomSplit, ...]]] = (
362 defaultdict(tuple)
363 )
365 @staticmethod
366 def _get_key(string: str) -> CustomSplitMapKey:
367 """
368 Returns:
369 A unique identifier that is used internally to map @string to a
370 group of custom splits.
371 """
372 return (id(string), string)
374 def add_custom_splits(
375 self, string: str, custom_splits: Iterable[CustomSplit]
376 ) -> None:
377 """Custom Split Map Setter Method
379 Side Effects:
380 Adds a mapping from @string to the custom splits @custom_splits.
381 """
382 key = self._get_key(string)
383 self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits)
385 def pop_custom_splits(self, string: str) -> list[CustomSplit]:
386 """Custom Split Map Getter Method
388 Returns:
389 * A list of the custom splits that are mapped to @string, if any
390 exist.
391 OR
392 * [], otherwise.
394 Side Effects:
395 Deletes the mapping between @string and its associated custom
396 splits (which are returned to the caller).
397 """
398 key = self._get_key(string)
400 custom_splits = self._CUSTOM_SPLIT_MAP[key]
401 del self._CUSTOM_SPLIT_MAP[key]
403 return list(custom_splits)
405 def has_custom_splits(self, string: str) -> bool:
406 """
407 Returns:
408 True iff @string is associated with a set of custom splits.
409 """
410 key = self._get_key(string)
411 return key in self._CUSTOM_SPLIT_MAP
414class StringMerger(StringTransformer, CustomSplitMapMixin):
415 """StringTransformer that merges strings together.
417 Requirements:
418 (A) The line contains adjacent strings such that ALL of the validation checks
419 listed in StringMerger._validate_msg(...)'s docstring pass.
420 OR
421 (B) The line contains a string which uses line continuation backslashes.
423 Transformations:
424 Depending on which of the two requirements above where met, either:
426 (A) The string group associated with the target string is merged.
427 OR
428 (B) All line-continuation backslashes are removed from the target string.
430 Collaborations:
431 StringMerger provides custom split information to StringSplitter.
432 """
434 def do_match(self, line: Line) -> TMatchResult:
435 LL = line.leaves
437 is_valid_index = is_valid_index_factory(LL)
439 string_indices = []
440 idx = 0
441 while is_valid_index(idx):
442 leaf = LL[idx]
443 if (
444 leaf.type == token.STRING
445 and is_valid_index(idx + 1)
446 and LL[idx + 1].type == token.STRING
447 ):
448 # Let's check if the string group contains an inline comment
449 # If we have a comment inline, we don't merge the strings
450 contains_comment = False
451 i = idx
452 while is_valid_index(i):
453 if LL[i].type != token.STRING:
454 break
455 if line.comments_after(LL[i]):
456 contains_comment = True
457 break
458 i += 1
460 if not contains_comment and not is_part_of_annotation(leaf):
461 string_indices.append(idx)
463 # Advance to the next non-STRING leaf.
464 idx += 2
465 while is_valid_index(idx) and LL[idx].type == token.STRING:
466 idx += 1
468 elif leaf.type == token.STRING and "\\\n" in leaf.value:
469 string_indices.append(idx)
470 # Advance to the next non-STRING leaf.
471 idx += 1
472 while is_valid_index(idx) and LL[idx].type == token.STRING:
473 idx += 1
475 else:
476 idx += 1
478 if string_indices:
479 return Ok(string_indices)
480 else:
481 return TErr("This line has no strings that need merging.")
483 def do_transform(
484 self, line: Line, string_indices: list[int]
485 ) -> Iterator[TResult[Line]]:
486 new_line = line
488 rblc_result = self._remove_backslash_line_continuation_chars(
489 new_line, string_indices
490 )
491 if isinstance(rblc_result, Ok):
492 new_line = rblc_result.ok()
494 msg_result = self._merge_string_group(new_line, string_indices)
495 if isinstance(msg_result, Ok):
496 new_line = msg_result.ok()
498 if isinstance(rblc_result, Err) and isinstance(msg_result, Err):
499 msg_cant_transform = msg_result.err()
500 rblc_cant_transform = rblc_result.err()
501 cant_transform = CannotTransform(
502 "StringMerger failed to merge any strings in this line."
503 )
505 # Chain the errors together using `__cause__`.
506 msg_cant_transform.__cause__ = rblc_cant_transform
507 cant_transform.__cause__ = msg_cant_transform
509 yield Err(cant_transform)
510 else:
511 yield Ok(new_line)
513 @staticmethod
514 def _remove_backslash_line_continuation_chars(
515 line: Line, string_indices: list[int]
516 ) -> TResult[Line]:
517 """
518 Merge strings that were split across multiple lines using
519 line-continuation backslashes.
521 Returns:
522 Ok(new_line), if @line contains backslash line-continuation
523 characters.
524 OR
525 Err(CannotTransform), otherwise.
526 """
527 LL = line.leaves
529 indices_to_transform = []
530 for string_idx in string_indices:
531 string_leaf = LL[string_idx]
532 if (
533 string_leaf.type == token.STRING
534 and "\\\n" in string_leaf.value
535 and not has_triple_quotes(string_leaf.value)
536 # In a raw string, a backslash-newline is part of the value.
537 and "r" not in get_string_prefix(string_leaf.value).lower()
538 ):
539 indices_to_transform.append(string_idx)
541 if not indices_to_transform:
542 return TErr(
543 "Found no string leaves that contain backslash line continuation"
544 " characters."
545 )
547 new_line = line.clone()
548 append_leaves(new_line, line, LL)
550 for string_idx in indices_to_transform:
551 new_string_leaf = new_line.leaves[string_idx]
552 new_string_leaf.value = new_string_leaf.value.replace("\\\n", "")
554 return Ok(new_line)
556 def _merge_string_group(
557 self, line: Line, string_indices: list[int]
558 ) -> TResult[Line]:
559 """
560 Merges string groups (i.e. set of adjacent strings).
562 Each index from `string_indices` designates one string group's first
563 leaf in `line.leaves`.
565 Returns:
566 Ok(new_line), if ALL of the validation checks found in
567 _validate_msg(...) pass.
568 OR
569 Err(CannotTransform), otherwise.
570 """
571 LL = line.leaves
573 is_valid_index = is_valid_index_factory(LL)
575 # A dict of {string_idx: tuple[num_of_strings, string_leaf]}.
576 merged_string_idx_dict: dict[int, tuple[int, Leaf]] = {}
577 for string_idx in string_indices:
578 vresult = self._validate_msg(line, string_idx)
579 if isinstance(vresult, Err):
580 continue
581 merged_string_idx_dict[string_idx] = self._merge_one_string_group(
582 LL, string_idx, is_valid_index
583 )
585 if not merged_string_idx_dict:
586 return TErr("No string group is merged")
588 # Build the final line ('new_line') that this method will later return.
589 new_line = line.clone()
590 previous_merged_string_idx = -1
591 previous_merged_num_of_strings = -1
592 # Leaves outside any merged string group are copied in runs rather than
593 # one at a time. append_leaves resumes the search for each leaf's
594 # position from where the previous sibling of the same parent was found,
595 # so copying a run of leaves that share a parent (the operand tuple of
596 # "%s ..." % (a, b, c, ...)) stays linear; a fresh call per leaf restarts
597 # that search from the front every time and is quadratic in the operands.
598 pending: list[Leaf] = []
599 for i, leaf in enumerate(LL):
600 if i in merged_string_idx_dict:
601 if pending:
602 append_leaves(new_line, line, pending)
603 pending = []
604 previous_merged_string_idx = i
605 previous_merged_num_of_strings, string_leaf = merged_string_idx_dict[i]
606 new_line.append(string_leaf)
608 if (
609 previous_merged_string_idx
610 <= i
611 < previous_merged_string_idx + previous_merged_num_of_strings
612 ):
613 for comment_leaf in line.comments_after(leaf):
614 new_line.append(comment_leaf, preformatted=True)
615 continue
617 pending.append(leaf)
619 if pending:
620 append_leaves(new_line, line, pending)
622 return Ok(new_line)
624 def _merge_one_string_group(
625 self, LL: list[Leaf], string_idx: int, is_valid_index: Callable[[int], bool]
626 ) -> tuple[int, Leaf]:
627 """
628 Merges one string group where the first string in the group is
629 `LL[string_idx]`.
631 Returns:
632 A tuple of `(num_of_strings, leaf)` where `num_of_strings` is the
633 number of strings merged and `leaf` is the newly merged string
634 to be replaced in the new line.
635 """
636 # If the string group is wrapped inside an Atom node, we must make sure
637 # to later replace that Atom with our new (merged) string leaf.
638 atom_node = LL[string_idx].parent
640 # We will place BREAK_MARK in between every two substrings that we
641 # merge. We will then later go through our final result and use the
642 # various instances of BREAK_MARK we find to add the right values to
643 # the custom split map.
644 BREAK_MARK = "@@@@@ BLACK BREAKPOINT MARKER @@@@@"
646 QUOTE = LL[string_idx].value[-1]
648 def make_naked(string: str, string_prefix: str) -> str:
649 """Strip @string (i.e. make it a "naked" string)
651 Pre-conditions:
652 * assert_is_leaf_string(@string)
654 Returns:
655 A string that is identical to @string except that
656 @string_prefix has been stripped, the surrounding QUOTE
657 characters have been removed, and any remaining QUOTE
658 characters have been escaped.
659 """
660 assert_is_leaf_string(string)
661 if "f" in string_prefix:
662 f_expressions = [
663 string[span[0] + 1 : span[1] - 1] # +-1 to get rid of curly braces
664 for span in iter_fexpr_spans(string)
665 ]
666 debug_expressions_contain_visible_quotes = any(
667 re.search(r"[\'\"].*(?<![!:=])={1}(?!=)(?![^\s:])", expression)
668 for expression in f_expressions
669 )
670 if not debug_expressions_contain_visible_quotes:
671 # We don't want to toggle visible quotes in debug f-strings, as
672 # that would modify the AST
673 string = _toggle_fexpr_quotes(string, QUOTE)
674 # After quotes toggling, quotes in expressions won't be escaped
675 # because quotes can't be reused in f-strings. So we can simply
676 # let the escaping logic below run without knowing f-string
677 # expressions.
679 RE_EVEN_BACKSLASHES = r"(?:(?<!\\)(?:\\\\)*)"
680 naked_string = string[len(string_prefix) + 1 : -1]
681 if "r" in string_prefix:
682 # Escaping quotes would change a raw string's value. Validation
683 # ensures all raw substrings already use the same delimiter.
684 return naked_string
685 naked_string = re.sub(
686 "(" + RE_EVEN_BACKSLASHES + ")" + QUOTE, r"\1\\" + QUOTE, naked_string
687 )
688 return naked_string
690 # Holds the CustomSplit objects that will later be added to the custom
691 # split map.
692 custom_splits = []
694 # Temporary storage for the 'has_prefix' part of the CustomSplit objects.
695 prefix_tracker = []
697 # Sets the 'prefix' variable. This is the prefix that the final merged
698 # string will have.
699 next_str_idx = string_idx
700 prefix = ""
701 while (
702 not prefix
703 and is_valid_index(next_str_idx)
704 and LL[next_str_idx].type == token.STRING
705 ):
706 prefix = get_string_prefix(LL[next_str_idx].value).lower()
707 next_str_idx += 1
709 # The next loop merges the string group. The final string will be
710 # contained in 'S'.
711 #
712 # The following convenience variables are used:
713 #
714 # S: string
715 # NS: naked string
716 # SS: next string
717 # NSS: naked next string
718 NS = ""
719 num_of_strings = 0
720 next_str_idx = string_idx
721 while is_valid_index(next_str_idx) and LL[next_str_idx].type == token.STRING:
722 num_of_strings += 1
724 SS = LL[next_str_idx].value
725 next_prefix = get_string_prefix(SS).lower()
727 # If this is an f-string group but this substring is not prefixed
728 # with 'f'...
729 if "f" in prefix and "f" not in next_prefix:
730 # Then we must escape any braces contained in this substring.
731 SS = re.sub(r"(\{|\})", r"\1\1", SS)
733 NSS = make_naked(SS, next_prefix)
735 has_prefix = bool(next_prefix)
736 prefix_tracker.append(has_prefix)
738 # Each NSS is already naked (prefix and quotes stripped, inner quotes
739 # escaped, f-string expression quotes toggled), and the parts are
740 # separated by BREAK_MARK which contains no quote or backslash, so the
741 # naked group is just their concatenation. Re-running make_naked over the
742 # whole accumulated string on every iteration rescans all previously
743 # merged substrings, which is quadratic in the size of the group.
744 NS = NS + NSS + BREAK_MARK
746 next_str_idx += 1
748 # Take a note on the index of the non-STRING leaf.
749 non_string_idx = next_str_idx
751 S = prefix + QUOTE + NS + QUOTE
752 S_leaf = Leaf(token.STRING, S)
753 if self.normalize_strings:
754 S_leaf.value = normalize_string_quotes(S_leaf.value)
756 # Fill the 'custom_splits' list with the appropriate CustomSplit objects.
757 temp_string = S_leaf.value[len(prefix) + 1 : -1]
758 for has_prefix in prefix_tracker:
759 mark_idx = temp_string.find(BREAK_MARK)
760 assert (
761 mark_idx >= 0
762 ), "Logic error while filling the custom string breakpoint cache."
764 temp_string = temp_string[mark_idx + len(BREAK_MARK) :]
765 breakpoint_idx = mark_idx + (len(prefix) if has_prefix else 0) + 1
766 custom_splits.append(CustomSplit(has_prefix, breakpoint_idx))
768 string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, ""))
770 if atom_node is not None:
771 # If not all children of the atom node are merged (this can happen
772 # when there is a standalone comment in the middle) ...
773 if non_string_idx - string_idx < len(atom_node.children):
774 # We need to replace the old STRING leaves with the new string leaf.
775 first_child_idx = LL[string_idx].remove()
776 for idx in range(string_idx + 1, non_string_idx):
777 LL[idx].remove()
778 if first_child_idx is not None:
779 atom_node.insert_child(first_child_idx, string_leaf)
780 else:
781 # Else replace the atom node with the new string leaf.
782 replace_child(atom_node, string_leaf)
784 self.add_custom_splits(string_leaf.value, custom_splits)
785 return num_of_strings, string_leaf
787 def _validate_msg(self, line: Line, string_idx: int) -> TResult[None]:
788 """Validate (M)erge (S)tring (G)roup
790 Transform-time string validation logic for _merge_string_group(...).
792 Returns:
793 * Ok(None), if ALL validation checks (listed below) pass.
794 OR
795 * Err(CannotTransform), if any of the following are true:
796 - The target string group does not contain ANY stand-alone comments.
797 - The target string is not in a string group (i.e. it has no
798 adjacent strings).
799 - The string group has more than one inline comment.
800 - The string group has an inline comment that appears to be a pragma.
801 - The set of all string prefixes in the string group is of
802 length greater than one and is not equal to {"", "f"}.
803 - Raw strings in the group use different quote types.
804 - The string group would merge f-strings with different quote types
805 and internal quotes.
806 - The string group is stringified type annotations. We don't want to
807 process stringified type annotations since pyright doesn't support
808 them spanning multiple string values. (NOTE: mypy, pytype, pyre do
809 support them, so we can change if pyright also gains support in the
810 future. See https://github.com/microsoft/pyright/issues/4359.)
811 """
812 # We first check for "inner" stand-alone comments (i.e. stand-alone
813 # comments that have a string leaf before them AND after them).
814 for inc in [1, -1]:
815 i = string_idx
816 found_sa_comment = False
817 is_valid_index = is_valid_index_factory(line.leaves)
818 while is_valid_index(i) and line.leaves[i].type in [
819 token.STRING,
820 STANDALONE_COMMENT,
821 ]:
822 if line.leaves[i].type == STANDALONE_COMMENT:
823 found_sa_comment = True
824 elif found_sa_comment:
825 return TErr(
826 "StringMerger does NOT merge string groups which contain "
827 "stand-alone comments."
828 )
830 i += inc
832 QUOTE = line.leaves[string_idx].value[-1]
834 num_of_inline_string_comments = 0
835 set_of_prefixes = set()
836 num_of_strings = 0
837 for leaf in line.leaves[string_idx:]:
838 if leaf.type != token.STRING:
839 # If the string group is trailed by a comma, we count the
840 # comments trailing the comma to be one of the string group's
841 # comments.
842 if leaf.type == token.COMMA and id(leaf) in line.comments:
843 num_of_inline_string_comments += 1
844 break
846 if has_triple_quotes(leaf.value):
847 return TErr("StringMerger does NOT merge multiline strings.")
849 num_of_strings += 1
850 prefix = get_string_prefix(leaf.value).lower()
851 if "r" in prefix and leaf.value[-1] != QUOTE:
852 return TErr("Cannot merge raw strings with different quote types.")
853 if "r" in prefix and "f" in prefix:
854 return TErr("Cannot merge raw f-strings.")
856 set_of_prefixes.add(prefix)
858 if (
859 "f" in prefix
860 and leaf.value[-1] != QUOTE
861 and (
862 "'" in leaf.value[len(prefix) + 1 : -1]
863 or '"' in leaf.value[len(prefix) + 1 : -1]
864 )
865 ):
866 return TErr(
867 "StringMerger does NOT merge f-strings with different quote types"
868 " and internal quotes."
869 )
871 if id(leaf) in line.comments:
872 num_of_inline_string_comments += 1
873 if contains_pragma_comment(line.comments[id(leaf)]):
874 return TErr("Cannot merge strings which have pragma comments.")
876 if num_of_strings < 2:
877 return TErr(
878 f"Not enough strings to merge (num_of_strings={num_of_strings})."
879 )
881 # Also check for pragma comments on tokens that follow the string
882 # group (e.g. a closing bracket). Merging strings when a pragma
883 # comment like `# type: ignore` follows would produce an unsplittable
884 # long line.
885 is_valid_index = is_valid_index_factory(line.leaves)
886 next_idx = string_idx + num_of_strings
887 while is_valid_index(next_idx):
888 next_leaf = line.leaves[next_idx]
889 if id(next_leaf) in line.comments:
890 if contains_pragma_comment(line.comments[id(next_leaf)]):
891 return TErr(
892 "Cannot merge strings when a pragma comment follows"
893 " the string group."
894 )
895 if next_leaf.type not in CLOSING_BRACKETS:
896 break
897 next_idx += 1
899 if num_of_inline_string_comments > 1:
900 return TErr(
901 f"Too many inline string comments ({num_of_inline_string_comments})."
902 )
904 if len(set_of_prefixes) > 1 and set_of_prefixes != {"", "f"}:
905 return TErr(f"Too many different prefixes ({set_of_prefixes}).")
907 if any("r" in prefix for prefix in set_of_prefixes):
908 # Keep long raw groups intact: merging and splitting them again can
909 # introduce unnecessary parentheses around an existing concatenation.
910 prefix = next(iter(set_of_prefixes))
911 merged_length = str_width(str(line).rstrip()) - (num_of_strings - 1) * (
912 len(prefix) + 3
913 )
914 if merged_length > self.line_length:
915 return TErr(
916 "Cannot merge a raw string group exceeding the line length."
917 )
919 return Ok(None)
922class StringParenStripper(StringTransformer, CustomSplitMapMixin):
923 """StringTransformer that strips surrounding parentheses from strings.
925 Requirements:
926 The line contains a string which is surrounded by parentheses and:
927 - The target string is NOT the only argument to a function call.
928 - The target string is NOT a "pointless" string.
929 - The target string is NOT a dictionary value.
930 - If the target string contains a PERCENT, the brackets are not
931 preceded or followed by an operator with higher precedence than
932 PERCENT.
934 Transformations:
935 The parentheses mentioned in the 'Requirements' section are stripped.
937 Collaborations:
938 StringParenStripper has its own inherent usefulness, but it is also
939 relied on to clean up the parentheses created by StringParenWrapper (in
940 the event that they are no longer needed).
941 """
943 def do_match(self, line: Line) -> TMatchResult:
944 LL = line.leaves
946 is_valid_index = is_valid_index_factory(LL)
948 string_indices = []
950 idx = -1
951 while True:
952 idx += 1
953 if idx >= len(LL):
954 break
955 leaf = LL[idx]
957 # Should be a string...
958 if leaf.type != token.STRING:
959 continue
961 # If this is a "pointless" string...
962 if (
963 leaf.parent
964 and leaf.parent.parent
965 and leaf.parent.parent.type == syms.simple_stmt
966 ):
967 continue
969 # Should be preceded by a non-empty LPAR...
970 if (
971 not is_valid_index(idx - 1)
972 or LL[idx - 1].type != token.LPAR
973 or is_empty_lpar(LL[idx - 1])
974 ):
975 continue
977 # That LPAR should NOT be preceded by a colon (which could be a
978 # dictionary value), function name, or a closing bracket (which
979 # could be a function returning a function or a list/dictionary
980 # containing a function)...
981 if is_valid_index(idx - 2) and (
982 LL[idx - 2].type == token.COLON
983 or LL[idx - 2].type == token.NAME
984 or LL[idx - 2].type in CLOSING_BRACKETS
985 ):
986 continue
988 string_idx = idx
990 # Skip the string trailer, if one exists.
991 string_parser = StringParser()
992 next_idx = string_parser.parse(LL, string_idx)
994 # if the leaves in the parsed string include a PERCENT, we need to
995 # make sure the initial LPAR is NOT preceded by an operator with
996 # higher or equal precedence to PERCENT
997 if is_valid_index(idx - 2):
998 # mypy can't quite follow unless we name this
999 before_lpar = LL[idx - 2]
1000 if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and (
1001 (
1002 before_lpar.type in {
1003 token.STAR,
1004 token.AT,
1005 token.SLASH,
1006 token.DOUBLESLASH,
1007 token.PERCENT,
1008 token.TILDE,
1009 token.DOUBLESTAR,
1010 token.AWAIT,
1011 token.LSQB,
1012 token.LPAR,
1013 }
1014 )
1015 or (
1016 # only unary PLUS/MINUS
1017 before_lpar.parent
1018 and before_lpar.parent.type == syms.factor
1019 and (before_lpar.type in {token.PLUS, token.MINUS})
1020 )
1021 ):
1022 continue
1024 # Should be followed by a non-empty RPAR...
1025 if (
1026 is_valid_index(next_idx)
1027 and LL[next_idx].type == token.RPAR
1028 and not is_empty_rpar(LL[next_idx])
1029 ):
1030 # That RPAR should NOT be followed by anything with higher
1031 # precedence than PERCENT
1032 if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in {
1033 token.DOUBLESTAR,
1034 token.LSQB,
1035 token.LPAR,
1036 token.DOT,
1037 }:
1038 continue
1040 # Keep the parens if stripping them would leave an already
1041 # too-long line with a merged string group glued to an `=`
1042 # prefix (e.g. a def parameter default). Black cannot split
1043 # such a line after the fact, while the parens let the layout
1044 # split the group at its custom breakpoints. See #3497.
1045 if (
1046 not is_line_short_enough(line, mode=line.mode)
1047 and is_valid_index(string_idx - 2)
1048 and LL[string_idx - 2].type == token.EQUAL
1049 and self.has_custom_splits(LL[string_idx].value)
1050 ):
1051 continue
1053 string_indices.append(string_idx)
1054 idx = string_idx
1055 while idx < len(LL) - 1 and LL[idx + 1].type == token.STRING:
1056 idx += 1
1058 if string_indices:
1059 return Ok(string_indices)
1060 return TErr("This line has no strings wrapped in parens.")
1062 def do_transform(
1063 self, line: Line, string_indices: list[int]
1064 ) -> Iterator[TResult[Line]]:
1065 LL = line.leaves
1067 string_and_rpar_indices: list[int] = []
1068 for string_idx in string_indices:
1069 string_parser = StringParser()
1070 rpar_idx = string_parser.parse(LL, string_idx)
1072 should_transform = True
1073 for leaf in (LL[string_idx - 1], LL[rpar_idx]):
1074 if line.comments_after(leaf):
1075 # Should not strip parentheses which have comments attached
1076 # to them.
1077 should_transform = False
1078 break
1079 if should_transform:
1080 string_and_rpar_indices.extend((string_idx, rpar_idx))
1082 if string_and_rpar_indices:
1083 yield Ok(self._transform_to_new_line(line, string_and_rpar_indices))
1084 else:
1085 yield Err(
1086 CannotTransform("All string groups have comments attached to them.")
1087 )
1089 def _transform_to_new_line(
1090 self, line: Line, string_and_rpar_indices: list[int]
1091 ) -> Line:
1092 LL = line.leaves
1094 new_line = line.clone()
1096 previous_idx = -1
1097 # We need to sort the indices, since string_idx and its matching
1098 # rpar_idx may not come in order, e.g. in
1099 # `("outer" % ("inner".join(items)))`, the "inner" string's
1100 # string_idx is smaller than "outer" string's rpar_idx.
1101 for idx in sorted(string_and_rpar_indices):
1102 leaf = LL[idx]
1103 lpar_or_rpar_idx = idx - 1 if leaf.type == token.STRING else idx
1104 append_leaves(new_line, line, LL[previous_idx + 1 : lpar_or_rpar_idx])
1105 if leaf.type == token.STRING:
1106 string_leaf = Leaf(token.STRING, LL[idx].value)
1107 LL[lpar_or_rpar_idx].remove() # Remove lpar.
1108 replace_child(LL[idx], string_leaf)
1109 new_line.append(string_leaf)
1110 for comment_leaf in line.comments_after(LL[idx]):
1111 new_line.append(comment_leaf, preformatted=True)
1112 else:
1113 LL[lpar_or_rpar_idx].remove() # This is a rpar.
1115 previous_idx = idx
1117 # Append the leaves after the last idx:
1118 append_leaves(new_line, line, LL[idx + 1 :])
1120 return new_line
1123class BaseStringSplitter(StringTransformer, CustomSplitMapMixin):
1124 """
1125 Abstract class for StringTransformers which transform a Line's strings by splitting
1126 them or placing them on their own lines where necessary to avoid going over
1127 the configured line length.
1129 Requirements:
1130 * The target string value is responsible for the line going over the
1131 line length limit. It follows that after all of black's other line
1132 split methods have been exhausted, this line (or one of the resulting
1133 lines after all line splits are performed) would still be over the
1134 line_length limit unless we split this string.
1135 AND
1137 * The target string is NOT a "pointless" string (i.e. a string that has
1138 no parent or siblings).
1139 AND
1141 * The target string is not followed by an inline comment that appears
1142 to be a pragma.
1143 AND
1145 * The target string is not a multiline (i.e. triple-quote) string.
1146 """
1148 STRING_OPERATORS: Final = COMPARATORS | (MATH_OPERATORS - {token.TILDE})
1150 MIN_SUBSTR_SIZE: Final = 6
1152 def _iter_nameescape_slices(self, string: str) -> Iterator[tuple[Index, Index]]:
1153 r"""
1154 Yields:
1155 All ranges of @string which, if @string were to be split there,
1156 would result in the splitting of an \N{...} expression (which is NOT
1157 allowed).
1158 """
1159 # True - the previous backslash was unescaped
1160 # False - the previous backslash was escaped *or* there was no backslash
1161 previous_was_unescaped_backslash = False
1162 it = iter(enumerate(string))
1163 for idx, c in it:
1164 if c == "\\":
1165 previous_was_unescaped_backslash = not previous_was_unescaped_backslash
1166 continue
1167 if not previous_was_unescaped_backslash or c != "N":
1168 previous_was_unescaped_backslash = False
1169 continue
1170 previous_was_unescaped_backslash = False
1172 begin = idx - 1 # the position of backslash before \N{...}
1173 for idx, c in it:
1174 if c == "}":
1175 end = idx
1176 break
1177 else:
1178 # malformed nameescape expression?
1179 # should have been detected by AST parsing earlier...
1180 raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
1181 yield begin, end
1183 def _iter_fexpr_slices(self, string: str) -> Iterator[tuple[Index, Index]]:
1184 """
1185 Yields:
1186 All ranges of @string which, if @string were to be split there,
1187 would result in the splitting of an f-expression (which is NOT
1188 allowed).
1189 """
1190 if "f" not in get_string_prefix(string).lower():
1191 return
1192 yield from iter_fexpr_spans(string)
1194 def _get_illegal_split_indices(
1195 self, string: str, has_named_escape: bool = True
1196 ) -> set[Index]:
1197 illegal_indices: set[Index] = set()
1198 iterators = [self._iter_fexpr_slices(string)]
1199 # Scanning for \N{...} ranges walks the whole string, so skip it when the
1200 # caller already knows the string cannot contain a named escape.
1201 if has_named_escape:
1202 iterators.append(self._iter_nameescape_slices(string))
1203 for it in iterators:
1204 for begin, end in it:
1205 illegal_indices.update(range(begin, end))
1206 return illegal_indices
1208 def _is_valid_split_index(
1209 self, string: str, i: Index, illegal_split_indices: set[Index]
1210 ) -> bool:
1211 """
1212 Returns:
1213 True iff @i meets all requirements to be a valid split index in @string.
1214 """
1215 if i < self.MIN_SUBSTR_SIZE or len(string) - i < self.MIN_SUBSTR_SIZE:
1216 return False
1218 if i in illegal_split_indices:
1219 return False
1221 is_space = string[i] == " "
1222 is_split_safe = i > 0 and string[i - 1] in SPLIT_SAFE_CHARS
1224 if not (is_space or is_split_safe):
1225 return False
1227 is_not_escaped = True
1228 j = i - 1
1229 while j >= 0 and string[j] == "\\":
1230 is_not_escaped = not is_not_escaped
1231 j -= 1
1233 return is_not_escaped
1235 def can_be_split(self, string: str) -> bool:
1236 """
1237 Returns:
1238 True iff @string contains at least one index where StringSplitter
1239 can split it.
1240 """
1241 if len(string) < 2 * self.MIN_SUBSTR_SIZE:
1242 return False
1244 has_named_escape = "\\N" in string
1245 illegal_split_indices = self._get_illegal_split_indices(
1246 string, has_named_escape
1247 )
1249 return any(
1250 self._is_valid_split_index(string, i, illegal_split_indices)
1251 for i in range(self.MIN_SUBSTR_SIZE, len(string) - self.MIN_SUBSTR_SIZE + 1)
1252 )
1254 @abstractmethod
1255 def do_splitter_match(self, line: Line) -> TMatchResult:
1256 """
1257 BaseStringSplitter asks its clients to override this method instead of
1258 `StringTransformer.do_match(...)`.
1260 Follows the same protocol as `StringTransformer.do_match(...)`.
1262 Refer to `help(StringTransformer.do_match)` for more information.
1263 """
1265 def do_match(self, line: Line) -> TMatchResult:
1266 match_result = self.do_splitter_match(line)
1267 if isinstance(match_result, Err):
1268 return match_result
1270 string_indices = match_result.ok()
1271 assert len(string_indices) == 1, (
1272 f"{self.__class__.__name__} should only find one match at a time, found"
1273 f" {len(string_indices)}"
1274 )
1275 string_idx = string_indices[0]
1276 vresult = self._validate(line, string_idx)
1277 if isinstance(vresult, Err):
1278 return vresult
1280 return match_result
1282 def _validate(self, line: Line, string_idx: int) -> TResult[None]:
1283 """
1284 Checks that @line meets all of the requirements listed in this classes'
1285 docstring. Refer to `help(BaseStringSplitter)` for a detailed
1286 description of those requirements.
1288 Returns:
1289 * Ok(None), if ALL of the requirements are met.
1290 OR
1291 * Err(CannotTransform), if ANY of the requirements are NOT met.
1292 """
1293 LL = line.leaves
1295 string_leaf = LL[string_idx]
1297 max_string_length = self._get_max_string_length(line, string_idx)
1298 if len(string_leaf.value) <= max_string_length:
1299 return TErr(
1300 "The string itself is not what is causing this line to be too long."
1301 )
1303 if not string_leaf.parent or [L.type for L in string_leaf.parent.children] == [
1304 token.STRING,
1305 token.NEWLINE,
1306 ]:
1307 return TErr(
1308 f"This string ({string_leaf.value}) appears to be pointless (i.e. has"
1309 " no parent)."
1310 )
1312 if id(line.leaves[string_idx]) in line.comments and contains_pragma_comment(
1313 line.comments[id(line.leaves[string_idx])]
1314 ):
1315 return TErr(
1316 "Line appears to end with an inline pragma comment. Splitting the line"
1317 " could modify the pragma's behavior."
1318 )
1320 if has_triple_quotes(string_leaf.value):
1321 return TErr("We cannot split multiline strings.")
1323 return Ok(None)
1325 def _get_max_string_length(self, line: Line, string_idx: int) -> int:
1326 """
1327 Calculates the max string length used when attempting to determine
1328 whether or not the target string is responsible for causing the line to
1329 go over the line length limit.
1331 WARNING: This method is tightly coupled to both StringSplitter and
1332 (especially) StringParenWrapper. There is probably a better way to
1333 accomplish what is being done here.
1335 Returns:
1336 max_string_length: such that `line.leaves[string_idx].value >
1337 max_string_length` implies that the target string IS responsible
1338 for causing this line to exceed the line length limit.
1339 """
1340 LL = line.leaves
1342 is_valid_index = is_valid_index_factory(LL)
1344 # We use the shorthand "WMA4" in comments to abbreviate "We must
1345 # account for". When giving examples, we use STRING to mean some/any
1346 # valid string.
1347 #
1348 # Finally, we use the following convenience variables:
1349 #
1350 # P: The leaf that is before the target string leaf.
1351 # N: The leaf that is after the target string leaf.
1352 # NN: The leaf that is after N.
1354 # WMA4 the whitespace at the beginning of the line.
1355 offset = line.depth * 4
1357 if is_valid_index(string_idx - 1):
1358 p_idx = string_idx - 1
1359 if (
1360 LL[string_idx - 1].type == token.LPAR
1361 and LL[string_idx - 1].value == ""
1362 and string_idx >= 2
1363 ):
1364 # If the previous leaf is an empty LPAR placeholder, we should skip it.
1365 p_idx -= 1
1367 P = LL[p_idx]
1368 if P.type in self.STRING_OPERATORS:
1369 # WMA4 a space and a string operator (e.g. `+ STRING` or `== STRING`).
1370 offset += len(str(P)) + 1
1372 if P.type == token.COMMA:
1373 # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`].
1374 offset += 3
1376 if P.type == token.NAME or P.value in ASSIGNMENTS:
1377 # This conditional branch is meant to handle dictionary keys,
1378 # variable assignments, 'return STRING' statement lines, and
1379 # 'else STRING' ternary expression lines.
1381 # WMA4 a single space.
1382 offset += 1
1384 # WMA4 the lengths of any leaves that came before that space,
1385 # but after any closing bracket before that space.
1386 for leaf in reversed(LL[: p_idx + 1]):
1387 offset += len(str(leaf))
1388 if leaf.type in CLOSING_BRACKETS:
1389 break
1391 if is_valid_index(string_idx + 1):
1392 N = LL[string_idx + 1]
1393 if N.type == token.RPAR and N.value == "" and len(LL) > string_idx + 2:
1394 # If the next leaf is an empty RPAR placeholder, we should skip it.
1395 N = LL[string_idx + 2]
1397 if N.type == token.COMMA:
1398 # WMA4 a single comma at the end of the string (e.g `STRING,`).
1399 offset += 1
1401 if is_valid_index(string_idx + 2):
1402 NN = LL[string_idx + 2]
1404 if N.type == token.DOT and NN.type == token.NAME:
1405 # This conditional branch is meant to handle method calls invoked
1406 # off of a string literal up to and including the LPAR character.
1408 # WMA4 the '.' character.
1409 offset += 1
1411 if (
1412 is_valid_index(string_idx + 3)
1413 and LL[string_idx + 3].type == token.LPAR
1414 ):
1415 # WMA4 the left parenthesis character.
1416 offset += 1
1418 # WMA4 the length of the method's name.
1419 offset += len(NN.value)
1421 has_comments = False
1422 for comment_leaf in line.comments_after(LL[string_idx]):
1423 if not has_comments:
1424 has_comments = True
1425 # WMA4 two spaces before the '#' character.
1426 offset += 2
1428 # WMA4 the length of the inline comment.
1429 offset += len(comment_leaf.value)
1431 max_string_length = count_chars_in_width(str(line), self.line_length - offset)
1432 return max_string_length
1434 def _prefer_paren_wrap_match(self, line: Line) -> int | None:
1435 """
1436 Returns:
1437 string_idx such that @LL[string_idx] is equal to our target (i.e.
1438 matched) string, if this line matches the "prefer paren wrap" statement
1439 requirements listed in the 'Requirements' section of the StringParenWrapper
1440 class's docstring.
1441 OR
1442 None, otherwise.
1443 """
1444 LL = line.leaves
1445 # The line must start with a string.
1446 if not LL or LL[0].type != token.STRING:
1447 return None
1449 # Atom strings prefer to be wrapped in parens only if they will actually
1450 # be split by StringSplitter, or if the line has comments that will be
1451 # moved to the LPAR.
1452 if (
1453 not self.can_be_split(LL[0].value)
1454 and not self.has_custom_splits(LL[0].value)
1455 and not line.comments
1456 ):
1457 return None
1459 matching_nodes = [
1460 syms.listmaker,
1461 syms.dictsetmaker,
1462 syms.testlist_gexp,
1463 ]
1464 # If the string is an immediate child of a list/set/tuple literal...
1465 if (
1466 parent_type(LL[0]) in matching_nodes
1467 or parent_type(LL[0].parent) in matching_nodes
1468 ):
1469 # And the string is surrounded by commas (or is the first/last child)...
1470 prev_sibling = LL[0].prev_sibling
1471 next_sibling = LL[0].next_sibling
1472 if (
1473 not prev_sibling
1474 and not next_sibling
1475 and parent_type(LL[0]) == syms.atom
1476 ):
1477 # If it's an atom string, we need to check the parent atom's siblings.
1478 parent = LL[0].parent
1479 assert parent is not None # For type checkers.
1480 prev_sibling = parent.prev_sibling
1481 next_sibling = parent.next_sibling
1482 if (not prev_sibling or prev_sibling.type == token.COMMA) and (
1483 not next_sibling or next_sibling.type == token.COMMA
1484 ):
1485 return 0
1487 return None
1490def iter_fexpr_spans(s: str) -> Iterator[tuple[int, int]]:
1491 """
1492 Yields spans corresponding to expressions in a given f-string.
1493 Spans are half-open ranges (left inclusive, right exclusive).
1494 Assumes the input string is a valid f-string, but will not crash if the input
1495 string is invalid.
1496 """
1497 stack: list[int] = [] # our curly paren stack
1498 i = 0
1499 while i < len(s):
1500 if s[i] == "{":
1501 # if we're in a string part of the f-string, ignore escaped curly braces
1502 if not stack and i + 1 < len(s) and s[i + 1] == "{":
1503 i += 2
1504 continue
1505 stack.append(i)
1506 i += 1
1507 continue
1509 if s[i] == "}":
1510 if not stack:
1511 i += 1
1512 continue
1513 j = stack.pop()
1514 # we've made it back out of the expression! yield the span
1515 if not stack:
1516 yield (j, i + 1)
1517 i += 1
1518 continue
1520 # if we're in an expression part of the f-string, fast-forward through strings
1521 # note that backslashes are not legal in the expression portion of f-strings
1522 if stack:
1523 delim = None
1524 if s[i : i + 3] in ("'''", '"""'):
1525 delim = s[i : i + 3]
1526 elif s[i] in ("'", '"'):
1527 delim = s[i]
1528 if delim:
1529 i += len(delim)
1530 while i < len(s) and s[i : i + len(delim)] != delim:
1531 i += 1
1532 i += len(delim)
1533 continue
1534 i += 1
1537def fstring_contains_expr(s: str) -> bool:
1538 return any(iter_fexpr_spans(s))
1541def _toggle_fexpr_quotes(fstring: str, old_quote: str) -> str:
1542 """
1543 Toggles quotes used in f-string expressions that are `old_quote`.
1545 f-string expressions can't contain backslashes, so we need to toggle the
1546 quotes if the f-string itself will end up using the same quote. We can
1547 simply toggle without escaping because, quotes can't be reused in f-string
1548 expressions. They will fail to parse.
1550 NOTE: If PEP 701 is accepted, above statement will no longer be true.
1551 Though if quotes can be reused, we can simply reuse them without updates or
1552 escaping, once Black figures out how to parse the new grammar.
1553 """
1554 new_quote = "'" if old_quote == '"' else '"'
1555 parts = []
1556 previous_index = 0
1557 for start, end in iter_fexpr_spans(fstring):
1558 parts.append(fstring[previous_index:start])
1559 parts.append(fstring[start:end].replace(old_quote, new_quote))
1560 previous_index = end
1561 parts.append(fstring[previous_index:])
1562 return "".join(parts)
1565class StringSplitter(BaseStringSplitter, CustomSplitMapMixin):
1566 """
1567 StringTransformer that splits "atom" strings (i.e. strings which exist on
1568 lines by themselves).
1570 Requirements:
1571 * The line consists ONLY of a single string (possibly prefixed by a
1572 string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE
1573 a trailing comma.
1574 AND
1575 * All of the requirements listed in BaseStringSplitter's docstring.
1577 Transformations:
1578 The string mentioned in the 'Requirements' section is split into as
1579 many substrings as necessary to adhere to the configured line length.
1581 In the final set of substrings, no substring should be smaller than
1582 MIN_SUBSTR_SIZE characters.
1584 The string will ONLY be split on spaces (i.e. each new substring should
1585 start with a space). Note that the string will NOT be split on a space
1586 which is escaped with a backslash.
1588 If the string is an f-string, it will NOT be split in the middle of an
1589 f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x
1590 else bar()} is an f-expression).
1592 If the string that is being split has an associated set of custom split
1593 records and those custom splits will NOT result in any line going over
1594 the configured line length, those custom splits are used. Otherwise the
1595 string is split as late as possible (from left-to-right) while still
1596 adhering to the transformation rules listed above.
1598 Collaborations:
1599 StringSplitter relies on StringMerger to construct the appropriate
1600 CustomSplit objects and add them to the custom split map.
1601 """
1603 def do_splitter_match(self, line: Line) -> TMatchResult:
1604 LL = line.leaves
1606 if self._prefer_paren_wrap_match(line) is not None:
1607 return TErr("Line needs to be wrapped in parens first.")
1609 # If the line is just STRING + COMMA (a one-item tuple) and not inside
1610 # brackets, we need to defer to StringParenWrapper to wrap it first.
1611 # Otherwise, splitting the string would create multiple expressions where
1612 # only the last has the comma, breaking AST equivalence. See issue #4912.
1613 if (
1614 not line.inside_brackets
1615 and len(LL) == 2
1616 and LL[0].type == token.STRING
1617 and LL[1].type == token.COMMA
1618 ):
1619 return TErr(
1620 "Line with trailing comma tuple needs to be wrapped in parens first."
1621 )
1623 is_valid_index = is_valid_index_factory(LL)
1625 idx = 0
1627 # The first two leaves MAY be the 'not in' keywords...
1628 if (
1629 is_valid_index(idx)
1630 and is_valid_index(idx + 1)
1631 and [LL[idx].type, LL[idx + 1].type] == [token.NAME, token.NAME]
1632 and str(LL[idx]) + str(LL[idx + 1]) == "not in"
1633 ):
1634 idx += 2
1635 # Else the first leaf MAY be a string operator symbol or the 'in' keyword...
1636 elif is_valid_index(idx) and (
1637 LL[idx].type in self.STRING_OPERATORS
1638 or LL[idx].type == token.NAME
1639 and str(LL[idx]) == "in"
1640 ):
1641 idx += 1
1643 # The next/first leaf MAY be an empty LPAR...
1644 if is_valid_index(idx) and is_empty_lpar(LL[idx]):
1645 idx += 1
1647 # The next/first leaf MUST be a string...
1648 if not is_valid_index(idx) or LL[idx].type != token.STRING:
1649 return TErr("Line does not start with a string.")
1651 string_idx = idx
1653 # Skip the string trailer, if one exists.
1654 string_parser = StringParser()
1655 idx = string_parser.parse(LL, string_idx)
1657 # That string MAY be followed by an empty RPAR...
1658 if is_valid_index(idx) and is_empty_rpar(LL[idx]):
1659 idx += 1
1661 # That string / empty RPAR leaf MAY be followed by a comma...
1662 if is_valid_index(idx) and LL[idx].type == token.COMMA:
1663 idx += 1
1665 # But no more leaves are allowed...
1666 if is_valid_index(idx):
1667 return TErr("This line does not end with a string.")
1669 return Ok([string_idx])
1671 def do_transform(
1672 self, line: Line, string_indices: list[int]
1673 ) -> Iterator[TResult[Line]]:
1674 LL = line.leaves
1675 assert len(string_indices) == 1, (
1676 f"{self.__class__.__name__} should only find one match at a time, found"
1677 f" {len(string_indices)}"
1678 )
1679 string_idx = string_indices[0]
1681 QUOTE = LL[string_idx].value[-1]
1683 is_valid_index = is_valid_index_factory(LL)
1684 insert_str_child = insert_str_child_factory(LL[string_idx])
1686 prefix = get_string_prefix(LL[string_idx].value).lower()
1688 # We MAY choose to drop the 'f' prefix from substrings that don't
1689 # contain any f-expressions, but ONLY if the original f-string
1690 # contains at least one f-expression. Otherwise, we will alter the AST
1691 # of the program.
1692 drop_pointless_f_prefix = ("f" in prefix) and fstring_contains_expr(
1693 LL[string_idx].value
1694 )
1696 first_string_line = True
1698 string_op_leaves = self._get_string_operator_leaves(LL)
1699 string_op_leaves_length = (
1700 sum(len(str(prefix_leaf)) for prefix_leaf in string_op_leaves) + 1
1701 if string_op_leaves
1702 else 0
1703 )
1705 def maybe_append_string_operators(new_line: Line) -> None:
1706 """
1707 Side Effects:
1708 If @line starts with a string operator and this is the first
1709 line we are constructing, this function appends the string
1710 operator to @new_line and replaces the old string operator leaf
1711 in the node structure. Otherwise this function does nothing.
1712 """
1713 maybe_prefix_leaves = string_op_leaves if first_string_line else []
1714 for i, prefix_leaf in enumerate(maybe_prefix_leaves):
1715 replace_child(LL[i], prefix_leaf)
1716 new_line.append(prefix_leaf)
1718 ends_with_comma = (
1719 is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA
1720 )
1722 def max_last_string_column() -> int:
1723 """
1724 Returns:
1725 The max allowed width of the string value used for the last
1726 line we will construct. Note that this value means the width
1727 rather than the number of characters (e.g., many East Asian
1728 characters expand to two columns).
1729 """
1730 result = self.line_length
1731 result -= line.depth * 4
1732 result -= 1 if ends_with_comma else 0
1733 result -= string_op_leaves_length
1734 return result
1736 # --- Calculate Max Break Width (for string value)
1737 # We start with the line length limit
1738 max_break_width = self.line_length
1739 # The last index of a string of length N is N-1.
1740 max_break_width -= 1
1741 # Leading whitespace is not present in the string value (e.g. Leaf.value).
1742 max_break_width -= line.depth * 4
1743 if max_break_width < 0:
1744 yield TErr(
1745 f"Unable to split {LL[string_idx].value} at such high of a line depth:"
1746 f" {line.depth}"
1747 )
1748 return
1750 # Check if StringMerger registered any custom splits.
1751 custom_splits = self.pop_custom_splits(LL[string_idx].value)
1752 # We use them ONLY if none of them would produce lines that exceed the
1753 # line limit.
1754 use_custom_breakpoints = bool(
1755 custom_splits
1756 and all(csplit.break_idx <= max_break_width for csplit in custom_splits)
1757 )
1759 # Temporary storage for the remaining chunk of the string line that
1760 # can't fit onto the line currently being constructed.
1761 rest_value = LL[string_idx].value
1763 # Each substring is a suffix of this value (plus the prefix and quote),
1764 # so a missing "\N" here means no substring can hold a named escape and
1765 # the per-substring scan for them can be skipped entirely.
1766 has_named_escape = "\\N" in rest_value
1768 def more_splits_should_be_made() -> bool:
1769 """
1770 Returns:
1771 True iff `rest_value` (the remaining string value from the last
1772 split), should be split again.
1773 """
1774 if use_custom_breakpoints:
1775 return len(custom_splits) > 1
1776 else:
1777 return str_width(rest_value) > max_last_string_column()
1779 string_line_results: list[Ok[Line]] = []
1780 while more_splits_should_be_made():
1781 if use_custom_breakpoints:
1782 # Custom User Split (manual)
1783 csplit = custom_splits.pop(0)
1784 break_idx = csplit.break_idx
1785 else:
1786 # Algorithmic Split (automatic)
1787 max_bidx = (
1788 count_chars_in_width(rest_value, max_break_width)
1789 - string_op_leaves_length
1790 )
1791 maybe_break_idx = self._get_break_idx(
1792 rest_value, max_bidx, has_named_escape
1793 )
1794 if maybe_break_idx is None:
1795 # If we are unable to algorithmically determine a good split
1796 # and this string has custom splits registered to it, we
1797 # fall back to using them--which means we have to start
1798 # over from the beginning.
1799 if custom_splits:
1800 rest_value = LL[string_idx].value
1801 string_line_results = []
1802 first_string_line = True
1803 use_custom_breakpoints = True
1804 continue
1806 # Otherwise, we stop splitting here.
1807 break
1809 break_idx = maybe_break_idx
1811 # --- Construct `next_value`
1812 next_value = rest_value[:break_idx] + QUOTE
1814 # HACK: The following 'if' statement is a hack to fix the custom
1815 # breakpoint index in the case of either: (a) substrings that were
1816 # f-strings but will have the 'f' prefix removed OR (b) substrings
1817 # that were not f-strings but will now become f-strings because of
1818 # redundant use of the 'f' prefix (i.e. none of the substrings
1819 # contain f-expressions but one or more of them had the 'f' prefix
1820 # anyway; in which case, we will prepend 'f' to _all_ substrings).
1821 #
1822 # There is probably a better way to accomplish what is being done
1823 # here...
1824 #
1825 # If this substring is an f-string, we _could_ remove the 'f'
1826 # prefix, and the current custom split did NOT originally use a
1827 # prefix...
1828 if (
1829 use_custom_breakpoints
1830 and not csplit.has_prefix
1831 and (
1832 # `next_value == prefix + QUOTE` happens when the custom
1833 # split is an empty string.
1834 next_value == prefix + QUOTE
1835 or next_value != self._normalize_f_string(next_value, prefix)
1836 )
1837 ):
1838 # Then `csplit.break_idx` will be off by one after removing
1839 # the 'f' prefix.
1840 break_idx += 1
1841 next_value = rest_value[:break_idx] + QUOTE
1843 if drop_pointless_f_prefix:
1844 next_value = self._normalize_f_string(next_value, prefix)
1846 # --- Construct `next_leaf`
1847 next_leaf = Leaf(token.STRING, next_value)
1848 insert_str_child(next_leaf)
1849 self._maybe_normalize_string_quotes(next_leaf)
1851 # --- Construct `next_line`
1852 next_line = line.clone()
1853 maybe_append_string_operators(next_line)
1854 next_line.append(next_leaf)
1855 string_line_results.append(Ok(next_line))
1857 rest_value = prefix + QUOTE + rest_value[break_idx:]
1858 first_string_line = False
1860 yield from string_line_results
1862 if drop_pointless_f_prefix:
1863 rest_value = self._normalize_f_string(rest_value, prefix)
1865 rest_leaf = Leaf(token.STRING, rest_value)
1866 insert_str_child(rest_leaf)
1868 # NOTE: I could not find a test case that verifies that the following
1869 # line is actually necessary, but it seems to be. Otherwise we risk
1870 # not normalizing the last substring, right?
1871 self._maybe_normalize_string_quotes(rest_leaf)
1873 last_line = line.clone()
1874 maybe_append_string_operators(last_line)
1876 # If there are any leaves to the right of the target string...
1877 if is_valid_index(string_idx + 1):
1878 # We use `temp_value` here to determine how long the last line
1879 # would be if we were to append all the leaves to the right of the
1880 # target string to the last string line.
1881 temp_value = rest_value
1882 for leaf in LL[string_idx + 1 :]:
1883 temp_value += str(leaf)
1884 if leaf.type == token.LPAR:
1885 break
1887 # Try to fit them all on the same line with the last substring...
1888 if (
1889 str_width(temp_value) <= max_last_string_column()
1890 or LL[string_idx + 1].type == token.COMMA
1891 ):
1892 last_line.append(rest_leaf)
1893 append_leaves(last_line, line, LL[string_idx + 1 :])
1894 yield Ok(last_line)
1895 # Otherwise, place the last substring on one line and everything
1896 # else on a line below that...
1897 else:
1898 last_line.append(rest_leaf)
1899 yield Ok(last_line)
1901 non_string_line = line.clone()
1902 append_leaves(non_string_line, line, LL[string_idx + 1 :])
1903 yield Ok(non_string_line)
1904 # Else the target string was the last leaf...
1905 else:
1906 last_line.append(rest_leaf)
1907 last_line.comments = line.comments.copy()
1908 yield Ok(last_line)
1910 def _get_break_idx(
1911 self, string: str, max_break_idx: int, has_named_escape: bool = True
1912 ) -> int | None:
1913 """
1914 This method contains the algorithm that StringSplitter uses to
1915 determine which character to split each string at.
1917 Args:
1918 @string: The substring that we are attempting to split.
1919 @max_break_idx: The ideal break index. We will return this value if it
1920 meets all the necessary conditions. In the likely event that it
1921 doesn't we will try to find the closest index BELOW @max_break_idx
1922 that does. If that fails, we will expand our search by also
1923 considering all valid indices ABOVE @max_break_idx.
1924 @has_named_escape: Whether the original string can contain a named
1925 escape. False lets us skip the per-substring scan for them.
1927 Pre-Conditions:
1928 * assert_is_leaf_string(@string)
1929 * 0 <= @max_break_idx < len(@string)
1931 Returns:
1932 break_idx, if an index is able to be found that meets all of the
1933 conditions listed in the 'Transformations' section of this classes'
1934 docstring.
1935 OR
1936 None, otherwise.
1937 """
1938 is_valid_index = is_valid_index_factory(string)
1940 assert is_valid_index(max_break_idx)
1941 assert_is_leaf_string(string)
1943 _illegal_split_indices = self._get_illegal_split_indices(
1944 string, has_named_escape
1945 )
1947 def passes_all_checks(i: Index) -> bool:
1948 return self._is_valid_split_index(string, i, _illegal_split_indices)
1950 # First, we check all indices BELOW @max_break_idx.
1951 break_idx = max_break_idx
1952 while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx):
1953 break_idx -= 1
1955 if not passes_all_checks(break_idx):
1956 # If that fails, we check all indices ABOVE @max_break_idx.
1957 #
1958 # If we are able to find a valid index here, the next line is going
1959 # to be longer than the specified line length, but it's probably
1960 # better than doing nothing at all.
1961 break_idx = max_break_idx + 1
1962 while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx):
1963 break_idx += 1
1965 if not is_valid_index(break_idx) or not passes_all_checks(break_idx):
1966 return None
1968 return break_idx
1970 def _maybe_normalize_string_quotes(self, leaf: Leaf) -> None:
1971 if self.normalize_strings:
1972 leaf.value = normalize_string_quotes(leaf.value)
1974 def _normalize_f_string(self, string: str, prefix: str) -> str:
1975 """
1976 Pre-Conditions:
1977 * assert_is_leaf_string(@string)
1979 Returns:
1980 * If @string is an f-string that contains no f-expressions, we
1981 return a string identical to @string except that the 'f' prefix
1982 has been stripped and all double braces (i.e. '{{' or '}}') have
1983 been normalized (i.e. turned into '{' or '}').
1984 OR
1985 * Otherwise, we return @string.
1986 """
1987 assert_is_leaf_string(string)
1989 if "f" in prefix and not fstring_contains_expr(string):
1990 new_prefix = prefix.replace("f", "")
1992 temp = string[len(prefix) :]
1993 temp = re.sub(r"\{\{", "{", temp)
1994 temp = re.sub(r"\}\}", "}", temp)
1995 new_string = temp
1997 return f"{new_prefix}{new_string}"
1998 else:
1999 return string
2001 def _get_string_operator_leaves(self, leaves: Iterable[Leaf]) -> list[Leaf]:
2002 LL = list(leaves)
2004 string_op_leaves = []
2005 i = 0
2006 while LL[i].type in self.STRING_OPERATORS or LL[i].type == token.NAME:
2007 prefix_leaf = Leaf(LL[i].type, str(LL[i]).strip())
2008 string_op_leaves.append(prefix_leaf)
2009 i += 1
2010 return string_op_leaves
2013class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin):
2014 """
2015 StringTransformer that wraps strings in parens and then splits at the LPAR.
2017 Requirements:
2018 All of the requirements listed in BaseStringSplitter's docstring in
2019 addition to the requirements listed below:
2021 * The line is a return/yield statement, which returns/yields a string.
2022 OR
2023 * The line is a function definition with a stringified return annotation.
2024 OR
2025 * The line is part of a ternary expression (e.g. `x = y if cond else
2026 z`) such that the line starts with `else <string>`, where <string> is
2027 some string.
2028 OR
2029 * The line is an assert statement, which ends with a string.
2030 OR
2031 * The line is an assignment statement (e.g. `x = <string>` or `x +=
2032 <string>`) such that the variable is being assigned the value of some
2033 string.
2034 OR
2035 * The line contains a binary operator followed by a string, including
2036 operators overloaded by the left operand.
2037 OR
2038 * The line is a dictionary key assignment where some valid key is being
2039 assigned the value of some string.
2040 OR
2041 * The line is a lambda expression and the value is a string.
2042 OR
2043 * The line starts with an "atom" string that prefers to be wrapped in
2044 parens. It's preferred to be wrapped when it's is an immediate child of
2045 a list/set/tuple literal, AND the string is surrounded by commas (or is
2046 the first/last child).
2048 Transformations:
2049 The chosen string is wrapped in parentheses and then split at the LPAR.
2051 We then have one line which ends with an LPAR and another line that
2052 starts with the chosen string. The latter line is then split again at
2053 the RPAR. This results in the RPAR (and possibly a trailing comma)
2054 being placed on its own line.
2056 NOTE: For binary operands, subsequent operators stay outside the new
2057 parentheses. Otherwise, leaves to the right of the chosen string are
2058 placed inside the parentheses, except for a trailing comma, which is
2059 placed after the RPAR. In effect, the chosen
2060 string is not necessarily being "wrapped" by parentheses. We can,
2061 however, count on the LPAR being placed directly before the chosen
2062 string.
2064 In other words, StringParenWrapper creates "atom" strings. These
2065 can then be split again by StringSplitter, if necessary.
2067 Collaborations:
2068 In the event that a string line split by StringParenWrapper is
2069 changed such that it no longer needs to be given its own line,
2070 StringParenWrapper relies on StringParenStripper to clean up the
2071 parentheses it created.
2073 For "atom" strings that prefers to be wrapped in parens, it requires
2074 StringSplitter to hold the split until the string is wrapped in parens.
2075 """
2077 def do_splitter_match(self, line: Line) -> TMatchResult:
2078 LL = line.leaves
2080 if line.leaves[-1].type in OPENING_BRACKETS:
2081 return TErr(
2082 "Cannot wrap parens around a line that ends in an opening bracket."
2083 )
2085 string_idx = (
2086 self._return_match(LL)
2087 or self._return_annotation_match(LL)
2088 or self._else_match(LL)
2089 or self._assert_match(LL)
2090 or self._assign_match(LL)
2091 or self._binary_match(line)
2092 or self._dict_or_lambda_match(LL)
2093 )
2095 if string_idx is None:
2096 string_idx = self._trailing_comma_tuple_match(line)
2098 if string_idx is None:
2099 string_idx = self._prefer_paren_wrap_match(line)
2100 if string_idx is not None and not self._wrapped_line_can_fit(
2101 line, string_idx
2102 ):
2103 return TErr(
2104 "We do not wrap strings in parentheses when the resultant line"
2105 " would still be over the line length and can't be split by"
2106 " StringSplitter."
2107 )
2109 if string_idx is not None:
2110 # If the string is implicitly concatenated with a string on another
2111 # line (e.g. raw strings, which StringMerger leaves alone), wrapping
2112 # it in parens on its own would produce invalid code.
2113 next_sibling = LL[string_idx].next_sibling
2114 if next_sibling is not None and next_sibling.type == token.STRING:
2115 if not any(leaf is next_sibling for leaf in LL):
2116 return TErr(
2117 "Cannot wrap a string that is implicitly concatenated with a"
2118 " string on another line."
2119 )
2121 # Wrapping would move the first string's comments to the LPAR.
2122 if line.comments:
2123 return TErr(
2124 "Cannot wrap an implicit concatenation that has comments."
2125 )
2127 # If the first string fits on the line, splitting at the
2128 # concatenation is enough.
2129 tail = "".join(str(leaf) for leaf in LL[string_idx + 1 :])
2130 if (
2131 str_width(line_to_string(line)) - str_width(tail)
2132 <= self.line_length
2133 ):
2134 return TErr(
2135 "The first string of the implicit concatenation fits on"
2136 " the line."
2137 )
2139 string_value = line.leaves[string_idx].value
2140 # If the string has neither spaces nor East Asian stops...
2141 if not any(
2142 char == " " or char in SPLIT_SAFE_CHARS for char in string_value
2143 ):
2144 # And will still violate the line length limit when split...
2145 max_string_width = self.line_length - ((line.depth + 1) * 4)
2146 if str_width(string_value) > max_string_width:
2147 # And has no associated custom splits...
2148 if not self.has_custom_splits(string_value):
2149 # Then we should NOT put this string on its own line.
2150 return TErr(
2151 "We do not wrap long strings in parentheses when the"
2152 " resultant line would still be over the specified line"
2153 " length and can't be split further by StringSplitter."
2154 )
2155 return Ok([string_idx])
2157 return TErr("This line does not contain any non-atomic strings.")
2159 def _wrapped_line_can_fit(self, line: Line, string_idx: int) -> bool:
2160 """
2161 Returns:
2162 True iff the line created by wrapping the string at @string_idx in
2163 parens fits within the line length, or StringSplitter can split it
2164 there. That line is one level deeper than @line.
2165 """
2166 string_value = line.leaves[string_idx].value
2167 new_depth = line.depth + 1
2168 if new_depth * 4 + str_width(string_value) <= self.line_length:
2169 return True
2171 return self.line_length - 1 - new_depth * 4 >= 0 and (
2172 self.can_be_split(string_value) or self.has_custom_splits(string_value)
2173 )
2175 @staticmethod
2176 def _return_match(LL: list[Leaf]) -> int | None:
2177 """
2178 Returns:
2179 string_idx such that @LL[string_idx] is equal to our target (i.e.
2180 matched) string, if this line matches the return/yield statement
2181 requirements listed in the 'Requirements' section of this classes'
2182 docstring.
2183 OR
2184 None, otherwise.
2185 """
2186 # If this line is a part of a return/yield statement and the first leaf
2187 # contains either the "return" or "yield" keywords...
2188 if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[
2189 0
2190 ].value in ["return", "yield"]:
2191 is_valid_index = is_valid_index_factory(LL)
2193 idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
2194 # The next visible leaf MUST contain a string...
2195 if is_valid_index(idx) and LL[idx].type == token.STRING:
2196 return idx
2198 return None
2200 @staticmethod
2201 def _return_annotation_match(LL: list[Leaf]) -> int | None:
2202 """Return the string index for a stringified return annotation."""
2203 is_valid_index = is_valid_index_factory(LL)
2205 for i, leaf in enumerate(LL):
2206 if leaf.type != token.RARROW:
2207 continue
2209 string_idx = (
2210 i + 2 if is_valid_index(i + 1) and is_empty_lpar(LL[i + 1]) else i + 1
2211 )
2212 if not is_valid_index(string_idx) or LL[string_idx].type != token.STRING:
2213 return None
2215 idx = StringParser().parse(LL, string_idx)
2216 if (
2217 is_valid_index(idx)
2218 and LL[idx].type == token.COLON
2219 and idx == len(LL) - 1
2220 ):
2221 return string_idx
2223 return None
2225 return None
2227 @staticmethod
2228 def _else_match(LL: list[Leaf]) -> int | None:
2229 """
2230 Returns:
2231 string_idx such that @LL[string_idx] is equal to our target (i.e.
2232 matched) string, if this line matches the ternary expression
2233 requirements listed in the 'Requirements' section of this classes'
2234 docstring.
2235 OR
2236 None, otherwise.
2237 """
2238 # If this line is a part of a ternary expression and the first leaf
2239 # contains the "else" keyword...
2240 if (
2241 parent_type(LL[0]) == syms.test
2242 and LL[0].type == token.NAME
2243 and LL[0].value == "else"
2244 ):
2245 is_valid_index = is_valid_index_factory(LL)
2247 idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
2248 # The next visible leaf MUST contain a string...
2249 if is_valid_index(idx) and LL[idx].type == token.STRING:
2250 return idx
2252 return None
2254 @staticmethod
2255 def _assert_match(LL: list[Leaf]) -> int | None:
2256 """
2257 Returns:
2258 string_idx such that @LL[string_idx] is equal to our target (i.e.
2259 matched) string, if this line matches the assert statement
2260 requirements listed in the 'Requirements' section of this classes'
2261 docstring.
2262 OR
2263 None, otherwise.
2264 """
2265 # If this line is a part of an assert statement and the first leaf
2266 # contains the "assert" keyword...
2267 if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert":
2268 is_valid_index = is_valid_index_factory(LL)
2270 for i, leaf in enumerate(LL):
2271 # We MUST find a comma...
2272 if leaf.type == token.COMMA:
2273 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
2275 # That comma MUST be followed by a string...
2276 if is_valid_index(idx) and LL[idx].type == token.STRING:
2277 string_idx = idx
2279 # Skip the string trailer, if one exists.
2280 string_parser = StringParser()
2281 idx = string_parser.parse(LL, string_idx)
2283 # But no more leaves are allowed...
2284 if not is_valid_index(idx):
2285 return string_idx
2287 return None
2289 @staticmethod
2290 def _assign_match(LL: list[Leaf]) -> int | None:
2291 """
2292 Returns:
2293 string_idx such that @LL[string_idx] is equal to our target (i.e.
2294 matched) string, if this line matches the assignment statement
2295 requirements listed in the 'Requirements' section of this classes'
2296 docstring.
2297 OR
2298 None, otherwise.
2299 """
2300 # If this line is a part of an expression statement or is a function
2301 # argument AND the first leaf contains a variable name...
2302 if (
2303 parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power]
2304 and LL[0].type == token.NAME
2305 ):
2306 is_valid_index = is_valid_index_factory(LL)
2308 for i, leaf in enumerate(LL):
2309 # An assignment operator must be followed by the target string.
2310 if leaf.value in ASSIGNMENTS and leaf.type != token.COLON:
2311 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
2313 # That symbol MUST be followed by a string...
2314 if is_valid_index(idx) and LL[idx].type == token.STRING:
2315 string_idx = idx
2317 # Skip any strings implicitly concatenated with it.
2318 while (
2319 is_valid_index(idx + 1) and LL[idx + 1].type == token.STRING
2320 ):
2321 idx += 1
2323 # Skip the string trailer, if one exists.
2324 string_parser = StringParser()
2325 idx = string_parser.parse(LL, idx)
2327 # The next leaf MAY be a comma iff this line is a part
2328 # of a function argument...
2329 if (
2330 parent_type(LL[0]) == syms.argument
2331 and is_valid_index(idx)
2332 and LL[idx].type == token.COMMA
2333 ):
2334 idx += 1
2336 # But no more leaves are allowed...
2337 if not is_valid_index(idx):
2338 return string_idx
2340 return None
2342 def _binary_match(self, line: Line) -> int | None:
2343 """Find a long string operand, including one in the middle of a chain."""
2344 LL = line.leaves
2345 is_valid_index = is_valid_index_factory(LL)
2346 for i, leaf in enumerate(LL):
2347 if leaf.type not in MATH_OPERATORS or leaf.type == token.TILDE:
2348 continue
2349 if parent_type(leaf) not in {
2350 syms.arith_expr,
2351 syms.term,
2352 syms.power,
2353 syms.shift_expr,
2354 syms.and_expr,
2355 syms.xor_expr,
2356 syms.expr,
2357 }:
2358 continue
2360 idx = i + 1
2361 if is_valid_index(idx) and is_empty_lpar(LL[idx]):
2362 idx += 1
2363 if not is_valid_index(idx) or LL[idx].type != token.STRING:
2364 continue
2365 operand_end = self._binary_operand_end(LL, idx)
2366 if is_valid_index(operand_end):
2367 if (
2368 LL[operand_end].type not in MATH_OPERATORS - {token.TILDE}
2369 or leaf.bracket_depth
2370 ):
2371 # Split enclosing calls/collections before wrapping a middle
2372 # operand, and leave other trailers to existing matchers.
2373 continue
2374 if not isinstance(self._validate(line, idx), Err):
2375 return idx
2377 return None
2379 @staticmethod
2380 def _binary_operand_end(LL: list[Leaf], string_idx: int) -> int:
2381 """Find the string's trailer without absorbing a following % operation."""
2382 idx = string_idx
2383 while idx + 1 < len(LL) and LL[idx + 1].type == token.STRING:
2384 idx += 1
2385 trailer_idx = idx + 1
2386 while trailer_idx < len(LL) and is_empty_par(LL[trailer_idx]):
2387 trailer_idx += 1
2388 if trailer_idx < len(LL) and LL[trailer_idx].type == token.PERCENT:
2389 return trailer_idx
2390 return StringParser().parse(LL, idx)
2392 @staticmethod
2393 def _dict_or_lambda_match(LL: list[Leaf]) -> int | None:
2394 """
2395 Returns:
2396 string_idx such that @LL[string_idx] is equal to our target (i.e.
2397 matched) string, if this line matches the dictionary key assignment
2398 statement or lambda expression requirements listed in the
2399 'Requirements' section of this classes' docstring.
2400 OR
2401 None, otherwise.
2402 """
2403 # If this line is a part of a dictionary key assignment or lambda expression...
2404 parent_types = [parent_type(LL[0]), parent_type(LL[0].parent)]
2405 if syms.dictsetmaker in parent_types or syms.lambdef in parent_types:
2406 is_valid_index = is_valid_index_factory(LL)
2408 for i, leaf in enumerate(LL):
2409 # We MUST find a colon, it can either be dict's or lambda's colon...
2410 if leaf.type == token.COLON and i < len(LL) - 1:
2411 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
2413 # That colon MUST be followed by a string...
2414 if is_valid_index(idx) and LL[idx].type == token.STRING:
2415 string_idx = idx
2417 # Skip any strings implicitly concatenated with it.
2418 while (
2419 is_valid_index(idx + 1) and LL[idx + 1].type == token.STRING
2420 ):
2421 idx += 1
2423 # Skip the string trailer, if one exists.
2424 string_parser = StringParser()
2425 idx = string_parser.parse(LL, idx)
2427 # That string MAY be followed by a comma...
2428 if is_valid_index(idx) and LL[idx].type == token.COMMA:
2429 idx += 1
2431 # But no more leaves are allowed...
2432 if not is_valid_index(idx):
2433 return string_idx
2435 return None
2437 @staticmethod
2438 def _trailing_comma_tuple_match(line: Line) -> int | None:
2439 """
2440 Returns:
2441 string_idx such that @line.leaves[string_idx] is equal to our target
2442 (i.e. matched) string, if the line is a bare trailing comma tuple
2443 (STRING + COMMA) not inside brackets.
2444 OR
2445 None, otherwise.
2447 This handles the case from issue #4912 where a long string with a
2448 trailing comma (making it a one-item tuple) needs to be wrapped in
2449 parentheses before splitting to preserve AST equivalence.
2450 """
2451 LL = line.leaves
2452 # Match: STRING followed by COMMA, not inside brackets
2453 if (
2454 not line.inside_brackets
2455 and len(LL) == 2
2456 and LL[0].type == token.STRING
2457 and LL[1].type == token.COMMA
2458 ):
2459 return 0
2461 return None
2463 def do_transform(
2464 self, line: Line, string_indices: list[int]
2465 ) -> Iterator[TResult[Line]]:
2466 LL = line.leaves
2467 assert len(string_indices) == 1, (
2468 f"{self.__class__.__name__} should only find one match at a time, found"
2469 f" {len(string_indices)}"
2470 )
2471 string_idx = string_indices[0]
2473 if self._binary_match(line) == string_idx:
2474 operand_end = self._binary_operand_end(LL, string_idx)
2475 if operand_end < len(LL):
2476 # Reuse the terminal-operand wrapper, then restore the suffix
2477 # after its RPAR. Wrapping the suffix too would reassociate
2478 # expressions such as x % STRING % y.
2479 operand_line = line.clone()
2480 append_leaves(operand_line, line, LL[:operand_end])
2481 results = list(self.do_transform(operand_line, [string_idx]))
2482 last_result = results.pop()
2483 yield from results
2484 if isinstance(last_result, Err):
2485 yield last_result
2486 return
2487 append_leaves(last_result.ok(), line, LL[operand_end:])
2488 yield last_result
2489 return
2491 is_valid_index = is_valid_index_factory(LL)
2493 comma_idx = -1
2494 ends_with_comma = False
2495 if LL[comma_idx].type == token.COMMA:
2496 ends_with_comma = True
2497 return_annotation_arrow_idx = (
2498 string_idx - 2 if is_empty_lpar(LL[string_idx - 1]) else string_idx - 1
2499 )
2500 ends_with_return_annotation = (
2501 LL[return_annotation_arrow_idx].type == token.RARROW
2502 and LL[-1].type == token.COLON
2503 )
2505 leaves_to_steal_comments_from = [LL[string_idx]]
2506 if ends_with_comma:
2507 leaves_to_steal_comments_from.append(LL[comma_idx])
2509 # --- First Line
2510 first_line = line.clone()
2511 left_leaves = LL[:string_idx]
2513 # We have to remember to account for (possibly invisible) LPAR and RPAR
2514 # leaves that already wrapped the target string. If these leaves do
2515 # exist, we will replace them with our own LPAR and RPAR leaves.
2516 old_parens_exist = False
2517 if left_leaves and left_leaves[-1].type == token.LPAR:
2518 old_parens_exist = True
2519 leaves_to_steal_comments_from.append(left_leaves[-1])
2520 left_leaves.pop()
2522 append_leaves(first_line, line, left_leaves)
2524 # Keep the operand in the tree while calculating the operator's spacing.
2525 # Removing it early can make an exponentiation look simple enough to hug.
2526 insert_str_child = insert_str_child_factory(LL[string_idx])
2527 lpar_leaf = Leaf(token.LPAR, "(")
2528 if old_parens_exist:
2529 replace_child(LL[string_idx - 1], lpar_leaf)
2530 else:
2531 insert_str_child(lpar_leaf)
2532 first_line.append(lpar_leaf)
2534 # We throw inline comments that were originally to the right of the
2535 # target string to the top line. They will now be shown to the right of
2536 # the LPAR.
2537 for leaf in leaves_to_steal_comments_from:
2538 for comment_leaf in line.comments_after(leaf):
2539 first_line.append(comment_leaf, preformatted=True)
2541 yield Ok(first_line)
2543 # --- Middle (String) Line
2544 # We only need to yield one (possibly too long) string line, since the
2545 # `StringSplitter` will break it down further if necessary.
2546 string_value = LL[string_idx].value
2547 string_line = Line(
2548 mode=line.mode,
2549 depth=line.depth + 1,
2550 inside_brackets=True,
2551 should_split_rhs=line.should_split_rhs,
2552 magic_trailing_comma=line.magic_trailing_comma,
2553 )
2554 string_leaf = Leaf(token.STRING, string_value)
2555 insert_str_child(string_leaf)
2556 string_line.append(string_leaf)
2558 old_rpar_leaf = None
2559 if is_valid_index(string_idx + 1):
2560 right_leaves = LL[string_idx + 1 :]
2561 if ends_with_comma:
2562 right_leaves.pop()
2563 if ends_with_return_annotation:
2564 right_leaves.pop()
2566 if old_parens_exist:
2567 assert right_leaves and right_leaves[-1].type == token.RPAR, (
2568 "Apparently, old parentheses do NOT exist?!"
2569 f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
2570 )
2571 old_rpar_leaf = right_leaves.pop()
2572 elif right_leaves and right_leaves[-1].type == token.RPAR:
2573 # Special case for lambda expressions as dict's value, e.g.:
2574 # my_dict = {
2575 # "key": lambda x: f"formatted: {x}",
2576 # }
2577 # After wrapping the dict's value with parentheses, the string is
2578 # followed by a RPAR but its opening bracket is lambda's, not
2579 # the string's:
2580 # "key": (lambda x: f"formatted: {x}"),
2581 opening_bracket = right_leaves[-1].opening_bracket
2582 if opening_bracket is not None and opening_bracket in left_leaves:
2583 index = left_leaves.index(opening_bracket)
2584 if (
2585 0 < index < len(left_leaves) - 1
2586 and left_leaves[index - 1].type == token.COLON
2587 and left_leaves[index + 1].value == "lambda"
2588 ):
2589 right_leaves.pop()
2591 append_leaves(string_line, line, right_leaves)
2593 yield Ok(string_line)
2595 # --- Last Line
2596 last_line = line.clone()
2597 last_line.bracket_tracker = first_line.bracket_tracker
2599 new_rpar_leaf = Leaf(token.RPAR, ")")
2600 if old_rpar_leaf is not None:
2601 replace_child(old_rpar_leaf, new_rpar_leaf)
2602 else:
2603 insert_str_child(new_rpar_leaf)
2604 last_line.append(new_rpar_leaf)
2606 # If the target string ended with a comma, we place this comma to the
2607 # right of the RPAR on the last line.
2608 if ends_with_comma:
2609 comma_leaf = Leaf(token.COMMA, ",")
2610 replace_child(LL[comma_idx], comma_leaf)
2611 last_line.append(comma_leaf)
2612 if ends_with_return_annotation:
2613 append_leaves(last_line, line, [LL[-1]])
2615 if old_rpar_leaf is not None:
2616 for comment_leaf in line.comments_after(old_rpar_leaf):
2617 last_line.append(comment_leaf, preformatted=True)
2619 yield Ok(last_line)
2622class StringParser:
2623 """
2624 A state machine that aids in parsing a string's "trailer", which can be
2625 either non-existent, an old-style formatting sequence (e.g. `% varX` or `%
2626 (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX,
2627 varY)`).
2629 NOTE: A new StringParser object MUST be instantiated for each string
2630 trailer we need to parse.
2632 Examples:
2633 We shall assume that `line` equals the `Line` object that corresponds
2634 to the following line of python code:
2635 ```
2636 x = "Some {}.".format("String") + some_other_string
2637 ```
2639 Furthermore, we will assume that `string_idx` is some index such that:
2640 ```
2641 assert line.leaves[string_idx].value == "Some {}."
2642 ```
2644 The following code snippet then holds:
2645 ```
2646 string_parser = StringParser()
2647 idx = string_parser.parse(line.leaves, string_idx)
2648 assert line.leaves[idx].type == token.PLUS
2649 ```
2650 """
2652 DEFAULT_TOKEN: Final = 20210605
2654 # String Parser States
2655 START: Final = 1
2656 DOT: Final = 2
2657 NAME: Final = 3
2658 PERCENT: Final = 4
2659 SINGLE_FMT_ARG: Final = 5
2660 LPAR: Final = 6
2661 RPAR: Final = 7
2662 DONE: Final = 8
2664 # Lookup Table for Next State
2665 _goto: Final[dict[tuple[ParserState, NodeType], ParserState]] = {
2666 # A string trailer may start with '.' OR '%'.
2667 (START, token.DOT): DOT,
2668 (START, token.PERCENT): PERCENT,
2669 (START, DEFAULT_TOKEN): DONE,
2670 # A '.' MUST be followed by an attribute or method name.
2671 (DOT, token.NAME): NAME,
2672 # A method name MUST be followed by an '(', whereas an attribute name
2673 # is the last symbol in the string trailer.
2674 (NAME, token.LPAR): LPAR,
2675 (NAME, DEFAULT_TOKEN): DONE,
2676 # A '%' symbol can be followed by an '(' or a single argument (e.g. a
2677 # string or variable name).
2678 (PERCENT, token.LPAR): LPAR,
2679 (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG,
2680 # If a '%' symbol is followed by a single argument, that argument is
2681 # the last leaf in the string trailer.
2682 (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE,
2683 # If present, a ')' symbol is the last symbol in a string trailer.
2684 # (NOTE: LPARS and nested RPARS are not included in this lookup table,
2685 # since they are treated as a special case by the parsing logic in this
2686 # classes' implementation.)
2687 (RPAR, DEFAULT_TOKEN): DONE,
2688 }
2690 def __init__(self) -> None:
2691 self._state = self.START
2692 self._unmatched_lpars = 0
2694 def parse(self, leaves: list[Leaf], string_idx: int) -> int:
2695 """
2696 Pre-conditions:
2697 * @leaves[@string_idx].type == token.STRING
2699 Returns:
2700 The index directly after the last leaf which is a part of the string
2701 trailer, if a "trailer" exists.
2702 OR
2703 @string_idx + 1, if no string "trailer" exists.
2704 """
2705 assert leaves[string_idx].type == token.STRING
2707 idx = string_idx + 1
2708 while idx < len(leaves) and self._next_state(leaves[idx]):
2709 idx += 1
2710 return idx
2712 def _next_state(self, leaf: Leaf) -> bool:
2713 """
2714 Pre-conditions:
2715 * On the first call to this function, @leaf MUST be the leaf that
2716 was directly after the string leaf in question (e.g. if our target
2717 string is `line.leaves[i]` then the first call to this method must
2718 be `line.leaves[i + 1]`).
2719 * On the next call to this function, the leaf parameter passed in
2720 MUST be the leaf directly following @leaf.
2722 Returns:
2723 True iff @leaf is a part of the string's trailer.
2724 """
2725 # We ignore empty LPAR or RPAR leaves.
2726 if is_empty_par(leaf):
2727 return True
2729 next_token = leaf.type
2730 if next_token == token.LPAR:
2731 self._unmatched_lpars += 1
2733 current_state = self._state
2735 # The LPAR parser state is a special case. We will return True until we
2736 # find the matching RPAR token.
2737 if current_state == self.LPAR:
2738 if next_token == token.RPAR:
2739 self._unmatched_lpars -= 1
2740 if self._unmatched_lpars == 0:
2741 self._state = self.RPAR
2742 # Otherwise, we use a lookup table to determine the next state.
2743 else:
2744 # If the lookup table matches the current state to the next
2745 # token, we use the lookup table.
2746 if (current_state, next_token) in self._goto:
2747 self._state = self._goto[current_state, next_token]
2748 else:
2749 # Otherwise, we check if the current state was assigned a
2750 # default.
2751 if (current_state, self.DEFAULT_TOKEN) in self._goto:
2752 self._state = self._goto[current_state, self.DEFAULT_TOKEN]
2753 # If no default has been assigned, then this parser has a logic
2754 # error.
2755 else:
2756 raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
2758 if self._state == self.DONE:
2759 return False
2761 return True
2764def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]:
2765 """
2766 Factory for a convenience function that is used to orphan @string_leaf
2767 and then insert multiple new leaves into the same part of the node
2768 structure that @string_leaf had originally occupied.
2770 Examples:
2771 Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N =
2772 string_leaf.parent`. Assume the node `N` has the following
2773 original structure:
2775 Node(
2776 expr_stmt, [
2777 Leaf(NAME, 'x'),
2778 Leaf(EQUAL, '='),
2779 Leaf(STRING, '"foo"'),
2780 ]
2781 )
2783 We then run the code snippet shown below.
2784 ```
2785 insert_str_child = insert_str_child_factory(string_leaf)
2787 lpar = Leaf(token.LPAR, '(')
2788 insert_str_child(lpar)
2790 bar = Leaf(token.STRING, '"bar"')
2791 insert_str_child(bar)
2793 rpar = Leaf(token.RPAR, ')')
2794 insert_str_child(rpar)
2795 ```
2797 After which point, it follows that `string_leaf.parent is None` and
2798 the node `N` now has the following structure:
2800 Node(
2801 expr_stmt, [
2802 Leaf(NAME, 'x'),
2803 Leaf(EQUAL, '='),
2804 Leaf(LPAR, '('),
2805 Leaf(STRING, '"bar"'),
2806 Leaf(RPAR, ')'),
2807 ]
2808 )
2809 """
2810 string_parent = string_leaf.parent
2811 string_child_idx = string_leaf.remove()
2813 def insert_str_child(child: LN) -> None:
2814 nonlocal string_child_idx
2816 assert string_parent is not None
2817 assert string_child_idx is not None
2819 string_parent.insert_child(string_child_idx, child)
2820 string_child_idx += 1
2822 return insert_str_child
2825def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]:
2826 """
2827 Examples:
2828 ```
2829 my_list = [1, 2, 3]
2831 is_valid_index = is_valid_index_factory(my_list)
2833 assert is_valid_index(0)
2834 assert is_valid_index(2)
2836 assert not is_valid_index(3)
2837 assert not is_valid_index(-1)
2838 ```
2839 """
2841 def is_valid_index(idx: int) -> bool:
2842 """
2843 Returns:
2844 True iff @idx is positive AND seq[@idx] does NOT raise an
2845 IndexError.
2846 """
2847 return 0 <= idx < len(seq)
2849 return is_valid_index