Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/black/trans.py: 12%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1062 statements  

1""" 

2String transformers that can split and merge strings. 

3""" 

4 

5import re 

6from abc import ABC, abstractmethod 

7from collections import defaultdict 

8from collections.abc import Callable, Collection, Iterable, Iterator, Sequence 

9from dataclasses import dataclass 

10from typing import Any, ClassVar, Final, Literal, TypeVar, Union 

11 

12from mypy_extensions import trait 

13 

14from black.comments import contains_pragma_comment 

15from black.lines import Line, append_leaves, is_line_short_enough, line_to_string 

16from black.mode import Feature, Mode 

17from black.nodes import ( 

18 ASSIGNMENTS, 

19 CLOSING_BRACKETS, 

20 COMPARATORS, 

21 MATH_OPERATORS, 

22 OPENING_BRACKETS, 

23 STANDALONE_COMMENT, 

24 is_empty_lpar, 

25 is_empty_par, 

26 is_empty_rpar, 

27 is_part_of_annotation, 

28 parent_type, 

29 replace_child, 

30 syms, 

31) 

32from black.rusty import Err, Ok, Result 

33from black.strings import ( 

34 assert_is_leaf_string, 

35 count_chars_in_width, 

36 get_string_prefix, 

37 has_triple_quotes, 

38 normalize_string_quotes, 

39 str_width, 

40) 

41from blib2to3.pgen2 import token 

42from blib2to3.pytree import Leaf, Node 

43 

44 

45class CannotTransform(Exception): 

46 """Base class for errors raised by Transformers.""" 

47 

48 

49# types 

50T = TypeVar("T") 

51LN = Union[Leaf, Node] 

52Transformer = Callable[[Line, Collection[Feature], Mode], Iterator[Line]] 

53Index = int 

54NodeType = int 

55ParserState = int 

56StringID = int 

57TResult = Result[T, CannotTransform] # (T)ransform Result 

58TMatchResult = TResult[list[Index]] 

59 

60SPLIT_SAFE_CHARS = frozenset(["\u3001", "\u3002", "\uff0c"]) # East Asian stops 

61 

62 

63def TErr(err_msg: str) -> Err[CannotTransform]: 

64 """(T)ransform Err 

65 

66 Convenience function used when working with the TResult type. 

67 """ 

68 cant_transform = CannotTransform(err_msg) 

69 return Err(cant_transform) 

70 

71 

72# Remove when `simplify_power_operator_hugging` becomes stable. 

73def hug_power_op( 

74 line: Line, features: Collection[Feature], mode: Mode 

75) -> Iterator[Line]: 

76 """A transformer which normalizes spacing around power operators.""" 

77 

78 # Performance optimization to avoid unnecessary Leaf clones and other ops. 

79 for leaf in line.leaves: 

80 if leaf.type == token.DOUBLESTAR: 

81 break 

82 else: 

83 raise CannotTransform("No doublestar token was found in the line.") 

84 

85 def is_simple_lookup(index: int, kind: Literal[1, -1]) -> bool: 

86 # Brackets and parentheses indicate calls, subscripts, etc. ... 

87 # basically stuff that doesn't count as "simple". Only a NAME lookup 

88 # or dotted lookup (eg. NAME.NAME) is OK. 

89 if kind == -1: 

90 return handle_is_simple_look_up_prev(line, index, {token.RPAR, token.RSQB}) 

91 else: 

92 return handle_is_simple_lookup_forward( 

93 line, index, {token.LPAR, token.LSQB} 

94 ) 

95 

96 def is_simple_operand(index: int, kind: Literal[1, -1]) -> bool: 

97 # An operand is considered "simple" if's a NAME, a numeric CONSTANT, a simple 

98 # lookup (see above), with or without a preceding unary operator. 

99 start = line.leaves[index] 

100 if start.type in {token.NAME, token.NUMBER}: 

101 return is_simple_lookup(index, kind) 

102 

103 if start.type in {token.PLUS, token.MINUS, token.TILDE}: 

104 if line.leaves[index + 1].type in {token.NAME, token.NUMBER}: 

105 # kind is always one as bases with a preceding unary op will be checked 

106 # for simplicity starting from the next token (so it'll hit the check 

107 # above). 

108 return is_simple_lookup(index + 1, kind=1) 

109 

110 return False 

111 

112 new_line = line.clone() 

113 should_hug = False 

114 for idx, leaf in enumerate(line.leaves): 

115 hug_this_leaf = should_hug 

116 should_hug = ( 

117 (0 < idx < len(line.leaves) - 1) 

118 and leaf.type == token.DOUBLESTAR 

119 and is_simple_operand(idx - 1, kind=-1) 

120 and line.leaves[idx - 1].value != "lambda" 

121 and is_simple_operand(idx + 1, kind=1) 

122 ) 

123 

124 if hug_this_leaf or should_hug: 

125 new_leaf = leaf.clone() 

126 new_leaf.prefix = "" 

127 else: 

128 # Only the operands around a hugged `**` need a copy. Reuse the leaf 

129 # otherwise: a clone has no parent, and the trailing-comma guards in 

130 # `Line.append` read the tree to tell a syntactically required comma 

131 # (a one-tuple, or a one-element subscript like `a[x,]`) from a magic 

132 # one. Without a parent they can't, so the comma was being dropped 

133 # under --skip-magic-trailing-comma. 

134 new_leaf = leaf 

135 

136 # We have to be careful to make a new line properly: 

137 # - bracket related metadata must be maintained (handled by Line.append) 

138 # - comments need to copied over, updating the leaf IDs they're attached to 

139 new_line.append(new_leaf, preformatted=True) 

140 for comment_leaf in line.comments_after(leaf): 

141 new_line.append(comment_leaf, preformatted=True) 

142 

143 yield new_line 

144 

145 

146# Remove when `simplify_power_operator_hugging` becomes stable. 

147def handle_is_simple_look_up_prev(line: Line, index: int, disallowed: set[int]) -> bool: 

148 """ 

149 Handling the determination of is_simple_lookup for the lines prior to the doublestar 

150 token. This is required because of the need to isolate the chained expression 

151 to determine the bracket or parenthesis belong to the single expression. 

152 """ 

153 contains_disallowed = False 

154 chain = [] 

155 

156 while 0 <= index < len(line.leaves): 

157 current = line.leaves[index] 

158 chain.append(current) 

159 if not contains_disallowed and current.type in disallowed: 

160 contains_disallowed = True 

161 if not is_expression_chained(chain): 

162 return not contains_disallowed 

163 

164 index -= 1 

165 

166 return True 

167 

168 

169# Remove when `simplify_power_operator_hugging` becomes stable. 

170def handle_is_simple_lookup_forward( 

171 line: Line, index: int, disallowed: set[int] 

172) -> bool: 

173 """ 

174 Handling decision is_simple_lookup for the lines behind the doublestar token. 

175 This function is simplified to keep consistent with the prior logic and the forward 

176 case are more straightforward and do not need to care about chained expressions. 

177 """ 

178 while 0 <= index < len(line.leaves): 

179 current = line.leaves[index] 

180 if current.type in disallowed: 

181 return False 

182 if current.type not in {token.NAME, token.DOT} or ( 

183 current.type == token.NAME and current.value == "for" 

184 ): 

185 # If the current token isn't disallowed, we'll assume this is simple as 

186 # only the disallowed tokens are semantically attached to this lookup 

187 # expression we're checking. Also, stop early if we hit the 'for' bit 

188 # of a comprehension. 

189 return True 

190 

191 index += 1 

192 

193 return True 

194 

195 

196# Remove when `simplify_power_operator_hugging` becomes stable. 

197def is_expression_chained(chained_leaves: list[Leaf]) -> bool: 

198 """ 

199 Function to determine if the variable is a chained call. 

200 (e.g., foo.lookup, foo().lookup, (foo.lookup())) will be recognized as chained call) 

201 """ 

202 if len(chained_leaves) < 2: 

203 return True 

204 

205 current_leaf = chained_leaves[-1] 

206 past_leaf = chained_leaves[-2] 

207 

208 if past_leaf.type == token.NAME: 

209 return current_leaf.type in {token.DOT} 

210 elif past_leaf.type in {token.RPAR, token.RSQB}: 

211 return current_leaf.type in {token.RSQB, token.RPAR} 

212 elif past_leaf.type in {token.LPAR, token.LSQB}: 

213 return current_leaf.type in {token.NAME, token.LPAR, token.LSQB} 

214 else: 

215 return False 

216 

217 

218class StringTransformer(ABC): 

219 """ 

220 An implementation of the Transformer protocol that relies on its 

221 subclasses overriding the template methods `do_match(...)` and 

222 `do_transform(...)`. 

223 

224 This Transformer works exclusively on strings (for example, by merging 

225 or splitting them). 

226 

227 The following sections can be found among the docstrings of each concrete 

228 StringTransformer subclass. 

229 

230 Requirements: 

231 Which requirements must be met of the given Line for this 

232 StringTransformer to be applied? 

233 

234 Transformations: 

235 If the given Line meets all of the above requirements, which string 

236 transformations can you expect to be applied to it by this 

237 StringTransformer? 

238 

239 Collaborations: 

240 What contractual agreements does this StringTransformer have with other 

241 StringTransformers? Such collaborations should be eliminated/minimized 

242 as much as possible. 

243 """ 

244 

245 __name__: Final = "StringTransformer" 

246 

247 # Ideally this would be a dataclass, but unfortunately mypyc breaks when used with 

248 # `abc.ABC`. 

249 def __init__(self, line_length: int, normalize_strings: bool) -> None: 

250 self.line_length = line_length 

251 self.normalize_strings = normalize_strings 

252 

253 @abstractmethod 

254 def do_match(self, line: Line) -> TMatchResult: 

255 """ 

256 Returns: 

257 * Ok(string_indices) such that for each index, `line.leaves[index]` 

258 is our target string if a match was able to be made. For 

259 transformers that don't result in more lines (e.g. StringMerger, 

260 StringParenStripper), multiple matches and transforms are done at 

261 once to reduce the complexity. 

262 OR 

263 * Err(CannotTransform), if no match could be made. 

264 """ 

265 

266 @abstractmethod 

267 def do_transform( 

268 self, line: Line, string_indices: list[int] 

269 ) -> Iterator[TResult[Line]]: 

270 """ 

271 Yields: 

272 * Ok(new_line) where new_line is the new transformed line. 

273 OR 

274 * Err(CannotTransform) if the transformation failed for some reason. The 

275 `do_match(...)` template method should usually be used to reject 

276 the form of the given Line, but in some cases it is difficult to 

277 know whether or not a Line meets the StringTransformer's 

278 requirements until the transformation is already midway. 

279 

280 Side Effects: 

281 This method should NOT mutate @line directly, but it MAY mutate the 

282 Line's underlying Node structure. (WARNING: If the underlying Node 

283 structure IS altered, then this method should NOT be allowed to 

284 yield an CannotTransform after that point.) 

285 """ 

286 

287 def __call__( 

288 self, line: Line, _features: Collection[Feature], _mode: Mode 

289 ) -> Iterator[Line]: 

290 """ 

291 StringTransformer instances have a call signature that mirrors that of 

292 the Transformer type. 

293 

294 Raises: 

295 CannotTransform(...) if the concrete StringTransformer class is unable 

296 to transform @line. 

297 """ 

298 # Optimization to avoid calling `self.do_match(...)` when the line does 

299 # not contain any string. 

300 if not any(leaf.type == token.STRING for leaf in line.leaves): 

301 raise CannotTransform("There are no strings in this line.") 

302 

303 match_result = self.do_match(line) 

304 

305 if isinstance(match_result, Err): 

306 cant_transform = match_result.err() 

307 raise CannotTransform( 

308 f"The string transformer {self.__class__.__name__} does not recognize" 

309 " this line as one that it can transform." 

310 ) from cant_transform 

311 

312 string_indices = match_result.ok() 

313 

314 for line_result in self.do_transform(line, string_indices): 

315 if isinstance(line_result, Err): 

316 cant_transform = line_result.err() 

317 raise CannotTransform( 

318 "StringTransformer failed while attempting to transform string." 

319 ) from cant_transform 

320 line = line_result.ok() 

321 yield line 

322 

323 

324@dataclass 

325class CustomSplit: 

326 """A custom (i.e. manual) string split. 

327 

328 A single CustomSplit instance represents a single substring. 

329 

330 Examples: 

331 Consider the following string: 

332 ``` 

333 "Hi there friend." 

334 " This is a custom" 

335 f" string {split}." 

336 ``` 

337 

338 This string will correspond to the following three CustomSplit instances: 

339 ``` 

340 CustomSplit(False, 16) 

341 CustomSplit(False, 17) 

342 CustomSplit(True, 16) 

343 ``` 

344 """ 

345 

346 has_prefix: bool 

347 break_idx: int 

348 

349 

350CustomSplitMapKey = tuple[StringID, str] 

351 

352 

353@trait 

354class CustomSplitMapMixin: 

355 """ 

356 This mixin class is used to map merged strings to a sequence of 

357 CustomSplits, which will then be used to re-split the strings iff none of 

358 the resultant substrings go over the configured max line length. 

359 """ 

360 

361 _CUSTOM_SPLIT_MAP: ClassVar[dict[CustomSplitMapKey, tuple[CustomSplit, ...]]] = ( 

362 defaultdict(tuple) 

363 ) 

364 

365 @staticmethod 

366 def _get_key(string: str) -> CustomSplitMapKey: 

367 """ 

368 Returns: 

369 A unique identifier that is used internally to map @string to a 

370 group of custom splits. 

371 """ 

372 return (id(string), string) 

373 

374 def add_custom_splits( 

375 self, string: str, custom_splits: Iterable[CustomSplit] 

376 ) -> None: 

