Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/black/brackets.py: 26%
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, Sequence
4from dataclasses import dataclass, field
5from typing import Final, Optional, 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: Optional[Leaf] = 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) -> Optional[Leaf]:
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 return COMMA_PRIORITY
230 return 0
233def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority:
234 """Return the priority of the `leaf` delimiter, given a line break before it.
236 The delimiter priorities returned here are from those delimiters that would
237 cause a line break before themselves.
239 Higher numbers are higher priority.
240 """
241 if is_vararg(leaf, within=VARARGS_PARENTS | UNPACKING_PARENTS):
242 # * and ** might also be MATH_OPERATORS but in this case they are not.
243 # Don't treat them as a delimiter.
244 return 0
246 if (
247 leaf.type == token.DOT
248 and leaf.parent
249 and leaf.parent.type not in {syms.import_from, syms.dotted_name}
250 and (previous is None or previous.type in CLOSING_BRACKETS)
251 ):
252 return DOT_PRIORITY
254 if (
255 leaf.type in MATH_OPERATORS
256 and leaf.parent
257 and leaf.parent.type not in {syms.factor, syms.star_expr}
258 ):
259 return MATH_PRIORITIES[leaf.type]
261 if leaf.type in COMPARATORS:
262 return COMPARATOR_PRIORITY
264 if (
265 leaf.type == token.STRING
266 and previous is not None
267 and previous.type == token.STRING
268 ):
269 return STRING_PRIORITY
271 if leaf.type not in {token.NAME, token.ASYNC}:
272 return 0
274 if (
275 leaf.value == "for"
276 and leaf.parent
277 and leaf.parent.type in {syms.comp_for, syms.old_comp_for}
278 or leaf.type == token.ASYNC
279 ):
280 if (
281 not isinstance(leaf.prev_sibling, Leaf)
282 or leaf.prev_sibling.value != "async"
283 ):
284 return COMPREHENSION_PRIORITY
286 if (
287 leaf.value == "if"
288 and leaf.parent
289 and leaf.parent.type in {syms.comp_if, syms.old_comp_if}
290 ):
291 return COMPREHENSION_PRIORITY
293 if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test:
294 return TERNARY_PRIORITY
296 if leaf.value == "is":
297 return COMPARATOR_PRIORITY
299 if (
300 leaf.value == "in"
301 and leaf.parent
302 and leaf.parent.type in {syms.comp_op, syms.comparison}
303 and not (
304 previous is not None
305 and previous.type == token.NAME
306 and previous.value == "not"
307 )
308 ):
309 return COMPARATOR_PRIORITY
311 if (
312 leaf.value == "not"
313 and leaf.parent
314 and leaf.parent.type == syms.comp_op
315 and not (
316 previous is not None
317 and previous.type == token.NAME
318 and previous.value == "is"
319 )
320 ):
321 return COMPARATOR_PRIORITY
323 if leaf.value in LOGIC_OPERATORS and leaf.parent:
324 return LOGIC_PRIORITY
326 return 0
329def max_delimiter_priority_in_atom(node: LN) -> Priority:
330 """Return maximum delimiter priority inside `node`.
332 This is specific to atoms with contents contained in a pair of parentheses.
333 If `node` isn't an atom or there are no enclosing parentheses, returns 0.
334 """
335 if node.type != syms.atom:
336 return 0
338 first = node.children[0]
339 last = node.children[-1]
340 if not (first.type == token.LPAR and last.type == token.RPAR):
341 return 0
343 bt = BracketTracker()
344 for c in node.children[1:-1]:
345 if isinstance(c, Leaf):
346 bt.mark(c)
347 else:
348 for leaf in c.leaves():
349 bt.mark(leaf)
350 try:
351 return bt.max_delimiter_priority()
353 except ValueError:
354 return 0
357def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> set[LeafID]:
358 """Return leaves that are inside matching brackets.
360 The input `leaves` can have non-matching brackets at the head or tail parts.
361 Matching brackets are included.
362 """
363 try:
364 # Start with the first opening bracket and ignore closing brackets before.
365 start_index = next(
366 i for i, l in enumerate(leaves) if l.type in OPENING_BRACKETS
367 )
368 except StopIteration:
369 return set()
370 bracket_stack = []
371 ids = set()
372 for i in range(start_index, len(leaves)):
373 leaf = leaves[i]
374 if leaf.type in OPENING_BRACKETS:
375 bracket_stack.append((BRACKET[leaf.type], i))
376 if leaf.type in CLOSING_BRACKETS:
377 if bracket_stack and leaf.type == bracket_stack[-1][0]:
378 _, start = bracket_stack.pop()
379 for j in range(start, i + 1):
380 ids.add(id(leaves[j]))
381 else:
382 break
383 return ids