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

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

188 statements  

1"""Builds on top of nodes.py to track brackets.""" 

2 

3from collections.abc import Iterable, Iterator, Sequence 

4from dataclasses import dataclass, field 

5from typing import Final, Union 

6 

7from black.nodes import ( 

8 BRACKET, 

9 CLOSING_BRACKETS, 

10 COMPARATORS, 

11 LOGIC_OPERATORS, 

12 MATH_OPERATORS, 

13 OPENING_BRACKETS, 

14 UNPACKING_PARENTS, 

15 VARARGS_PARENTS, 

16 is_vararg, 

17 syms, 

18) 

19from blib2to3.pgen2 import token 

20from blib2to3.pytree import Leaf, Node 

21 

22# types 

23LN = Union[Leaf, Node] 

24Depth = int 

25LeafID = int 

26NodeType = int 

27Priority = int 

28 

29 

30COMPREHENSION_PRIORITY: Final = 20 

31COMMA_PRIORITY: Final = 18 

32TERNARY_PRIORITY: Final = 16 

33LOGIC_PRIORITY: Final = 14 

34STRING_PRIORITY: Final = 12 

35COMPARATOR_PRIORITY: Final = 10 

36MATH_PRIORITIES: Final = { 

37 token.VBAR: 9, 

38 token.CIRCUMFLEX: 8, 

39 token.AMPER: 7, 

40 token.LEFTSHIFT: 6, 

41 token.RIGHTSHIFT: 6, 

42 token.PLUS: 5, 

43 token.MINUS: 5, 

44 token.STAR: 4, 

45 token.SLASH: 4, 

46 token.DOUBLESLASH: 4, 

47 token.PERCENT: 4, 

48 token.AT: 4, 

49 token.TILDE: 3, 

50 token.DOUBLESTAR: 2, 

51} 

52DOT_PRIORITY: Final = 1 

53 

54 

55class BracketMatchError(Exception): 

56 """Raised when an opening bracket is unable to be matched to a closing bracket.""" 

57 

58 

59@dataclass 

60class BracketTracker: 

61 """Keeps track of brackets on a line.""" 

62 

63 depth: int = 0 

64 bracket_match: dict[tuple[Depth, NodeType], Leaf] = field(default_factory=dict) 

65 delimiters: dict[LeafID, Priority] = field(default_factory=dict) 

66 previous: Leaf | None = None 

67 _for_loop_depths: list[int] = field(default_factory=list) 

68 _lambda_argument_depths: list[int] = field(default_factory=list) 

69 invisible: list[Leaf] = field(default_factory=list) 

70 

71 def mark(self, leaf: Leaf) -> None: 

72 """Mark `leaf` with bracket-related metadata. Keep track of delimiters. 

73 

74 All leaves receive an int `bracket_depth` field that stores how deep 

75 within brackets a given leaf is. 0 means there are no enclosing brackets 

76 that started on this line. 

77 

78 If a leaf is itself a closing bracket and there is a matching opening 

79 bracket earlier, it receives an `opening_bracket` field with which it forms a 

80 pair. This is a one-directional link to avoid reference cycles. Closing 

81 bracket without opening happens on lines continued from previous 

82 breaks, e.g. `) -> "ReturnType":` as part of a funcdef where we place 

83 the return type annotation on its own line of the previous closing RPAR. 

84 

85 If a leaf is a delimiter (a token on which Black can split the line if 

86 needed) and it's on depth 0, its `id()` is stored in the tracker's 

87 `delimiters` field. 

88 """ 

89 if leaf.type == token.COMMENT: 

90 return 

91 

92 if ( 

93 self.depth == 0 

94 and leaf.type in CLOSING_BRACKETS 

95 and (self.depth, leaf.type) not in self.bracket_match 

96 ): 

97 return 

98 

99 self.maybe_decrement_after_for_loop_variable(leaf) 

100 self.maybe_decrement_after_lambda_arguments(leaf) 

101 if leaf.type in CLOSING_BRACKETS: 

102 self.depth -= 1 

103 try: 

104 opening_bracket = self.bracket_match.pop((self.depth, leaf.type)) 