377 """Custom Split Map Setter Method 

378 

379 Side Effects: 

380 Adds a mapping from @string to the custom splits @custom_splits. 

381 """ 

382 key = self._get_key(string) 

383 self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits) 

384 

385 def pop_custom_splits(self, string: str) -> list[CustomSplit]: 

386 """Custom Split Map Getter Method 

387 

388 Returns: 

389 * A list of the custom splits that are mapped to @string, if any 

390 exist. 

391 OR 

392 * [], otherwise. 

393 

394 Side Effects: 

395 Deletes the mapping between @string and its associated custom 

396 splits (which are returned to the caller). 

397 """ 

398 key = self._get_key(string) 

399 

400 custom_splits = self._CUSTOM_SPLIT_MAP[key] 

401 del self._CUSTOM_SPLIT_MAP[key] 

402 

403 return list(custom_splits) 

404 

405 def has_custom_splits(self, string: str) -> bool: 

406 """ 

407 Returns: 

408 True iff @string is associated with a set of custom splits. 

409 """ 

410 key = self._get_key(string) 

411 return key in self._CUSTOM_SPLIT_MAP 

412 

413 

414class StringMerger(StringTransformer, CustomSplitMapMixin): 

415 """StringTransformer that merges strings together. 

416 

417 Requirements: 

418 (A) The line contains adjacent strings such that ALL of the validation checks 

419 listed in StringMerger._validate_msg(...)'s docstring pass. 

420 OR 

421 (B) The line contains a string which uses line continuation backslashes. 

422 

423 Transformations: 

424 Depending on which of the two requirements above where met, either: 

425 

426 (A) The string group associated with the target string is merged. 

427 OR 

428 (B) All line-continuation backslashes are removed from the target string. 

429 

430 Collaborations: 

431 StringMerger provides custom split information to StringSplitter. 

432 """ 

433 

434 def do_match(self, line: Line) -> TMatchResult: 

435 LL = line.leaves 

436 

437 is_valid_index = is_valid_index_factory(LL) 

438 

439 string_indices = [] 

440 idx = 0 

441 while is_valid_index(idx): 

442 leaf = LL[idx] 

443 if ( 

444 leaf.type == token.STRING 

445 and is_valid_index(idx + 1) 

446 and LL[idx + 1].type == token.STRING 

447 ): 

448 # Let's check if the string group contains an inline comment 

449 # If we have a comment inline, we don't merge the strings 

450 contains_comment = False 

451 i = idx 

452 while is_valid_index(i): 

453 if LL[i].type != token.STRING: 

454 break 

455 if line.comments_after(LL[i]): 

456 contains_comment = True 

457 break 

458 i += 1 

459 

460 if not contains_comment and not is_part_of_annotation(leaf): 

461 string_indices.append(idx) 

462 

463 # Advance to the next non-STRING leaf. 

464 idx += 2 

465 while is_valid_index(idx) and LL[idx].type == token.STRING: 

466 idx += 1 

467 

468 elif leaf.type == token.STRING and "\\\n" in leaf.value: 

469 string_indices.append(idx) 

470 # Advance to the next non-STRING leaf. 

471 idx += 1 

472 while is_valid_index(idx) and LL[idx].type == token.STRING: 

473 idx += 1 

474 

475 else: 

476 idx += 1 

477 

478 if string_indices: 

479 return Ok(string_indices) 

480 else: 

481 return TErr("This line has no strings that need merging.") 

482 

483 def do_transform( 

484 self, line: Line, string_indices: list[int] 

485 ) -> Iterator[TResult[Line]]: 

486 new_line = line 

487 

488 rblc_result = self._remove_backslash_line_continuation_chars( 

489 new_line, string_indices 

490 ) 

491 if isinstance(rblc_result, Ok): 

492 new_line = rblc_result.ok() 

493 

494 msg_result = self._merge_string_group(new_line, string_indices) 

495 if isinstance(msg_result, Ok): 

496 new_line = msg_result.ok() 

497 

498 if isinstance(rblc_result, Err) and isinstance(msg_result, Err): 

499 msg_cant_transform = msg_result.err() 

500 rblc_cant_transform = rblc_result.err() 

501 cant_transform = CannotTransform( 

502 "StringMerger failed to merge any strings in this line." 

503 ) 

504 

505 # Chain the errors together using `__cause__`. 

506 msg_cant_transform.__cause__ = rblc_cant_transform 

507 cant_transform.__cause__ = msg_cant_transform 

508 

509 yield Err(cant_transform) 

510 else: 

511 yield Ok(new_line) 

512 

513 @staticmethod 

514 def _remove_backslash_line_continuation_chars( 

515 line: Line, string_indices: list[int] 

516 ) -> TResult[Line]: 

517 """ 

518 Merge strings that were split across multiple lines using 

519 line-continuation backslashes. 

520 

521 Returns: 

522 Ok(new_line), if @line contains backslash line-continuation 

523 characters. 

524 OR 

525 Err(CannotTransform), otherwise. 

526 """ 

527 LL = line.leaves 

528 

529 indices_to_transform = [] 

530 for string_idx in string_indices: 

531 string_leaf = LL[string_idx] 

532 if ( 

533 string_leaf.type == token.STRING 

534 and "\\\n" in string_leaf.value 

535 and not has_triple_quotes(string_leaf.value) 

536 # In a raw string, a backslash-newline is part of the value. 

537 and "r" not in get_string_prefix(string_leaf.value).lower() 

538 ): 

539 indices_to_transform.append(string_idx) 

540 

541 if not indices_to_transform: 

542 return TErr( 

543 "Found no string leaves that contain backslash line continuation" 

544 " characters." 

545 ) 

546 

547 new_line = line.clone() 

548 append_leaves(new_line, line, LL) 

549 

550 for string_idx in indices_to_transform: 

551 new_string_leaf = new_line.leaves[string_idx] 

552 new_string_leaf.value = new_string_leaf.value.replace("\\\n", "") 

553 

554 return Ok(new_line) 

555 

556 def _merge_string_group( 

557 self, line: Line, string_indices: list[int] 

558 ) -> TResult[Line]: 

559 """ 

560 Merges string groups (i.e. set of adjacent strings). 

561 

562 Each index from `string_indices` designates one string group's first 

563 leaf in `line.leaves`. 

564 

565 Returns: 

566 Ok(new_line), if ALL of the validation checks found in 

567 _validate_msg(...) pass. 

568 OR 

569 Err(CannotTransform), otherwise. 

570 """ 

571 LL = line.leaves 

572 

573 is_valid_index = is_valid_index_factory(LL) 

574 

575 # A dict of {string_idx: tuple[num_of_strings, string_leaf]}. 

576 merged_string_idx_dict: dict[int, tuple[int, Leaf]] = {} 

577 for string_idx in string_indices: 

578 vresult = self._validate_msg(line, string_idx) 

579 if isinstance(vresult, Err): 

580 continue 

581 merged_string_idx_dict[string_idx] = self._merge_one_string_group( 

582 LL, string_idx, is_valid_index 

583 ) 

584 

585 if not merged_string_idx_dict: 

586 return TErr("No string group is merged") 

587 

588 # Build the final line ('new_line') that this method will later return. 

589 new_line = line.clone() 

590 previous_merged_string_idx = -1 

591 previous_merged_num_of_strings = -1 

592 # Leaves outside any merged string group are copied in runs rather than 

593 # one at a time. append_leaves resumes the search for each leaf's 

594 # position from where the previous sibling of the same parent was found, 

595 # so copying a run of leaves that share a parent (the operand tuple of 

596 # "%s ..." % (a, b, c, ...)) stays linear; a fresh call per leaf restarts 

597 # that search from the front every time and is quadratic in the operands. 

598 pending: list[Leaf] = [] 

599 for i, leaf in enumerate(LL): 

600 if i in merged_string_idx_dict: 

601 if pending: 

602 append_leaves(new_line, line, pending) 

603 pending = [] 

604 previous_merged_string_idx = i 

605 previous_merged_num_of_strings, string_leaf = merged_string_idx_dict[i] 

606 new_line.append(string_leaf) 

607 

608 if ( 

609 previous_merged_string_idx 

610 <= i 

611 < previous_merged_string_idx + previous_merged_num_of_strings 

612 ): 

613 for comment_leaf in line.comments_after(leaf): 

614 new_line.append(comment_leaf, preformatted=True) 

615 continue 

616 

617 pending.append(leaf) 

618 

619 if pending: 

620 append_leaves(new_line, line, pending) 

621 

622 return Ok(new_line) 

623 

624 def _merge_one_string_group( 

625 self, LL: list[Leaf], string_idx: int, is_valid_index: Callable[[int], bool] 

626 ) -> tuple[int, Leaf]: 

627 """ 

628 Merges one string group where the first string in the group is 

629 `LL[string_idx]`. 

630 

631 Returns: 

632 A tuple of `(num_of_strings, leaf)` where `num_of_strings` is the 

633 number of strings merged and `leaf` is the newly merged string 

634 to be replaced in the new line. 

635 """ 

636 # If the string group is wrapped inside an Atom node, we must make sure 

637 # to later replace that Atom with our new (merged) string leaf. 

638 atom_node = LL[string_idx].parent 

639 

640 # We will place BREAK_MARK in between every two substrings that we 

641 # merge. We will then later go through our final result and use the 

642 # various instances of BREAK_MARK we find to add the right values to 

643 # the custom split map. 

644 BREAK_MARK = "@@@@@ BLACK BREAKPOINT MARKER @@@@@" 

645 

646 QUOTE = LL[string_idx].value[-1] 

647 

648 def make_naked(string: str, string_prefix: str) -> str: 

649 """Strip @string (i.e. make it a "naked" string) 

650 

651 Pre-conditions: 

652 * assert_is_leaf_string(@string) 

653 

654 Returns: 

655 A string that is identical to @string except that 

656 @string_prefix has been stripped, the surrounding QUOTE 

657 characters have been removed, and any remaining QUOTE 

658 characters have been escaped. 

659 """ 

660 assert_is_leaf_string(string) 

661 if "f" in string_prefix: 

662 f_expressions = [ 

663 string[span[0] + 1 : span[1] - 1] # +-1 to get rid of curly braces 

664 for span in iter_fexpr_spans(string) 

665 ] 

666 debug_expressions_contain_visible_quotes = any( 

667 re.search(r"[\'\"].*(?<![!:=])={1}(?!=)(?![^\s:])", expression) 

668 for expression in f_expressions 

669 ) 

670 if not debug_expressions_contain_visible_quotes: 

671 # We don't want to toggle visible quotes in debug f-strings, as 

672 # that would modify the AST 

673 string = _toggle_fexpr_quotes(string, QUOTE) 

674 # After quotes toggling, quotes in expressions won't be escaped 

675 # because quotes can't be reused in f-strings. So we can simply 

676 # let the escaping logic below run without knowing f-string 

677 # expressions. 

678 

679 RE_EVEN_BACKSLASHES = r"(?:(?<!\\)(?:\\\\)*)" 

680 naked_string = string[len(string_prefix) + 1 : -1] 

681 if "r" in string_prefix: 

682 # Escaping quotes would change a raw string's value. Validation 

683 # ensures all raw substrings already use the same delimiter. 

684 return naked_string 

685 naked_string = re.sub( 

686 "(" + RE_EVEN_BACKSLASHES + ")" + QUOTE, r"\1\\" + QUOTE, naked_string 

687 ) 

688 return naked_string 

689 

690 # Holds the CustomSplit objects that will later be added to the custom 

691 # split map. 

692 custom_splits = [] 

693 

694 # Temporary storage for the 'has_prefix' part of the CustomSplit objects. 

695 prefix_tracker = [] 

696 

697 # Sets the 'prefix' variable. This is the prefix that the final merged 

698 # string will have. 

699 next_str_idx = string_idx 

700 prefix = "" 

701 while ( 

702 not prefix 

703 and is_valid_index(next_str_idx) 

704 and LL[next_str_idx].type == token.STRING 

705 ): 

706 prefix = get_string_prefix(LL[next_str_idx].value).lower() 

707 next_str_idx += 1 

708 

709 # The next loop merges the string group. The final string will be 

710 # contained in 'S'. 

711 # 

712 # The following convenience variables are used: 

713 # 

714 # S: string 

715 # NS: naked string 

716 # SS: next string 

717 # NSS: naked next string 

718 NS = "" 

719 num_of_strings = 0 

720 next_str_idx = string_idx 

721 while is_valid_index(next_str_idx) and LL[next_str_idx].type == token.STRING: 

722 num_of_strings += 1 

723 

724 SS = LL[next_str_idx].value 

725 next_prefix = get_string_prefix(SS).lower() 

726 

727 # If this is an f-string group but this substring is not prefixed 

728 # with 'f'... 

729 if "f" in prefix and "f" not in next_prefix: 

730 # Then we must escape any braces contained in this substring. 

731 SS = re.sub(r"(\{|\})", r"\1\1", SS) 

732 

733 NSS = make_naked(SS, next_prefix) 

734 

735 has_prefix = bool(next_prefix) 

736 prefix_tracker.append(has_prefix) 

737 

738 # Each NSS is already naked (prefix and quotes stripped, inner quotes 

739 # escaped, f-string expression quotes toggled), and the parts are 

740 # separated by BREAK_MARK which contains no quote or backslash, so the 

741 # naked group is just their concatenation. Re-running make_naked over the 

742 # whole accumulated string on every iteration rescans all previously 

743 # merged substrings, which is quadratic in the size of the group. 

744 NS = NS + NSS + BREAK_MARK 

745 

746 next_str_idx += 1 

747 

748 # Take a note on the index of the non-STRING leaf. 

749 non_string_idx = next_str_idx 

750 

