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 # 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
250 return 0
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.
256 The delimiter priorities returned here are from those delimiters that would
257 cause a line break before themselves.
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
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
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]
281 if leaf.type in COMPARATORS:
282 return COMPARATOR_PRIORITY
284 if (
285 leaf.type == token.STRING
286 and previous is not None
287 and previous.type == token.STRING
288 ):
289 return STRING_PRIORITY
291 if leaf.type not in {token.NAME, token.ASYNC}:
292 return 0
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
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
313 if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test:
314 return TERNARY_PRIORITY
316 if leaf.value == "is":
317 return COMPARATOR_PRIORITY
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
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
343 if leaf.value in LOGIC_OPERATORS and leaf.parent:
344 return LOGIC_PRIORITY
346 return 0
349def max_delimiter_priority_in_atom(node: LN) -> Priority:
350 """Return maximum delimiter priority inside `node`.
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
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
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()
369 except ValueError:
370 return 0
373def _top_level_leaves(children: Iterable[LN]) -> Iterator[Leaf]:
374 """Yield the leaves that can be top-level (depth 0) delimiters of an atom.
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)
403def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> set[LeafID]:
404 """Return leaves that are inside matching brackets.
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