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

184 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 return COMMA_PRIORITY 

240 

241 return 0 

242 

243 

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

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

246 

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

248 cause a line break before themselves. 

249 

250 Higher numbers are higher priority. 

251 """ 

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

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

254 # Don't treat them as a delimiter. 

255 return 0 

256 

257 if ( 

258 leaf.type == token.DOT 

259 and leaf.parent 

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

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

262 ): 

263 return DOT_PRIORITY 

264 

265 if ( 

266 leaf.type in MATH_OPERATORS 

267 and leaf.parent 

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

269 ): 

270 return MATH_PRIORITIES[leaf.type] 

271 

272 if leaf.type in COMPARATORS: 

273 return COMPARATOR_PRIORITY 

274 

275 if ( 

276 leaf.type == token.STRING 

277 and previous is not None 

278 and previous.type == token.STRING 

279 ): 

280 return STRING_PRIORITY 

281 

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

283 return 0 

284 

285 if ( 

286 leaf.value == "for" 

287 and leaf.parent 

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

289 or leaf.type == token.ASYNC 

290 ): 

291 if ( 

292 not isinstance(leaf.prev_sibling, Leaf) 

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

294 ): 

295 return COMPREHENSION_PRIORITY 

296 

297 if ( 

298 leaf.value == "if" 

299 and leaf.parent 

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

301 ): 

302 return COMPREHENSION_PRIORITY 

303 

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

305 return TERNARY_PRIORITY 

306 

307 if leaf.value == "is": 

308 return COMPARATOR_PRIORITY 

309 

310 if ( 

311 leaf.value == "in" 

312 and leaf.parent 

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

314 and not ( 

315 previous is not None 

316 and previous.type == token.NAME 

317 and previous.value == "not" 

318 ) 

319 ): 

320 return COMPARATOR_PRIORITY 

321 

322 if ( 

323 leaf.value == "not" 

324 and leaf.parent 

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

326 and not ( 

327 previous is not None 

328 and previous.type == token.NAME 

329 and previous.value == "is" 

330 ) 

331 ): 

332 return COMPARATOR_PRIORITY 

333 

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

335 return LOGIC_PRIORITY 

336 

337 return 0 

338 

339 

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

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

342 

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

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

345 """ 

346 if node.type != syms.atom: 

347 return 0 

348 

349 first = node.children[0] 

350 last = node.children[-1] 

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

352 return 0 

353 

354 bt = BracketTracker() 

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

356 bt.mark(leaf) 

357 try: 

358 return bt.max_delimiter_priority() 

359 

360 except ValueError: 

361 return 0 

362 

363 

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

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

366 

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

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

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

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

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

372 ``((((...))))``. 

373 """ 

374 for child in children: 

375 if isinstance(child, Leaf): 

376 yield child 

377 continue 

378 if not child.children: 

379 continue 

380 first = child.children[0] 

381 last = child.children[-1] 

382 if ( 

383 isinstance(first, Leaf) 

384 and isinstance(last, Leaf) 

385 and first.type in OPENING_BRACKETS 

386 and last.type in CLOSING_BRACKETS 

387 ): 

388 yield first 

389 yield last 

390 else: 

391 yield from _top_level_leaves(child.children) 

392 

393 

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

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

396 

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

398 Matching brackets are included. 

399 """ 

400 try: 

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

402 start_index = next( 

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

404 ) 

405 except StopIteration: 

406 return set() 

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

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

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

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

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

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

413 # enclosed leaf once per surrounding bracket. 

414 ids: set[LeafID] = set() 

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

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

417 leaf = leaves[i] 

418 if leaf.type in OPENING_BRACKETS: 

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

420 elif leaf.type in CLOSING_BRACKETS: 

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

422 _, level_ids = bracket_stack.pop() 

423 level_ids.append(id(leaf)) 

424 ids.update(level_ids) 

425 else: 

426 break 

427 elif bracket_stack: 

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

429 return ids