751 S = prefix + QUOTE + NS + QUOTE 

752 S_leaf = Leaf(token.STRING, S) 

753 if self.normalize_strings: 

754 S_leaf.value = normalize_string_quotes(S_leaf.value) 

755 

756 # Fill the 'custom_splits' list with the appropriate CustomSplit objects. 

757 temp_string = S_leaf.value[len(prefix) + 1 : -1] 

758 for has_prefix in prefix_tracker: 

759 mark_idx = temp_string.find(BREAK_MARK) 

760 assert ( 

761 mark_idx >= 0 

762 ), "Logic error while filling the custom string breakpoint cache." 

763 

764 temp_string = temp_string[mark_idx + len(BREAK_MARK) :] 

765 breakpoint_idx = mark_idx + (len(prefix) if has_prefix else 0) + 1 

766 custom_splits.append(CustomSplit(has_prefix, breakpoint_idx)) 

767 

768 string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, "")) 

769 

770 if atom_node is not None: 

771 # If not all children of the atom node are merged (this can happen 

772 # when there is a standalone comment in the middle) ... 

773 if non_string_idx - string_idx < len(atom_node.children): 

774 # We need to replace the old STRING leaves with the new string leaf. 

775 first_child_idx = LL[string_idx].remove() 

776 for idx in range(string_idx + 1, non_string_idx): 

777 LL[idx].remove() 

778 if first_child_idx is not None: 

779 atom_node.insert_child(first_child_idx, string_leaf) 

780 else: 

781 # Else replace the atom node with the new string leaf. 

782 replace_child(atom_node, string_leaf) 

783 

784 self.add_custom_splits(string_leaf.value, custom_splits) 

785 return num_of_strings, string_leaf 

786 

787 def _validate_msg(self, line: Line, string_idx: int) -> TResult[None]: 

788 """Validate (M)erge (S)tring (G)roup 

789 

790 Transform-time string validation logic for _merge_string_group(...). 

791 

792 Returns: 

793 * Ok(None), if ALL validation checks (listed below) pass. 

794 OR 

795 * Err(CannotTransform), if any of the following are true: 

796 - The target string group does not contain ANY stand-alone comments. 

797 - The target string is not in a string group (i.e. it has no 

798 adjacent strings). 

799 - The string group has more than one inline comment. 

800 - The string group has an inline comment that appears to be a pragma. 

801 - The set of all string prefixes in the string group is of 

802 length greater than one and is not equal to {"", "f"}. 

803 - Raw strings in the group use different quote types. 

804 - The string group would merge f-strings with different quote types 

805 and internal quotes. 

806 - The string group is stringified type annotations. We don't want to 

807 process stringified type annotations since pyright doesn't support 

808 them spanning multiple string values. (NOTE: mypy, pytype, pyre do 

809 support them, so we can change if pyright also gains support in the 

810 future. See https://github.com/microsoft/pyright/issues/4359.) 

811 """ 

812 # We first check for "inner" stand-alone comments (i.e. stand-alone 

813 # comments that have a string leaf before them AND after them). 

814 for inc in [1, -1]: 

815 i = string_idx 

816 found_sa_comment = False 

817 is_valid_index = is_valid_index_factory(line.leaves) 

818 while is_valid_index(i) and line.leaves[i].type in [ 

819 token.STRING, 

820 STANDALONE_COMMENT, 

821 ]: 

822 if line.leaves[i].type == STANDALONE_COMMENT: 

823 found_sa_comment = True 

824 elif found_sa_comment: 

825 return TErr( 

826 "StringMerger does NOT merge string groups which contain " 

827 "stand-alone comments." 

828 ) 

829 

830 i += inc 

831 

832 QUOTE = line.leaves[string_idx].value[-1] 

833 

834 num_of_inline_string_comments = 0 

835 set_of_prefixes = set() 

836 num_of_strings = 0 

837 for leaf in line.leaves[string_idx:]: 

838 if leaf.type != token.STRING: 

839 # If the string group is trailed by a comma, we count the 

840 # comments trailing the comma to be one of the string group's 

841 # comments. 

842 if leaf.type == token.COMMA and id(leaf) in line.comments: 

843 num_of_inline_string_comments += 1 

844 break 

845 

846 if has_triple_quotes(leaf.value): 

847 return TErr("StringMerger does NOT merge multiline strings.") 

848 

849 num_of_strings += 1 

850 prefix = get_string_prefix(leaf.value).lower() 

851 if "r" in prefix and leaf.value[-1] != QUOTE: 

852 return TErr("Cannot merge raw strings with different quote types.") 

853 if "r" in prefix and "f" in prefix: 

854 return TErr("Cannot merge raw f-strings.") 

855 

856 set_of_prefixes.add(prefix) 

857 

858 if ( 

859 "f" in prefix 

860 and leaf.value[-1] != QUOTE 

861 and ( 

862 "'" in leaf.value[len(prefix) + 1 : -1] 

863 or '"' in leaf.value[len(prefix) + 1 : -1] 

864 ) 

865 ): 

866 return TErr( 

867 "StringMerger does NOT merge f-strings with different quote types" 

868 " and internal quotes." 

869 ) 

870 

871 if id(leaf) in line.comments: 

872 num_of_inline_string_comments += 1 

873 if contains_pragma_comment(line.comments[id(leaf)]): 

874 return TErr("Cannot merge strings which have pragma comments.") 

875 

876 if num_of_strings < 2: 

877 return TErr( 

878 f"Not enough strings to merge (num_of_strings={num_of_strings})." 

879 ) 

880 

881 # Also check for pragma comments on tokens that follow the string 

882 # group (e.g. a closing bracket). Merging strings when a pragma 

883 # comment like `# type: ignore` follows would produce an unsplittable 

884 # long line. 

885 is_valid_index = is_valid_index_factory(line.leaves) 

886 next_idx = string_idx + num_of_strings 

887 while is_valid_index(next_idx): 

888 next_leaf = line.leaves[next_idx] 

889 if id(next_leaf) in line.comments: 

890 if contains_pragma_comment(line.comments[id(next_leaf)]): 

891 return TErr( 

892 "Cannot merge strings when a pragma comment follows" 

893 " the string group." 

894 ) 

895 if next_leaf.type not in CLOSING_BRACKETS: 

896 break 

897 next_idx += 1 

898 

899 if num_of_inline_string_comments > 1: 

900 return TErr( 

901 f"Too many inline string comments ({num_of_inline_string_comments})." 

902 ) 

903 

904 if len(set_of_prefixes) > 1 and set_of_prefixes != {"", "f"}: 

905 return TErr(f"Too many different prefixes ({set_of_prefixes}).") 

906 

907 if any("r" in prefix for prefix in set_of_prefixes): 

908 # Keep long raw groups intact: merging and splitting them again can 

909 # introduce unnecessary parentheses around an existing concatenation. 

910 prefix = next(iter(set_of_prefixes)) 

911 merged_length = str_width(str(line).rstrip()) - (num_of_strings - 1) * ( 

912 len(prefix) + 3 

913 ) 

914 if merged_length > self.line_length: 

915 return TErr( 

916 "Cannot merge a raw string group exceeding the line length." 

917 ) 

918 

919 return Ok(None) 

920 

921 

922class StringParenStripper(StringTransformer, CustomSplitMapMixin): 

923 """StringTransformer that strips surrounding parentheses from strings. 

924 

925 Requirements: 

926 The line contains a string which is surrounded by parentheses and: 

927 - The target string is NOT the only argument to a function call. 

928 - The target string is NOT a "pointless" string. 

929 - The target string is NOT a dictionary value. 

930 - If the target string contains a PERCENT, the brackets are not 

931 preceded or followed by an operator with higher precedence than 

932 PERCENT. 

933 

934 Transformations: 

935 The parentheses mentioned in the 'Requirements' section are stripped. 

936 

937 Collaborations: 

938 StringParenStripper has its own inherent usefulness, but it is also 

939 relied on to clean up the parentheses created by StringParenWrapper (in 

940 the event that they are no longer needed). 

941 """ 

942 

943 def do_match(self, line: Line) -> TMatchResult: 

944 LL = line.leaves 

945 

946 is_valid_index = is_valid_index_factory(LL) 

947 

948 string_indices = [] 

949 

950 idx = -1 

951 while True: 

952 idx += 1 

953 if idx >= len(LL): 

954 break 

955 leaf = LL[idx] 

956 

957 # Should be a string... 

958 if leaf.type != token.STRING: 

959 continue 

960 

961 # If this is a "pointless" string... 

962 if ( 

963 leaf.parent 

964 and leaf.parent.parent 

965 and leaf.parent.parent.type == syms.simple_stmt 

966 ): 

967 continue 

968 

969 # Should be preceded by a non-empty LPAR... 

970 if ( 

971 not is_valid_index(idx - 1) 

972 or LL[idx - 1].type != token.LPAR 

973 or is_empty_lpar(LL[idx - 1]) 

974 ): 

975 continue 

976 

977 # That LPAR should NOT be preceded by a colon (which could be a 

978 # dictionary value), function name, or a closing bracket (which 

979 # could be a function returning a function or a list/dictionary 

980 # containing a function)... 

981 if is_valid_index(idx - 2) and ( 

982 LL[idx - 2].type == token.COLON 

983 or LL[idx - 2].type == token.NAME 

984 or LL[idx - 2].type in CLOSING_BRACKETS 

985 ): 

986 continue 

987 

988 string_idx = idx 

989 

990 # Skip the string trailer, if one exists. 

991 string_parser = StringParser() 

992 next_idx = string_parser.parse(LL, string_idx) 

993 

994 # if the leaves in the parsed string include a PERCENT, we need to 

995 # make sure the initial LPAR is NOT preceded by an operator with 

996 # higher or equal precedence to PERCENT 

997 if is_valid_index(idx - 2): 

998 # mypy can't quite follow unless we name this 

999 before_lpar = LL[idx - 2] 

1000 if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and ( 

1001 ( 

1002 before_lpar.type in { 

1003 token.STAR, 

1004 token.AT, 

1005 token.SLASH, 

1006 token.DOUBLESLASH, 

1007 token.PERCENT, 

1008 token.TILDE, 

1009 token.DOUBLESTAR, 

1010 token.AWAIT, 

1011 token.LSQB, 

1012 token.LPAR, 

1013 } 

1014 ) 

1015 or ( 

1016 # only unary PLUS/MINUS 

1017 before_lpar.parent 

1018 and before_lpar.parent.type == syms.factor 

1019 and (before_lpar.type in {token.PLUS, token.MINUS}) 

1020 ) 

1021 ): 

1022 continue 

1023 

1024 # Should be followed by a non-empty RPAR... 

1025 if ( 

1026 is_valid_index(next_idx) 

1027 and LL[next_idx].type == token.RPAR 

1028 and not is_empty_rpar(LL[next_idx]) 

1029 ): 

1030 # That RPAR should NOT be followed by anything with higher 

1031 # precedence than PERCENT 

1032 if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in { 

1033 token.DOUBLESTAR, 

1034 token.LSQB, 

1035 token.LPAR, 

1036 token.DOT, 

1037 }: 

1038 continue 

1039 

1040 # Keep the parens if stripping them would leave an already 

1041 # too-long line with a merged string group glued to an `=` 

1042 # prefix (e.g. a def parameter default). Black cannot split 

1043 # such a line after the fact, while the parens let the layout 

1044 # split the group at its custom breakpoints. See #3497. 

1045 if ( 

1046 not is_line_short_enough(line, mode=line.mode) 

1047 and is_valid_index(string_idx - 2) 

1048 and LL[string_idx - 2].type == token.EQUAL 

1049 and self.has_custom_splits(LL[string_idx].value) 

1050 ): 

1051 continue 

1052 

1053 string_indices.append(string_idx) 

1054 idx = string_idx 

1055 while idx < len(LL) - 1 and LL[idx + 1].type == token.STRING: 

1056 idx += 1 

1057 

1058 if string_indices: 

1059 return Ok(string_indices) 

1060 return TErr("This line has no strings wrapped in parens.") 

1061 

1062 def do_transform( 

1063 self, line: Line, string_indices: list[int] 

1064 ) -> Iterator[TResult[Line]]: 

1065 LL = line.leaves 

1066 

1067 string_and_rpar_indices: list[int] = [] 

1068 for string_idx in string_indices: 

1069 string_parser = StringParser() 

1070 rpar_idx = string_parser.parse(LL, string_idx) 

1071 

1072 should_transform = True 

1073 for leaf in (LL[string_idx - 1], LL[rpar_idx]): 

1074 if line.comments_after(leaf): 

1075 # Should not strip parentheses which have comments attached 

1076 # to them. 

1077 should_transform = False 

1078 break 

1079 if should_transform: 

1080 string_and_rpar_indices.extend((string_idx, rpar_idx)) 

1081 

1082 if string_and_rpar_indices: 

1083 yield Ok(self._transform_to_new_line(line, string_and_rpar_indices)) 

1084 else: 

1085 yield Err( 

1086 CannotTransform("All string groups have comments attached to them.") 

1087 ) 

1088 

1089 def _transform_to_new_line( 

1090 self, line: Line, string_and_rpar_indices: list[int] 

1091 ) -> Line: 

1092 LL = line.leaves 

1093 

1094 new_line = line.clone() 

1095 

1096 previous_idx = -1 

1097 # We need to sort the indices, since string_idx and its matching 

1098 # rpar_idx may not come in order, e.g. in 

1099 # `("outer" % ("inner".join(items)))`, the "inner" string's 

1100 # string_idx is smaller than "outer" string's rpar_idx. 

1101 for idx in sorted(string_and_rpar_indices): 

1102 leaf = LL[idx] 

1103 lpar_or_rpar_idx = idx - 1 if leaf.type == token.STRING else idx 

