All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
2 String transformers that can split and merge strings.
4 from abc import ABC, abstractmethod
5 from collections import defaultdict
6 from dataclasses import dataclass
26 if sys.version_info < (3, 8):
27 from typing_extensions import Literal, Final
29 from typing import Literal, Final
31 from mypy_extensions import trait
33 from black.rusty import Result, Ok, Err
35 from black.mode import Feature
36 from black.nodes import syms, replace_child, parent_type
37 from black.nodes import is_empty_par, is_empty_lpar, is_empty_rpar
38 from black.nodes import OPENING_BRACKETS, CLOSING_BRACKETS, STANDALONE_COMMENT
39 from black.lines import Line, append_leaves
40 from black.brackets import BracketMatchError
41 from black.comments import contains_pragma_comment
42 from black.strings import has_triple_quotes, get_string_prefix, assert_is_leaf_string
43 from black.strings import normalize_string_quotes
45 from blib2to3.pytree import Leaf, Node
46 from blib2to3.pgen2 import token
49 class CannotTransform(Exception):
50 """Base class for errors raised by Transformers."""
55 LN = Union[Leaf, Node]
56 Transformer = Callable[[Line, Collection[Feature]], Iterator[Line]]
61 TResult = Result[T, CannotTransform] # (T)ransform Result
62 TMatchResult = TResult[Index]
65 def TErr(err_msg: str) -> Err[CannotTransform]:
68 Convenience function used when working with the TResult type.
70 cant_transform = CannotTransform(err_msg)
71 return Err(cant_transform)
74 def hug_power_op(line: Line, features: Collection[Feature]) -> Iterator[Line]:
75 """A transformer which normalizes spacing around power operators."""
77 # Performance optimization to avoid unnecessary Leaf clones and other ops.
78 for leaf in line.leaves:
79 if leaf.type == token.DOUBLESTAR:
82 raise CannotTransform("No doublestar token was found in the line.")
84 def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool:
85 # Brackets and parentheses indicate calls, subscripts, etc. ...
86 # basically stuff that doesn't count as "simple". Only a NAME lookup
87 # or dotted lookup (eg. NAME.NAME) is OK.
89 disallowed = {token.RPAR, token.RSQB}
91 disallowed = {token.LPAR, token.LSQB}
93 while 0 <= index < len(line.leaves):
94 current = line.leaves[index]
95 if current.type in disallowed:
97 if current.type not in {token.NAME, token.DOT} or current.value == "for":
98 # If the current token isn't disallowed, we'll assume this is simple as
99 # only the disallowed tokens are semantically attached to this lookup
100 # expression we're checking. Also, stop early if we hit the 'for' bit
101 # of a comprehension.
108 def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool:
109 # An operand is considered "simple" if's a NAME, a numeric CONSTANT, a simple
110 # lookup (see above), with or without a preceding unary operator.
111 start = line.leaves[index]
112 if start.type in {token.NAME, token.NUMBER}:
113 return is_simple_lookup(index, step=(1 if kind == "exponent" else -1))
115 if start.type in {token.PLUS, token.MINUS, token.TILDE}:
116 if line.leaves[index + 1].type in {token.NAME, token.NUMBER}:
117 # step is always one as bases with a preceding unary op will be checked
118 # for simplicity starting from the next token (so it'll hit the check
120 return is_simple_lookup(index + 1, step=1)
124 leaves: List[Leaf] = []
126 for idx, leaf in enumerate(line.leaves):
127 new_leaf = leaf.clone()
133 (0 < idx < len(line.leaves) - 1)
134 and leaf.type == token.DOUBLESTAR
135 and is_simple_operand(idx - 1, kind="base")
136 and line.leaves[idx - 1].value != "lambda"
137 and is_simple_operand(idx + 1, kind="exponent")
142 leaves.append(new_leaf)
148 comments=line.comments,
149 bracket_tracker=line.bracket_tracker,
150 inside_brackets=line.inside_brackets,
151 should_split_rhs=line.should_split_rhs,
152 magic_trailing_comma=line.magic_trailing_comma,
156 class StringTransformer(ABC):
158 An implementation of the Transformer protocol that relies on its
159 subclasses overriding the template methods `do_match(...)` and
162 This Transformer works exclusively on strings (for example, by merging
165 The following sections can be found among the docstrings of each concrete
166 StringTransformer subclass.
169 Which requirements must be met of the given Line for this
170 StringTransformer to be applied?
173 If the given Line meets all of the above requirements, which string
174 transformations can you expect to be applied to it by this
178 What contractual agreements does this StringTransformer have with other
179 StringTransfomers? Such collaborations should be eliminated/minimized
183 __name__: Final = "StringTransformer"
185 # Ideally this would be a dataclass, but unfortunately mypyc breaks when used with
187 def __init__(self, line_length: int, normalize_strings: bool) -> None:
188 self.line_length = line_length
189 self.normalize_strings = normalize_strings
192 def do_match(self, line: Line) -> TMatchResult:
195 * Ok(string_idx) such that `line.leaves[string_idx]` is our target
196 string, if a match was able to be made.
198 * Err(CannotTransform), if a match was not able to be made.
202 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
205 * Ok(new_line) where new_line is the new transformed line.
207 * Err(CannotTransform) if the transformation failed for some reason. The
208 `do_match(...)` template method should usually be used to reject
209 the form of the given Line, but in some cases it is difficult to
210 know whether or not a Line meets the StringTransformer's
211 requirements until the transformation is already midway.
214 This method should NOT mutate @line directly, but it MAY mutate the
215 Line's underlying Node structure. (WARNING: If the underlying Node
216 structure IS altered, then this method should NOT be allowed to
217 yield an CannotTransform after that point.)
220 def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line]:
222 StringTransformer instances have a call signature that mirrors that of
223 the Transformer type.
226 CannotTransform(...) if the concrete StringTransformer class is unable
229 # Optimization to avoid calling `self.do_match(...)` when the line does
230 # not contain any string.
231 if not any(leaf.type == token.STRING for leaf in line.leaves):
232 raise CannotTransform("There are no strings in this line.")
234 match_result = self.do_match(line)
236 if isinstance(match_result, Err):
237 cant_transform = match_result.err()
238 raise CannotTransform(
239 f"The string transformer {self.__class__.__name__} does not recognize"
240 " this line as one that it can transform."
241 ) from cant_transform
243 string_idx = match_result.ok()
245 for line_result in self.do_transform(line, string_idx):
246 if isinstance(line_result, Err):
247 cant_transform = line_result.err()
248 raise CannotTransform(
249 "StringTransformer failed while attempting to transform string."
250 ) from cant_transform
251 line = line_result.ok()
257 """A custom (i.e. manual) string split.
259 A single CustomSplit instance represents a single substring.
262 Consider the following string:
269 This string will correspond to the following three CustomSplit instances:
271 CustomSplit(False, 16)
272 CustomSplit(False, 17)
273 CustomSplit(True, 16)
282 class CustomSplitMapMixin:
284 This mixin class is used to map merged strings to a sequence of
285 CustomSplits, which will then be used to re-split the strings iff none of
286 the resultant substrings go over the configured max line length.
289 _Key: ClassVar = Tuple[StringID, str]
290 _CUSTOM_SPLIT_MAP: ClassVar[Dict[_Key, Tuple[CustomSplit, ...]]] = defaultdict(
295 def _get_key(string: str) -> "CustomSplitMapMixin._Key":
298 A unique identifier that is used internally to map @string to a
299 group of custom splits.
301 return (id(string), string)
303 def add_custom_splits(
304 self, string: str, custom_splits: Iterable[CustomSplit]
306 """Custom Split Map Setter Method
309 Adds a mapping from @string to the custom splits @custom_splits.
311 key = self._get_key(string)
312 self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits)
314 def pop_custom_splits(self, string: str) -> List[CustomSplit]:
315 """Custom Split Map Getter Method
318 * A list of the custom splits that are mapped to @string, if any
324 Deletes the mapping between @string and its associated custom
325 splits (which are returned to the caller).
327 key = self._get_key(string)
329 custom_splits = self._CUSTOM_SPLIT_MAP[key]
330 del self._CUSTOM_SPLIT_MAP[key]
332 return list(custom_splits)
334 def has_custom_splits(self, string: str) -> bool:
337 True iff @string is associated with a set of custom splits.
339 key = self._get_key(string)
340 return key in self._CUSTOM_SPLIT_MAP
343 class StringMerger(StringTransformer, CustomSplitMapMixin):
344 """StringTransformer that merges strings together.
347 (A) The line contains adjacent strings such that ALL of the validation checks
348 listed in StringMerger.__validate_msg(...)'s docstring pass.
350 (B) The line contains a string which uses line continuation backslashes.
353 Depending on which of the two requirements above where met, either:
355 (A) The string group associated with the target string is merged.
357 (B) All line-continuation backslashes are removed from the target string.
360 StringMerger provides custom split information to StringSplitter.
363 def do_match(self, line: Line) -> TMatchResult:
366 is_valid_index = is_valid_index_factory(LL)
368 for (i, leaf) in enumerate(LL):
370 leaf.type == token.STRING
371 and is_valid_index(i + 1)
372 and LL[i + 1].type == token.STRING
376 if leaf.type == token.STRING and "\\\n" in leaf.value:
379 return TErr("This line has no strings that need merging.")
381 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
383 rblc_result = self._remove_backslash_line_continuation_chars(
386 if isinstance(rblc_result, Ok):
387 new_line = rblc_result.ok()
389 msg_result = self._merge_string_group(new_line, string_idx)
390 if isinstance(msg_result, Ok):
391 new_line = msg_result.ok()
393 if isinstance(rblc_result, Err) and isinstance(msg_result, Err):
394 msg_cant_transform = msg_result.err()
395 rblc_cant_transform = rblc_result.err()
396 cant_transform = CannotTransform(
397 "StringMerger failed to merge any strings in this line."
400 # Chain the errors together using `__cause__`.
401 msg_cant_transform.__cause__ = rblc_cant_transform
402 cant_transform.__cause__ = msg_cant_transform
404 yield Err(cant_transform)
409 def _remove_backslash_line_continuation_chars(
410 line: Line, string_idx: int
413 Merge strings that were split across multiple lines using
414 line-continuation backslashes.
417 Ok(new_line), if @line contains backslash line-continuation
420 Err(CannotTransform), otherwise.
424 string_leaf = LL[string_idx]
426 string_leaf.type == token.STRING
427 and "\\\n" in string_leaf.value
428 and not has_triple_quotes(string_leaf.value)
431 f"String leaf {string_leaf} does not contain any backslash line"
432 " continuation characters."
435 new_line = line.clone()
436 new_line.comments = line.comments.copy()
437 append_leaves(new_line, line, LL)
439 new_string_leaf = new_line.leaves[string_idx]
440 new_string_leaf.value = new_string_leaf.value.replace("\\\n", "")
444 def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]:
446 Merges string group (i.e. set of adjacent strings) where the first
447 string in the group is `line.leaves[string_idx]`.
450 Ok(new_line), if ALL of the validation checks found in
451 __validate_msg(...) pass.
453 Err(CannotTransform), otherwise.
457 is_valid_index = is_valid_index_factory(LL)
459 vresult = self._validate_msg(line, string_idx)
460 if isinstance(vresult, Err):
463 # If the string group is wrapped inside an Atom node, we must make sure
464 # to later replace that Atom with our new (merged) string leaf.
465 atom_node = LL[string_idx].parent
467 # We will place BREAK_MARK in between every two substrings that we
468 # merge. We will then later go through our final result and use the
469 # various instances of BREAK_MARK we find to add the right values to
470 # the custom split map.
471 BREAK_MARK = "@@@@@ BLACK BREAKPOINT MARKER @@@@@"
473 QUOTE = LL[string_idx].value[-1]
475 def make_naked(string: str, string_prefix: str) -> str:
476 """Strip @string (i.e. make it a "naked" string)
479 * assert_is_leaf_string(@string)
482 A string that is identical to @string except that
483 @string_prefix has been stripped, the surrounding QUOTE
484 characters have been removed, and any remaining QUOTE
485 characters have been escaped.
487 assert_is_leaf_string(string)
489 RE_EVEN_BACKSLASHES = r"(?:(?<!\\)(?:\\\\)*)"
490 naked_string = string[len(string_prefix) + 1 : -1]
491 naked_string = re.sub(
492 "(" + RE_EVEN_BACKSLASHES + ")" + QUOTE, r"\1\\" + QUOTE, naked_string
496 # Holds the CustomSplit objects that will later be added to the custom
500 # Temporary storage for the 'has_prefix' part of the CustomSplit objects.
503 # Sets the 'prefix' variable. This is the prefix that the final merged
505 next_str_idx = string_idx
509 and is_valid_index(next_str_idx)
510 and LL[next_str_idx].type == token.STRING
512 prefix = get_string_prefix(LL[next_str_idx].value).lower()
515 # The next loop merges the string group. The final string will be
518 # The following convenience variables are used:
523 # NSS: naked next string
527 next_str_idx = string_idx
528 while is_valid_index(next_str_idx) and LL[next_str_idx].type == token.STRING:
531 SS = LL[next_str_idx].value
532 next_prefix = get_string_prefix(SS).lower()
534 # If this is an f-string group but this substring is not prefixed
536 if "f" in prefix and "f" not in next_prefix:
537 # Then we must escape any braces contained in this substring.
538 SS = re.sub(r"(\{|\})", r"\1\1", SS)
540 NSS = make_naked(SS, next_prefix)
542 has_prefix = bool(next_prefix)
543 prefix_tracker.append(has_prefix)
545 S = prefix + QUOTE + NS + NSS + BREAK_MARK + QUOTE
546 NS = make_naked(S, prefix)
550 S_leaf = Leaf(token.STRING, S)
551 if self.normalize_strings:
552 S_leaf.value = normalize_string_quotes(S_leaf.value)
554 # Fill the 'custom_splits' list with the appropriate CustomSplit objects.
555 temp_string = S_leaf.value[len(prefix) + 1 : -1]
556 for has_prefix in prefix_tracker:
557 mark_idx = temp_string.find(BREAK_MARK)
560 ), "Logic error while filling the custom string breakpoint cache."
562 temp_string = temp_string[mark_idx + len(BREAK_MARK) :]
563 breakpoint_idx = mark_idx + (len(prefix) if has_prefix else 0) + 1
564 custom_splits.append(CustomSplit(has_prefix, breakpoint_idx))
566 string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, ""))
568 if atom_node is not None:
569 replace_child(atom_node, string_leaf)
571 # Build the final line ('new_line') that this method will later return.
572 new_line = line.clone()
573 for (i, leaf) in enumerate(LL):
575 new_line.append(string_leaf)
577 if string_idx <= i < string_idx + num_of_strings:
578 for comment_leaf in line.comments_after(LL[i]):
579 new_line.append(comment_leaf, preformatted=True)
582 append_leaves(new_line, line, [leaf])
584 self.add_custom_splits(string_leaf.value, custom_splits)
588 def _validate_msg(line: Line, string_idx: int) -> TResult[None]:
589 """Validate (M)erge (S)tring (G)roup
591 Transform-time string validation logic for __merge_string_group(...).
594 * Ok(None), if ALL validation checks (listed below) pass.
596 * Err(CannotTransform), if any of the following are true:
597 - The target string group does not contain ANY stand-alone comments.
598 - The target string is not in a string group (i.e. it has no
600 - The string group has more than one inline comment.
601 - The string group has an inline comment that appears to be a pragma.
602 - The set of all string prefixes in the string group is of
603 length greater than one and is not equal to {"", "f"}.
604 - The string group consists of raw strings.
606 # We first check for "inner" stand-alone comments (i.e. stand-alone
607 # comments that have a string leaf before them AND after them).
610 found_sa_comment = False
611 is_valid_index = is_valid_index_factory(line.leaves)
612 while is_valid_index(i) and line.leaves[i].type in [
616 if line.leaves[i].type == STANDALONE_COMMENT:
617 found_sa_comment = True
618 elif found_sa_comment:
620 "StringMerger does NOT merge string groups which contain "
621 "stand-alone comments."
626 num_of_inline_string_comments = 0
627 set_of_prefixes = set()
629 for leaf in line.leaves[string_idx:]:
630 if leaf.type != token.STRING:
631 # If the string group is trailed by a comma, we count the
632 # comments trailing the comma to be one of the string group's
634 if leaf.type == token.COMMA and id(leaf) in line.comments:
635 num_of_inline_string_comments += 1
638 if has_triple_quotes(leaf.value):
639 return TErr("StringMerger does NOT merge multiline strings.")
642 prefix = get_string_prefix(leaf.value).lower()
644 return TErr("StringMerger does NOT merge raw strings.")
646 set_of_prefixes.add(prefix)
648 if id(leaf) in line.comments:
649 num_of_inline_string_comments += 1
650 if contains_pragma_comment(line.comments[id(leaf)]):
651 return TErr("Cannot merge strings which have pragma comments.")
653 if num_of_strings < 2:
655 f"Not enough strings to merge (num_of_strings={num_of_strings})."
658 if num_of_inline_string_comments > 1:
660 f"Too many inline string comments ({num_of_inline_string_comments})."
663 if len(set_of_prefixes) > 1 and set_of_prefixes != {"", "f"}:
664 return TErr(f"Too many different prefixes ({set_of_prefixes}).")
669 class StringParenStripper(StringTransformer):
670 """StringTransformer that strips surrounding parentheses from strings.
673 The line contains a string which is surrounded by parentheses and:
674 - The target string is NOT the only argument to a function call.
675 - The target string is NOT a "pointless" string.
676 - If the target string contains a PERCENT, the brackets are not
677 preceded or followed by an operator with higher precedence than
681 The parentheses mentioned in the 'Requirements' section are stripped.
684 StringParenStripper has its own inherent usefulness, but it is also
685 relied on to clean up the parentheses created by StringParenWrapper (in
686 the event that they are no longer needed).
689 def do_match(self, line: Line) -> TMatchResult:
692 is_valid_index = is_valid_index_factory(LL)
694 for (idx, leaf) in enumerate(LL):
695 # Should be a string...
696 if leaf.type != token.STRING:
699 # If this is a "pointless" string...
702 and leaf.parent.parent
703 and leaf.parent.parent.type == syms.simple_stmt
707 # Should be preceded by a non-empty LPAR...
709 not is_valid_index(idx - 1)
710 or LL[idx - 1].type != token.LPAR
711 or is_empty_lpar(LL[idx - 1])
715 # That LPAR should NOT be preceded by a function name or a closing
716 # bracket (which could be a function which returns a function or a
717 # list/dictionary that contains a function)...
718 if is_valid_index(idx - 2) and (
719 LL[idx - 2].type == token.NAME or LL[idx - 2].type in CLOSING_BRACKETS
725 # Skip the string trailer, if one exists.
726 string_parser = StringParser()
727 next_idx = string_parser.parse(LL, string_idx)
729 # if the leaves in the parsed string include a PERCENT, we need to
730 # make sure the initial LPAR is NOT preceded by an operator with
731 # higher or equal precedence to PERCENT
732 if is_valid_index(idx - 2):
733 # mypy can't quite follow unless we name this
734 before_lpar = LL[idx - 2]
735 if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and (
752 # only unary PLUS/MINUS
754 and before_lpar.parent.type == syms.factor
755 and (before_lpar.type in {token.PLUS, token.MINUS})
760 # Should be followed by a non-empty RPAR...
762 is_valid_index(next_idx)
763 and LL[next_idx].type == token.RPAR
764 and not is_empty_rpar(LL[next_idx])
766 # That RPAR should NOT be followed by anything with higher
767 # precedence than PERCENT
768 if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in {
776 return Ok(string_idx)
778 return TErr("This line has no strings wrapped in parens.")
780 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
783 string_parser = StringParser()
784 rpar_idx = string_parser.parse(LL, string_idx)
786 for leaf in (LL[string_idx - 1], LL[rpar_idx]):
787 if line.comments_after(leaf):
789 "Will not strip parentheses which have comments attached to them."
793 new_line = line.clone()
794 new_line.comments = line.comments.copy()
796 append_leaves(new_line, line, LL[: string_idx - 1])
797 except BracketMatchError:
798 # HACK: I believe there is currently a bug somewhere in
799 # right_hand_split() that is causing brackets to not be tracked
800 # properly by a shared BracketTracker.
801 append_leaves(new_line, line, LL[: string_idx - 1], preformatted=True)
803 string_leaf = Leaf(token.STRING, LL[string_idx].value)
804 LL[string_idx - 1].remove()
805 replace_child(LL[string_idx], string_leaf)
806 new_line.append(string_leaf)
809 new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :]
812 LL[rpar_idx].remove()
817 class BaseStringSplitter(StringTransformer):
819 Abstract class for StringTransformers which transform a Line's strings by splitting
820 them or placing them on their own lines where necessary to avoid going over
821 the configured line length.
824 * The target string value is responsible for the line going over the
825 line length limit. It follows that after all of black's other line
826 split methods have been exhausted, this line (or one of the resulting
827 lines after all line splits are performed) would still be over the
828 line_length limit unless we split this string.
830 * The target string is NOT a "pointless" string (i.e. a string that has
831 no parent or siblings).
833 * The target string is not followed by an inline comment that appears
836 * The target string is not a multiline (i.e. triple-quote) string.
839 STRING_OPERATORS: Final = [
852 def do_splitter_match(self, line: Line) -> TMatchResult:
854 BaseStringSplitter asks its clients to override this method instead of
855 `StringTransformer.do_match(...)`.
857 Follows the same protocol as `StringTransformer.do_match(...)`.
859 Refer to `help(StringTransformer.do_match)` for more information.
862 def do_match(self, line: Line) -> TMatchResult:
863 match_result = self.do_splitter_match(line)
864 if isinstance(match_result, Err):
867 string_idx = match_result.ok()
868 vresult = self._validate(line, string_idx)
869 if isinstance(vresult, Err):
874 def _validate(self, line: Line, string_idx: int) -> TResult[None]:
876 Checks that @line meets all of the requirements listed in this classes'
877 docstring. Refer to `help(BaseStringSplitter)` for a detailed
878 description of those requirements.
881 * Ok(None), if ALL of the requirements are met.
883 * Err(CannotTransform), if ANY of the requirements are NOT met.
887 string_leaf = LL[string_idx]
889 max_string_length = self._get_max_string_length(line, string_idx)
890 if len(string_leaf.value) <= max_string_length:
892 "The string itself is not what is causing this line to be too long."
895 if not string_leaf.parent or [L.type for L in string_leaf.parent.children] == [
900 f"This string ({string_leaf.value}) appears to be pointless (i.e. has"
904 if id(line.leaves[string_idx]) in line.comments and contains_pragma_comment(
905 line.comments[id(line.leaves[string_idx])]
908 "Line appears to end with an inline pragma comment. Splitting the line"
909 " could modify the pragma's behavior."
912 if has_triple_quotes(string_leaf.value):
913 return TErr("We cannot split multiline strings.")
917 def _get_max_string_length(self, line: Line, string_idx: int) -> int:
919 Calculates the max string length used when attempting to determine
920 whether or not the target string is responsible for causing the line to
921 go over the line length limit.
923 WARNING: This method is tightly coupled to both StringSplitter and
924 (especially) StringParenWrapper. There is probably a better way to
925 accomplish what is being done here.
928 max_string_length: such that `line.leaves[string_idx].value >
929 max_string_length` implies that the target string IS responsible
930 for causing this line to exceed the line length limit.
934 is_valid_index = is_valid_index_factory(LL)
936 # We use the shorthand "WMA4" in comments to abbreviate "We must
937 # account for". When giving examples, we use STRING to mean some/any
940 # Finally, we use the following convenience variables:
942 # P: The leaf that is before the target string leaf.
943 # N: The leaf that is after the target string leaf.
944 # NN: The leaf that is after N.
946 # WMA4 the whitespace at the beginning of the line.
947 offset = line.depth * 4
949 if is_valid_index(string_idx - 1):
950 p_idx = string_idx - 1
952 LL[string_idx - 1].type == token.LPAR
953 and LL[string_idx - 1].value == ""
956 # If the previous leaf is an empty LPAR placeholder, we should skip it.
960 if P.type in self.STRING_OPERATORS:
961 # WMA4 a space and a string operator (e.g. `+ STRING` or `== STRING`).
962 offset += len(str(P)) + 1
964 if P.type == token.COMMA:
965 # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`].
968 if P.type in [token.COLON, token.EQUAL, token.PLUSEQUAL, token.NAME]:
969 # This conditional branch is meant to handle dictionary keys,
970 # variable assignments, 'return STRING' statement lines, and
971 # 'else STRING' ternary expression lines.
973 # WMA4 a single space.
976 # WMA4 the lengths of any leaves that came before that space,
977 # but after any closing bracket before that space.
978 for leaf in reversed(LL[: p_idx + 1]):
979 offset += len(str(leaf))
980 if leaf.type in CLOSING_BRACKETS:
983 if is_valid_index(string_idx + 1):
984 N = LL[string_idx + 1]
985 if N.type == token.RPAR and N.value == "" and len(LL) > string_idx + 2:
986 # If the next leaf is an empty RPAR placeholder, we should skip it.
987 N = LL[string_idx + 2]
989 if N.type == token.COMMA:
990 # WMA4 a single comma at the end of the string (e.g `STRING,`).
993 if is_valid_index(string_idx + 2):
994 NN = LL[string_idx + 2]
996 if N.type == token.DOT and NN.type == token.NAME:
997 # This conditional branch is meant to handle method calls invoked
998 # off of a string literal up to and including the LPAR character.
1000 # WMA4 the '.' character.
1004 is_valid_index(string_idx + 3)
1005 and LL[string_idx + 3].type == token.LPAR
1007 # WMA4 the left parenthesis character.
1010 # WMA4 the length of the method's name.
1011 offset += len(NN.value)
1013 has_comments = False
1014 for comment_leaf in line.comments_after(LL[string_idx]):
1015 if not has_comments:
1017 # WMA4 two spaces before the '#' character.
1020 # WMA4 the length of the inline comment.
1021 offset += len(comment_leaf.value)
1023 max_string_length = self.line_length - offset
1024 return max_string_length
1027 def iter_fexpr_spans(s: str) -> Iterator[Tuple[int, int]]:
1029 Yields spans corresponding to expressions in a given f-string.
1030 Spans are half-open ranges (left inclusive, right exclusive).
1031 Assumes the input string is a valid f-string, but will not crash if the input
1034 stack: List[int] = [] # our curly paren stack
1038 # if we're in a string part of the f-string, ignore escaped curly braces
1039 if not stack and i + 1 < len(s) and s[i + 1] == "{":
1051 # we've made it back out of the expression! yield the span
1057 # if we're in an expression part of the f-string, fast forward through strings
1058 # note that backslashes are not legal in the expression portion of f-strings
1061 if s[i : i + 3] in ("'''", '"""'):
1062 delim = s[i : i + 3]
1063 elif s[i] in ("'", '"'):
1067 while i < len(s) and s[i : i + len(delim)] != delim:
1074 def fstring_contains_expr(s: str) -> bool:
1075 return any(iter_fexpr_spans(s))
1078 class StringSplitter(BaseStringSplitter, CustomSplitMapMixin):
1080 StringTransformer that splits "atom" strings (i.e. strings which exist on
1081 lines by themselves).
1084 * The line consists ONLY of a single string (possibly prefixed by a
1085 string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE
1088 * All of the requirements listed in BaseStringSplitter's docstring.
1091 The string mentioned in the 'Requirements' section is split into as
1092 many substrings as necessary to adhere to the configured line length.
1094 In the final set of substrings, no substring should be smaller than
1095 MIN_SUBSTR_SIZE characters.
1097 The string will ONLY be split on spaces (i.e. each new substring should
1098 start with a space). Note that the string will NOT be split on a space
1099 which is escaped with a backslash.
1101 If the string is an f-string, it will NOT be split in the middle of an
1102 f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x
1103 else bar()} is an f-expression).
1105 If the string that is being split has an associated set of custom split
1106 records and those custom splits will NOT result in any line going over
1107 the configured line length, those custom splits are used. Otherwise the
1108 string is split as late as possible (from left-to-right) while still
1109 adhering to the transformation rules listed above.
1112 StringSplitter relies on StringMerger to construct the appropriate
1113 CustomSplit objects and add them to the custom split map.
1116 MIN_SUBSTR_SIZE: Final = 6
1118 def do_splitter_match(self, line: Line) -> TMatchResult:
1121 is_valid_index = is_valid_index_factory(LL)
1125 # The first two leaves MAY be the 'not in' keywords...
1128 and is_valid_index(idx + 1)
1129 and [LL[idx].type, LL[idx + 1].type] == [token.NAME, token.NAME]
1130 and str(LL[idx]) + str(LL[idx + 1]) == "not in"
1133 # Else the first leaf MAY be a string operator symbol or the 'in' keyword...
1134 elif is_valid_index(idx) and (
1135 LL[idx].type in self.STRING_OPERATORS
1136 or LL[idx].type == token.NAME
1137 and str(LL[idx]) == "in"
1141 # The next/first leaf MAY be an empty LPAR...
1142 if is_valid_index(idx) and is_empty_lpar(LL[idx]):
1145 # The next/first leaf MUST be a string...
1146 if not is_valid_index(idx) or LL[idx].type != token.STRING:
1147 return TErr("Line does not start with a string.")
1151 # Skip the string trailer, if one exists.
1152 string_parser = StringParser()
1153 idx = string_parser.parse(LL, string_idx)
1155 # That string MAY be followed by an empty RPAR...
1156 if is_valid_index(idx) and is_empty_rpar(LL[idx]):
1159 # That string / empty RPAR leaf MAY be followed by a comma...
1160 if is_valid_index(idx) and LL[idx].type == token.COMMA:
1163 # But no more leaves are allowed...
1164 if is_valid_index(idx):
1165 return TErr("This line does not end with a string.")
1167 return Ok(string_idx)
1169 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
1172 QUOTE = LL[string_idx].value[-1]
1174 is_valid_index = is_valid_index_factory(LL)
1175 insert_str_child = insert_str_child_factory(LL[string_idx])
1177 prefix = get_string_prefix(LL[string_idx].value).lower()
1179 # We MAY choose to drop the 'f' prefix from substrings that don't
1180 # contain any f-expressions, but ONLY if the original f-string
1181 # contains at least one f-expression. Otherwise, we will alter the AST
1183 drop_pointless_f_prefix = ("f" in prefix) and fstring_contains_expr(
1184 LL[string_idx].value
1187 first_string_line = True
1189 string_op_leaves = self._get_string_operator_leaves(LL)
1190 string_op_leaves_length = (
1191 sum([len(str(prefix_leaf)) for prefix_leaf in string_op_leaves]) + 1
1196 def maybe_append_string_operators(new_line: Line) -> None:
1199 If @line starts with a string operator and this is the first
1200 line we are constructing, this function appends the string
1201 operator to @new_line and replaces the old string operator leaf
1202 in the node structure. Otherwise this function does nothing.
1204 maybe_prefix_leaves = string_op_leaves if first_string_line else []
1205 for i, prefix_leaf in enumerate(maybe_prefix_leaves):
1206 replace_child(LL[i], prefix_leaf)
1207 new_line.append(prefix_leaf)
1210 is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA
1213 def max_last_string() -> int:
1216 The max allowed length of the string value used for the last
1217 line we will construct.
1219 result = self.line_length
1220 result -= line.depth * 4
1221 result -= 1 if ends_with_comma else 0
1222 result -= string_op_leaves_length
1225 # --- Calculate Max Break Index (for string value)
1226 # We start with the line length limit
1227 max_break_idx = self.line_length
1228 # The last index of a string of length N is N-1.
1230 # Leading whitespace is not present in the string value (e.g. Leaf.value).
1231 max_break_idx -= line.depth * 4
1232 if max_break_idx < 0:
1234 f"Unable to split {LL[string_idx].value} at such high of a line depth:"
1239 # Check if StringMerger registered any custom splits.
1240 custom_splits = self.pop_custom_splits(LL[string_idx].value)
1241 # We use them ONLY if none of them would produce lines that exceed the
1243 use_custom_breakpoints = bool(
1245 and all(csplit.break_idx <= max_break_idx for csplit in custom_splits)
1248 # Temporary storage for the remaining chunk of the string line that
1249 # can't fit onto the line currently being constructed.
1250 rest_value = LL[string_idx].value
1252 def more_splits_should_be_made() -> bool:
1255 True iff `rest_value` (the remaining string value from the last
1256 split), should be split again.
1258 if use_custom_breakpoints:
1259 return len(custom_splits) > 1
1261 return len(rest_value) > max_last_string()
1263 string_line_results: List[Ok[Line]] = []
1264 while more_splits_should_be_made():
1265 if use_custom_breakpoints:
1266 # Custom User Split (manual)
1267 csplit = custom_splits.pop(0)
1268 break_idx = csplit.break_idx
1270 # Algorithmic Split (automatic)
1271 max_bidx = max_break_idx - string_op_leaves_length
1272 maybe_break_idx = self._get_break_idx(rest_value, max_bidx)
1273 if maybe_break_idx is None:
1274 # If we are unable to algorithmically determine a good split
1275 # and this string has custom splits registered to it, we
1276 # fall back to using them--which means we have to start
1277 # over from the beginning.
1279 rest_value = LL[string_idx].value
1280 string_line_results = []
1281 first_string_line = True
1282 use_custom_breakpoints = True
1285 # Otherwise, we stop splitting here.
1288 break_idx = maybe_break_idx
1290 # --- Construct `next_value`
1291 next_value = rest_value[:break_idx] + QUOTE
1293 # HACK: The following 'if' statement is a hack to fix the custom
1294 # breakpoint index in the case of either: (a) substrings that were
1295 # f-strings but will have the 'f' prefix removed OR (b) substrings
1296 # that were not f-strings but will now become f-strings because of
1297 # redundant use of the 'f' prefix (i.e. none of the substrings
1298 # contain f-expressions but one or more of them had the 'f' prefix
1299 # anyway; in which case, we will prepend 'f' to _all_ substrings).
1301 # There is probably a better way to accomplish what is being done
1304 # If this substring is an f-string, we _could_ remove the 'f'
1305 # prefix, and the current custom split did NOT originally use a
1308 next_value != self._normalize_f_string(next_value, prefix)
1309 and use_custom_breakpoints
1310 and not csplit.has_prefix
1312 # Then `csplit.break_idx` will be off by one after removing
1315 next_value = rest_value[:break_idx] + QUOTE
1317 if drop_pointless_f_prefix:
1318 next_value = self._normalize_f_string(next_value, prefix)
1320 # --- Construct `next_leaf`
1321 next_leaf = Leaf(token.STRING, next_value)
1322 insert_str_child(next_leaf)
1323 self._maybe_normalize_string_quotes(next_leaf)
1325 # --- Construct `next_line`
1326 next_line = line.clone()
1327 maybe_append_string_operators(next_line)
1328 next_line.append(next_leaf)
1329 string_line_results.append(Ok(next_line))
1331 rest_value = prefix + QUOTE + rest_value[break_idx:]
1332 first_string_line = False
1334 yield from string_line_results
1336 if drop_pointless_f_prefix:
1337 rest_value = self._normalize_f_string(rest_value, prefix)
1339 rest_leaf = Leaf(token.STRING, rest_value)
1340 insert_str_child(rest_leaf)
1342 # NOTE: I could not find a test case that verifies that the following
1343 # line is actually necessary, but it seems to be. Otherwise we risk
1344 # not normalizing the last substring, right?
1345 self._maybe_normalize_string_quotes(rest_leaf)
1347 last_line = line.clone()
1348 maybe_append_string_operators(last_line)
1350 # If there are any leaves to the right of the target string...
1351 if is_valid_index(string_idx + 1):
1352 # We use `temp_value` here to determine how long the last line
1353 # would be if we were to append all the leaves to the right of the
1354 # target string to the last string line.
1355 temp_value = rest_value
1356 for leaf in LL[string_idx + 1 :]:
1357 temp_value += str(leaf)
1358 if leaf.type == token.LPAR:
1361 # Try to fit them all on the same line with the last substring...
1363 len(temp_value) <= max_last_string()
1364 or LL[string_idx + 1].type == token.COMMA
1366 last_line.append(rest_leaf)
1367 append_leaves(last_line, line, LL[string_idx + 1 :])
1369 # Otherwise, place the last substring on one line and everything
1370 # else on a line below that...
1372 last_line.append(rest_leaf)
1375 non_string_line = line.clone()
1376 append_leaves(non_string_line, line, LL[string_idx + 1 :])
1377 yield Ok(non_string_line)
1378 # Else the target string was the last leaf...
1380 last_line.append(rest_leaf)
1381 last_line.comments = line.comments.copy()
1384 def _iter_nameescape_slices(self, string: str) -> Iterator[Tuple[Index, Index]]:
1387 All ranges of @string which, if @string were to be split there,
1388 would result in the splitting of an \\N{...} expression (which is NOT
1391 # True - the previous backslash was unescaped
1392 # False - the previous backslash was escaped *or* there was no backslash
1393 previous_was_unescaped_backslash = False
1394 it = iter(enumerate(string))
1397 previous_was_unescaped_backslash = not previous_was_unescaped_backslash
1399 if not previous_was_unescaped_backslash or c != "N":
1400 previous_was_unescaped_backslash = False
1402 previous_was_unescaped_backslash = False
1404 begin = idx - 1 # the position of backslash before \N{...}
1410 # malformed nameescape expression?
1411 # should have been detected by AST parsing earlier...
1412 raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
1415 def _iter_fexpr_slices(self, string: str) -> Iterator[Tuple[Index, Index]]:
1418 All ranges of @string which, if @string were to be split there,
1419 would result in the splitting of an f-expression (which is NOT
1422 if "f" not in get_string_prefix(string).lower():
1424 yield from iter_fexpr_spans(string)
1426 def _get_illegal_split_indices(self, string: str) -> Set[Index]:
1427 illegal_indices: Set[Index] = set()
1429 self._iter_fexpr_slices(string),
1430 self._iter_nameescape_slices(string),
1432 for it in iterators:
1433 for begin, end in it:
1434 illegal_indices.update(range(begin, end + 1))
1435 return illegal_indices
1437 def _get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]:
1439 This method contains the algorithm that StringSplitter uses to
1440 determine which character to split each string at.
1443 @string: The substring that we are attempting to split.
1444 @max_break_idx: The ideal break index. We will return this value if it
1445 meets all the necessary conditions. In the likely event that it
1446 doesn't we will try to find the closest index BELOW @max_break_idx
1447 that does. If that fails, we will expand our search by also
1448 considering all valid indices ABOVE @max_break_idx.
1451 * assert_is_leaf_string(@string)
1452 * 0 <= @max_break_idx < len(@string)
1455 break_idx, if an index is able to be found that meets all of the
1456 conditions listed in the 'Transformations' section of this classes'
1461 is_valid_index = is_valid_index_factory(string)
1463 assert is_valid_index(max_break_idx)
1464 assert_is_leaf_string(string)
1466 _illegal_split_indices = self._get_illegal_split_indices(string)
1468 def breaks_unsplittable_expression(i: Index) -> bool:
1471 True iff returning @i would result in the splitting of an
1472 unsplittable expression (which is NOT allowed).
1474 return i in _illegal_split_indices
1476 def passes_all_checks(i: Index) -> bool:
1479 True iff ALL of the conditions listed in the 'Transformations'
1480 section of this classes' docstring would be be met by returning @i.
1482 is_space = string[i] == " "
1484 is_not_escaped = True
1486 while is_valid_index(j) and string[j] == "\\":
1487 is_not_escaped = not is_not_escaped
1491 len(string[i:]) >= self.MIN_SUBSTR_SIZE
1492 and len(string[:i]) >= self.MIN_SUBSTR_SIZE
1498 and not breaks_unsplittable_expression(i)
1501 # First, we check all indices BELOW @max_break_idx.
1502 break_idx = max_break_idx
1503 while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx):
1506 if not passes_all_checks(break_idx):
1507 # If that fails, we check all indices ABOVE @max_break_idx.
1509 # If we are able to find a valid index here, the next line is going
1510 # to be longer than the specified line length, but it's probably
1511 # better than doing nothing at all.
1512 break_idx = max_break_idx + 1
1513 while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx):
1516 if not is_valid_index(break_idx) or not passes_all_checks(break_idx):
1521 def _maybe_normalize_string_quotes(self, leaf: Leaf) -> None:
1522 if self.normalize_strings:
1523 leaf.value = normalize_string_quotes(leaf.value)
1525 def _normalize_f_string(self, string: str, prefix: str) -> str:
1528 * assert_is_leaf_string(@string)
1531 * If @string is an f-string that contains no f-expressions, we
1532 return a string identical to @string except that the 'f' prefix
1533 has been stripped and all double braces (i.e. '{{' or '}}') have
1534 been normalized (i.e. turned into '{' or '}').
1536 * Otherwise, we return @string.
1538 assert_is_leaf_string(string)
1540 if "f" in prefix and not fstring_contains_expr(string):
1541 new_prefix = prefix.replace("f", "")
1543 temp = string[len(prefix) :]
1544 temp = re.sub(r"\{\{", "{", temp)
1545 temp = re.sub(r"\}\}", "}", temp)
1548 return f"{new_prefix}{new_string}"
1552 def _get_string_operator_leaves(self, leaves: Iterable[Leaf]) -> List[Leaf]:
1555 string_op_leaves = []
1557 while LL[i].type in self.STRING_OPERATORS + [token.NAME]:
1558 prefix_leaf = Leaf(LL[i].type, str(LL[i]).strip())
1559 string_op_leaves.append(prefix_leaf)
1561 return string_op_leaves
1564 class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin):
1566 StringTransformer that splits non-"atom" strings (i.e. strings that do not
1567 exist on lines by themselves).
1570 All of the requirements listed in BaseStringSplitter's docstring in
1571 addition to the requirements listed below:
1573 * The line is a return/yield statement, which returns/yields a string.
1575 * The line is part of a ternary expression (e.g. `x = y if cond else
1576 z`) such that the line starts with `else <string>`, where <string> is
1579 * The line is an assert statement, which ends with a string.
1581 * The line is an assignment statement (e.g. `x = <string>` or `x +=
1582 <string>`) such that the variable is being assigned the value of some
1585 * The line is a dictionary key assignment where some valid key is being
1586 assigned the value of some string.
1589 The chosen string is wrapped in parentheses and then split at the LPAR.
1591 We then have one line which ends with an LPAR and another line that
1592 starts with the chosen string. The latter line is then split again at
1593 the RPAR. This results in the RPAR (and possibly a trailing comma)
1594 being placed on its own line.
1596 NOTE: If any leaves exist to the right of the chosen string (except
1597 for a trailing comma, which would be placed after the RPAR), those
1598 leaves are placed inside the parentheses. In effect, the chosen
1599 string is not necessarily being "wrapped" by parentheses. We can,
1600 however, count on the LPAR being placed directly before the chosen
1603 In other words, StringParenWrapper creates "atom" strings. These
1604 can then be split again by StringSplitter, if necessary.
1607 In the event that a string line split by StringParenWrapper is
1608 changed such that it no longer needs to be given its own line,
1609 StringParenWrapper relies on StringParenStripper to clean up the
1610 parentheses it created.
1613 def do_splitter_match(self, line: Line) -> TMatchResult:
1616 if line.leaves[-1].type in OPENING_BRACKETS:
1618 "Cannot wrap parens around a line that ends in an opening bracket."
1622 self._return_match(LL)
1623 or self._else_match(LL)
1624 or self._assert_match(LL)
1625 or self._assign_match(LL)
1626 or self._dict_match(LL)
1629 if string_idx is not None:
1630 string_value = line.leaves[string_idx].value
1631 # If the string has no spaces...
1632 if " " not in string_value:
1633 # And will still violate the line length limit when split...
1634 max_string_length = self.line_length - ((line.depth + 1) * 4)
1635 if len(string_value) > max_string_length:
1636 # And has no associated custom splits...
1637 if not self.has_custom_splits(string_value):
1638 # Then we should NOT put this string on its own line.
1640 "We do not wrap long strings in parentheses when the"
1641 " resultant line would still be over the specified line"
1642 " length and can't be split further by StringSplitter."
1644 return Ok(string_idx)
1646 return TErr("This line does not contain any non-atomic strings.")
1649 def _return_match(LL: List[Leaf]) -> Optional[int]:
1652 string_idx such that @LL[string_idx] is equal to our target (i.e.
1653 matched) string, if this line matches the return/yield statement
1654 requirements listed in the 'Requirements' section of this classes'
1659 # If this line is apart of a return/yield statement and the first leaf
1660 # contains either the "return" or "yield" keywords...
1661 if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[
1663 ].value in ["return", "yield"]:
1664 is_valid_index = is_valid_index_factory(LL)
1666 idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1667 # The next visible leaf MUST contain a string...
1668 if is_valid_index(idx) and LL[idx].type == token.STRING:
1674 def _else_match(LL: List[Leaf]) -> Optional[int]:
1677 string_idx such that @LL[string_idx] is equal to our target (i.e.
1678 matched) string, if this line matches the ternary expression
1679 requirements listed in the 'Requirements' section of this classes'
1684 # If this line is apart of a ternary expression and the first leaf
1685 # contains the "else" keyword...
1687 parent_type(LL[0]) == syms.test
1688 and LL[0].type == token.NAME
1689 and LL[0].value == "else"
1691 is_valid_index = is_valid_index_factory(LL)
1693 idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1694 # The next visible leaf MUST contain a string...
1695 if is_valid_index(idx) and LL[idx].type == token.STRING:
1701 def _assert_match(LL: List[Leaf]) -> Optional[int]:
1704 string_idx such that @LL[string_idx] is equal to our target (i.e.
1705 matched) string, if this line matches the assert statement
1706 requirements listed in the 'Requirements' section of this classes'
1711 # If this line is apart of an assert statement and the first leaf
1712 # contains the "assert" keyword...
1713 if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert":
1714 is_valid_index = is_valid_index_factory(LL)
1716 for (i, leaf) in enumerate(LL):
1717 # We MUST find a comma...
1718 if leaf.type == token.COMMA:
1719 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1721 # That comma MUST be followed by a string...
1722 if is_valid_index(idx) and LL[idx].type == token.STRING:
1725 # Skip the string trailer, if one exists.
1726 string_parser = StringParser()
1727 idx = string_parser.parse(LL, string_idx)
1729 # But no more leaves are allowed...
1730 if not is_valid_index(idx):
1736 def _assign_match(LL: List[Leaf]) -> Optional[int]:
1739 string_idx such that @LL[string_idx] is equal to our target (i.e.
1740 matched) string, if this line matches the assignment statement
1741 requirements listed in the 'Requirements' section of this classes'
1746 # If this line is apart of an expression statement or is a function
1747 # argument AND the first leaf contains a variable name...
1749 parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power]
1750 and LL[0].type == token.NAME
1752 is_valid_index = is_valid_index_factory(LL)
1754 for (i, leaf) in enumerate(LL):
1755 # We MUST find either an '=' or '+=' symbol...
1756 if leaf.type in [token.EQUAL, token.PLUSEQUAL]:
1757 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1759 # That symbol MUST be followed by a string...
1760 if is_valid_index(idx) and LL[idx].type == token.STRING:
1763 # Skip the string trailer, if one exists.
1764 string_parser = StringParser()
1765 idx = string_parser.parse(LL, string_idx)
1767 # The next leaf MAY be a comma iff this line is apart
1768 # of a function argument...
1770 parent_type(LL[0]) == syms.argument
1771 and is_valid_index(idx)
1772 and LL[idx].type == token.COMMA
1776 # But no more leaves are allowed...
1777 if not is_valid_index(idx):
1783 def _dict_match(LL: List[Leaf]) -> Optional[int]:
1786 string_idx such that @LL[string_idx] is equal to our target (i.e.
1787 matched) string, if this line matches the dictionary key assignment
1788 statement requirements listed in the 'Requirements' section of this
1793 # If this line is apart of a dictionary key assignment...
1794 if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]:
1795 is_valid_index = is_valid_index_factory(LL)
1797 for (i, leaf) in enumerate(LL):
1798 # We MUST find a colon...
1799 if leaf.type == token.COLON:
1800 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1802 # That colon MUST be followed by a string...
1803 if is_valid_index(idx) and LL[idx].type == token.STRING:
1806 # Skip the string trailer, if one exists.
1807 string_parser = StringParser()
1808 idx = string_parser.parse(LL, string_idx)
1810 # That string MAY be followed by a comma...
1811 if is_valid_index(idx) and LL[idx].type == token.COMMA:
1814 # But no more leaves are allowed...
1815 if not is_valid_index(idx):
1820 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
1823 is_valid_index = is_valid_index_factory(LL)
1824 insert_str_child = insert_str_child_factory(LL[string_idx])
1827 ends_with_comma = False
1828 if LL[comma_idx].type == token.COMMA:
1829 ends_with_comma = True
1831 leaves_to_steal_comments_from = [LL[string_idx]]
1833 leaves_to_steal_comments_from.append(LL[comma_idx])
1836 first_line = line.clone()
1837 left_leaves = LL[:string_idx]
1839 # We have to remember to account for (possibly invisible) LPAR and RPAR
1840 # leaves that already wrapped the target string. If these leaves do
1841 # exist, we will replace them with our own LPAR and RPAR leaves.
1842 old_parens_exist = False
1843 if left_leaves and left_leaves[-1].type == token.LPAR:
1844 old_parens_exist = True
1845 leaves_to_steal_comments_from.append(left_leaves[-1])
1848 append_leaves(first_line, line, left_leaves)
1850 lpar_leaf = Leaf(token.LPAR, "(")
1851 if old_parens_exist:
1852 replace_child(LL[string_idx - 1], lpar_leaf)
1854 insert_str_child(lpar_leaf)
1855 first_line.append(lpar_leaf)
1857 # We throw inline comments that were originally to the right of the
1858 # target string to the top line. They will now be shown to the right of
1860 for leaf in leaves_to_steal_comments_from:
1861 for comment_leaf in line.comments_after(leaf):
1862 first_line.append(comment_leaf, preformatted=True)
1864 yield Ok(first_line)
1866 # --- Middle (String) Line
1867 # We only need to yield one (possibly too long) string line, since the
1868 # `StringSplitter` will break it down further if necessary.
1869 string_value = LL[string_idx].value
1872 depth=line.depth + 1,
1873 inside_brackets=True,
1874 should_split_rhs=line.should_split_rhs,
1875 magic_trailing_comma=line.magic_trailing_comma,
1877 string_leaf = Leaf(token.STRING, string_value)
1878 insert_str_child(string_leaf)
1879 string_line.append(string_leaf)
1881 old_rpar_leaf = None
1882 if is_valid_index(string_idx + 1):
1883 right_leaves = LL[string_idx + 1 :]
1887 if old_parens_exist:
1888 assert right_leaves and right_leaves[-1].type == token.RPAR, (
1889 "Apparently, old parentheses do NOT exist?!"
1890 f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
1892 old_rpar_leaf = right_leaves.pop()
1894 append_leaves(string_line, line, right_leaves)
1896 yield Ok(string_line)
1899 last_line = line.clone()
1900 last_line.bracket_tracker = first_line.bracket_tracker
1902 new_rpar_leaf = Leaf(token.RPAR, ")")
1903 if old_rpar_leaf is not None:
1904 replace_child(old_rpar_leaf, new_rpar_leaf)
1906 insert_str_child(new_rpar_leaf)
1907 last_line.append(new_rpar_leaf)
1909 # If the target string ended with a comma, we place this comma to the
1910 # right of the RPAR on the last line.
1912 comma_leaf = Leaf(token.COMMA, ",")
1913 replace_child(LL[comma_idx], comma_leaf)
1914 last_line.append(comma_leaf)
1921 A state machine that aids in parsing a string's "trailer", which can be
1922 either non-existent, an old-style formatting sequence (e.g. `% varX` or `%
1923 (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX,
1926 NOTE: A new StringParser object MUST be instantiated for each string
1927 trailer we need to parse.
1930 We shall assume that `line` equals the `Line` object that corresponds
1931 to the following line of python code:
1933 x = "Some {}.".format("String") + some_other_string
1936 Furthermore, we will assume that `string_idx` is some index such that:
1938 assert line.leaves[string_idx].value == "Some {}."
1941 The following code snippet then holds:
1943 string_parser = StringParser()
1944 idx = string_parser.parse(line.leaves, string_idx)
1945 assert line.leaves[idx].type == token.PLUS
1949 DEFAULT_TOKEN: Final = 20210605
1951 # String Parser States
1956 SINGLE_FMT_ARG: Final = 5
1961 # Lookup Table for Next State
1962 _goto: Final[Dict[Tuple[ParserState, NodeType], ParserState]] = {
1963 # A string trailer may start with '.' OR '%'.
1964 (START, token.DOT): DOT,
1965 (START, token.PERCENT): PERCENT,
1966 (START, DEFAULT_TOKEN): DONE,
1967 # A '.' MUST be followed by an attribute or method name.
1968 (DOT, token.NAME): NAME,
1969 # A method name MUST be followed by an '(', whereas an attribute name
1970 # is the last symbol in the string trailer.
1971 (NAME, token.LPAR): LPAR,
1972 (NAME, DEFAULT_TOKEN): DONE,
1973 # A '%' symbol can be followed by an '(' or a single argument (e.g. a
1974 # string or variable name).
1975 (PERCENT, token.LPAR): LPAR,
1976 (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG,
1977 # If a '%' symbol is followed by a single argument, that argument is
1978 # the last leaf in the string trailer.
1979 (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE,
1980 # If present, a ')' symbol is the last symbol in a string trailer.
1981 # (NOTE: LPARS and nested RPARS are not included in this lookup table,
1982 # since they are treated as a special case by the parsing logic in this
1983 # classes' implementation.)
1984 (RPAR, DEFAULT_TOKEN): DONE,
1987 def __init__(self) -> None:
1988 self._state = self.START
1989 self._unmatched_lpars = 0
1991 def parse(self, leaves: List[Leaf], string_idx: int) -> int:
1994 * @leaves[@string_idx].type == token.STRING
1997 The index directly after the last leaf which is apart of the string
1998 trailer, if a "trailer" exists.
2000 @string_idx + 1, if no string "trailer" exists.
2002 assert leaves[string_idx].type == token.STRING
2004 idx = string_idx + 1
2005 while idx < len(leaves) and self._next_state(leaves[idx]):
2009 def _next_state(self, leaf: Leaf) -> bool:
2012 * On the first call to this function, @leaf MUST be the leaf that
2013 was directly after the string leaf in question (e.g. if our target
2014 string is `line.leaves[i]` then the first call to this method must
2015 be `line.leaves[i + 1]`).
2016 * On the next call to this function, the leaf parameter passed in
2017 MUST be the leaf directly following @leaf.
2020 True iff @leaf is apart of the string's trailer.
2022 # We ignore empty LPAR or RPAR leaves.
2023 if is_empty_par(leaf):
2026 next_token = leaf.type
2027 if next_token == token.LPAR:
2028 self._unmatched_lpars += 1
2030 current_state = self._state
2032 # The LPAR parser state is a special case. We will return True until we
2033 # find the matching RPAR token.
2034 if current_state == self.LPAR:
2035 if next_token == token.RPAR:
2036 self._unmatched_lpars -= 1
2037 if self._unmatched_lpars == 0:
2038 self._state = self.RPAR
2039 # Otherwise, we use a lookup table to determine the next state.
2041 # If the lookup table matches the current state to the next
2042 # token, we use the lookup table.
2043 if (current_state, next_token) in self._goto:
2044 self._state = self._goto[current_state, next_token]
2046 # Otherwise, we check if a the current state was assigned a
2048 if (current_state, self.DEFAULT_TOKEN) in self._goto:
2049 self._state = self._goto[current_state, self.DEFAULT_TOKEN]
2050 # If no default has been assigned, then this parser has a logic
2053 raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
2055 if self._state == self.DONE:
2061 def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]:
2063 Factory for a convenience function that is used to orphan @string_leaf
2064 and then insert multiple new leaves into the same part of the node
2065 structure that @string_leaf had originally occupied.
2068 Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N =
2069 string_leaf.parent`. Assume the node `N` has the following
2076 Leaf(STRING, '"foo"'),
2080 We then run the code snippet shown below.
2082 insert_str_child = insert_str_child_factory(string_leaf)
2084 lpar = Leaf(token.LPAR, '(')
2085 insert_str_child(lpar)
2087 bar = Leaf(token.STRING, '"bar"')
2088 insert_str_child(bar)
2090 rpar = Leaf(token.RPAR, ')')
2091 insert_str_child(rpar)
2094 After which point, it follows that `string_leaf.parent is None` and
2095 the node `N` now has the following structure:
2102 Leaf(STRING, '"bar"'),
2107 string_parent = string_leaf.parent
2108 string_child_idx = string_leaf.remove()
2110 def insert_str_child(child: LN) -> None:
2111 nonlocal string_child_idx
2113 assert string_parent is not None
2114 assert string_child_idx is not None
2116 string_parent.insert_child(string_child_idx, child)
2117 string_child_idx += 1
2119 return insert_str_child
2122 def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]:
2128 is_valid_index = is_valid_index_factory(my_list)
2130 assert is_valid_index(0)
2131 assert is_valid_index(2)
2133 assert not is_valid_index(3)
2134 assert not is_valid_index(-1)
2138 def is_valid_index(idx: int) -> bool:
2141 True iff @idx is positive AND seq[@idx] does NOT raise an
2144 return 0 <= idx < len(seq)
2146 return is_valid_index