105 except KeyError as e: 

106 raise BracketMatchError( 

107 "Unable to match a closing bracket to the following opening" 

108 f" bracket: {leaf}" 

109 ) from e 

110 leaf.opening_bracket = opening_bracket 

111 if not leaf.value: 

112 self.invisible.append(leaf) 

113 leaf.bracket_depth = self.depth 

114 if self.depth == 0: 

115 delim = is_split_before_delimiter(leaf, self.previous) 

116 if delim and self.previous is not None: 

117 self.delimiters[id(self.previous)] = delim 

118 else: 

119 delim = is_split_after_delimiter(leaf) 

120 if delim: 

121 self.delimiters[id(leaf)] = delim 

122 if leaf.type in OPENING_BRACKETS: 

123 self.bracket_match[self.depth, BRACKET[leaf.type]] = leaf 

124 self.depth += 1 

125 if not leaf.value: 

126 self.invisible.append(leaf) 

127 self.previous = leaf 

128 self.maybe_increment_lambda_arguments(leaf) 

129 self.maybe_increment_for_loop_variable(leaf) 

130 

131 def any_open_for_or_lambda(self) -> bool: 

132 """Return True if there is an open for or lambda expression on the line. 

133 

134 See maybe_increment_for_loop_variable and maybe_increment_lambda_arguments 

135 for details.""" 

136 return bool(self._for_loop_depths or self._lambda_argument_depths) 

137 

138 def any_open_brackets(self) -> bool: 

139 """Return True if there is an yet unmatched open bracket on the line.""" 

140 return bool(self.bracket_match) 

141 

142 def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority: 

143 """Return the highest priority of a delimiter found on the line. 

144 

145 Values are consistent with what `is_split_*_delimiter()` return. 

146 Raises ValueError on no delimiters. 

147 """ 

148 return max(v for k, v in self.delimiters.items() if k not in exclude) 

149 

150 def delimiter_count_with_priority(self, priority: Priority = 0) -> int: 

151 """Return the number of delimiters with the given `priority`. 

152 

153 If no `priority` is passed, defaults to max priority on the line. 

154 """ 

155 if not self.delimiters: 

156 return 0 

157 

158 priority = priority or self.max_delimiter_priority() 

159 return sum(1 for p in self.delimiters.values() if p == priority) 

160 

161 def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool: 

162 """In a for loop, or comprehension, the variables are often unpacks. 

163 

164 To avoid splitting on the comma in this situation, increase the depth of 

165 tokens between `for` and `in`. 

166 """ 

167 if leaf.type == token.NAME and leaf.value == "for": 

168 self.depth += 1 

169 self._for_loop_depths.append(self.depth) 

170 return True 

171 

172 return False 

173 

174 def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool: 

175 """See `maybe_increment_for_loop_variable` above for explanation.""" 

176 if ( 

177 self._for_loop_depths 

178 and self._for_loop_depths[-1] == self.depth 

179 and leaf.type == token.NAME 

180 and leaf.value == "in" 

181 ): 

182 self.depth -= 1 

183 self._for_loop_depths.pop() 

184 return True 

185 

186 return False 

187 

188 def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool: 

189 """In a lambda expression, there might be more than one argument. 

190 

191 To avoid splitting on the comma in this situation, increase the depth of 

192 tokens between `lambda` and `:`. 

193 """ 

194 if leaf.type == token.NAME and leaf.value == "lambda": 

195 self.depth += 1 

196 self._lambda_argument_depths.append(self.depth) 

197 return True 

198 

199 return False 

200 

201 def maybe_decrement_after_lambda_arguments(self, leaf: Leaf) -> bool: 

202 """See `maybe_increment_lambda_arguments` above for explanation.""" 

203 if ( 

204 self._lambda_argument_depths 

205 and self._lambda_argument_depths[-1] == self.depth 

206 and leaf.type == token.COLON 

207 ): 

208 self.depth -= 1 

209 self._lambda_argument_depths.pop() 

210 return True 

211 

212 return False 

213 

214 def get_open_lsqb(self) -> Leaf | None: 