1104 append_leaves(new_line, line, LL[previous_idx + 1 : lpar_or_rpar_idx]) 

1105 if leaf.type == token.STRING: 

1106 string_leaf = Leaf(token.STRING, LL[idx].value) 

1107 LL[lpar_or_rpar_idx].remove() # Remove lpar. 

1108 replace_child(LL[idx], string_leaf) 

1109 new_line.append(string_leaf) 

1110 for comment_leaf in line.comments_after(LL[idx]): 

1111 new_line.append(comment_leaf, preformatted=True) 

1112 else: 

1113 LL[lpar_or_rpar_idx].remove() # This is a rpar. 

1114 

1115 previous_idx = idx 

1116 

1117 # Append the leaves after the last idx: 

1118 append_leaves(new_line, line, LL[idx + 1 :]) 

1119 

1120 return new_line 

1121 

1122 

1123class BaseStringSplitter(StringTransformer, CustomSplitMapMixin): 

1124 """ 

1125 Abstract class for StringTransformers which transform a Line's strings by splitting 

1126 them or placing them on their own lines where necessary to avoid going over 

1127 the configured line length. 

1128 

1129 Requirements: 

1130 * The target string value is responsible for the line going over the 

1131 line length limit. It follows that after all of black's other line 

1132 split methods have been exhausted, this line (or one of the resulting 

1133 lines after all line splits are performed) would still be over the 

1134 line_length limit unless we split this string. 

1135 AND 

1136 

1137 * The target string is NOT a "pointless" string (i.e. a string that has 

1138 no parent or siblings). 

1139 AND 

1140 

1141 * The target string is not followed by an inline comment that appears 

1142 to be a pragma. 

1143 AND 

1144 

1145 * The target string is not a multiline (i.e. triple-quote) string. 

1146 """ 

1147 

1148 STRING_OPERATORS: Final = COMPARATORS | (MATH_OPERATORS - {token.TILDE}) 

1149 

1150 MIN_SUBSTR_SIZE: Final = 6 

1151 

1152 def _iter_nameescape_slices(self, string: str) -> Iterator[tuple[Index, Index]]: 

1153 r""" 

1154 Yields: 

1155 All ranges of @string which, if @string were to be split there, 

1156 would result in the splitting of an \N{...} expression (which is NOT 

1157 allowed). 

1158 """ 

1159 # True - the previous backslash was unescaped 

1160 # False - the previous backslash was escaped *or* there was no backslash 

1161 previous_was_unescaped_backslash = False 

1162 it = iter(enumerate(string)) 

1163 for idx, c in it: 

1164 if c == "\\": 

1165 previous_was_unescaped_backslash = not previous_was_unescaped_backslash 

1166 continue 

1167 if not previous_was_unescaped_backslash or c != "N": 

1168 previous_was_unescaped_backslash = False 

1169 continue 

1170 previous_was_unescaped_backslash = False 

1171 

1172 begin = idx - 1 # the position of backslash before \N{...} 

1173 for idx, c in it: 

1174 if c == "}": 

1175 end = idx 

1176 break 

1177 else: 

1178 # malformed nameescape expression? 

1179 # should have been detected by AST parsing earlier... 

1180 raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!") 

1181 yield begin, end 

1182 

1183 def _iter_fexpr_slices(self, string: str) -> Iterator[tuple[Index, Index]]: 

1184 """ 

1185 Yields: 

1186 All ranges of @string which, if @string were to be split there, 

1187 would result in the splitting of an f-expression (which is NOT 

1188 allowed). 

1189 """ 

1190 if "f" not in get_string_prefix(string).lower(): 

1191 return 

1192 yield from iter_fexpr_spans(string) 

1193 

1194 def _get_illegal_split_indices( 

1195 self, string: str, has_named_escape: bool = True 

1196 ) -> set[Index]: 

1197 illegal_indices: set[Index] = set() 

1198 iterators = [self._iter_fexpr_slices(string)] 

1199 # Scanning for \N{...} ranges walks the whole string, so skip it when the 

1200 # caller already knows the string cannot contain a named escape. 

1201 if has_named_escape: 

1202 iterators.append(self._iter_nameescape_slices(string)) 

1203 for it in iterators: 

1204 for begin, end in it: 

1205 illegal_indices.update(range(begin, end)) 

1206 return illegal_indices 

1207 

1208 def _is_valid_split_index( 

1209 self, string: str, i: Index, illegal_split_indices: set[Index] 

1210 ) -> bool: 

1211 """ 

1212 Returns: 

1213 True iff @i meets all requirements to be a valid split index in @string. 

1214 """ 

1215 if i < self.MIN_SUBSTR_SIZE or len(string) - i < self.MIN_SUBSTR_SIZE: 

1216 return False 

1217 

1218 if i in illegal_split_indices: 

1219 return False 

1220 

1221 is_space = string[i] == " " 

1222 is_split_safe = i > 0 and string[i - 1] in SPLIT_SAFE_CHARS 

1223 

1224 if not (is_space or is_split_safe): 

1225 return False 

1226 

1227 is_not_escaped = True 

1228 j = i - 1 

1229 while j >= 0 and string[j] == "\\": 

1230 is_not_escaped = not is_not_escaped 

1231 j -= 1 

1232 

1233 return is_not_escaped 

1234 

1235 def can_be_split(self, string: str) -> bool: 

1236 """ 

1237 Returns: 

1238 True iff @string contains at least one index where StringSplitter 

1239 can split it. 

1240 """ 

1241 if len(string) < 2 * self.MIN_SUBSTR_SIZE: 

1242 return False 

1243 

1244 has_named_escape = "\\N" in string 

1245 illegal_split_indices = self._get_illegal_split_indices( 

1246 string, has_named_escape 

1247 ) 

1248 

1249 return any( 

1250 self._is_valid_split_index(string, i, illegal_split_indices) 

1251 for i in range(self.MIN_SUBSTR_SIZE, len(string) - self.MIN_SUBSTR_SIZE + 1) 

1252 ) 

1253 

1254 @abstractmethod 

1255 def do_splitter_match(self, line: Line) -> TMatchResult: 

1256 """ 

1257 BaseStringSplitter asks its clients to override this method instead of 

1258 `StringTransformer.do_match(...)`. 

1259 

1260 Follows the same protocol as `StringTransformer.do_match(...)`. 

1261 

1262 Refer to `help(StringTransformer.do_match)` for more information. 

1263 """ 

1264 

1265 def do_match(self, line: Line) -> TMatchResult: 

1266 match_result = self.do_splitter_match(line) 

1267 if isinstance(match_result, Err): 

1268 return match_result 

1269 

1270 string_indices = match_result.ok() 

1271 assert len(string_indices) == 1, ( 

1272 f"{self.__class__.__name__} should only find one match at a time, found" 

1273 f" {len(string_indices)}" 

1274 ) 

1275 string_idx = string_indices[0] 

1276 vresult = self._validate(line, string_idx) 

1277 if isinstance(vresult, Err): 

1278 return vresult 

1279 

1280 return match_result 

1281 

1282 def _validate(self, line: Line, string_idx: int) -> TResult[None]: 

1283 """ 

1284 Checks that @line meets all of the requirements listed in this classes' 

1285 docstring. Refer to `help(BaseStringSplitter)` for a detailed 

1286 description of those requirements. 

1287 

1288 Returns: 

1289 * Ok(None), if ALL of the requirements are met. 

1290 OR 

1291 * Err(CannotTransform), if ANY of the requirements are NOT met. 

1292 """ 

1293 LL = line.leaves 

1294 

1295 string_leaf = LL[string_idx] 

1296 

1297 max_string_length = self._get_max_string_length(line, string_idx) 

1298 if len(string_leaf.value) <= max_string_length: 

1299 return TErr( 

1300 "The string itself is not what is causing this line to be too long." 

1301 ) 

1302 

1303 if not string_leaf.parent or [L.type for L in string_leaf.parent.children] == [ 

1304 token.STRING, 

1305 token.NEWLINE, 

1306 ]: 

1307 return TErr( 

1308 f"This string ({string_leaf.value}) appears to be pointless (i.e. has" 

1309 " no parent)." 

1310 ) 

1311 

1312 if id(line.leaves[string_idx]) in line.comments and contains_pragma_comment( 

1313 line.comments[id(line.leaves[string_idx])] 

1314 ): 

1315 return TErr( 

1316 "Line appears to end with an inline pragma comment. Splitting the line" 

1317 " could modify the pragma's behavior." 

1318 ) 

1319 

1320 if has_triple_quotes(string_leaf.value): 

1321 return TErr("We cannot split multiline strings.") 

1322 

1323 return Ok(None) 

1324 

1325 def _get_max_string_length(self, line: Line, string_idx: int) -> int: 

1326 """ 

1327 Calculates the max string length used when attempting to determine 

1328 whether or not the target string is responsible for causing the line to 

1329 go over the line length limit. 

1330 

1331 WARNING: This method is tightly coupled to both StringSplitter and 

1332 (especially) StringParenWrapper. There is probably a better way to 

1333 accomplish what is being done here. 

1334 

1335 Returns: 

1336 max_string_length: such that `line.leaves[string_idx].value > 

1337 max_string_length` implies that the target string IS responsible 

1338 for causing this line to exceed the line length limit. 

1339 """ 

1340 LL = line.leaves 

1341 

1342 is_valid_index = is_valid_index_factory(LL) 

1343 

1344 # We use the shorthand "WMA4" in comments to abbreviate "We must 

1345 # account for". When giving examples, we use STRING to mean some/any 

1346 # valid string. 

1347 # 

1348 # Finally, we use the following convenience variables: 

1349 # 

1350 # P: The leaf that is before the target string leaf. 

1351 # N: The leaf that is after the target string leaf. 

1352 # NN: The leaf that is after N. 

1353 

1354 # WMA4 the whitespace at the beginning of the line. 

1355 offset = line.depth * 4 

1356 

1357 if is_valid_index(string_idx - 1): 

1358 p_idx = string_idx - 1 

1359 if ( 

1360 LL[string_idx - 1].type == token.LPAR 

1361 and LL[string_idx - 1].value == "" 

1362 and string_idx >= 2 

1363 ): 

1364 # If the previous leaf is an empty LPAR placeholder, we should skip it. 

1365 p_idx -= 1 

1366 

1367 P = LL[p_idx] 

1368 if P.type in self.STRING_OPERATORS: 

1369 # WMA4 a space and a string operator (e.g. `+ STRING` or `== STRING`). 

1370 offset += len(str(P)) + 1 

1371 

1372 if P.type == token.COMMA: 

1373 # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`]. 

1374 offset += 3 

1375 

1376 if P.type == token.NAME or P.value in ASSIGNMENTS: 

1377 # This conditional branch is meant to handle dictionary keys, 

1378 # variable assignments, 'return STRING' statement lines, and 

1379 # 'else STRING' ternary expression lines. 

1380 

1381 # WMA4 a single space. 

1382 offset += 1 

1383 

1384 # WMA4 the lengths of any leaves that came before that space, 

1385 # but after any closing bracket before that space. 

1386 for leaf in reversed(LL[: p_idx + 1]): 

1387 offset += len(str(leaf)) 

1388 if leaf.type in CLOSING_BRACKETS: 

1389 break 

1390 

1391 if is_valid_index(string_idx + 1): 

1392 N = LL[string_idx + 1] 

1393 if N.type == token.RPAR and N.value == "" and len(LL) > string_idx + 2: 

1394 # If the next leaf is an empty RPAR placeholder, we should skip it. 

1395 N = LL[string_idx + 2] 

1396 

1397 if N.type == token.COMMA: 

1398 # WMA4 a single comma at the end of the string (e.g `STRING,`). 

1399 offset += 1 

1400 

1401 if is_valid_index(string_idx + 2): 

1402 NN = LL[string_idx + 2] 

1403 

1404 if N.type == token.DOT and NN.type == token.NAME: 

1405 # This conditional branch is meant to handle method calls invoked 

1406 # off of a string literal up to and including the LPAR character. 

1407 

1408 # WMA4 the '.' character. 

1409 offset += 1 

1410 

1411 if ( 

1412 is_valid_index(string_idx + 3) 

1413 and LL[string_idx + 3].type == token.LPAR 

1414 ): 

1415 # WMA4 the left parenthesis character. 

1416 offset += 1 

1417 

1418 # WMA4 the length of the method's name. 

1419 offset += len(NN.value) 

1420 

1421 has_comments = False 

1422 for comment_leaf in line.comments_after(LL[string_idx]): 

1423 if not has_comments: 

1424 has_comments = True 

1425 # WMA4 two spaces before the '#' character. 

1426 offset += 2 

1427 

1428 # WMA4 the length of the inline comment. 

1429 offset += len(comment_leaf.value) 

1430 

1431 max_string_length = count_chars_in_width(str(line), self.line_length - offset) 

1432 return max_string_length 

1433 

1434 def _prefer_paren_wrap_match(self, line: Line) -> int | None: 

1435 """ 

1436 Returns: 

1437 string_idx such that @LL[string_idx] is equal to our target (i.e. 

1438 matched) string, if this line matches the "prefer paren wrap" statement 

1439 requirements listed in the 'Requirements' section of the StringParenWrapper 

1440 class's docstring. 

1441 OR 

1442 None, otherwise. 

1443 """ 

1444 LL = line.leaves 

1445 # The line must start with a string. 

1446 if not LL or LL[0].type != token.STRING: 

1447 return None 

1448 

1449 # Atom strings prefer to be wrapped in parens only if they will actually 

1450 # be split by StringSplitter, or if the line has comments that will be 

1451 # moved to the LPAR. 

