Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/black/brackets.py: 95%
167 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:15 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:15 +0000
1"""Builds on top of nodes.py to track brackets."""
3import sys
4from dataclasses import dataclass, field
5from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
7if sys.version_info < (3, 8):
8 from typing_extensions import Final
9else:
10 from typing import Final
12from black.nodes import (
13 BRACKET,
14 CLOSING_BRACKETS,
15 COMPARATORS,
16 LOGIC_OPERATORS,
17 MATH_OPERATORS,
18 OPENING_BRACKETS,
19 UNPACKING_PARENTS,
20 VARARGS_PARENTS,
21 is_vararg,
22 syms,
23)
24from blib2to3.pgen2 import token
25from blib2to3.pytree import Leaf, Node
27# types
28LN = Union[Leaf, Node]
29Depth = int
30LeafID = int
31NodeType = int
32Priority = int
35COMPREHENSION_PRIORITY: Final = 20
36COMMA_PRIORITY: Final = 18
37TERNARY_PRIORITY: Final = 16
38LOGIC_PRIORITY: Final = 14
39STRING_PRIORITY: Final = 12
40COMPARATOR_PRIORITY: Final = 10
41MATH_PRIORITIES: Final = {
42 token.VBAR: 9,
43 token.CIRCUMFLEX: 8,
44 token.AMPER: 7,
45 token.LEFTSHIFT: 6,
46 token.RIGHTSHIFT: 6,
47 token.PLUS: 5,
48 token.MINUS: 5,
49 token.STAR: 4,
50 token.SLASH: 4,
51 token.DOUBLESLASH: 4,
52 token.PERCENT: 4,
53 token.AT: 4,
54 token.TILDE: 3,
55 token.DOUBLESTAR: 2,
56}
57DOT_PRIORITY: Final = 1
60class BracketMatchError(Exception):
61 """Raised when an opening bracket is unable to be matched to a closing bracket."""
64@dataclass
65class BracketTracker:
66 """Keeps track of brackets on a line."""
68 depth: int = 0
69 bracket_match: Dict[Tuple[Depth, NodeType], Leaf] = field(default_factory=dict)
70 delimiters: Dict[LeafID, Priority] = field(default_factory=dict)
71 previous: Optional[Leaf] = None
72 _for_loop_depths: List[int] = field(default_factory=list)
73 _lambda_argument_depths: List[int] = field(default_factory=list)
74 invisible: List[Leaf] = field(default_factory=list)
76 def mark(self, leaf: Leaf) -> None:
77 """Mark `leaf` with bracket-related metadata. Keep track of delimiters.
79 All leaves receive an int `bracket_depth` field that stores how deep
80 within brackets a given leaf is. 0 means there are no enclosing brackets
81 that started on this line.
83 If a leaf is itself a closing bracket and there is a matching opening
84 bracket earlier, it receives an `opening_bracket` field with which it forms a
85 pair. This is a one-directional link to avoid reference cycles. Closing
86 bracket without opening happens on lines continued from previous
87 breaks, e.g. `) -> "ReturnType":` as part of a funcdef where we place
88 the return type annotation on its own line of the previous closing RPAR.
90 If a leaf is a delimiter (a token on which Black can split the line if
91 needed) and it's on depth 0, its `id()` is stored in the tracker's
92 `delimiters` field.
93 """
94 if leaf.type == token.COMMENT:
95 return
97 if (
98 self.depth == 0
99 and leaf.type in CLOSING_BRACKETS
100 and (self.depth, leaf.type) not in self.bracket_match
101 ):
102 return
104 self.maybe_decrement_after_for_loop_variable(leaf)
105 self.maybe_decrement_after_lambda_arguments(leaf)
106 if leaf.type in CLOSING_BRACKETS:
107 self.depth -= 1
108 try:
109 opening_bracket = self.bracket_match.pop((self.depth, leaf.type))
110 except KeyError as e:
111 raise BracketMatchError(
112 "Unable to match a closing bracket to the following opening"
113 f" bracket: {leaf}"
114 ) from e
115 leaf.opening_bracket = opening_bracket
116 if not leaf.value:
117 self.invisible.append(leaf)
118 leaf.bracket_depth = self.depth
119 if self.depth == 0:
120 delim = is_split_before_delimiter(leaf, self.previous)
121 if delim and self.previous is not None:
122 self.delimiters[id(self.previous)] = delim
123 else:
124 delim = is_split_after_delimiter(leaf, self.previous)
125 if delim:
126 self.delimiters[id(leaf)] = delim
127 if leaf.type in OPENING_BRACKETS:
128 self.bracket_match[self.depth, BRACKET[leaf.type]] = leaf
129 self.depth += 1
130 if not leaf.value:
131 self.invisible.append(leaf)
132 self.previous = leaf
133 self.maybe_increment_lambda_arguments(leaf)
134 self.maybe_increment_for_loop_variable(leaf)
136 def any_open_brackets(self) -> bool:
137 """Return True if there is an yet unmatched open bracket on the line."""
138 return bool(self.bracket_match)
140 def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority:
141 """Return the highest priority of a delimiter found on the line.
143 Values are consistent with what `is_split_*_delimiter()` return.
144 Raises ValueError on no delimiters.
145 """
146 return max(v for k, v in self.delimiters.items() if k not in exclude)
148 def delimiter_count_with_priority(self, priority: Priority = 0) -> int:
149 """Return the number of delimiters with the given `priority`.
151 If no `priority` is passed, defaults to max priority on the line.
152 """
153 if not self.delimiters:
154 return 0
156 priority = priority or self.max_delimiter_priority()
157 return sum(1 for p in self.delimiters.values() if p == priority)
159 def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool:
160 """In a for loop, or comprehension, the variables are often unpacks.
162 To avoid splitting on the comma in this situation, increase the depth of
163 tokens between `for` and `in`.
164 """
165 if leaf.type == token.NAME and leaf.value == "for":
166 self.depth += 1
167 self._for_loop_depths.append(self.depth)
168 return True
170 return False
172 def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool:
173 """See `maybe_increment_for_loop_variable` above for explanation."""
174 if (
175 self._for_loop_depths
176 and self._for_loop_depths[-1] == self.depth
177 and leaf.type == token.NAME
178 and leaf.value == "in"
179 ):
180 self.depth -= 1
181 self._for_loop_depths.pop()
182 return True
184 return False
186 def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool:
187 """In a lambda expression, there might be more than one argument.
189 To avoid splitting on the comma in this situation, increase the depth of
190 tokens between `lambda` and `:`.
191 """
192 if leaf.type == token.NAME and leaf.value == "lambda":
193 self.depth += 1
194 self._lambda_argument_depths.append(self.depth)
195 return True
197 return False
199 def maybe_decrement_after_lambda_arguments(self, leaf: Leaf) -> bool:
200 """See `maybe_increment_lambda_arguments` above for explanation."""
201 if (
202 self._lambda_argument_depths
203 and self._lambda_argument_depths[-1] == self.depth
204 and leaf.type == token.COLON
205 ):
206 self.depth -= 1
207 self._lambda_argument_depths.pop()
208 return True
210 return False
212 def get_open_lsqb(self) -> Optional[Leaf]:
213 """Return the most recent opening square bracket (if any)."""
214 return self.bracket_match.get((self.depth - 1, token.RSQB))
217def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority:
218 """Return the priority of the `leaf` delimiter, given a line break after it.
220 The delimiter priorities returned here are from those delimiters that would
221 cause a line break after themselves.
223 Higher numbers are higher priority.
224 """
225 if leaf.type == token.COMMA:
226 return COMMA_PRIORITY
228 return 0
231def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority:
232 """Return the priority of the `leaf` delimiter, given a line break before it.
234 The delimiter priorities returned here are from those delimiters that would
235 cause a line break before themselves.
237 Higher numbers are higher priority.
238 """
239 if is_vararg(leaf, within=VARARGS_PARENTS | UNPACKING_PARENTS):
240 # * and ** might also be MATH_OPERATORS but in this case they are not.
241 # Don't treat them as a delimiter.
242 return 0
244 if (
245 leaf.type == token.DOT
246 and leaf.parent
247 and leaf.parent.type not in {syms.import_from, syms.dotted_name}
248 and (previous is None or previous.type in CLOSING_BRACKETS)
249 ):
250 return DOT_PRIORITY
252 if (
253 leaf.type in MATH_OPERATORS
254 and leaf.parent
255 and leaf.parent.type not in {syms.factor, syms.star_expr}
256 ):
257 return MATH_PRIORITIES[leaf.type]
259 if leaf.type in COMPARATORS:
260 return COMPARATOR_PRIORITY
262 if (
263 leaf.type == token.STRING
264 and previous is not None
265 and previous.type == token.STRING
266 ):
267 return STRING_PRIORITY
269 if leaf.type not in {token.NAME, token.ASYNC}:
270 return 0
272 if (
273 leaf.value == "for"
274 and leaf.parent
275 and leaf.parent.type in {syms.comp_for, syms.old_comp_for}
276 or leaf.type == token.ASYNC
277 ):
278 if (
279 not isinstance(leaf.prev_sibling, Leaf)
280 or leaf.prev_sibling.value != "async"
281 ):
282 return COMPREHENSION_PRIORITY
284 if (
285 leaf.value == "if"
286 and leaf.parent
287 and leaf.parent.type in {syms.comp_if, syms.old_comp_if}
288 ):
289 return COMPREHENSION_PRIORITY
291 if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test:
292 return TERNARY_PRIORITY
294 if leaf.value == "is":
295 return COMPARATOR_PRIORITY
297 if (
298 leaf.value == "in"
299 and leaf.parent
300 and leaf.parent.type in {syms.comp_op, syms.comparison}
301 and not (
302 previous is not None
303 and previous.type == token.NAME
304 and previous.value == "not"
305 )
306 ):
307 return COMPARATOR_PRIORITY
309 if (
310 leaf.value == "not"
311 and leaf.parent
312 and leaf.parent.type == syms.comp_op
313 and not (
314 previous is not None
315 and previous.type == token.NAME
316 and previous.value == "is"
317 )
318 ):
319 return COMPARATOR_PRIORITY
321 if leaf.value in LOGIC_OPERATORS and leaf.parent:
322 return LOGIC_PRIORITY
324 return 0
327def max_delimiter_priority_in_atom(node: LN) -> Priority:
328 """Return maximum delimiter priority inside `node`.
330 This is specific to atoms with contents contained in a pair of parentheses.
331 If `node` isn't an atom or there are no enclosing parentheses, returns 0.
332 """
333 if node.type != syms.atom:
334 return 0
336 first = node.children[0]
337 last = node.children[-1]
338 if not (first.type == token.LPAR and last.type == token.RPAR):
339 return 0
341 bt = BracketTracker()
342 for c in node.children[1:-1]:
343 if isinstance(c, Leaf):
344 bt.mark(c)
345 else:
346 for leaf in c.leaves():
347 bt.mark(leaf)
348 try:
349 return bt.max_delimiter_priority()
351 except ValueError:
352 return 0
355def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> Set[LeafID]:
356 """Return leaves that are inside matching brackets.
358 The input `leaves` can have non-matching brackets at the head or tail parts.
359 Matching brackets are included.
360 """
361 try:
362 # Start with the first opening bracket and ignore closing brackets before.
363 start_index = next(
364 i for i, l in enumerate(leaves) if l.type in OPENING_BRACKETS
365 )
366 except StopIteration:
367 return set()
368 bracket_stack = []
369 ids = set()
370 for i in range(start_index, len(leaves)):
371 leaf = leaves[i]
372 if leaf.type in OPENING_BRACKETS:
373 bracket_stack.append((BRACKET[leaf.type], i))
374 if leaf.type in CLOSING_BRACKETS:
375 if bracket_stack and leaf.type == bracket_stack[-1][0]:
376 _, start = bracket_stack.pop()
377 for j in range(start, i + 1):
378 ids.add(id(leaves[j]))
379 else:
380 break
381 return ids