215 """Return the most recent opening square bracket (if any).""" 

216 return self.bracket_match.get((self.depth - 1, token.RSQB)) 

217 

218 

219def is_split_after_delimiter(leaf: Leaf) -> Priority: 

220 """Return the priority of the `leaf` delimiter, given a line break after it. 

221 

222 The delimiter priorities returned here are from those delimiters that would 

223 cause a line break after themselves. 

224 

225 Higher numbers are higher priority. 

226 """ 

227 if leaf.type == token.COMMA: 

228 # For-target commas are tuple-packing, not list delimiters; splitting 

229 # on them produces invalid Python if `for` lands on another line (#4296). 

230 parent = leaf.parent 

231 if parent is not None and parent.type == syms.exprlist: 

232 grandparent = parent.parent 

233 if grandparent is not None and grandparent.type in { 

234 syms.comp_for, 

235 syms.old_comp_for, 

236 syms.for_stmt, 

237 }: 

238 return 0 

239 # Lambda parameter commas aren't delimiters either. They are normally hidden 

240 # by the lambda depth, but not when a comment splits `lambda` from them. 

241 if parent is not None and parent.type == syms.varargslist: 

242 grandparent = parent.parent 

243 if grandparent is not None and grandparent.type in { 

244 syms.lambdef, 

245 syms.old_lambdef, 

246 }: 

247 return 0 

248 return COMMA_PRIORITY 

249 

250 return 0 

251 

252 

253def is_split_before_delimiter(leaf: Leaf, previous: Leaf | None = None) -> Priority: 

254 """Return the priority of the `leaf` delimiter, given a line break before it. 

255 

256 The delimiter priorities returned here are from those delimiters that would 

257 cause a line break before themselves. 

258 

259 Higher numbers are higher priority. 

260 """ 

261 if is_vararg(leaf, within=VARARGS_PARENTS | UNPACKING_PARENTS): 

262 # * and ** might also be MATH_OPERATORS but in this case they are not. 

263 # Don't treat them as a delimiter. 

264 return 0 

265 

266 if ( 

267 leaf.type == token.DOT 

268 and leaf.parent 

269 and leaf.parent.type not in {syms.import_from, syms.dotted_name} 

270 and (previous is None or previous.type in CLOSING_BRACKETS) 

271 ): 

272 return DOT_PRIORITY 

273 

274 if ( 

275 leaf.type in MATH_OPERATORS 

276 and leaf.parent 

277 and leaf.parent.type not in {syms.factor, syms.star_expr} 

278 ): 

279 return MATH_PRIORITIES[leaf.type] 

280 

281 if leaf.type in COMPARATORS: 

282 return COMPARATOR_PRIORITY 

283 

284 if ( 

285 leaf.type == token.STRING 

286 and previous is not None 

287 and previous.type == token.STRING 

288 ): 

289 return STRING_PRIORITY 

290 

291 if leaf.type not in {token.NAME, token.ASYNC}: 

292 return 0 

293 

294 if ( 

295 leaf.value == "for" 

296 and leaf.parent 

297 and leaf.parent.type in {syms.comp_for, syms.old_comp_for} 

298 or leaf.type == token.ASYNC 

299 ): 

300 if ( 

301 not isinstance(leaf.prev_sibling, Leaf) 

302 or leaf.prev_sibling.value != "async" 

303 ): 

304 return COMPREHENSION_PRIORITY 

305 

306 if ( 

307 leaf.value == "if" 

308 and leaf.parent 

309 and leaf.parent.type in {syms.comp_if, syms.old_comp_if} 

310 ): 

311 return COMPREHENSION_PRIORITY 

312 

313 if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test: 

314 return TERNARY_PRIORITY 

315 

316 if leaf.value == "is": 

317 return COMPARATOR_PRIORITY 

318 

319 if ( 

320 leaf.value == "in" 

321 and leaf.parent 

322 and leaf.parent.type in {syms.comp_op, syms.comparison} 

323 and not ( 

324 previous is not None 

325 and previous.type == token.NAME 

326 and previous.value == "not" 

327 ) 

328 ): 

329 return COMPARATOR_PRIORITY 