1452 if ( 

1453 not self.can_be_split(LL[0].value) 

1454 and not self.has_custom_splits(LL[0].value) 

1455 and not line.comments 

1456 ): 

1457 return None 

1458 

1459 matching_nodes = [ 

1460 syms.listmaker, 

1461 syms.dictsetmaker, 

1462 syms.testlist_gexp, 

1463 ] 

1464 # If the string is an immediate child of a list/set/tuple literal... 

1465 if ( 

1466 parent_type(LL[0]) in matching_nodes 

1467 or parent_type(LL[0].parent) in matching_nodes 

1468 ): 

1469 # And the string is surrounded by commas (or is the first/last child)... 

1470 prev_sibling = LL[0].prev_sibling 

1471 next_sibling = LL[0].next_sibling 

1472 if ( 

1473 not prev_sibling 

1474 and not next_sibling 

1475 and parent_type(LL[0]) == syms.atom 

1476 ): 

1477 # If it's an atom string, we need to check the parent atom's siblings. 

1478 parent = LL[0].parent 

1479 assert parent is not None # For type checkers. 

1480 prev_sibling = parent.prev_sibling 

1481 next_sibling = parent.next_sibling 

1482 if (not prev_sibling or prev_sibling.type == token.COMMA) and ( 

1483 not next_sibling or next_sibling.type == token.COMMA 

1484 ): 

1485 return 0 

1486 

1487 return None 

1488 

1489 

1490def iter_fexpr_spans(s: str) -> Iterator[tuple[int, int]]: 

1491 """ 

1492 Yields spans corresponding to expressions in a given f-string. 

1493 Spans are half-open ranges (left inclusive, right exclusive). 

1494 Assumes the input string is a valid f-string, but will not crash if the input 

1495 string is invalid. 

1496 """ 

1497 stack: list[int] = [] # our curly paren stack 

1498 i = 0 

1499 while i < len(s): 

1500 if s[i] == "{": 

1501 # if we're in a string part of the f-string, ignore escaped curly braces 

1502 if not stack and i + 1 < len(s) and s[i + 1] == "{": 

1503 i += 2 

1504 continue 

1505 stack.append(i) 

1506 i += 1 

1507 continue 

1508 

1509 if s[i] == "}": 

1510 if not stack: 

1511 i += 1 

1512 continue 

1513 j = stack.pop() 

1514 # we've made it back out of the expression! yield the span 

1515 if not stack: 

1516 yield (j, i + 1) 

1517 i += 1 

1518 continue 

1519 

1520 # if we're in an expression part of the f-string, fast-forward through strings 

1521 # note that backslashes are not legal in the expression portion of f-strings 

1522 if stack: 

1523 delim = None 

1524 if s[i : i + 3] in ("'''", '"""'): 

1525 delim = s[i : i + 3] 

1526 elif s[i] in ("'", '"'): 

1527 delim = s[i] 

1528 if delim: 

1529 i += len(delim) 

1530 while i < len(s) and s[i : i + len(delim)] != delim: 

1531 i += 1 

1532 i += len(delim) 

1533 continue 

1534 i += 1 

1535 

1536 

1537def fstring_contains_expr(s: str) -> bool: 

1538 return any(iter_fexpr_spans(s)) 

1539 

1540 

1541def _toggle_fexpr_quotes(fstring: str, old_quote: str) -> str: 

1542 """ 

1543 Toggles quotes used in f-string expressions that are `old_quote`. 

1544 

1545 f-string expressions can't contain backslashes, so we need to toggle the 

1546 quotes if the f-string itself will end up using the same quote. We can 

1547 simply toggle without escaping because, quotes can't be reused in f-string 

1548 expressions. They will fail to parse. 

1549 

1550 NOTE: If PEP 701 is accepted, above statement will no longer be true. 

1551 Though if quotes can be reused, we can simply reuse them without updates or 

1552 escaping, once Black figures out how to parse the new grammar. 

1553 """ 

1554 new_quote = "'" if old_quote == '"' else '"' 

1555 parts = [] 

1556 previous_index = 0 

1557 for start, end in iter_fexpr_spans(fstring): 

1558 parts.append(fstring[previous_index:start]) 

1559 parts.append(fstring[start:end].replace(old_quote, new_quote)) 

1560 previous_index = end 

1561 parts.append(fstring[previous_index:]) 

1562 return "".join(parts) 

1563 

1564 

1565class StringSplitter(BaseStringSplitter, CustomSplitMapMixin): 

1566 """ 

1567 StringTransformer that splits "atom" strings (i.e. strings which exist on 

1568 lines by themselves). 

1569 

1570 Requirements: 

1571 * The line consists ONLY of a single string (possibly prefixed by a 

1572 string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE 

1573 a trailing comma. 

1574 AND 

1575 * All of the requirements listed in BaseStringSplitter's docstring. 

1576 

1577 Transformations: 

1578 The string mentioned in the 'Requirements' section is split into as 

1579 many substrings as necessary to adhere to the configured line length. 

1580 

1581 In the final set of substrings, no substring should be smaller than 

1582 MIN_SUBSTR_SIZE characters. 

1583 

1584 The string will ONLY be split on spaces (i.e. each new substring should 

1585 start with a space). Note that the string will NOT be split on a space 

1586 which is escaped with a backslash. 

1587 

1588 If the string is an f-string, it will NOT be split in the middle of an 

1589 f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x 

1590 else bar()} is an f-expression). 

1591 

1592 If the string that is being split has an associated set of custom split 

1593 records and those custom splits will NOT result in any line going over 

1594 the configured line length, those custom splits are used. Otherwise the 

1595 string is split as late as possible (from left-to-right) while still 

1596 adhering to the transformation rules listed above. 

1597 

1598 Collaborations: 

1599 StringSplitter relies on StringMerger to construct the appropriate 

1600 CustomSplit objects and add them to the custom split map. 

1601 """ 

1602 

1603 def do_splitter_match(self, line: Line) -> TMatchResult: 

1604 LL = line.leaves 

1605 

1606 if self._prefer_paren_wrap_match(line) is not None: 

1607 return TErr("Line needs to be wrapped in parens first.") 

1608 

1609 # If the line is just STRING + COMMA (a one-item tuple) and not inside 

1610 # brackets, we need to defer to StringParenWrapper to wrap it first. 

1611 # Otherwise, splitting the string would create multiple expressions where 

1612 # only the last has the comma, breaking AST equivalence. See issue #4912. 

1613 if ( 

1614 not line.inside_brackets 

1615 and len(LL) == 2 

1616 and LL[0].type == token.STRING 

1617 and LL[1].type == token.COMMA 

1618 ): 

1619 return TErr( 

1620 "Line with trailing comma tuple needs to be wrapped in parens first." 

1621 ) 

1622 

1623 is_valid_index = is_valid_index_factory(LL) 

1624 

1625 idx = 0 

1626 

1627 # The first two leaves MAY be the 'not in' keywords... 

1628 if ( 

1629 is_valid_index(idx) 

1630 and is_valid_index(idx + 1) 

1631 and [LL[idx].type, LL[idx + 1].type] == [token.NAME, token.NAME] 

1632 and str(LL[idx]) + str(LL[idx + 1]) == "not in" 

1633 ): 

1634 idx += 2 

1635 # Else the first leaf MAY be a string operator symbol or the 'in' keyword... 

1636 elif is_valid_index(idx) and ( 

1637 LL[idx].type in self.STRING_OPERATORS 

1638 or LL[idx].type == token.NAME 

1639 and str(LL[idx]) == "in" 

1640 ): 

1641 idx += 1 

1642 

1643 # The next/first leaf MAY be an empty LPAR... 

1644 if is_valid_index(idx) and is_empty_lpar(LL[idx]): 

1645 idx += 1 

1646 

1647 # The next/first leaf MUST be a string... 

1648 if not is_valid_index(idx) or LL[idx].type != token.STRING: 

1649 return TErr("Line does not start with a string.") 

1650 

1651 string_idx = idx 

1652 

1653 # Skip the string trailer, if one exists. 

1654 string_parser = StringParser() 

1655 idx = string_parser.parse(LL, string_idx) 

1656 

1657 # That string MAY be followed by an empty RPAR... 

1658 if is_valid_index(idx) and is_empty_rpar(LL[idx]): 

1659 idx += 1 

1660 

1661 # That string / empty RPAR leaf MAY be followed by a comma... 

1662 if is_valid_index(idx) and LL[idx].type == token.COMMA: 

1663 idx += 1 

1664 

1665 # But no more leaves are allowed... 

1666 if is_valid_index(idx): 

1667 return TErr("This line does not end with a string.") 

1668 

1669 return Ok([string_idx]) 

1670 

1671 def do_transform( 

1672 self, line: Line, string_indices: list[int] 

1673 ) -> Iterator[TResult[Line]]: 

1674 LL = line.leaves 

1675 assert len(string_indices) == 1, ( 

1676 f"{self.__class__.__name__} should only find one match at a time, found" 

1677 f" {len(string_indices)}" 

1678 ) 

1679 string_idx = string_indices[0] 

1680 

1681 QUOTE = LL[string_idx].value[-1] 

1682 

1683 is_valid_index = is_valid_index_factory(LL) 

1684 insert_str_child = insert_str_child_factory(LL[string_idx]) 

1685 

1686 prefix = get_string_prefix(LL[string_idx].value).lower() 

1687 

1688 # We MAY choose to drop the 'f' prefix from substrings that don't 

1689 # contain any f-expressions, but ONLY if the original f-string 

1690 # contains at least one f-expression. Otherwise, we will alter the AST 

1691 # of the program. 

1692 drop_pointless_f_prefix = ("f" in prefix) and fstring_contains_expr( 

1693 LL[string_idx].value 

1694 ) 

1695 

1696 first_string_line = True 

1697 

1698 string_op_leaves = self._get_string_operator_leaves(LL) 

1699 string_op_leaves_length = ( 

1700 sum(len(str(prefix_leaf)) for prefix_leaf in string_op_leaves) + 1 

1701 if string_op_leaves 

1702 else 0 

1703 ) 

1704 

1705 def maybe_append_string_operators(new_line: Line) -> None: 

1706 """ 

1707 Side Effects: 

1708 If @line starts with a string operator and this is the first 

1709 line we are constructing, this function appends the string 

1710 operator to @new_line and replaces the old string operator leaf 

1711 in the node structure. Otherwise this function does nothing. 

1712 """ 

1713 maybe_prefix_leaves = string_op_leaves if first_string_line else [] 

1714 for i, prefix_leaf in enumerate(maybe_prefix_leaves): 

1715 replace_child(LL[i], prefix_leaf) 

1716 new_line.append(prefix_leaf) 

1717 

1718 ends_with_comma = ( 

1719 is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA 

1720 ) 

1721 

1722 def max_last_string_column() -> int: 

1723 """ 

1724 Returns: 

1725 The max allowed width of the string value used for the last 

1726 line we will construct. Note that this value means the width 

1727 rather than the number of characters (e.g., many East Asian 

1728 characters expand to two columns). 

1729 """ 

1730 result = self.line_length 

1731 result -= line.depth * 4 

1732 result -= 1 if ends_with_comma else 0 

1733 result -= string_op_leaves_length 

1734 return result 

1735 

1736 # --- Calculate Max Break Width (for string value) 

1737 # We start with the line length limit 

1738 max_break_width = self.line_length 

1739 # The last index of a string of length N is N-1. 

1740 max_break_width -= 1 

1741 # Leading whitespace is not present in the string value (e.g. Leaf.value). 

1742 max_break_width -= line.depth * 4 

1743 if max_break_width < 0: 

1744 yield TErr( 

1745 f"Unable to split {LL[string_idx].value} at such high of a line depth:" 

1746 f" {line.depth}" 

1747 ) 

1748 return 

1749 

1750 # Check if StringMerger registered any custom splits. 

1751 custom_splits = self.pop_custom_splits(LL[string_idx].value) 

1752 # We use them ONLY if none of them would produce lines that exceed the 

1753 # line limit. 

1754 use_custom_breakpoints = bool( 

1755 custom_splits 

1756 and all(csplit.break_idx <= max_break_width for csplit in custom_splits) 

1757 ) 

1758 

1759 # Temporary storage for the remaining chunk of the string line that 

1760 # can't fit onto the line currently being constructed. 

1761 rest_value = LL[string_idx].value 

1762 

1763 # Each substring is a suffix of this value (plus the prefix and quote), 

1764 # so a missing "\N" here means no substring can hold a named escape and 

1765 # the per-substring scan for them can be skipped entirely. 

1766 has_named_escape = "\\N" in rest_value 

1767 

1768 def more_splits_should_be_made() -> bool: 

1769 """ 

1770 Returns: 

1771 True iff `rest_value` (the remaining string value from the last 

1772 split), should be split again. 

1773 """ 

1774 if use_custom_breakpoints: 

1775 return len(custom_splits) > 1 

1776 else: 

1777 return str_width(rest_value) > max_last_string_column() 

1778 

1779 string_line_results: list[Ok[Line]] = [] 

1780 while more_splits_should_be_made(): 

1781 if use_custom_breakpoints: 

1782 # Custom User Split (manual) 

1783 csplit = custom_splits.pop(0) 

1784 break_idx = csplit.break_idx 

1785 else: 

1786 # Algorithmic Split (automatic) 

1787 max_bidx = ( 

1788 count_chars_in_width(rest_value, max_break_width) 

1789 - string_op_leaves_length 

1790 ) 

1791 maybe_break_idx = self._get_break_idx( 

1792 rest_value, max_bidx, has_named_escape 

1793 ) 

1794 if maybe_break_idx is None: 

1795 # If we are unable to algorithmically determine a good split 

1796 # and this string has custom splits registered to it, we 

1797 # fall back to using them--which means we have to start 

1798 # over from the beginning. 

