All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 """Builds on top of nodes.py to track brackets."""
3 from dataclasses import dataclass, field
5 from typing import Dict, Iterable, List, Optional, Tuple, Union
7 if sys.version_info < (3, 8):
8 from typing_extensions import Final
10 from typing import Final
12 from blib2to3.pytree import Leaf, Node
13 from blib2to3.pgen2 import token
15 from black.nodes import syms, is_vararg, VARARGS_PARENTS, UNPACKING_PARENTS
16 from black.nodes import BRACKET, OPENING_BRACKETS, CLOSING_BRACKETS
17 from black.nodes import MATH_OPERATORS, COMPARATORS, LOGIC_OPERATORS
20 LN = Union[Leaf, Node]
27 COMPREHENSION_PRIORITY: Final = 20
28 COMMA_PRIORITY: Final = 18
29 TERNARY_PRIORITY: Final = 16
30 LOGIC_PRIORITY: Final = 14
31 STRING_PRIORITY: Final = 12
32 COMPARATOR_PRIORITY: Final = 10
33 MATH_PRIORITIES: Final = {
49 DOT_PRIORITY: Final = 1
52 class BracketMatchError(Exception):
53 """Raised when an opening bracket is unable to be matched to a closing bracket."""
58 """Keeps track of brackets on a line."""
61 bracket_match: Dict[Tuple[Depth, NodeType], Leaf] = field(default_factory=dict)
62 delimiters: Dict[LeafID, Priority] = field(default_factory=dict)
63 previous: Optional[Leaf] = None
64 _for_loop_depths: List[int] = field(default_factory=list)
65 _lambda_argument_depths: List[int] = field(default_factory=list)
66 invisible: List[Leaf] = field(default_factory=list)
68 def mark(self, leaf: Leaf) -> None:
69 """Mark `leaf` with bracket-related metadata. Keep track of delimiters.
71 All leaves receive an int `bracket_depth` field that stores how deep
72 within brackets a given leaf is. 0 means there are no enclosing brackets
73 that started on this line.
75 If a leaf is itself a closing bracket, it receives an `opening_bracket`
76 field that it forms a pair with. This is a one-directional link to
77 avoid reference cycles.
79 If a leaf is a delimiter (a token on which Black can split the line if
80 needed) and it's on depth 0, its `id()` is stored in the tracker's
83 if leaf.type == token.COMMENT:
86 self.maybe_decrement_after_for_loop_variable(leaf)
87 self.maybe_decrement_after_lambda_arguments(leaf)
88 if leaf.type in CLOSING_BRACKETS:
91 opening_bracket = self.bracket_match.pop((self.depth, leaf.type))
93 raise BracketMatchError(
94 "Unable to match a closing bracket to the following opening"
97 leaf.opening_bracket = opening_bracket
99 self.invisible.append(leaf)
100 leaf.bracket_depth = self.depth
102 delim = is_split_before_delimiter(leaf, self.previous)
103 if delim and self.previous is not None:
104 self.delimiters[id(self.previous)] = delim
106 delim = is_split_after_delimiter(leaf, self.previous)
108 self.delimiters[id(leaf)] = delim
109 if leaf.type in OPENING_BRACKETS:
110 self.bracket_match[self.depth, BRACKET[leaf.type]] = leaf
113 self.invisible.append(leaf)
115 self.maybe_increment_lambda_arguments(leaf)
116 self.maybe_increment_for_loop_variable(leaf)
118 def any_open_brackets(self) -> bool:
119 """Return True if there is an yet unmatched open bracket on the line."""
120 return bool(self.bracket_match)
122 def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority:
123 """Return the highest priority of a delimiter found on the line.
125 Values are consistent with what `is_split_*_delimiter()` return.
126 Raises ValueError on no delimiters.
128 return max(v for k, v in self.delimiters.items() if k not in exclude)
130 def delimiter_count_with_priority(self, priority: Priority = 0) -> int:
131 """Return the number of delimiters with the given `priority`.
133 If no `priority` is passed, defaults to max priority on the line.
135 if not self.delimiters:
138 priority = priority or self.max_delimiter_priority()
139 return sum(1 for p in self.delimiters.values() if p == priority)
141 def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool:
142 """In a for loop, or comprehension, the variables are often unpacks.
144 To avoid splitting on the comma in this situation, increase the depth of
145 tokens between `for` and `in`.
147 if leaf.type == token.NAME and leaf.value == "for":
149 self._for_loop_depths.append(self.depth)
154 def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool:
155 """See `maybe_increment_for_loop_variable` above for explanation."""
157 self._for_loop_depths
158 and self._for_loop_depths[-1] == self.depth
159 and leaf.type == token.NAME
160 and leaf.value == "in"
163 self._for_loop_depths.pop()
168 def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool:
169 """In a lambda expression, there might be more than one argument.
171 To avoid splitting on the comma in this situation, increase the depth of
172 tokens between `lambda` and `:`.
174 if leaf.type == token.NAME and leaf.value == "lambda":
176 self._lambda_argument_depths.append(self.depth)
181 def maybe_decrement_after_lambda_arguments(self, leaf: Leaf) -> bool:
182 """See `maybe_increment_lambda_arguments` above for explanation."""
184 self._lambda_argument_depths
185 and self._lambda_argument_depths[-1] == self.depth
186 and leaf.type == token.COLON
189 self._lambda_argument_depths.pop()
194 def get_open_lsqb(self) -> Optional[Leaf]:
195 """Return the most recent opening square bracket (if any)."""
196 return self.bracket_match.get((self.depth - 1, token.RSQB))
199 def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority:
200 """Return the priority of the `leaf` delimiter, given a line break after it.
202 The delimiter priorities returned here are from those delimiters that would
203 cause a line break after themselves.
205 Higher numbers are higher priority.
207 if leaf.type == token.COMMA:
208 return COMMA_PRIORITY
213 def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority:
214 """Return the priority of the `leaf` delimiter, given a line break before it.
216 The delimiter priorities returned here are from those delimiters that would
217 cause a line break before themselves.
219 Higher numbers are higher priority.
221 if is_vararg(leaf, within=VARARGS_PARENTS | UNPACKING_PARENTS):
222 # * and ** might also be MATH_OPERATORS but in this case they are not.
223 # Don't treat them as a delimiter.
227 leaf.type == token.DOT
229 and leaf.parent.type not in {syms.import_from, syms.dotted_name}
230 and (previous is None or previous.type in CLOSING_BRACKETS)
235 leaf.type in MATH_OPERATORS
237 and leaf.parent.type not in {syms.factor, syms.star_expr}
239 return MATH_PRIORITIES[leaf.type]
241 if leaf.type in COMPARATORS:
242 return COMPARATOR_PRIORITY
245 leaf.type == token.STRING
246 and previous is not None
247 and previous.type == token.STRING
249 return STRING_PRIORITY
251 if leaf.type not in {token.NAME, token.ASYNC}:
257 and leaf.parent.type in {syms.comp_for, syms.old_comp_for}
258 or leaf.type == token.ASYNC
261 not isinstance(leaf.prev_sibling, Leaf)
262 or leaf.prev_sibling.value != "async"
264 return COMPREHENSION_PRIORITY
269 and leaf.parent.type in {syms.comp_if, syms.old_comp_if}
271 return COMPREHENSION_PRIORITY
273 if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test:
274 return TERNARY_PRIORITY
276 if leaf.value == "is":
277 return COMPARATOR_PRIORITY
282 and leaf.parent.type in {syms.comp_op, syms.comparison}
285 and previous.type == token.NAME
286 and previous.value == "not"
289 return COMPARATOR_PRIORITY
294 and leaf.parent.type == syms.comp_op
297 and previous.type == token.NAME
298 and previous.value == "is"
301 return COMPARATOR_PRIORITY
303 if leaf.value in LOGIC_OPERATORS and leaf.parent:
304 return LOGIC_PRIORITY
309 def max_delimiter_priority_in_atom(node: LN) -> Priority:
310 """Return maximum delimiter priority inside `node`.
312 This is specific to atoms with contents contained in a pair of parentheses.
313 If `node` isn't an atom or there are no enclosing parentheses, returns 0.
315 if node.type != syms.atom:
318 first = node.children[0]
319 last = node.children[-1]
320 if not (first.type == token.LPAR and last.type == token.RPAR):
323 bt = BracketTracker()
324 for c in node.children[1:-1]:
325 if isinstance(c, Leaf):
328 for leaf in c.leaves():
331 return bt.max_delimiter_priority()