330 

331 if ( 

332 leaf.value == "not" 

333 and leaf.parent 

334 and leaf.parent.type == syms.comp_op 

335 and not ( 

336 previous is not None 

337 and previous.type == token.NAME 

338 and previous.value == "is" 

339 ) 

340 ): 

341 return COMPARATOR_PRIORITY 

342 

343 if leaf.value in LOGIC_OPERATORS and leaf.parent: 

344 return LOGIC_PRIORITY 

345 

346 return 0 

347 

348 

349def max_delimiter_priority_in_atom(node: LN) -> Priority: 

350 """Return maximum delimiter priority inside `node`. 

351 

352 This is specific to atoms with contents contained in a pair of parentheses. 

353 If `node` isn't an atom or there are no enclosing parentheses, returns 0. 

354 """ 

355 if node.type != syms.atom: 

356 return 0 

357 

358 first = node.children[0] 

359 last = node.children[-1] 

360 if not (first.type == token.LPAR and last.type == token.RPAR): 

361 return 0 

362 

363 bt = BracketTracker() 

364 for leaf in _top_level_leaves(node.children[1:-1]): 

365 bt.mark(leaf) 

366 try: 

367 return bt.max_delimiter_priority() 

368 

369 except ValueError: 

370 return 0 

371 

372 

373def _top_level_leaves(children: Iterable[LN]) -> Iterator[Leaf]: 

374 """Yield the leaves that can be top-level (depth 0) delimiters of an atom. 

375 

376 Anything inside a nested bracket pair is at depth >= 1 and therefore cannot 

377 be a top-level delimiter, so only the pair's brackets are kept (to balance 

378 the depth tracker) and its interior is skipped. This keeps the scan 

379 proportional to the atom's own length instead of re-walking every descendant 

380 on every call, which matters for deeply nested expressions like 

381 ``((((...))))``. 

382 """ 

383 for child in children: 

384 if isinstance(child, Leaf): 

385 yield child 

386 continue 

387 if not child.children: 

388 continue 

389 first = child.children[0] 

390 last = child.children[-1] 

391 if ( 

392 isinstance(first, Leaf) 

393 and isinstance(last, Leaf) 

394 and first.type in OPENING_BRACKETS 

395 and last.type in CLOSING_BRACKETS 

396 ): 

397 yield first 

398 yield last 

399 else: 

400 yield from _top_level_leaves(child.children) 

401 

402 

403def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> set[LeafID]: 

404 """Return leaves that are inside matching brackets. 

405 

406 The input `leaves` can have non-matching brackets at the head or tail parts. 

407 Matching brackets are included. 

408 """ 

409 try: 

410 # Start with the first opening bracket and ignore closing brackets before. 

411 start_index = next( 

412 i for i, l in enumerate(leaves) if l.type in OPENING_BRACKETS 

413 ) 

414 except StopIteration: 

415 return set() 

416 # Each open bracket collects the ids of the leaves at its own nesting level. 

417 # When the matching closing bracket is reached the pair is confirmed as inside 

418 # matching brackets, so those ids are kept (the ids of any nested pair are 

419 # already kept, having been flushed when that inner pair closed). Unmatched 

420 # opening brackets are dropped along with the ids they collected. Collecting 

421 # each leaf under a single level keeps this linear instead of re-adding every 

422 # enclosed leaf once per surrounding bracket. 

423 ids: set[LeafID] = set() 

424 bracket_stack: list[tuple[int, list[LeafID]]] = [] 

425 for i in range(start_index, len(leaves)): 

426 leaf = leaves[i] 

427 if leaf.type in OPENING_BRACKETS: 

428 bracket_stack.append((BRACKET[leaf.type], [id(leaf)])) 

429 elif leaf.type in CLOSING_BRACKETS: 

430 if bracket_stack and leaf.type == bracket_stack[-1][0]: 

431 _, level_ids = bracket_stack.pop() 

432 level_ids.append(id(leaf)) 

433 ids.update(level_ids) 

434 else: 

435 break 

436 elif bracket_stack: 

437 bracket_stack[-1][1].append(id(leaf)) 

438 return ids