1799 if custom_splits: 

1800 rest_value = LL[string_idx].value 

1801 string_line_results = [] 

1802 first_string_line = True 

1803 use_custom_breakpoints = True 

1804 continue 

1805 

1806 # Otherwise, we stop splitting here. 

1807 break 

1808 

1809 break_idx = maybe_break_idx 

1810 

1811 # --- Construct `next_value` 

1812 next_value = rest_value[:break_idx] + QUOTE 

1813 

1814 # HACK: The following 'if' statement is a hack to fix the custom 

1815 # breakpoint index in the case of either: (a) substrings that were 

1816 # f-strings but will have the 'f' prefix removed OR (b) substrings 

1817 # that were not f-strings but will now become f-strings because of 

1818 # redundant use of the 'f' prefix (i.e. none of the substrings 

1819 # contain f-expressions but one or more of them had the 'f' prefix 

1820 # anyway; in which case, we will prepend 'f' to _all_ substrings). 

1821 # 

1822 # There is probably a better way to accomplish what is being done 

1823 # here... 

1824 # 

1825 # If this substring is an f-string, we _could_ remove the 'f' 

1826 # prefix, and the current custom split did NOT originally use a 

1827 # prefix... 

1828 if ( 

1829 use_custom_breakpoints 

1830 and not csplit.has_prefix 

1831 and ( 

1832 # `next_value == prefix + QUOTE` happens when the custom 

1833 # split is an empty string. 

1834 next_value == prefix + QUOTE 

1835 or next_value != self._normalize_f_string(next_value, prefix) 

1836 ) 

1837 ): 

1838 # Then `csplit.break_idx` will be off by one after removing 

1839 # the 'f' prefix. 

1840 break_idx += 1 

1841 next_value = rest_value[:break_idx] + QUOTE 

1842 

1843 if drop_pointless_f_prefix: 

1844 next_value = self._normalize_f_string(next_value, prefix) 

1845 

1846 # --- Construct `next_leaf` 

1847 next_leaf = Leaf(token.STRING, next_value) 

1848 insert_str_child(next_leaf) 

1849 self._maybe_normalize_string_quotes(next_leaf) 

1850 

1851 # --- Construct `next_line` 

1852 next_line = line.clone() 

1853 maybe_append_string_operators(next_line) 

1854 next_line.append(next_leaf) 

1855 string_line_results.append(Ok(next_line)) 

1856 

1857 rest_value = prefix + QUOTE + rest_value[break_idx:] 

1858 first_string_line = False 

1859 

1860 yield from string_line_results 

1861 

1862 if drop_pointless_f_prefix: 

1863 rest_value = self._normalize_f_string(rest_value, prefix) 

1864 

1865 rest_leaf = Leaf(token.STRING, rest_value) 

1866 insert_str_child(rest_leaf) 

1867 

1868 # NOTE: I could not find a test case that verifies that the following 

1869 # line is actually necessary, but it seems to be. Otherwise we risk 

1870 # not normalizing the last substring, right? 

1871 self._maybe_normalize_string_quotes(rest_leaf) 

1872 

1873 last_line = line.clone() 

1874 maybe_append_string_operators(last_line) 

1875 

1876 # If there are any leaves to the right of the target string... 

1877 if is_valid_index(string_idx + 1): 

1878 # We use `temp_value` here to determine how long the last line 

1879 # would be if we were to append all the leaves to the right of the 

1880 # target string to the last string line. 

1881 temp_value = rest_value 

1882 for leaf in LL[string_idx + 1 :]: 

1883 temp_value += str(leaf) 

1884 if leaf.type == token.LPAR: 

1885 break 

1886 

1887 # Try to fit them all on the same line with the last substring... 

1888 if ( 

1889 str_width(temp_value) <= max_last_string_column() 

1890 or LL[string_idx + 1].type == token.COMMA 

1891 ): 

1892 last_line.append(rest_leaf) 

1893 append_leaves(last_line, line, LL[string_idx + 1 :]) 

1894 yield Ok(last_line) 

1895 # Otherwise, place the last substring on one line and everything 

1896 # else on a line below that... 

1897 else: 

1898 last_line.append(rest_leaf) 

1899 yield Ok(last_line) 

1900 

1901 non_string_line = line.clone() 

1902 append_leaves(non_string_line, line, LL[string_idx + 1 :]) 

1903 yield Ok(non_string_line) 

1904 # Else the target string was the last leaf... 

1905 else: 

1906 last_line.append(rest_leaf) 

1907 last_line.comments = line.comments.copy() 

1908 yield Ok(last_line) 

1909 

1910 def _get_break_idx( 

1911 self, string: str, max_break_idx: int, has_named_escape: bool = True 

1912 ) -> int | None: 

1913 """ 

1914 This method contains the algorithm that StringSplitter uses to 

1915 determine which character to split each string at. 

1916 

1917 Args: 

1918 @string: The substring that we are attempting to split. 

1919 @max_break_idx: The ideal break index. We will return this value if it 

1920 meets all the necessary conditions. In the likely event that it 

1921 doesn't we will try to find the closest index BELOW @max_break_idx 

1922 that does. If that fails, we will expand our search by also 

1923 considering all valid indices ABOVE @max_break_idx. 

1924 @has_named_escape: Whether the original string can contain a named 

1925 escape. False lets us skip the per-substring scan for them. 

1926 

1927 Pre-Conditions: 

1928 * assert_is_leaf_string(@string) 

1929 * 0 <= @max_break_idx < len(@string) 

1930 

1931 Returns: 

1932 break_idx, if an index is able to be found that meets all of the 

1933 conditions listed in the 'Transformations' section of this classes' 

1934 docstring. 

1935 OR 

1936 None, otherwise. 

1937 """ 

1938 is_valid_index = is_valid_index_factory(string) 

1939 

1940 assert is_valid_index(max_break_idx) 

1941 assert_is_leaf_string(string) 

1942 

1943 _illegal_split_indices = self._get_illegal_split_indices( 

1944 string, has_named_escape 

1945 ) 

1946 

1947 def passes_all_checks(i: Index) -> bool: 

1948 return self._is_valid_split_index(string, i, _illegal_split_indices) 

1949 

1950 # First, we check all indices BELOW @max_break_idx. 

1951 break_idx = max_break_idx 

1952 while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx): 

1953 break_idx -= 1 

1954 

1955 if not passes_all_checks(break_idx): 

1956 # If that fails, we check all indices ABOVE @max_break_idx. 

1957 # 

1958 # If we are able to find a valid index here, the next line is going 

1959 # to be longer than the specified line length, but it's probably 

1960 # better than doing nothing at all. 

1961 break_idx = max_break_idx + 1 

1962 while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx): 

1963 break_idx += 1 

1964 

1965 if not is_valid_index(break_idx) or not passes_all_checks(break_idx): 

1966 return None 

1967 

1968 return break_idx 

1969 

1970 def _maybe_normalize_string_quotes(self, leaf: Leaf) -> None: 

1971 if self.normalize_strings: 

1972 leaf.value = normalize_string_quotes(leaf.value) 

1973 

1974 def _normalize_f_string(self, string: str, prefix: str) -> str: 

1975 """ 

1976 Pre-Conditions: 

1977 * assert_is_leaf_string(@string) 

1978 

1979 Returns: 

1980 * If @string is an f-string that contains no f-expressions, we 

1981 return a string identical to @string except that the 'f' prefix 

1982 has been stripped and all double braces (i.e. '{{' or '}}') have 

1983 been normalized (i.e. turned into '{' or '}'). 

1984 OR 

1985 * Otherwise, we return @string. 

1986 """ 

1987 assert_is_leaf_string(string) 

1988 

1989 if "f" in prefix and not fstring_contains_expr(string): 

1990 new_prefix = prefix.replace("f", "") 

1991 

1992 temp = string[len(prefix) :] 

1993 temp = re.sub(r"\{\{", "{", temp) 

1994 temp = re.sub(r"\}\}", "}", temp) 

1995 new_string = temp 

1996 

1997 return f"{new_prefix}{new_string}" 

1998 else: 

1999 return string 

2000 

2001 def _get_string_operator_leaves(self, leaves: Iterable[Leaf]) -> list[Leaf]: 

2002 LL = list(leaves) 

2003 

2004 string_op_leaves = [] 

2005 i = 0 

2006 while LL[i].type in self.STRING_OPERATORS or LL[i].type == token.NAME: 

2007 prefix_leaf = Leaf(LL[i].type, str(LL[i]).strip()) 

2008 string_op_leaves.append(prefix_leaf) 

2009 i += 1 

2010 return string_op_leaves 

2011 

2012 

2013class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin): 

2014 """ 

2015 StringTransformer that wraps strings in parens and then splits at the LPAR. 

2016 

2017 Requirements: 

2018 All of the requirements listed in BaseStringSplitter's docstring in 

2019 addition to the requirements listed below: 

2020 

2021 * The line is a return/yield statement, which returns/yields a string. 

2022 OR 

2023 * The line is a function definition with a stringified return annotation. 

2024 OR 

2025 * The line is part of a ternary expression (e.g. `x = y if cond else 

2026 z`) such that the line starts with `else <string>`, where <string> is 

2027 some string. 

2028 OR 

2029 * The line is an assert statement, which ends with a string. 

2030 OR 

2031 * The line is an assignment statement (e.g. `x = <string>` or `x += 

2032 <string>`) such that the variable is being assigned the value of some 

2033 string. 

2034 OR 

2035 * The line contains a binary operator followed by a string, including 

2036 operators overloaded by the left operand. 

2037 OR 

2038 * The line is a dictionary key assignment where some valid key is being 

2039 assigned the value of some string. 

2040 OR 

2041 * The line is a lambda expression and the value is a string. 

2042 OR 

2043 * The line starts with an "atom" string that prefers to be wrapped in 

2044 parens. It's preferred to be wrapped when it's is an immediate child of 

2045 a list/set/tuple literal, AND the string is surrounded by commas (or is 

2046 the first/last child). 

2047 

2048 Transformations: 

2049 The chosen string is wrapped in parentheses and then split at the LPAR. 

2050 

2051 We then have one line which ends with an LPAR and another line that 

2052 starts with the chosen string. The latter line is then split again at 

2053 the RPAR. This results in the RPAR (and possibly a trailing comma) 

2054 being placed on its own line. 

2055 

2056 NOTE: For binary operands, subsequent operators stay outside the new 

2057 parentheses. Otherwise, leaves to the right of the chosen string are 

2058 placed inside the parentheses, except for a trailing comma, which is 

2059 placed after the RPAR. In effect, the chosen 

2060 string is not necessarily being "wrapped" by parentheses. We can, 

2061 however, count on the LPAR being placed directly before the chosen 

2062 string. 

2063 

2064 In other words, StringParenWrapper creates "atom" strings. These 

2065 can then be split again by StringSplitter, if necessary. 

2066 

2067 Collaborations: 

2068 In the event that a string line split by StringParenWrapper is 

2069 changed such that it no longer needs to be given its own line, 

2070 StringParenWrapper relies on StringParenStripper to clean up the 

2071 parentheses it created. 

2072 

2073 For "atom" strings that prefers to be wrapped in parens, it requires 

2074 StringSplitter to hold the split until the string is wrapped in parens. 

2075 """ 

2076 

2077 def do_splitter_match(self, line: Line) -> TMatchResult: 

2078 LL = line.leaves 

2079 

2080 if line.leaves[-1].type in OPENING_BRACKETS: 

2081 return TErr( 

2082 "Cannot wrap parens around a line that ends in an opening bracket." 

2083 ) 

2084 

2085 string_idx = ( 

2086 self._return_match(LL) 

2087 or self._return_annotation_match(LL) 

2088 or self._else_match(LL) 

2089 or self._assert_match(LL) 

2090 or self._assign_match(LL) 

2091 or self._binary_match(line) 

2092 or self._dict_or_lambda_match(LL) 

2093 ) 

2094 

2095 if string_idx is None: 

2096 string_idx = self._trailing_comma_tuple_match(line) 

2097 

2098 if string_idx is None: 

2099 string_idx = self._prefer_paren_wrap_match(line) 

2100 if string_idx is not None and not self._wrapped_line_can_fit( 

2101 line, string_idx 

2102 ): 

2103 return TErr( 

2104 "We do not wrap strings in parentheses when the resultant line" 

2105 " would still be over the line length and can't be split by" 

2106 " StringSplitter." 

2107 ) 

2108 

2109 if string_idx is not None: 

2110 # If the string is implicitly concatenated with a string on another 

2111 # line (e.g. raw strings, which StringMerger leaves alone), wrapping 

2112 # it in parens on its own would produce invalid code. 

2113 next_sibling = LL[string_idx].next_sibling 

2114 if next_sibling is not None and next_sibling.type == token.STRING: 

2115 if not any(leaf is next_sibling for leaf in LL): 

2116 return TErr( 

2117 "Cannot wrap a string that is implicitly concatenated with a" 

2118 " string on another line." 

2119 ) 

2120 

2121 # Wrapping would move the first string's comments to the LPAR. 

2122 if line.comments: 

2123 return TErr( 

2124 "Cannot wrap an implicit concatenation that has comments." 

2125 ) 

2126 

2127 # If the first string fits on the line, splitting at the 

2128 # concatenation is enough. 

2129 tail = "".join(str(leaf) for leaf in LL[string_idx + 1 :]) 

2130 if ( 

2131 str_width(line_to_string(line)) - str_width(tail) 

2132 <= self.line_length 

2133 ): 

2134 return TErr( 

2135 "The first string of the implicit concatenation fits on" 

2136 " the line." 

2137 ) 

2138 

2139 string_value = line.leaves[string_idx].value 

2140 # If the string has neither spaces nor East Asian stops... 

