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 new_line = line.clone()
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 # We have to be careful to make a new line properly:
143 # - bracket related metadata must be maintained (handled by Line.append)
144 # - comments need to copied over, updating the leaf IDs they're attached to
145 new_line.append(new_leaf, preformatted=True)
146 for comment_leaf in line.comments_after(leaf):
147 new_line.append(comment_leaf, preformatted=True)
152 class StringTransformer(ABC):
154 An implementation of the Transformer protocol that relies on its
155 subclasses overriding the template methods `do_match(...)` and
158 This Transformer works exclusively on strings (for example, by merging
161 The following sections can be found among the docstrings of each concrete
162 StringTransformer subclass.
165 Which requirements must be met of the given Line for this
166 StringTransformer to be applied?
169 If the given Line meets all of the above requirements, which string
170 transformations can you expect to be applied to it by this
174 What contractual agreements does this StringTransformer have with other
175 StringTransfomers? Such collaborations should be eliminated/minimized
179 __name__: Final = "StringTransformer"
181 # Ideally this would be a dataclass, but unfortunately mypyc breaks when used with
183 def __init__(self, line_length: int, normalize_strings: bool) -> None:
184 self.line_length = line_length
185 self.normalize_strings = normalize_strings
188 def do_match(self, line: Line) -> TMatchResult:
191 * Ok(string_idx) such that `line.leaves[string_idx]` is our target
192 string, if a match was able to be made.
194 * Err(CannotTransform), if a match was not able to be made.
198 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
201 * Ok(new_line) where new_line is the new transformed line.
203 * Err(CannotTransform) if the transformation failed for some reason. The
204 `do_match(...)` template method should usually be used to reject
205 the form of the given Line, but in some cases it is difficult to
206 know whether or not a Line meets the StringTransformer's
207 requirements until the transformation is already midway.
210 This method should NOT mutate @line directly, but it MAY mutate the
211 Line's underlying Node structure. (WARNING: If the underlying Node
212 structure IS altered, then this method should NOT be allowed to
213 yield an CannotTransform after that point.)
216 def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line]:
218 StringTransformer instances have a call signature that mirrors that of
219 the Transformer type.
222 CannotTransform(...) if the concrete StringTransformer class is unable
225 # Optimization to avoid calling `self.do_match(...)` when the line does
226 # not contain any string.
227 if not any(leaf.type == token.STRING for leaf in line.leaves):
228 raise CannotTransform("There are no strings in this line.")
230 match_result = self.do_match(line)
232 if isinstance(match_result, Err):
233 cant_transform = match_result.err()
234 raise CannotTransform(
235 f"The string transformer {self.__class__.__name__} does not recognize"
236 " this line as one that it can transform."
237 ) from cant_transform
239 string_idx = match_result.ok()
241 for line_result in self.do_transform(line, string_idx):
242 if isinstance(line_result, Err):
243 cant_transform = line_result.err()
244 raise CannotTransform(
245 "StringTransformer failed while attempting to transform string."
246 ) from cant_transform
247 line = line_result.ok()
253 """A custom (i.e. manual) string split.
255 A single CustomSplit instance represents a single substring.
258 Consider the following string:
265 This string will correspond to the following three CustomSplit instances:
267 CustomSplit(False, 16)
268 CustomSplit(False, 17)
269 CustomSplit(True, 16)
278 class CustomSplitMapMixin:
280 This mixin class is used to map merged strings to a sequence of
281 CustomSplits, which will then be used to re-split the strings iff none of
282 the resultant substrings go over the configured max line length.
285 _Key: ClassVar = Tuple[StringID, str]
286 _CUSTOM_SPLIT_MAP: ClassVar[Dict[_Key, Tuple[CustomSplit, ...]]] = defaultdict(
291 def _get_key(string: str) -> "CustomSplitMapMixin._Key":
294 A unique identifier that is used internally to map @string to a
295 group of custom splits.
297 return (id(string), string)
299 def add_custom_splits(
300 self, string: str, custom_splits: Iterable[CustomSplit]
302 """Custom Split Map Setter Method
305 Adds a mapping from @string to the custom splits @custom_splits.
307 key = self._get_key(string)
308 self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits)
310 def pop_custom_splits(self, string: str) -> List[CustomSplit]:
311 """Custom Split Map Getter Method
314 * A list of the custom splits that are mapped to @string, if any
320 Deletes the mapping between @string and its associated custom
321 splits (which are returned to the caller).
323 key = self._get_key(string)
325 custom_splits = self._CUSTOM_SPLIT_MAP[key]
326 del self._CUSTOM_SPLIT_MAP[key]
328 return list(custom_splits)
330 def has_custom_splits(self, string: str) -> bool:
333 True iff @string is associated with a set of custom splits.
335 key = self._get_key(string)
336 return key in self._CUSTOM_SPLIT_MAP
339 class StringMerger(StringTransformer, CustomSplitMapMixin):
340 """StringTransformer that merges strings together.
343 (A) The line contains adjacent strings such that ALL of the validation checks
344 listed in StringMerger.__validate_msg(...)'s docstring pass.
346 (B) The line contains a string which uses line continuation backslashes.
349 Depending on which of the two requirements above where met, either:
351 (A) The string group associated with the target string is merged.
353 (B) All line-continuation backslashes are removed from the target string.
356 StringMerger provides custom split information to StringSplitter.
359 def do_match(self, line: Line) -> TMatchResult:
362 is_valid_index = is_valid_index_factory(LL)
364 for i, leaf in enumerate(LL):
366 leaf.type == token.STRING
367 and is_valid_index(i + 1)
368 and LL[i + 1].type == token.STRING
372 if leaf.type == token.STRING and "\\\n" in leaf.value:
375 return TErr("This line has no strings that need merging.")
377 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
379 rblc_result = self._remove_backslash_line_continuation_chars(
382 if isinstance(rblc_result, Ok):
383 new_line = rblc_result.ok()
385 msg_result = self._merge_string_group(new_line, string_idx)
386 if isinstance(msg_result, Ok):
387 new_line = msg_result.ok()
389 if isinstance(rblc_result, Err) and isinstance(msg_result, Err):
390 msg_cant_transform = msg_result.err()
391 rblc_cant_transform = rblc_result.err()
392 cant_transform = CannotTransform(
393 "StringMerger failed to merge any strings in this line."
396 # Chain the errors together using `__cause__`.
397 msg_cant_transform.__cause__ = rblc_cant_transform
398 cant_transform.__cause__ = msg_cant_transform
400 yield Err(cant_transform)
405 def _remove_backslash_line_continuation_chars(
406 line: Line, string_idx: int
409 Merge strings that were split across multiple lines using
410 line-continuation backslashes.
413 Ok(new_line), if @line contains backslash line-continuation
416 Err(CannotTransform), otherwise.
420 string_leaf = LL[string_idx]
422 string_leaf.type == token.STRING
423 and "\\\n" in string_leaf.value
424 and not has_triple_quotes(string_leaf.value)
427 f"String leaf {string_leaf} does not contain any backslash line"
428 " continuation characters."
431 new_line = line.clone()
432 new_line.comments = line.comments.copy()
433 append_leaves(new_line, line, LL)
435 new_string_leaf = new_line.leaves[string_idx]
436 new_string_leaf.value = new_string_leaf.value.replace("\\\n", "")
440 def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]:
442 Merges string group (i.e. set of adjacent strings) where the first
443 string in the group is `line.leaves[string_idx]`.
446 Ok(new_line), if ALL of the validation checks found in
447 __validate_msg(...) pass.
449 Err(CannotTransform), otherwise.
453 is_valid_index = is_valid_index_factory(LL)
455 vresult = self._validate_msg(line, string_idx)
456 if isinstance(vresult, Err):
459 # If the string group is wrapped inside an Atom node, we must make sure
460 # to later replace that Atom with our new (merged) string leaf.
461 atom_node = LL[string_idx].parent
463 # We will place BREAK_MARK in between every two substrings that we
464 # merge. We will then later go through our final result and use the
465 # various instances of BREAK_MARK we find to add the right values to
466 # the custom split map.
467 BREAK_MARK = "@@@@@ BLACK BREAKPOINT MARKER @@@@@"
469 QUOTE = LL[string_idx].value[-1]
471 def make_naked(string: str, string_prefix: str) -> str:
472 """Strip @string (i.e. make it a "naked" string)
475 * assert_is_leaf_string(@string)
478 A string that is identical to @string except that
479 @string_prefix has been stripped, the surrounding QUOTE
480 characters have been removed, and any remaining QUOTE
481 characters have been escaped.
483 assert_is_leaf_string(string)
485 RE_EVEN_BACKSLASHES = r"(?:(?<!\\)(?:\\\\)*)"
486 naked_string = string[len(string_prefix) + 1 : -1]
487 naked_string = re.sub(
488 "(" + RE_EVEN_BACKSLASHES + ")" + QUOTE, r"\1\\" + QUOTE, naked_string
492 # Holds the CustomSplit objects that will later be added to the custom
496 # Temporary storage for the 'has_prefix' part of the CustomSplit objects.
499 # Sets the 'prefix' variable. This is the prefix that the final merged
501 next_str_idx = string_idx
505 and is_valid_index(next_str_idx)
506 and LL[next_str_idx].type == token.STRING
508 prefix = get_string_prefix(LL[next_str_idx].value).lower()
511 # The next loop merges the string group. The final string will be
514 # The following convenience variables are used:
519 # NSS: naked next string
523 next_str_idx = string_idx
524 while is_valid_index(next_str_idx) and LL[next_str_idx].type == token.STRING:
527 SS = LL[next_str_idx].value
528 next_prefix = get_string_prefix(SS).lower()
530 # If this is an f-string group but this substring is not prefixed
532 if "f" in prefix and "f" not in next_prefix:
533 # Then we must escape any braces contained in this substring.
534 SS = re.sub(r"(\{|\})", r"\1\1", SS)
536 NSS = make_naked(SS, next_prefix)
538 has_prefix = bool(next_prefix)
539 prefix_tracker.append(has_prefix)
541 S = prefix + QUOTE + NS + NSS + BREAK_MARK + QUOTE
542 NS = make_naked(S, prefix)
546 S_leaf = Leaf(token.STRING, S)
547 if self.normalize_strings:
548 S_leaf.value = normalize_string_quotes(S_leaf.value)
550 # Fill the 'custom_splits' list with the appropriate CustomSplit objects.
551 temp_string = S_leaf.value[len(prefix) + 1 : -1]
552 for has_prefix in prefix_tracker:
553 mark_idx = temp_string.find(BREAK_MARK)
556 ), "Logic error while filling the custom string breakpoint cache."
558 temp_string = temp_string[mark_idx + len(BREAK_MARK) :]
559 breakpoint_idx = mark_idx + (len(prefix) if has_prefix else 0) + 1
560 custom_splits.append(CustomSplit(has_prefix, breakpoint_idx))
562 string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, ""))
564 if atom_node is not None:
565 replace_child(atom_node, string_leaf)
567 # Build the final line ('new_line') that this method will later return.
568 new_line = line.clone()
569 for i, leaf in enumerate(LL):
571 new_line.append(string_leaf)
573 if string_idx <= i < string_idx + num_of_strings:
574 for comment_leaf in line.comments_after(LL[i]):
575 new_line.append(comment_leaf, preformatted=True)
578 append_leaves(new_line, line, [leaf])
580 self.add_custom_splits(string_leaf.value, custom_splits)
584 def _validate_msg(line: Line, string_idx: int) -> TResult[None]:
585 """Validate (M)erge (S)tring (G)roup
587 Transform-time string validation logic for __merge_string_group(...).
590 * Ok(None), if ALL validation checks (listed below) pass.
592 * Err(CannotTransform), if any of the following are true:
593 - The target string group does not contain ANY stand-alone comments.
594 - The target string is not in a string group (i.e. it has no
596 - The string group has more than one inline comment.
597 - The string group has an inline comment that appears to be a pragma.
598 - The set of all string prefixes in the string group is of
599 length greater than one and is not equal to {"", "f"}.
600 - The string group consists of raw strings.
602 # We first check for "inner" stand-alone comments (i.e. stand-alone
603 # comments that have a string leaf before them AND after them).
606 found_sa_comment = False
607 is_valid_index = is_valid_index_factory(line.leaves)
608 while is_valid_index(i) and line.leaves[i].type in [
612 if line.leaves[i].type == STANDALONE_COMMENT:
613 found_sa_comment = True
614 elif found_sa_comment:
616 "StringMerger does NOT merge string groups which contain "
617 "stand-alone comments."
622 num_of_inline_string_comments = 0
623 set_of_prefixes = set()
625 for leaf in line.leaves[string_idx:]:
626 if leaf.type != token.STRING:
627 # If the string group is trailed by a comma, we count the
628 # comments trailing the comma to be one of the string group's
630 if leaf.type == token.COMMA and id(leaf) in line.comments:
631 num_of_inline_string_comments += 1
634 if has_triple_quotes(leaf.value):
635 return TErr("StringMerger does NOT merge multiline strings.")
638 prefix = get_string_prefix(leaf.value).lower()
640 return TErr("StringMerger does NOT merge raw strings.")
642 set_of_prefixes.add(prefix)
644 if id(leaf) in line.comments:
645 num_of_inline_string_comments += 1
646 if contains_pragma_comment(line.comments[id(leaf)]):
647 return TErr("Cannot merge strings which have pragma comments.")
649 if num_of_strings < 2:
651 f"Not enough strings to merge (num_of_strings={num_of_strings})."
654 if num_of_inline_string_comments > 1:
656 f"Too many inline string comments ({num_of_inline_string_comments})."
659 if len(set_of_prefixes) > 1 and set_of_prefixes != {"", "f"}:
660 return TErr(f"Too many different prefixes ({set_of_prefixes}).")
665 class StringParenStripper(StringTransformer):
666 """StringTransformer that strips surrounding parentheses from strings.
669 The line contains a string which is surrounded by parentheses and:
670 - The target string is NOT the only argument to a function call.
671 - The target string is NOT a "pointless" string.
672 - If the target string contains a PERCENT, the brackets are not
673 preceded or followed by an operator with higher precedence than
677 The parentheses mentioned in the 'Requirements' section are stripped.
680 StringParenStripper has its own inherent usefulness, but it is also
681 relied on to clean up the parentheses created by StringParenWrapper (in
682 the event that they are no longer needed).
685 def do_match(self, line: Line) -> TMatchResult:
688 is_valid_index = is_valid_index_factory(LL)
690 for idx, leaf in enumerate(LL):
691 # Should be a string...
692 if leaf.type != token.STRING:
695 # If this is a "pointless" string...
698 and leaf.parent.parent
699 and leaf.parent.parent.type == syms.simple_stmt
703 # Should be preceded by a non-empty LPAR...
705 not is_valid_index(idx - 1)
706 or LL[idx - 1].type != token.LPAR
707 or is_empty_lpar(LL[idx - 1])
711 # That LPAR should NOT be preceded by a function name or a closing
712 # bracket (which could be a function which returns a function or a
713 # list/dictionary that contains a function)...
714 if is_valid_index(idx - 2) and (
715 LL[idx - 2].type == token.NAME or LL[idx - 2].type in CLOSING_BRACKETS
721 # Skip the string trailer, if one exists.
722 string_parser = StringParser()
723 next_idx = string_parser.parse(LL, string_idx)
725 # if the leaves in the parsed string include a PERCENT, we need to
726 # make sure the initial LPAR is NOT preceded by an operator with
727 # higher or equal precedence to PERCENT
728 if is_valid_index(idx - 2):
729 # mypy can't quite follow unless we name this
730 before_lpar = LL[idx - 2]
731 if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and (
748 # only unary PLUS/MINUS
750 and before_lpar.parent.type == syms.factor
751 and (before_lpar.type in {token.PLUS, token.MINUS})
756 # Should be followed by a non-empty RPAR...
758 is_valid_index(next_idx)
759 and LL[next_idx].type == token.RPAR
760 and not is_empty_rpar(LL[next_idx])
762 # That RPAR should NOT be followed by anything with higher
763 # precedence than PERCENT
764 if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in {
772 return Ok(string_idx)
774 return TErr("This line has no strings wrapped in parens.")
776 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
779 string_parser = StringParser()
780 rpar_idx = string_parser.parse(LL, string_idx)
782 for leaf in (LL[string_idx - 1], LL[rpar_idx]):
783 if line.comments_after(leaf):
785 "Will not strip parentheses which have comments attached to them."
789 new_line = line.clone()
790 new_line.comments = line.comments.copy()
792 append_leaves(new_line, line, LL[: string_idx - 1])
793 except BracketMatchError:
794 # HACK: I believe there is currently a bug somewhere in
795 # right_hand_split() that is causing brackets to not be tracked
796 # properly by a shared BracketTracker.
797 append_leaves(new_line, line, LL[: string_idx - 1], preformatted=True)
799 string_leaf = Leaf(token.STRING, LL[string_idx].value)
800 LL[string_idx - 1].remove()
801 replace_child(LL[string_idx], string_leaf)
802 new_line.append(string_leaf)
805 new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :]
808 LL[rpar_idx].remove()
813 class BaseStringSplitter(StringTransformer):
815 Abstract class for StringTransformers which transform a Line's strings by splitting
816 them or placing them on their own lines where necessary to avoid going over
817 the configured line length.
820 * The target string value is responsible for the line going over the
821 line length limit. It follows that after all of black's other line
822 split methods have been exhausted, this line (or one of the resulting
823 lines after all line splits are performed) would still be over the
824 line_length limit unless we split this string.
826 * The target string is NOT a "pointless" string (i.e. a string that has
827 no parent or siblings).
829 * The target string is not followed by an inline comment that appears
832 * The target string is not a multiline (i.e. triple-quote) string.
835 STRING_OPERATORS: Final = [
848 def do_splitter_match(self, line: Line) -> TMatchResult:
850 BaseStringSplitter asks its clients to override this method instead of
851 `StringTransformer.do_match(...)`.
853 Follows the same protocol as `StringTransformer.do_match(...)`.
855 Refer to `help(StringTransformer.do_match)` for more information.
858 def do_match(self, line: Line) -> TMatchResult:
859 match_result = self.do_splitter_match(line)
860 if isinstance(match_result, Err):
863 string_idx = match_result.ok()
864 vresult = self._validate(line, string_idx)
865 if isinstance(vresult, Err):
870 def _validate(self, line: Line, string_idx: int) -> TResult[None]:
872 Checks that @line meets all of the requirements listed in this classes'
873 docstring. Refer to `help(BaseStringSplitter)` for a detailed
874 description of those requirements.
877 * Ok(None), if ALL of the requirements are met.
879 * Err(CannotTransform), if ANY of the requirements are NOT met.
883 string_leaf = LL[string_idx]
885 max_string_length = self._get_max_string_length(line, string_idx)
886 if len(string_leaf.value) <= max_string_length:
888 "The string itself is not what is causing this line to be too long."
891 if not string_leaf.parent or [L.type for L in string_leaf.parent.children] == [
896 f"This string ({string_leaf.value}) appears to be pointless (i.e. has"
900 if id(line.leaves[string_idx]) in line.comments and contains_pragma_comment(
901 line.comments[id(line.leaves[string_idx])]
904 "Line appears to end with an inline pragma comment. Splitting the line"
905 " could modify the pragma's behavior."
908 if has_triple_quotes(string_leaf.value):
909 return TErr("We cannot split multiline strings.")
913 def _get_max_string_length(self, line: Line, string_idx: int) -> int:
915 Calculates the max string length used when attempting to determine
916 whether or not the target string is responsible for causing the line to
917 go over the line length limit.
919 WARNING: This method is tightly coupled to both StringSplitter and
920 (especially) StringParenWrapper. There is probably a better way to
921 accomplish what is being done here.
924 max_string_length: such that `line.leaves[string_idx].value >
925 max_string_length` implies that the target string IS responsible
926 for causing this line to exceed the line length limit.
930 is_valid_index = is_valid_index_factory(LL)
932 # We use the shorthand "WMA4" in comments to abbreviate "We must
933 # account for". When giving examples, we use STRING to mean some/any
936 # Finally, we use the following convenience variables:
938 # P: The leaf that is before the target string leaf.
939 # N: The leaf that is after the target string leaf.
940 # NN: The leaf that is after N.
942 # WMA4 the whitespace at the beginning of the line.
943 offset = line.depth * 4
945 if is_valid_index(string_idx - 1):
946 p_idx = string_idx - 1
948 LL[string_idx - 1].type == token.LPAR
949 and LL[string_idx - 1].value == ""
952 # If the previous leaf is an empty LPAR placeholder, we should skip it.
956 if P.type in self.STRING_OPERATORS:
957 # WMA4 a space and a string operator (e.g. `+ STRING` or `== STRING`).
958 offset += len(str(P)) + 1
960 if P.type == token.COMMA:
961 # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`].
964 if P.type in [token.COLON, token.EQUAL, token.PLUSEQUAL, token.NAME]:
965 # This conditional branch is meant to handle dictionary keys,
966 # variable assignments, 'return STRING' statement lines, and
967 # 'else STRING' ternary expression lines.
969 # WMA4 a single space.
972 # WMA4 the lengths of any leaves that came before that space,
973 # but after any closing bracket before that space.
974 for leaf in reversed(LL[: p_idx + 1]):
975 offset += len(str(leaf))
976 if leaf.type in CLOSING_BRACKETS:
979 if is_valid_index(string_idx + 1):
980 N = LL[string_idx + 1]
981 if N.type == token.RPAR and N.value == "" and len(LL) > string_idx + 2:
982 # If the next leaf is an empty RPAR placeholder, we should skip it.
983 N = LL[string_idx + 2]
985 if N.type == token.COMMA:
986 # WMA4 a single comma at the end of the string (e.g `STRING,`).
989 if is_valid_index(string_idx + 2):
990 NN = LL[string_idx + 2]
992 if N.type == token.DOT and NN.type == token.NAME:
993 # This conditional branch is meant to handle method calls invoked
994 # off of a string literal up to and including the LPAR character.
996 # WMA4 the '.' character.
1000 is_valid_index(string_idx + 3)
1001 and LL[string_idx + 3].type == token.LPAR
1003 # WMA4 the left parenthesis character.
1006 # WMA4 the length of the method's name.
1007 offset += len(NN.value)
1009 has_comments = False
1010 for comment_leaf in line.comments_after(LL[string_idx]):
1011 if not has_comments:
1013 # WMA4 two spaces before the '#' character.
1016 # WMA4 the length of the inline comment.
1017 offset += len(comment_leaf.value)
1019 max_string_length = self.line_length - offset
1020 return max_string_length
1023 def iter_fexpr_spans(s: str) -> Iterator[Tuple[int, int]]:
1025 Yields spans corresponding to expressions in a given f-string.
1026 Spans are half-open ranges (left inclusive, right exclusive).
1027 Assumes the input string is a valid f-string, but will not crash if the input
1030 stack: List[int] = [] # our curly paren stack
1034 # if we're in a string part of the f-string, ignore escaped curly braces
1035 if not stack and i + 1 < len(s) and s[i + 1] == "{":
1047 # we've made it back out of the expression! yield the span
1053 # if we're in an expression part of the f-string, fast forward through strings
1054 # note that backslashes are not legal in the expression portion of f-strings
1057 if s[i : i + 3] in ("'''", '"""'):
1058 delim = s[i : i + 3]
1059 elif s[i] in ("'", '"'):
1063 while i < len(s) and s[i : i + len(delim)] != delim:
1070 def fstring_contains_expr(s: str) -> bool:
1071 return any(iter_fexpr_spans(s))
1074 class StringSplitter(BaseStringSplitter, CustomSplitMapMixin):
1076 StringTransformer that splits "atom" strings (i.e. strings which exist on
1077 lines by themselves).
1080 * The line consists ONLY of a single string (possibly prefixed by a
1081 string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE
1084 * All of the requirements listed in BaseStringSplitter's docstring.
1087 The string mentioned in the 'Requirements' section is split into as
1088 many substrings as necessary to adhere to the configured line length.
1090 In the final set of substrings, no substring should be smaller than
1091 MIN_SUBSTR_SIZE characters.
1093 The string will ONLY be split on spaces (i.e. each new substring should
1094 start with a space). Note that the string will NOT be split on a space
1095 which is escaped with a backslash.
1097 If the string is an f-string, it will NOT be split in the middle of an
1098 f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x
1099 else bar()} is an f-expression).
1101 If the string that is being split has an associated set of custom split
1102 records and those custom splits will NOT result in any line going over
1103 the configured line length, those custom splits are used. Otherwise the
1104 string is split as late as possible (from left-to-right) while still
1105 adhering to the transformation rules listed above.
1108 StringSplitter relies on StringMerger to construct the appropriate
1109 CustomSplit objects and add them to the custom split map.
1112 MIN_SUBSTR_SIZE: Final = 6
1114 def do_splitter_match(self, line: Line) -> TMatchResult:
1117 is_valid_index = is_valid_index_factory(LL)
1121 # The first two leaves MAY be the 'not in' keywords...
1124 and is_valid_index(idx + 1)
1125 and [LL[idx].type, LL[idx + 1].type] == [token.NAME, token.NAME]
1126 and str(LL[idx]) + str(LL[idx + 1]) == "not in"
1129 # Else the first leaf MAY be a string operator symbol or the 'in' keyword...
1130 elif is_valid_index(idx) and (
1131 LL[idx].type in self.STRING_OPERATORS
1132 or LL[idx].type == token.NAME
1133 and str(LL[idx]) == "in"
1137 # The next/first leaf MAY be an empty LPAR...
1138 if is_valid_index(idx) and is_empty_lpar(LL[idx]):
1141 # The next/first leaf MUST be a string...
1142 if not is_valid_index(idx) or LL[idx].type != token.STRING:
1143 return TErr("Line does not start with a string.")
1147 # Skip the string trailer, if one exists.
1148 string_parser = StringParser()
1149 idx = string_parser.parse(LL, string_idx)
1151 # That string MAY be followed by an empty RPAR...
1152 if is_valid_index(idx) and is_empty_rpar(LL[idx]):
1155 # That string / empty RPAR leaf MAY be followed by a comma...
1156 if is_valid_index(idx) and LL[idx].type == token.COMMA:
1159 # But no more leaves are allowed...
1160 if is_valid_index(idx):
1161 return TErr("This line does not end with a string.")
1163 return Ok(string_idx)
1165 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
1168 QUOTE = LL[string_idx].value[-1]
1170 is_valid_index = is_valid_index_factory(LL)
1171 insert_str_child = insert_str_child_factory(LL[string_idx])
1173 prefix = get_string_prefix(LL[string_idx].value).lower()
1175 # We MAY choose to drop the 'f' prefix from substrings that don't
1176 # contain any f-expressions, but ONLY if the original f-string
1177 # contains at least one f-expression. Otherwise, we will alter the AST
1179 drop_pointless_f_prefix = ("f" in prefix) and fstring_contains_expr(
1180 LL[string_idx].value
1183 first_string_line = True
1185 string_op_leaves = self._get_string_operator_leaves(LL)
1186 string_op_leaves_length = (
1187 sum([len(str(prefix_leaf)) for prefix_leaf in string_op_leaves]) + 1
1192 def maybe_append_string_operators(new_line: Line) -> None:
1195 If @line starts with a string operator and this is the first
1196 line we are constructing, this function appends the string
1197 operator to @new_line and replaces the old string operator leaf
1198 in the node structure. Otherwise this function does nothing.
1200 maybe_prefix_leaves = string_op_leaves if first_string_line else []
1201 for i, prefix_leaf in enumerate(maybe_prefix_leaves):
1202 replace_child(LL[i], prefix_leaf)
1203 new_line.append(prefix_leaf)
1206 is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA
1209 def max_last_string() -> int:
1212 The max allowed length of the string value used for the last
1213 line we will construct.
1215 result = self.line_length
1216 result -= line.depth * 4
1217 result -= 1 if ends_with_comma else 0
1218 result -= string_op_leaves_length
1221 # --- Calculate Max Break Index (for string value)
1222 # We start with the line length limit
1223 max_break_idx = self.line_length
1224 # The last index of a string of length N is N-1.
1226 # Leading whitespace is not present in the string value (e.g. Leaf.value).
1227 max_break_idx -= line.depth * 4
1228 if max_break_idx < 0:
1230 f"Unable to split {LL[string_idx].value} at such high of a line depth:"
1235 # Check if StringMerger registered any custom splits.
1236 custom_splits = self.pop_custom_splits(LL[string_idx].value)
1237 # We use them ONLY if none of them would produce lines that exceed the
1239 use_custom_breakpoints = bool(
1241 and all(csplit.break_idx <= max_break_idx for csplit in custom_splits)
1244 # Temporary storage for the remaining chunk of the string line that
1245 # can't fit onto the line currently being constructed.
1246 rest_value = LL[string_idx].value
1248 def more_splits_should_be_made() -> bool:
1251 True iff `rest_value` (the remaining string value from the last
1252 split), should be split again.
1254 if use_custom_breakpoints:
1255 return len(custom_splits) > 1
1257 return len(rest_value) > max_last_string()
1259 string_line_results: List[Ok[Line]] = []
1260 while more_splits_should_be_made():
1261 if use_custom_breakpoints:
1262 # Custom User Split (manual)
1263 csplit = custom_splits.pop(0)
1264 break_idx = csplit.break_idx
1266 # Algorithmic Split (automatic)
1267 max_bidx = max_break_idx - string_op_leaves_length
1268 maybe_break_idx = self._get_break_idx(rest_value, max_bidx)
1269 if maybe_break_idx is None:
1270 # If we are unable to algorithmically determine a good split
1271 # and this string has custom splits registered to it, we
1272 # fall back to using them--which means we have to start
1273 # over from the beginning.
1275 rest_value = LL[string_idx].value
1276 string_line_results = []
1277 first_string_line = True
1278 use_custom_breakpoints = True
1281 # Otherwise, we stop splitting here.
1284 break_idx = maybe_break_idx
1286 # --- Construct `next_value`
1287 next_value = rest_value[:break_idx] + QUOTE
1289 # HACK: The following 'if' statement is a hack to fix the custom
1290 # breakpoint index in the case of either: (a) substrings that were
1291 # f-strings but will have the 'f' prefix removed OR (b) substrings
1292 # that were not f-strings but will now become f-strings because of
1293 # redundant use of the 'f' prefix (i.e. none of the substrings
1294 # contain f-expressions but one or more of them had the 'f' prefix
1295 # anyway; in which case, we will prepend 'f' to _all_ substrings).
1297 # There is probably a better way to accomplish what is being done
1300 # If this substring is an f-string, we _could_ remove the 'f'
1301 # prefix, and the current custom split did NOT originally use a
1304 next_value != self._normalize_f_string(next_value, prefix)
1305 and use_custom_breakpoints
1306 and not csplit.has_prefix
1308 # Then `csplit.break_idx` will be off by one after removing
1311 next_value = rest_value[:break_idx] + QUOTE
1313 if drop_pointless_f_prefix:
1314 next_value = self._normalize_f_string(next_value, prefix)
1316 # --- Construct `next_leaf`
1317 next_leaf = Leaf(token.STRING, next_value)
1318 insert_str_child(next_leaf)
1319 self._maybe_normalize_string_quotes(next_leaf)
1321 # --- Construct `next_line`
1322 next_line = line.clone()
1323 maybe_append_string_operators(next_line)
1324 next_line.append(next_leaf)
1325 string_line_results.append(Ok(next_line))
1327 rest_value = prefix + QUOTE + rest_value[break_idx:]
1328 first_string_line = False
1330 yield from string_line_results
1332 if drop_pointless_f_prefix:
1333 rest_value = self._normalize_f_string(rest_value, prefix)
1335 rest_leaf = Leaf(token.STRING, rest_value)
1336 insert_str_child(rest_leaf)
1338 # NOTE: I could not find a test case that verifies that the following
1339 # line is actually necessary, but it seems to be. Otherwise we risk
1340 # not normalizing the last substring, right?
1341 self._maybe_normalize_string_quotes(rest_leaf)
1343 last_line = line.clone()
1344 maybe_append_string_operators(last_line)
1346 # If there are any leaves to the right of the target string...
1347 if is_valid_index(string_idx + 1):
1348 # We use `temp_value` here to determine how long the last line
1349 # would be if we were to append all the leaves to the right of the
1350 # target string to the last string line.
1351 temp_value = rest_value
1352 for leaf in LL[string_idx + 1 :]:
1353 temp_value += str(leaf)
1354 if leaf.type == token.LPAR:
1357 # Try to fit them all on the same line with the last substring...
1359 len(temp_value) <= max_last_string()
1360 or LL[string_idx + 1].type == token.COMMA
1362 last_line.append(rest_leaf)
1363 append_leaves(last_line, line, LL[string_idx + 1 :])
1365 # Otherwise, place the last substring on one line and everything
1366 # else on a line below that...
1368 last_line.append(rest_leaf)
1371 non_string_line = line.clone()
1372 append_leaves(non_string_line, line, LL[string_idx + 1 :])
1373 yield Ok(non_string_line)
1374 # Else the target string was the last leaf...
1376 last_line.append(rest_leaf)
1377 last_line.comments = line.comments.copy()
1380 def _iter_nameescape_slices(self, string: str) -> Iterator[Tuple[Index, Index]]:
1383 All ranges of @string which, if @string were to be split there,
1384 would result in the splitting of an \\N{...} expression (which is NOT
1387 # True - the previous backslash was unescaped
1388 # False - the previous backslash was escaped *or* there was no backslash
1389 previous_was_unescaped_backslash = False
1390 it = iter(enumerate(string))
1393 previous_was_unescaped_backslash = not previous_was_unescaped_backslash
1395 if not previous_was_unescaped_backslash or c != "N":
1396 previous_was_unescaped_backslash = False
1398 previous_was_unescaped_backslash = False
1400 begin = idx - 1 # the position of backslash before \N{...}
1406 # malformed nameescape expression?
1407 # should have been detected by AST parsing earlier...
1408 raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
1411 def _iter_fexpr_slices(self, string: str) -> Iterator[Tuple[Index, Index]]:
1414 All ranges of @string which, if @string were to be split there,
1415 would result in the splitting of an f-expression (which is NOT
1418 if "f" not in get_string_prefix(string).lower():
1420 yield from iter_fexpr_spans(string)
1422 def _get_illegal_split_indices(self, string: str) -> Set[Index]:
1423 illegal_indices: Set[Index] = set()
1425 self._iter_fexpr_slices(string),
1426 self._iter_nameescape_slices(string),
1428 for it in iterators:
1429 for begin, end in it:
1430 illegal_indices.update(range(begin, end + 1))
1431 return illegal_indices
1433 def _get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]:
1435 This method contains the algorithm that StringSplitter uses to
1436 determine which character to split each string at.
1439 @string: The substring that we are attempting to split.
1440 @max_break_idx: The ideal break index. We will return this value if it
1441 meets all the necessary conditions. In the likely event that it
1442 doesn't we will try to find the closest index BELOW @max_break_idx
1443 that does. If that fails, we will expand our search by also
1444 considering all valid indices ABOVE @max_break_idx.
1447 * assert_is_leaf_string(@string)
1448 * 0 <= @max_break_idx < len(@string)
1451 break_idx, if an index is able to be found that meets all of the
1452 conditions listed in the 'Transformations' section of this classes'
1457 is_valid_index = is_valid_index_factory(string)
1459 assert is_valid_index(max_break_idx)
1460 assert_is_leaf_string(string)
1462 _illegal_split_indices = self._get_illegal_split_indices(string)
1464 def breaks_unsplittable_expression(i: Index) -> bool:
1467 True iff returning @i would result in the splitting of an
1468 unsplittable expression (which is NOT allowed).
1470 return i in _illegal_split_indices
1472 def passes_all_checks(i: Index) -> bool:
1475 True iff ALL of the conditions listed in the 'Transformations'
1476 section of this classes' docstring would be be met by returning @i.
1478 is_space = string[i] == " "
1480 is_not_escaped = True
1482 while is_valid_index(j) and string[j] == "\\":
1483 is_not_escaped = not is_not_escaped
1487 len(string[i:]) >= self.MIN_SUBSTR_SIZE
1488 and len(string[:i]) >= self.MIN_SUBSTR_SIZE
1494 and not breaks_unsplittable_expression(i)
1497 # First, we check all indices BELOW @max_break_idx.
1498 break_idx = max_break_idx
1499 while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx):
1502 if not passes_all_checks(break_idx):
1503 # If that fails, we check all indices ABOVE @max_break_idx.
1505 # If we are able to find a valid index here, the next line is going
1506 # to be longer than the specified line length, but it's probably
1507 # better than doing nothing at all.
1508 break_idx = max_break_idx + 1
1509 while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx):
1512 if not is_valid_index(break_idx) or not passes_all_checks(break_idx):
1517 def _maybe_normalize_string_quotes(self, leaf: Leaf) -> None:
1518 if self.normalize_strings:
1519 leaf.value = normalize_string_quotes(leaf.value)
1521 def _normalize_f_string(self, string: str, prefix: str) -> str:
1524 * assert_is_leaf_string(@string)
1527 * If @string is an f-string that contains no f-expressions, we
1528 return a string identical to @string except that the 'f' prefix
1529 has been stripped and all double braces (i.e. '{{' or '}}') have
1530 been normalized (i.e. turned into '{' or '}').
1532 * Otherwise, we return @string.
1534 assert_is_leaf_string(string)
1536 if "f" in prefix and not fstring_contains_expr(string):
1537 new_prefix = prefix.replace("f", "")
1539 temp = string[len(prefix) :]
1540 temp = re.sub(r"\{\{", "{", temp)
1541 temp = re.sub(r"\}\}", "}", temp)
1544 return f"{new_prefix}{new_string}"
1548 def _get_string_operator_leaves(self, leaves: Iterable[Leaf]) -> List[Leaf]:
1551 string_op_leaves = []
1553 while LL[i].type in self.STRING_OPERATORS + [token.NAME]:
1554 prefix_leaf = Leaf(LL[i].type, str(LL[i]).strip())
1555 string_op_leaves.append(prefix_leaf)
1557 return string_op_leaves
1560 class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin):
1562 StringTransformer that splits non-"atom" strings (i.e. strings that do not
1563 exist on lines by themselves).
1566 All of the requirements listed in BaseStringSplitter's docstring in
1567 addition to the requirements listed below:
1569 * The line is a return/yield statement, which returns/yields a string.
1571 * The line is part of a ternary expression (e.g. `x = y if cond else
1572 z`) such that the line starts with `else <string>`, where <string> is
1575 * The line is an assert statement, which ends with a string.
1577 * The line is an assignment statement (e.g. `x = <string>` or `x +=
1578 <string>`) such that the variable is being assigned the value of some
1581 * The line is a dictionary key assignment where some valid key is being
1582 assigned the value of some string.
1585 The chosen string is wrapped in parentheses and then split at the LPAR.
1587 We then have one line which ends with an LPAR and another line that
1588 starts with the chosen string. The latter line is then split again at
1589 the RPAR. This results in the RPAR (and possibly a trailing comma)
1590 being placed on its own line.
1592 NOTE: If any leaves exist to the right of the chosen string (except
1593 for a trailing comma, which would be placed after the RPAR), those
1594 leaves are placed inside the parentheses. In effect, the chosen
1595 string is not necessarily being "wrapped" by parentheses. We can,
1596 however, count on the LPAR being placed directly before the chosen
1599 In other words, StringParenWrapper creates "atom" strings. These
1600 can then be split again by StringSplitter, if necessary.
1603 In the event that a string line split by StringParenWrapper is
1604 changed such that it no longer needs to be given its own line,
1605 StringParenWrapper relies on StringParenStripper to clean up the
1606 parentheses it created.
1609 def do_splitter_match(self, line: Line) -> TMatchResult:
1612 if line.leaves[-1].type in OPENING_BRACKETS:
1614 "Cannot wrap parens around a line that ends in an opening bracket."
1618 self._return_match(LL)
1619 or self._else_match(LL)
1620 or self._assert_match(LL)
1621 or self._assign_match(LL)
1622 or self._dict_match(LL)
1625 if string_idx is not None:
1626 string_value = line.leaves[string_idx].value
1627 # If the string has no spaces...
1628 if " " not in string_value:
1629 # And will still violate the line length limit when split...
1630 max_string_length = self.line_length - ((line.depth + 1) * 4)
1631 if len(string_value) > max_string_length:
1632 # And has no associated custom splits...
1633 if not self.has_custom_splits(string_value):
1634 # Then we should NOT put this string on its own line.
1636 "We do not wrap long strings in parentheses when the"
1637 " resultant line would still be over the specified line"
1638 " length and can't be split further by StringSplitter."
1640 return Ok(string_idx)
1642 return TErr("This line does not contain any non-atomic strings.")
1645 def _return_match(LL: List[Leaf]) -> Optional[int]:
1648 string_idx such that @LL[string_idx] is equal to our target (i.e.
1649 matched) string, if this line matches the return/yield statement
1650 requirements listed in the 'Requirements' section of this classes'
1655 # If this line is apart of a return/yield statement and the first leaf
1656 # contains either the "return" or "yield" keywords...
1657 if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[
1659 ].value in ["return", "yield"]:
1660 is_valid_index = is_valid_index_factory(LL)
1662 idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1663 # The next visible leaf MUST contain a string...
1664 if is_valid_index(idx) and LL[idx].type == token.STRING:
1670 def _else_match(LL: List[Leaf]) -> Optional[int]:
1673 string_idx such that @LL[string_idx] is equal to our target (i.e.
1674 matched) string, if this line matches the ternary expression
1675 requirements listed in the 'Requirements' section of this classes'
1680 # If this line is apart of a ternary expression and the first leaf
1681 # contains the "else" keyword...
1683 parent_type(LL[0]) == syms.test
1684 and LL[0].type == token.NAME
1685 and LL[0].value == "else"
1687 is_valid_index = is_valid_index_factory(LL)
1689 idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1690 # The next visible leaf MUST contain a string...
1691 if is_valid_index(idx) and LL[idx].type == token.STRING:
1697 def _assert_match(LL: List[Leaf]) -> Optional[int]:
1700 string_idx such that @LL[string_idx] is equal to our target (i.e.
1701 matched) string, if this line matches the assert statement
1702 requirements listed in the 'Requirements' section of this classes'
1707 # If this line is apart of an assert statement and the first leaf
1708 # contains the "assert" keyword...
1709 if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert":
1710 is_valid_index = is_valid_index_factory(LL)
1712 for i, leaf in enumerate(LL):
1713 # We MUST find a comma...
1714 if leaf.type == token.COMMA:
1715 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1717 # That comma MUST be followed by a string...
1718 if is_valid_index(idx) and LL[idx].type == token.STRING:
1721 # Skip the string trailer, if one exists.
1722 string_parser = StringParser()
1723 idx = string_parser.parse(LL, string_idx)
1725 # But no more leaves are allowed...
1726 if not is_valid_index(idx):
1732 def _assign_match(LL: List[Leaf]) -> Optional[int]:
1735 string_idx such that @LL[string_idx] is equal to our target (i.e.
1736 matched) string, if this line matches the assignment statement
1737 requirements listed in the 'Requirements' section of this classes'
1742 # If this line is apart of an expression statement or is a function
1743 # argument AND the first leaf contains a variable name...
1745 parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power]
1746 and LL[0].type == token.NAME
1748 is_valid_index = is_valid_index_factory(LL)
1750 for i, leaf in enumerate(LL):
1751 # We MUST find either an '=' or '+=' symbol...
1752 if leaf.type in [token.EQUAL, token.PLUSEQUAL]:
1753 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1755 # That symbol MUST be followed by a string...
1756 if is_valid_index(idx) and LL[idx].type == token.STRING:
1759 # Skip the string trailer, if one exists.
1760 string_parser = StringParser()
1761 idx = string_parser.parse(LL, string_idx)
1763 # The next leaf MAY be a comma iff this line is apart
1764 # of a function argument...
1766 parent_type(LL[0]) == syms.argument
1767 and is_valid_index(idx)
1768 and LL[idx].type == token.COMMA
1772 # But no more leaves are allowed...
1773 if not is_valid_index(idx):
1779 def _dict_match(LL: List[Leaf]) -> Optional[int]:
1782 string_idx such that @LL[string_idx] is equal to our target (i.e.
1783 matched) string, if this line matches the dictionary key assignment
1784 statement requirements listed in the 'Requirements' section of this
1789 # If this line is apart of a dictionary key assignment...
1790 if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]:
1791 is_valid_index = is_valid_index_factory(LL)
1793 for i, leaf in enumerate(LL):
1794 # We MUST find a colon...
1795 if leaf.type == token.COLON:
1796 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1798 # That colon MUST be followed by a string...
1799 if is_valid_index(idx) and LL[idx].type == token.STRING:
1802 # Skip the string trailer, if one exists.
1803 string_parser = StringParser()
1804 idx = string_parser.parse(LL, string_idx)
1806 # That string MAY be followed by a comma...
1807 if is_valid_index(idx) and LL[idx].type == token.COMMA:
1810 # But no more leaves are allowed...
1811 if not is_valid_index(idx):
1816 def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
1819 is_valid_index = is_valid_index_factory(LL)
1820 insert_str_child = insert_str_child_factory(LL[string_idx])
1823 ends_with_comma = False
1824 if LL[comma_idx].type == token.COMMA:
1825 ends_with_comma = True
1827 leaves_to_steal_comments_from = [LL[string_idx]]
1829 leaves_to_steal_comments_from.append(LL[comma_idx])
1832 first_line = line.clone()
1833 left_leaves = LL[:string_idx]
1835 # We have to remember to account for (possibly invisible) LPAR and RPAR
1836 # leaves that already wrapped the target string. If these leaves do
1837 # exist, we will replace them with our own LPAR and RPAR leaves.
1838 old_parens_exist = False
1839 if left_leaves and left_leaves[-1].type == token.LPAR:
1840 old_parens_exist = True
1841 leaves_to_steal_comments_from.append(left_leaves[-1])
1844 append_leaves(first_line, line, left_leaves)
1846 lpar_leaf = Leaf(token.LPAR, "(")
1847 if old_parens_exist:
1848 replace_child(LL[string_idx - 1], lpar_leaf)
1850 insert_str_child(lpar_leaf)
1851 first_line.append(lpar_leaf)
1853 # We throw inline comments that were originally to the right of the
1854 # target string to the top line. They will now be shown to the right of
1856 for leaf in leaves_to_steal_comments_from:
1857 for comment_leaf in line.comments_after(leaf):
1858 first_line.append(comment_leaf, preformatted=True)
1860 yield Ok(first_line)
1862 # --- Middle (String) Line
1863 # We only need to yield one (possibly too long) string line, since the
1864 # `StringSplitter` will break it down further if necessary.
1865 string_value = LL[string_idx].value
1868 depth=line.depth + 1,
1869 inside_brackets=True,
1870 should_split_rhs=line.should_split_rhs,
1871 magic_trailing_comma=line.magic_trailing_comma,
1873 string_leaf = Leaf(token.STRING, string_value)
1874 insert_str_child(string_leaf)
1875 string_line.append(string_leaf)
1877 old_rpar_leaf = None
1878 if is_valid_index(string_idx + 1):
1879 right_leaves = LL[string_idx + 1 :]
1883 if old_parens_exist:
1884 assert right_leaves and right_leaves[-1].type == token.RPAR, (
1885 "Apparently, old parentheses do NOT exist?!"
1886 f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
1888 old_rpar_leaf = right_leaves.pop()
1890 append_leaves(string_line, line, right_leaves)
1892 yield Ok(string_line)
1895 last_line = line.clone()
1896 last_line.bracket_tracker = first_line.bracket_tracker
1898 new_rpar_leaf = Leaf(token.RPAR, ")")
1899 if old_rpar_leaf is not None:
1900 replace_child(old_rpar_leaf, new_rpar_leaf)
1902 insert_str_child(new_rpar_leaf)
1903 last_line.append(new_rpar_leaf)
1905 # If the target string ended with a comma, we place this comma to the
1906 # right of the RPAR on the last line.
1908 comma_leaf = Leaf(token.COMMA, ",")
1909 replace_child(LL[comma_idx], comma_leaf)
1910 last_line.append(comma_leaf)
1917 A state machine that aids in parsing a string's "trailer", which can be
1918 either non-existent, an old-style formatting sequence (e.g. `% varX` or `%
1919 (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX,
1922 NOTE: A new StringParser object MUST be instantiated for each string
1923 trailer we need to parse.
1926 We shall assume that `line` equals the `Line` object that corresponds
1927 to the following line of python code:
1929 x = "Some {}.".format("String") + some_other_string
1932 Furthermore, we will assume that `string_idx` is some index such that:
1934 assert line.leaves[string_idx].value == "Some {}."
1937 The following code snippet then holds:
1939 string_parser = StringParser()
1940 idx = string_parser.parse(line.leaves, string_idx)
1941 assert line.leaves[idx].type == token.PLUS
1945 DEFAULT_TOKEN: Final = 20210605
1947 # String Parser States
1952 SINGLE_FMT_ARG: Final = 5
1957 # Lookup Table for Next State
1958 _goto: Final[Dict[Tuple[ParserState, NodeType], ParserState]] = {
1959 # A string trailer may start with '.' OR '%'.
1960 (START, token.DOT): DOT,
1961 (START, token.PERCENT): PERCENT,
1962 (START, DEFAULT_TOKEN): DONE,
1963 # A '.' MUST be followed by an attribute or method name.
1964 (DOT, token.NAME): NAME,
1965 # A method name MUST be followed by an '(', whereas an attribute name
1966 # is the last symbol in the string trailer.
1967 (NAME, token.LPAR): LPAR,
1968 (NAME, DEFAULT_TOKEN): DONE,
1969 # A '%' symbol can be followed by an '(' or a single argument (e.g. a
1970 # string or variable name).
1971 (PERCENT, token.LPAR): LPAR,
1972 (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG,
1973 # If a '%' symbol is followed by a single argument, that argument is
1974 # the last leaf in the string trailer.
1975 (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE,
1976 # If present, a ')' symbol is the last symbol in a string trailer.
1977 # (NOTE: LPARS and nested RPARS are not included in this lookup table,
1978 # since they are treated as a special case by the parsing logic in this
1979 # classes' implementation.)
1980 (RPAR, DEFAULT_TOKEN): DONE,
1983 def __init__(self) -> None:
1984 self._state = self.START
1985 self._unmatched_lpars = 0
1987 def parse(self, leaves: List[Leaf], string_idx: int) -> int:
1990 * @leaves[@string_idx].type == token.STRING
1993 The index directly after the last leaf which is apart of the string
1994 trailer, if a "trailer" exists.
1996 @string_idx + 1, if no string "trailer" exists.
1998 assert leaves[string_idx].type == token.STRING
2000 idx = string_idx + 1
2001 while idx < len(leaves) and self._next_state(leaves[idx]):
2005 def _next_state(self, leaf: Leaf) -> bool:
2008 * On the first call to this function, @leaf MUST be the leaf that
2009 was directly after the string leaf in question (e.g. if our target
2010 string is `line.leaves[i]` then the first call to this method must
2011 be `line.leaves[i + 1]`).
2012 * On the next call to this function, the leaf parameter passed in
2013 MUST be the leaf directly following @leaf.
2016 True iff @leaf is apart of the string's trailer.
2018 # We ignore empty LPAR or RPAR leaves.
2019 if is_empty_par(leaf):
2022 next_token = leaf.type
2023 if next_token == token.LPAR:
2024 self._unmatched_lpars += 1
2026 current_state = self._state
2028 # The LPAR parser state is a special case. We will return True until we
2029 # find the matching RPAR token.
2030 if current_state == self.LPAR:
2031 if next_token == token.RPAR:
2032 self._unmatched_lpars -= 1
2033 if self._unmatched_lpars == 0:
2034 self._state = self.RPAR
2035 # Otherwise, we use a lookup table to determine the next state.
2037 # If the lookup table matches the current state to the next
2038 # token, we use the lookup table.
2039 if (current_state, next_token) in self._goto:
2040 self._state = self._goto[current_state, next_token]
2042 # Otherwise, we check if a the current state was assigned a
2044 if (current_state, self.DEFAULT_TOKEN) in self._goto:
2045 self._state = self._goto[current_state, self.DEFAULT_TOKEN]
2046 # If no default has been assigned, then this parser has a logic
2049 raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
2051 if self._state == self.DONE:
2057 def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]:
2059 Factory for a convenience function that is used to orphan @string_leaf
2060 and then insert multiple new leaves into the same part of the node
2061 structure that @string_leaf had originally occupied.
2064 Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N =
2065 string_leaf.parent`. Assume the node `N` has the following
2072 Leaf(STRING, '"foo"'),
2076 We then run the code snippet shown below.
2078 insert_str_child = insert_str_child_factory(string_leaf)
2080 lpar = Leaf(token.LPAR, '(')
2081 insert_str_child(lpar)
2083 bar = Leaf(token.STRING, '"bar"')
2084 insert_str_child(bar)
2086 rpar = Leaf(token.RPAR, ')')
2087 insert_str_child(rpar)
2090 After which point, it follows that `string_leaf.parent is None` and
2091 the node `N` now has the following structure:
2098 Leaf(STRING, '"bar"'),
2103 string_parent = string_leaf.parent
2104 string_child_idx = string_leaf.remove()
2106 def insert_str_child(child: LN) -> None:
2107 nonlocal string_child_idx
2109 assert string_parent is not None
2110 assert string_child_idx is not None
2112 string_parent.insert_child(string_child_idx, child)
2113 string_child_idx += 1
2115 return insert_str_child
2118 def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]:
2124 is_valid_index = is_valid_index_factory(my_list)
2126 assert is_valid_index(0)
2127 assert is_valid_index(2)
2129 assert not is_valid_index(3)
2130 assert not is_valid_index(-1)
2134 def is_valid_index(idx: int) -> bool:
2137 True iff @idx is positive AND seq[@idx] does NOT raise an
2140 return 0 <= idx < len(seq)
2142 return is_valid_index