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
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
1"""Builds on top of nodes.py to track brackets."""
3from collections.abc import Iterable, Iterator, Sequence
4from dataclasses import dataclass, field
5from typing import Final, Union
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
22# types
23LN = Union[Leaf, Node]
24Depth = int
25LeafID = int
26NodeType = int
27Priority = int
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
55class BracketMatchError(Exception):
56 """Raised when an opening bracket is unable to be matched to a closing bracket."""
59@dataclass
60class BracketTracker:
61 """Keeps track of brackets on a line."""
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)
71 def mark(self, leaf: Leaf) -> None:
72 """Mark `leaf` with bracket-related metadata. Keep track of delimiters.
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.
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.
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
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
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)
131 def any_open_for_or_lambda(self) -> bool:
132 """Return True if there is an open for or lambda expression on the line.
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)
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)
142 def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority:
143 """Return the highest priority of a delimiter found on the line.
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)
150 def delimiter_count_with_priority(self, priority: Priority = 0) -> int:
151 """Return the number of delimiters with the given `priority`.
153 If no `priority` is passed, defaults to max priority on the line.
154 """
155 if not self.delimiters:
156 return 0
158 priority = priority or self.max_delimiter_priority()
159 return sum(1 for p in self.delimiters.values() if p == priority)
161 def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool:
162 """In a for loop, or comprehension, the variables are often unpacks.
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
172 return False
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
186 return False
188 def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool:
189 """In a lambda expression, there might be more than one argument.
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
199 return False
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
212 return False
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))
219def is_split_after_delimiter(leaf: Leaf) -> Priority:
220 """Return the priority of the `leaf` delimiter, given a line break after it.
222 The delimiter priorities returned here are from those delimiters that would
223 cause a line break after themselves.
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
241 return 0
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.
247 The delimiter priorities returned here are from those delimiters that would
248 cause a line break before themselves.
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
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
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]
272 if leaf.type in COMPARATORS:
273 return COMPARATOR_PRIORITY
275 if (
276 leaf.type == token.STRING
277 and previous is not None
278 and previous.type == token.STRING
279 ):
280 return STRING_PRIORITY
282 if leaf.type not in {token.NAME, token.ASYNC}:
283 return 0
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
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
304 if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test:
305 return TERNARY_PRIORITY
307 if leaf.value == "is":
308 return COMPARATOR_PRIORITY
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
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
334 if leaf.value in LOGIC_OPERATORS and leaf.parent:
335 return LOGIC_PRIORITY
337 return 0
340def max_delimiter_priority_in_atom(node: LN) -> Priority:
341 """Return maximum delimiter priority inside `node`.
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
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
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()
360 except ValueError:
361 return 0
364def _top_level_leaves(children: Iterable[LN]) -> Iterator[Leaf]:
365 """Yield the leaves that can be top-level (depth 0) delimiters of an atom.
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)
394def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> set[LeafID]:
395 """Return leaves that are inside matching brackets.
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