2141 if not any( 

2142 char == " " or char in SPLIT_SAFE_CHARS for char in string_value 

2143 ): 

2144 # And will still violate the line length limit when split... 

2145 max_string_width = self.line_length - ((line.depth + 1) * 4) 

2146 if str_width(string_value) > max_string_width: 

2147 # And has no associated custom splits... 

2148 if not self.has_custom_splits(string_value): 

2149 # Then we should NOT put this string on its own line. 

2150 return TErr( 

2151 "We do not wrap long strings in parentheses when the" 

2152 " resultant line would still be over the specified line" 

2153 " length and can't be split further by StringSplitter." 

2154 ) 

2155 return Ok([string_idx]) 

2156 

2157 return TErr("This line does not contain any non-atomic strings.") 

2158 

2159 def _wrapped_line_can_fit(self, line: Line, string_idx: int) -> bool: 

2160 """ 

2161 Returns: 

2162 True iff the line created by wrapping the string at @string_idx in 

2163 parens fits within the line length, or StringSplitter can split it 

2164 there. That line is one level deeper than @line. 

2165 """ 

2166 string_value = line.leaves[string_idx].value 

2167 new_depth = line.depth + 1 

2168 if new_depth * 4 + str_width(string_value) <= self.line_length: 

2169 return True 

2170 

2171 return self.line_length - 1 - new_depth * 4 >= 0 and ( 

2172 self.can_be_split(string_value) or self.has_custom_splits(string_value) 

2173 ) 

2174 

2175 @staticmethod 

2176 def _return_match(LL: list[Leaf]) -> int | None: 

2177 """ 

2178 Returns: 

2179 string_idx such that @LL[string_idx] is equal to our target (i.e. 

2180 matched) string, if this line matches the return/yield statement 

2181 requirements listed in the 'Requirements' section of this classes' 

2182 docstring. 

2183 OR 

2184 None, otherwise. 

2185 """ 

2186 # If this line is a part of a return/yield statement and the first leaf 

2187 # contains either the "return" or "yield" keywords... 

2188 if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[ 

2189 0 

2190 ].value in ["return", "yield"]: 

2191 is_valid_index = is_valid_index_factory(LL) 

2192 

2193 idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1 

2194 # The next visible leaf MUST contain a string... 

2195 if is_valid_index(idx) and LL[idx].type == token.STRING: 

2196 return idx 

2197 

2198 return None 

2199 

2200 @staticmethod 

2201 def _return_annotation_match(LL: list[Leaf]) -> int | None: 

2202 """Return the string index for a stringified return annotation.""" 

2203 is_valid_index = is_valid_index_factory(LL) 

2204 

2205 for i, leaf in enumerate(LL): 

2206 if leaf.type != token.RARROW: 

2207 continue 

2208 

2209 string_idx = ( 

2210 i + 2 if is_valid_index(i + 1) and is_empty_lpar(LL[i + 1]) else i + 1 

2211 ) 

2212 if not is_valid_index(string_idx) or LL[string_idx].type != token.STRING: 

2213 return None 

2214 

2215 idx = StringParser().parse(LL, string_idx) 

2216 if ( 

2217 is_valid_index(idx) 

2218 and LL[idx].type == token.COLON 

2219 and idx == len(LL) - 1 

2220 ): 

2221 return string_idx 

2222 

2223 return None 

2224 

2225 return None 

2226 

2227 @staticmethod 

2228 def _else_match(LL: list[Leaf]) -> int | None: 

2229 """ 

2230 Returns: 

2231 string_idx such that @LL[string_idx] is equal to our target (i.e. 

2232 matched) string, if this line matches the ternary expression 

2233 requirements listed in the 'Requirements' section of this classes' 

2234 docstring. 

2235 OR 

2236 None, otherwise. 

2237 """ 

2238 # If this line is a part of a ternary expression and the first leaf 

2239 # contains the "else" keyword... 

2240 if ( 

2241 parent_type(LL[0]) == syms.test 

2242 and LL[0].type == token.NAME 

2243 and LL[0].value == "else" 

2244 ): 

2245 is_valid_index = is_valid_index_factory(LL) 

2246 

2247 idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1 

2248 # The next visible leaf MUST contain a string... 

2249 if is_valid_index(idx) and LL[idx].type == token.STRING: 

2250 return idx 

2251 

2252 return None 

2253 

2254 @staticmethod 

2255 def _assert_match(LL: list[Leaf]) -> int | None: 

2256 """ 

2257 Returns: 

2258 string_idx such that @LL[string_idx] is equal to our target (i.e. 

2259 matched) string, if this line matches the assert statement 

2260 requirements listed in the 'Requirements' section of this classes' 

2261 docstring. 

2262 OR 

2263 None, otherwise. 

2264 """ 

2265 # If this line is a part of an assert statement and the first leaf 

2266 # contains the "assert" keyword... 

2267 if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert": 

2268 is_valid_index = is_valid_index_factory(LL) 

2269 

2270 for i, leaf in enumerate(LL): 

2271 # We MUST find a comma... 

2272 if leaf.type == token.COMMA: 

2273 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 

2274 

2275 # That comma MUST be followed by a string... 

2276 if is_valid_index(idx) and LL[idx].type == token.STRING: 

2277 string_idx = idx 

2278 

2279 # Skip the string trailer, if one exists. 

2280 string_parser = StringParser() 

2281 idx = string_parser.parse(LL, string_idx) 

2282 

2283 # But no more leaves are allowed... 

2284 if not is_valid_index(idx): 

2285 return string_idx 

2286 

2287 return None 

2288 

2289 @staticmethod 

2290 def _assign_match(LL: list[Leaf]) -> int | None: 

2291 """ 

2292 Returns: 

2293 string_idx such that @LL[string_idx] is equal to our target (i.e. 

2294 matched) string, if this line matches the assignment statement 

2295 requirements listed in the 'Requirements' section of this classes' 

2296 docstring. 

2297 OR 

2298 None, otherwise. 

2299 """ 

2300 # If this line is a part of an expression statement or is a function 

2301 # argument AND the first leaf contains a variable name... 

2302 if ( 

2303 parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power] 

2304 and LL[0].type == token.NAME 

2305 ): 

2306 is_valid_index = is_valid_index_factory(LL) 

2307 

2308 for i, leaf in enumerate(LL): 

2309 # An assignment operator must be followed by the target string. 

2310 if leaf.value in ASSIGNMENTS and leaf.type != token.COLON: 

2311 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 

2312 

2313 # That symbol MUST be followed by a string... 

2314 if is_valid_index(idx) and LL[idx].type == token.STRING: 

2315 string_idx = idx 

2316 

2317 # Skip any strings implicitly concatenated with it. 

2318 while ( 

2319 is_valid_index(idx + 1) and LL[idx + 1].type == token.STRING 

2320 ): 

2321 idx += 1 

2322 

2323 # Skip the string trailer, if one exists. 

2324 string_parser = StringParser() 

2325 idx = string_parser.parse(LL, idx) 

2326 

2327 # The next leaf MAY be a comma iff this line is a part 

2328 # of a function argument... 

2329 if ( 

2330 parent_type(LL[0]) == syms.argument 

2331 and is_valid_index(idx) 

2332 and LL[idx].type == token.COMMA 

2333 ): 

2334 idx += 1 

2335 

2336 # But no more leaves are allowed... 

2337 if not is_valid_index(idx): 

2338 return string_idx 

2339 

2340 return None 

2341 

2342 def _binary_match(self, line: Line) -> int | None: 

2343 """Find a long string operand, including one in the middle of a chain.""" 

2344 LL = line.leaves 

2345 is_valid_index = is_valid_index_factory(LL) 

2346 for i, leaf in enumerate(LL): 

2347 if leaf.type not in MATH_OPERATORS or leaf.type == token.TILDE: 

2348 continue 

2349 if parent_type(leaf) not in { 

2350 syms.arith_expr, 

2351 syms.term, 

2352 syms.power, 

2353 syms.shift_expr, 

2354 syms.and_expr, 

2355 syms.xor_expr, 

2356 syms.expr, 

2357 }: 

2358 continue 

2359 

2360 idx = i + 1 

2361 if is_valid_index(idx) and is_empty_lpar(LL[idx]): 

2362 idx += 1 

2363 if not is_valid_index(idx) or LL[idx].type != token.STRING: 

2364 continue 

2365 operand_end = self._binary_operand_end(LL, idx) 

2366 if is_valid_index(operand_end): 

2367 if ( 

2368 LL[operand_end].type not in MATH_OPERATORS - {token.TILDE} 

2369 or leaf.bracket_depth 

2370 ): 

2371 # Split enclosing calls/collections before wrapping a middle 

2372 # operand, and leave other trailers to existing matchers. 

2373 continue 

2374 if not isinstance(self._validate(line, idx), Err): 

2375 return idx 

2376 

2377 return None 

2378 

2379 @staticmethod 

2380 def _binary_operand_end(LL: list[Leaf], string_idx: int) -> int: 

2381 """Find the string's trailer without absorbing a following % operation.""" 

2382 idx = string_idx 

2383 while idx + 1 < len(LL) and LL[idx + 1].type == token.STRING: 

2384 idx += 1 

2385 trailer_idx = idx + 1 

2386 while trailer_idx < len(LL) and is_empty_par(LL[trailer_idx]): 

2387 trailer_idx += 1 

2388 if trailer_idx < len(LL) and LL[trailer_idx].type == token.PERCENT: 

2389 return trailer_idx 

2390 return StringParser().parse(LL, idx) 

2391 

2392 @staticmethod 

2393 def _dict_or_lambda_match(LL: list[Leaf]) -> int | None: 

2394 """ 

2395 Returns: 

2396 string_idx such that @LL[string_idx] is equal to our target (i.e. 

2397 matched) string, if this line matches the dictionary key assignment 

2398 statement or lambda expression requirements listed in the 

2399 'Requirements' section of this classes' docstring. 

2400 OR 

2401 None, otherwise. 

2402 """ 

2403 # If this line is a part of a dictionary key assignment or lambda expression... 

2404 parent_types = [parent_type(LL[0]), parent_type(LL[0].parent)] 

2405 if syms.dictsetmaker in parent_types or syms.lambdef in parent_types: 

2406 is_valid_index = is_valid_index_factory(LL) 

2407 

2408 for i, leaf in enumerate(LL): 

2409 # We MUST find a colon, it can either be dict's or lambda's colon... 

2410 if leaf.type == token.COLON and i < len(LL) - 1: 

2411 idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 

2412 

2413 # That colon MUST be followed by a string... 

2414 if is_valid_index(idx) and LL[idx].type == token.STRING: 

2415 string_idx = idx 

2416 

2417 # Skip any strings implicitly concatenated with it. 

2418 while ( 

2419 is_valid_index(idx + 1) and LL[idx + 1].type == token.STRING 

2420 ): 

2421 idx += 1 

2422 

2423 # Skip the string trailer, if one exists. 

2424 string_parser = StringParser() 

2425 idx = string_parser.parse(LL, idx) 

2426 

2427 # That string MAY be followed by a comma... 

2428 if is_valid_index(idx) and LL[idx].type == token.COMMA: 

2429 idx += 1 

2430 

2431 # But no more leaves are allowed... 

2432 if not is_valid_index(idx): 

2433 return string_idx 

2434 

2435 return None 

2436 

2437 @staticmethod 

2438 def _trailing_comma_tuple_match(line: Line) -> int | None: 

2439 """ 

2440 Returns: 

2441 string_idx such that @line.leaves[string_idx] is equal to our target 

2442 (i.e. matched) string, if the line is a bare trailing comma tuple 

2443 (STRING + COMMA) not inside brackets. 

2444 OR 

2445 None, otherwise. 

2446 

2447 This handles the case from issue #4912 where a long string with a 

2448 trailing comma (making it a one-item tuple) needs to be wrapped in 

2449 parentheses before splitting to preserve AST equivalence. 

2450 """ 

2451 LL = line.leaves 

2452 # Match: STRING followed by COMMA, not inside brackets 

2453 if ( 

2454 not line.inside_brackets 

2455 and len(LL) == 2 

2456 and LL[0].type == token.STRING 

2457 and LL[1].type == token.COMMA 

2458 ): 

2459 return 0 

2460 

2461 return None 

2462 

2463 def do_transform( 

2464 self, line: Line, string_indices: list[int] 

2465 ) -> Iterator[TResult[Line]]: 

2466 LL = line.leaves 

2467 assert len(string_indices) == 1, ( 

2468 f"{self.__class__.__name__} should only find one match at a time, found" 

2469 f" {len(string_indices)}" 

2470 ) 

2471 string_idx = string_indices[0] 

2472 

2473 if self._binary_match(line) == string_idx: 

2474 operand_end = self._binary_operand_end(LL, string_idx) 

2475 if operand_end < len(LL): 

2476 # Reuse the terminal-operand wrapper, then restore the suffix 

2477 # after its RPAR. Wrapping the suffix too would reassociate 

2478 # expressions such as x % STRING % y. 

2479 operand_line = line.clone() 

2480 append_leaves(operand_line, line, LL[:operand_end]) 

2481 results = list(self.do_transform(operand_line, [string_idx])) 

2482 last_result = results.pop() 

2483 yield from results 

2484 if isinstance(last_result, Err): 

2485 yield last_result 

2486 return 

2487 append_leaves(last_result.ok(), line, LL[operand_end:]) 

2488 yield last_result 

2489 return 

2490 

2491 is_valid_index = is_valid_index_factory(LL) 

2492 

2493 comma_idx = -1 

2494 ends_with_comma = False 

2495 if LL[comma_idx].type == token.COMMA: 

2496 ends_with_comma = True 

2497 return_annotation_arrow_idx = ( 

2498 string_idx - 2 if is_empty_lpar(LL[string_idx - 1]) else string_idx - 1 

2499 ) 

2500 ends_with_return_annotation = ( 

2501 LL[return_annotation_arrow_idx].type == token.RARROW 

2502 and LL[-1].type == token.COLON 

2503 ) 

2504 

2505 leaves_to_steal_comments_from = [LL[string_idx]] 

2506 if ends_with_comma: 

2507 leaves_to_steal_comments_from.append(LL[comma_idx]) 

2508 

2509 # --- First Line 

2510 first_line = line.clone() 

2511 left_leaves = LL[:string_idx] 

2512 

2513 # We have to remember to account for (possibly invisible) LPAR and RPAR 

2514 # leaves that already wrapped the target string. If these leaves do 

2515 # exist, we will replace them with our own LPAR and RPAR leaves. 

2516 old_parens_exist = False 

2517 if left_leaves and left_leaves[-1].type == token.LPAR: 

2518 old_parens_exist = True 

2519 leaves_to_steal_comments_from.append(left_leaves[-1]) 

2520 left_leaves.pop() 

2521 

2522 append_leaves(first_line, line, left_leaves) 

2523 

2524 # Keep the operand in the tree while calculating the operator's spacing. 

2525 # Removing it early can make an exponentiation look simple enough to hug. 

2526 insert_str_child = insert_str_child_factory(LL[string_idx]) 

2527 lpar_leaf = Leaf(token.LPAR, "(") 

2528 if old_parens_exist: 

2529 replace_child(LL[string_idx - 1], lpar_leaf) 

2530 else: 

2531 insert_str_child(lpar_leaf) 

2532 first_line.append(lpar_leaf) 

2533 

2534 # We throw inline comments that were originally to the right of the 

2535 # target string to the top line. They will now be shown to the right of 

2536 # the LPAR. 

2537 for leaf in leaves_to_steal_comments_from: 

2538 for comment_leaf in line.comments_after(leaf): 

2539 first_line.append(comment_leaf, preformatted=True) 

2540 

2541 yield Ok(first_line) 

2542 

2543 # --- Middle (String) Line 

2544 # We only need to yield one (possibly too long) string line, since the 

2545 # `StringSplitter` will break it down further if necessary. 

2546 string_value = LL[string_idx].value 

2547 string_line = Line( 

2548 mode=line.mode, 

2549 depth=line.depth + 1, 

2550 inside_brackets=True, 

2551 should_split_rhs=line.should_split_rhs, 

2552 magic_trailing_comma=line.magic_trailing_comma, 

2553 ) 

2554 string_leaf = Leaf(token.STRING, string_value) 

2555 insert_str_child(string_leaf) 

2556 string_line.append(string_leaf) 

2557 

2558 old_rpar_leaf = None 

2559 if is_valid_index(string_idx + 1): 

2560 right_leaves = LL[string_idx + 1 :] 

2561 if ends_with_comma: 

2562 right_leaves.pop() 

2563 if ends_with_return_annotation: 

2564 right_leaves.pop() 

2565 

2566 if old_parens_exist: 

2567 assert right_leaves and right_leaves[-1].type == token.RPAR, ( 

2568 "Apparently, old parentheses do NOT exist?!" 

2569 f" (left_leaves={left_leaves}, right_leaves={right_leaves})" 

2570 ) 

2571 old_rpar_leaf = right_leaves.pop() 

2572 elif right_leaves and right_leaves[-1].type == token.RPAR: 

2573 # Special case for lambda expressions as dict's value, e.g.: 

2574 # my_dict = { 

2575 # "key": lambda x: f"formatted: {x}", 

2576 # } 

2577 # After wrapping the dict's value with parentheses, the string is 

2578 # followed by a RPAR but its opening bracket is lambda's, not 

2579 # the string's: 

2580 # "key": (lambda x: f"formatted: {x}"), 

2581 opening_bracket = right_leaves[-1].opening_bracket 

2582 if opening_bracket is not None and opening_bracket in left_leaves: 

2583 index = left_leaves.index(opening_bracket) 

2584 if ( 

2585 0 < index < len(left_leaves) - 1 

2586 and left_leaves[index - 1].type == token.COLON 

2587 and left_leaves[index + 1].value == "lambda" 

2588 ): 

2589 right_leaves.pop() 

2590 

2591 append_leaves(string_line, line, right_leaves) 

2592 

2593 yield Ok(string_line) 

2594 

2595 # --- Last Line 

2596 last_line = line.clone() 

2597 last_line.bracket_tracker = first_line.bracket_tracker 

2598 

2599 new_rpar_leaf = Leaf(token.RPAR, ")") 

2600 if old_rpar_leaf is not None: 

2601 replace_child(old_rpar_leaf, new_rpar_leaf) 

2602 else: 

2603 insert_str_child(new_rpar_leaf) 

2604 last_line.append(new_rpar_leaf) 

2605 

2606 # If the target string ended with a comma, we place this comma to the 

2607 # right of the RPAR on the last line. 

2608 if ends_with_comma: 

2609 comma_leaf = Leaf(token.COMMA, ",") 

2610 replace_child(LL[comma_idx], comma_leaf) 

2611 last_line.append(comma_leaf) 

2612 if ends_with_return_annotation: 

2613 append_leaves(last_line, line, [LL[-1]]) 

2614 

2615 if old_rpar_leaf is not None: 

2616 for comment_leaf in line.comments_after(old_rpar_leaf): 

2617 last_line.append(comment_leaf, preformatted=True) 

2618 

2619 yield Ok(last_line) 

2620 

2621 

2622class StringParser: 

2623 """ 

2624 A state machine that aids in parsing a string's "trailer", which can be 

2625 either non-existent, an old-style formatting sequence (e.g. `% varX` or `% 

2626 (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX, 

2627 varY)`). 

2628 

2629 NOTE: A new StringParser object MUST be instantiated for each string 

2630 trailer we need to parse. 

2631 

2632 Examples: 

2633 We shall assume that `line` equals the `Line` object that corresponds 

2634 to the following line of python code: 

2635 ``` 

2636 x = "Some {}.".format("String") + some_other_string 

2637 ``` 

2638 

2639 Furthermore, we will assume that `string_idx` is some index such that: 

2640 ``` 

2641 assert line.leaves[string_idx].value == "Some {}." 

2642 ``` 

2643 

2644 The following code snippet then holds: 

2645 ``` 

2646 string_parser = StringParser() 

2647 idx = string_parser.parse(line.leaves, string_idx) 

2648 assert line.leaves[idx].type == token.PLUS 

2649 ``` 

2650 """ 

2651 

2652 DEFAULT_TOKEN: Final = 20210605 

2653 

2654 # String Parser States 

2655 START: Final = 1 

2656 DOT: Final = 2 

2657 NAME: Final = 3 

2658 PERCENT: Final = 4 

2659 SINGLE_FMT_ARG: Final = 5 

2660 LPAR: Final = 6 

2661 RPAR: Final = 7 

2662 DONE: Final = 8 

2663 

2664 # Lookup Table for Next State 

2665 _goto: Final[dict[tuple[ParserState, NodeType], ParserState]] = { 

2666 # A string trailer may start with '.' OR '%'. 

2667 (START, token.DOT): DOT, 

2668 (START, token.PERCENT): PERCENT, 

2669 (START, DEFAULT_TOKEN): DONE, 

2670 # A '.' MUST be followed by an attribute or method name. 

2671 (DOT, token.NAME): NAME, 

2672 # A method name MUST be followed by an '(', whereas an attribute name 

2673 # is the last symbol in the string trailer. 

2674 (NAME, token.LPAR): LPAR, 

2675 (NAME, DEFAULT_TOKEN): DONE, 

2676 # A '%' symbol can be followed by an '(' or a single argument (e.g. a 

2677 # string or variable name). 

2678 (PERCENT, token.LPAR): LPAR, 

2679 (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG, 

2680 # If a '%' symbol is followed by a single argument, that argument is 

2681 # the last leaf in the string trailer. 

2682 (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE, 

2683 # If present, a ')' symbol is the last symbol in a string trailer. 

2684 # (NOTE: LPARS and nested RPARS are not included in this lookup table, 

2685 # since they are treated as a special case by the parsing logic in this 

2686 # classes' implementation.) 

2687 (RPAR, DEFAULT_TOKEN): DONE, 

2688 } 

2689 

2690 def __init__(self) -> None: 

2691 self._state = self.START 

2692 self._unmatched_lpars = 0 

2693 

2694 def parse(self, leaves: list[Leaf], string_idx: int) -> int: 

2695 """ 

2696 Pre-conditions: 

2697 * @leaves[@string_idx].type == token.STRING 

2698 

2699 Returns: 

2700 The index directly after the last leaf which is a part of the string 

2701 trailer, if a "trailer" exists. 

2702 OR 

2703 @string_idx + 1, if no string "trailer" exists. 

2704 """ 

2705 assert leaves[string_idx].type == token.STRING 

2706 

2707 idx = string_idx + 1 

2708 while idx < len(leaves) and self._next_state(leaves[idx]): 

2709 idx += 1 

2710 return idx 

2711 

2712 def _next_state(self, leaf: Leaf) -> bool: 

2713 """ 

2714 Pre-conditions: 

2715 * On the first call to this function, @leaf MUST be the leaf that 

2716 was directly after the string leaf in question (e.g. if our target 

2717 string is `line.leaves[i]` then the first call to this method must 

2718 be `line.leaves[i + 1]`). 

2719 * On the next call to this function, the leaf parameter passed in 

2720 MUST be the leaf directly following @leaf. 

2721 

2722 Returns: 

2723 True iff @leaf is a part of the string's trailer. 

2724 """ 

2725 # We ignore empty LPAR or RPAR leaves. 

2726 if is_empty_par(leaf): 

2727 return True 

2728 

2729 next_token = leaf.type 

2730 if next_token == token.LPAR: 

2731 self._unmatched_lpars += 1 

2732 

2733 current_state = self._state 

2734 

2735 # The LPAR parser state is a special case. We will return True until we 

2736 # find the matching RPAR token. 

2737 if current_state == self.LPAR: 

2738 if next_token == token.RPAR: 

2739 self._unmatched_lpars -= 1 

2740 if self._unmatched_lpars == 0: 

2741 self._state = self.RPAR 

2742 # Otherwise, we use a lookup table to determine the next state. 

2743 else: 

2744 # If the lookup table matches the current state to the next 

2745 # token, we use the lookup table. 

2746 if (current_state, next_token) in self._goto: 

2747 self._state = self._goto[current_state, next_token] 

2748 else: 

2749 # Otherwise, we check if the current state was assigned a 

2750 # default. 

2751 if (current_state, self.DEFAULT_TOKEN) in self._goto: 

2752 self._state = self._goto[current_state, self.DEFAULT_TOKEN] 

2753 # If no default has been assigned, then this parser has a logic 

2754 # error. 

2755 else: 

2756 raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!") 

2757 

2758 if self._state == self.DONE: 

2759 return False 

2760 

2761 return True 

2762 

2763 

2764def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]: 

2765 """ 

2766 Factory for a convenience function that is used to orphan @string_leaf 

2767 and then insert multiple new leaves into the same part of the node 

2768 structure that @string_leaf had originally occupied. 

2769 

2770 Examples: 

2771 Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N = 

2772 string_leaf.parent`. Assume the node `N` has the following 

2773 original structure: 

2774 

2775 Node( 

2776 expr_stmt, [ 

2777 Leaf(NAME, 'x'), 

2778 Leaf(EQUAL, '='), 

2779 Leaf(STRING, '"foo"'), 

2780 ] 

2781 ) 

2782 

2783 We then run the code snippet shown below. 

2784 ``` 

2785 insert_str_child = insert_str_child_factory(string_leaf) 

2786 

2787 lpar = Leaf(token.LPAR, '(') 

2788 insert_str_child(lpar) 

2789 

2790 bar = Leaf(token.STRING, '"bar"') 

2791 insert_str_child(bar) 

2792 

2793 rpar = Leaf(token.RPAR, ')') 

2794 insert_str_child(rpar) 

2795 ``` 

2796 

2797 After which point, it follows that `string_leaf.parent is None` and 

2798 the node `N` now has the following structure: 

2799 

2800 Node( 

2801 expr_stmt, [ 

2802 Leaf(NAME, 'x'), 

2803 Leaf(EQUAL, '='), 

2804 Leaf(LPAR, '('), 

2805 Leaf(STRING, '"bar"'), 

2806 Leaf(RPAR, ')'), 

2807 ] 

2808 ) 

2809 """ 

2810 string_parent = string_leaf.parent 

2811 string_child_idx = string_leaf.remove() 

2812 

2813 def insert_str_child(child: LN) -> None: 

2814 nonlocal string_child_idx 

2815 

2816 assert string_parent is not None 

2817 assert string_child_idx is not None 

2818 

2819 string_parent.insert_child(string_child_idx, child) 

2820 string_child_idx += 1 

2821 

2822 return insert_str_child 

2823 

2824 

2825def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]: 

2826 """ 

2827 Examples: 

2828 ``` 

2829 my_list = [1, 2, 3] 

2830 

2831 is_valid_index = is_valid_index_factory(my_list) 

2832 

2833 assert is_valid_index(0) 

2834 assert is_valid_index(2) 

2835 

2836 assert not is_valid_index(3) 

2837 assert not is_valid_index(-1) 

2838 ``` 

2839 """ 

2840 

2841 def is_valid_index(idx: int) -> bool: 

2842 """ 

2843 Returns: 

2844 True iff @idx is positive AND seq[@idx] does NOT raise an 

2845 IndexError. 

2846 """ 

2847 return 0 <= idx < len(seq) 

2848 

2849 return is_valid_index