codekingpro/portable-devtools
114k
1# This module contains ``ast.unparse()``, defined here2# to improve the import time for the ``ast`` module.3import sys4from _ast import *5from ast import NodeVisitor6from contextlib import contextmanager, nullcontext7from enum import IntEnum, auto, _simple_enum8 9# Large float and imaginary literals get turned into infinities in the AST.10# We unparse those infinities to INFSTR.11_INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1)12 13@_simple_enum(IntEnum)14class _Precedence:15 """Precedence table that originated from python grammar."""16 17 NAMED_EXPR = auto() # <target> := <expr1>18 TUPLE = auto() # <expr1>, <expr2>19 YIELD = auto() # 'yield', 'yield from'20 TEST = auto() # 'if'-'else', 'lambda'21 OR = auto() # 'or'22 AND = auto() # 'and'23 NOT = auto() # 'not'24 CMP = auto() # '<', '>', '==', '>=', '<=', '!=',25 # 'in', 'not in', 'is', 'is not'26 EXPR = auto()27 BOR = EXPR # '|'28 BXOR = auto() # '^'29 BAND = auto() # '&'30 SHIFT = auto() # '<<', '>>'31 ARITH = auto() # '+', '-'32 TERM = auto() # '*', '@', '/', '%', '//'33 FACTOR = auto() # unary '+', '-', '~'34 POWER = auto() # '**'35 AWAIT = auto() # 'await'36 ATOM = auto()37 38 def next(self):39 try:40 return self.__class__(self + 1)41 except ValueError:42 return self43 44 45_SINGLE_QUOTES = ("'", '"')46_MULTI_QUOTES = ('"""', "'''")47_ALL_QUOTES = (*_SINGLE_QUOTES, *_MULTI_QUOTES)48 49class Unparser(NodeVisitor):50 """Methods in this class recursively traverse an AST and51 output source code for the abstract syntax; original formatting52 is disregarded."""53 54 def __init__(self):55 self._source = []56 self._precedences = {}57 self._type_ignores = {}58 self._indent = 059 self._in_try_star = False60 self._in_interactive = False61 62 def interleave(self, inter, f, seq):63 """Call f on each item in seq, calling inter() in between."""64 seq = iter(seq)65 try:66 f(next(seq))67 except StopIteration:68 pass69 else:70 for x in seq:71 inter()72 f(x)73 74 def items_view(self, traverser, items):75 """Traverse and separate the given *items* with a comma and append it to76 the buffer. If *items* is a single item sequence, a trailing comma77 will be added."""78 if len(items) == 1:79 traverser(items[0])80 self.write(",")81 else:82 self.interleave(lambda: self.write(", "), traverser, items)83 84 def maybe_newline(self):85 """Adds a newline if it isn't the start of generated source"""86 if self._source:87 self.write("\n")88 89 def maybe_semicolon(self):90 """Adds a "; " delimiter if it isn't the start of generated source"""91 if self._source:92 self.write("; ")93 94 def fill(self, text="", *, allow_semicolon=True):95 """Indent a piece of text and append it, according to the current96 indentation level, or only delineate with semicolon if applicable"""97 if self._in_interactive and not self._indent and allow_semicolon:98 self.maybe_semicolon()99 self.write(text)100 else:101 self.maybe_newline()102 self.write(" " * self._indent + text)103 104 def write(self, *text):105 """Add new source parts"""106 self._source.extend(text)107 108 @contextmanager109 def buffered(self, buffer = None):110 if buffer is None:111 buffer = []112 113 original_source = self._source114 self._source = buffer115 yield buffer116 self._source = original_source117 118 @contextmanager119 def block(self, *, extra = None):120 """A context manager for preparing the source for blocks. It adds121 the character':', increases the indentation on enter and decreases122 the indentation on exit. If *extra* is given, it will be directly123 appended after the colon character.124 """125 self.write(":")126 if extra:127 self.write(extra)128 self._indent += 1129 yield130 self._indent -= 1131 132 @contextmanager133 def delimit(self, start, end):134 """A context manager for preparing the source for expressions. It adds135 *start* to the buffer and enters, after exit it adds *end*."""136 137 self.write(start)138 yield139 self.write(end)140 141 def delimit_if(self, start, end, condition):142 if condition:143 return self.delimit(start, end)144 else:145 return nullcontext()146 147 def require_parens(self, precedence, node):148 """Shortcut to adding precedence related parens"""149 return self.delimit_if("(", ")", self.get_precedence(node) > precedence)150 151 def get_precedence(self, node):152 return self._precedences.get(node, _Precedence.TEST)153 154 def set_precedence(self, precedence, *nodes):155 for node in nodes:156 self._precedences[node] = precedence157 158 def get_raw_docstring(self, node):159 """If a docstring node is found in the body of the *node* parameter,160 return that docstring node, None otherwise.161 162 Logic mirrored from ``_PyAST_GetDocString``."""163 if not isinstance(164 node, (AsyncFunctionDef, FunctionDef, ClassDef, Module)165 ) or len(node.body) < 1:166 return None167 node = node.body[0]168 if not isinstance(node, Expr):169 return None170 node = node.value171 if isinstance(node, Constant) and isinstance(node.value, str):172 return node173 174 def get_type_comment(self, node):175 comment = self._type_ignores.get(node.lineno) or node.type_comment176 if comment is not None:177 return f" # type: {comment}"178 179 def traverse(self, node):180 if isinstance(node, list):181 for item in node:182 self.traverse(item)183 else:184 super().visit(node)185 186 # Note: as visit() resets the output text, do NOT rely on187 # NodeVisitor.generic_visit to handle any nodes (as it calls back in to188 # the subclass visit() method, which resets self._source to an empty list)189 def visit(self, node):190 """Outputs a source code string that, if converted back to an ast191 (using ast.parse) will generate an AST equivalent to *node*"""192 self._source = []193 self.traverse(node)194 return "".join(self._source)195 196 def _write_docstring_and_traverse_body(self, node):197 if (docstring := self.get_raw_docstring(node)):198 self._write_docstring(docstring)199 self.traverse(node.body[1:])200 else:201 self.traverse(node.body)202 203 def visit_Module(self, node):204 self._type_ignores = {205 ignore.lineno: f"ignore{ignore.tag}"206 for ignore in node.type_ignores207 }208 try:209 self._write_docstring_and_traverse_body(node)210 finally:211 self._type_ignores.clear()212 213 def visit_Interactive(self, node):214 self._in_interactive = True215 try:216 self._write_docstring_and_traverse_body(node)217 finally:218 self._in_interactive = False219 220 def visit_FunctionType(self, node):221 with self.delimit("(", ")"):222 self.interleave(223 lambda: self.write(", "), self.traverse, node.argtypes224 )225 226 self.write(" -> ")227 self.traverse(node.returns)228 229 def visit_Expr(self, node):230 self.fill()231 self.set_precedence(_Precedence.YIELD, node.value)232 self.traverse(node.value)233 234 def visit_NamedExpr(self, node):235 with self.require_parens(_Precedence.NAMED_EXPR, node):236 self.set_precedence(_Precedence.ATOM, node.target, node.value)237 self.traverse(node.target)238 self.write(" := ")239 self.traverse(node.value)240 241 def visit_Import(self, node):242 self.fill("import ")243 self.interleave(lambda: self.write(", "), self.traverse, node.names)244 245 def visit_ImportFrom(self, node):246 self.fill("from ")247 self.write("." * (node.level or 0))248 if node.module:249 self.write(node.module)250 self.write(" import ")251 self.interleave(lambda: self.write(", "), self.traverse, node.names)252 253 def visit_Assign(self, node):254 self.fill()255 for target in node.targets:256 self.set_precedence(_Precedence.TUPLE, target)257 self.traverse(target)258 self.write(" = ")259 self.traverse(node.value)260 if type_comment := self.get_type_comment(node):261 self.write(type_comment)262 263 def visit_AugAssign(self, node):264 self.fill()265 self.traverse(node.target)266 self.write(" " + self.binop[node.op.__class__.__name__] + "= ")267 self.traverse(node.value)268 269 def visit_AnnAssign(self, node):270 self.fill()271 with self.delimit_if("(", ")", not node.simple and isinstance(node.target, Name)):272 self.traverse(node.target)273 self.write(": ")274 self.traverse(node.annotation)275 if node.value:276 self.write(" = ")277 self.traverse(node.value)278 279 def visit_Return(self, node):280 self.fill("return")281 if node.value:282 self.write(" ")283 self.traverse(node.value)284 285 def visit_Pass(self, node):286 self.fill("pass")287 288 def visit_Break(self, node):289 self.fill("break")290 291 def visit_Continue(self, node):292 self.fill("continue")293 294 def visit_Delete(self, node):295 self.fill("del ")296 self.interleave(lambda: self.write(", "), self.traverse, node.targets)297 298 def visit_Assert(self, node):299 self.fill("assert ")300 self.traverse(node.test)301 if node.msg:302 self.write(", ")303 self.traverse(node.msg)304 305 def visit_Global(self, node):306 self.fill("global ")307 self.interleave(lambda: self.write(", "), self.write, node.names)308 309 def visit_Nonlocal(self, node):310 self.fill("nonlocal ")311 self.interleave(lambda: self.write(", "), self.write, node.names)312 313 def visit_Await(self, node):314 with self.require_parens(_Precedence.AWAIT, node):315 self.write("await")316 if node.value:317 self.write(" ")318 self.set_precedence(_Precedence.ATOM, node.value)319 self.traverse(node.value)320 321 def visit_Yield(self, node):322 with self.require_parens(_Precedence.YIELD, node):323 self.write("yield")324 if node.value:325 self.write(" ")326 self.set_precedence(_Precedence.ATOM, node.value)327 self.traverse(node.value)328 329 def visit_YieldFrom(self, node):330 with self.require_parens(_Precedence.YIELD, node):331 self.write("yield from ")332 if not node.value:333 raise ValueError("Node can't be used without a value attribute.")334 self.set_precedence(_Precedence.ATOM, node.value)335 self.traverse(node.value)336 337 def visit_Raise(self, node):338 self.fill("raise")339 if not node.exc:340 if node.cause:341 raise ValueError(f"Node can't use cause without an exception.")342 return343 self.write(" ")344 self.traverse(node.exc)345 if node.cause:346 self.write(" from ")347 self.traverse(node.cause)348 349 def do_visit_try(self, node):350 self.fill("try", allow_semicolon=False)351 with self.block():352 self.traverse(node.body)353 for ex in node.handlers:354 self.traverse(ex)355 if node.orelse:356 self.fill("else", allow_semicolon=False)357 with self.block():358 self.traverse(node.orelse)359 if node.finalbody:360 self.fill("finally", allow_semicolon=False)361 with self.block():362 self.traverse(node.finalbody)363 364 def visit_Try(self, node):365 prev_in_try_star = self._in_try_star366 try:367 self._in_try_star = False368 self.do_visit_try(node)369 finally:370 self._in_try_star = prev_in_try_star371 372 def visit_TryStar(self, node):373 prev_in_try_star = self._in_try_star374 try:375 self._in_try_star = True376 self.do_visit_try(node)377 finally:378 self._in_try_star = prev_in_try_star379 380 def visit_ExceptHandler(self, node):381 self.fill("except*" if self._in_try_star else "except", allow_semicolon=False)382 if node.type:383 self.write(" ")384 self.traverse(node.type)385 if node.name:386 self.write(" as ")387 self.write(node.name)388 with self.block():389 self.traverse(node.body)390 391 def visit_ClassDef(self, node):392 self.maybe_newline()393 for deco in node.decorator_list:394 self.fill("@", allow_semicolon=False)395 self.traverse(deco)396 self.fill("class " + node.name, allow_semicolon=False)397 if hasattr(node, "type_params"):398 self._type_params_helper(node.type_params)399 with self.delimit_if("(", ")", condition = node.bases or node.keywords):400 comma = False401 for e in node.bases:402 if comma:403 self.write(", ")404 else:405 comma = True406 self.traverse(e)407 for e in node.keywords:408 if comma:409 self.write(", ")410 else:411 comma = True412 self.traverse(e)413 414 with self.block():415 self._write_docstring_and_traverse_body(node)416 417 def visit_FunctionDef(self, node):418 self._function_helper(node, "def")419 420 def visit_AsyncFunctionDef(self, node):421 self._function_helper(node, "async def")422 423 def _function_helper(self, node, fill_suffix):424 self.maybe_newline()425 for deco in node.decorator_list:426 self.fill("@", allow_semicolon=False)427 self.traverse(deco)428 def_str = fill_suffix + " " + node.name429 self.fill(def_str, allow_semicolon=False)430 if hasattr(node, "type_params"):431 self._type_params_helper(node.type_params)432 with self.delimit("(", ")"):433 self.traverse(node.args)434 if node.returns:435 self.write(" -> ")436 self.traverse(node.returns)437 with self.block(extra=self.get_type_comment(node)):438 self._write_docstring_and_traverse_body(node)439 440 def _type_params_helper(self, type_params):441 if type_params is not None and len(type_params) > 0:442 with self.delimit("[", "]"):443 self.interleave(lambda: self.write(", "), self.traverse, type_params)444 445 def visit_TypeVar(self, node):446 self.write(node.name)447 if node.bound:448 self.write(": ")449 self.traverse(node.bound)450 if node.default_value:451 self.write(" = ")452 self.traverse(node.default_value)453 454 def visit_TypeVarTuple(self, node):455 self.write("*" + node.name)456 if node.default_value:457 self.write(" = ")458 self.traverse(node.default_value)459 460 def visit_ParamSpec(self, node):461 self.write("**" + node.name)462 if node.default_value:463 self.write(" = ")464 self.traverse(node.default_value)465 466 def visit_TypeAlias(self, node):467 self.fill("type ")468 self.traverse(node.name)469 self._type_params_helper(node.type_params)470 self.write(" = ")471 self.traverse(node.value)472 473 def visit_For(self, node):474 self._for_helper("for ", node)475 476 def visit_AsyncFor(self, node):477 self._for_helper("async for ", node)478 479 def _for_helper(self, fill, node):480 self.fill(fill, allow_semicolon=False)481 self.set_precedence(_Precedence.TUPLE, node.target)482 self.traverse(node.target)483 self.write(" in ")484 self.traverse(node.iter)485 with self.block(extra=self.get_type_comment(node)):486 self.traverse(node.body)487 if node.orelse:488 self.fill("else", allow_semicolon=False)489 with self.block():490 self.traverse(node.orelse)491 492 def visit_If(self, node):493 self.fill("if ", allow_semicolon=False)494 self.traverse(node.test)495 with self.block():496 self.traverse(node.body)497 # collapse nested ifs into equivalent elifs.498 while node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], If):499 node = node.orelse[0]500 self.fill("elif ", allow_semicolon=False)501 self.traverse(node.test)502 with self.block():503 self.traverse(node.body)504 # final else505 if node.orelse:506 self.fill("else", allow_semicolon=False)507 with self.block():508 self.traverse(node.orelse)509 510 def visit_While(self, node):511 self.fill("while ", allow_semicolon=False)512 self.traverse(node.test)513 with self.block():514 self.traverse(node.body)515 if node.orelse:516 self.fill("else", allow_semicolon=False)517 with self.block():518 self.traverse(node.orelse)519 520 def visit_With(self, node):521 self.fill("with ", allow_semicolon=False)522 self.interleave(lambda: self.write(", "), self.traverse, node.items)523 with self.block(extra=self.get_type_comment(node)):524 self.traverse(node.body)525 526 def visit_AsyncWith(self, node):527 self.fill("async with ", allow_semicolon=False)528 self.interleave(lambda: self.write(", "), self.traverse, node.items)529 with self.block(extra=self.get_type_comment(node)):530 self.traverse(node.body)531 532 def _str_literal_helper(533 self, string, *, quote_types=_ALL_QUOTES, escape_special_whitespace=False534 ):535 """Helper for writing string literals, minimizing escapes.536 Returns the tuple (string literal to write, possible quote types).537 """538 def escape_char(c):539 # \n and \t are non-printable, but we only escape them if540 # escape_special_whitespace is True541 if not escape_special_whitespace and c in "\n\t":542 return c543 # Always escape backslashes and other non-printable characters544 if c == "\\" or not c.isprintable():545 return c.encode("unicode_escape").decode("ascii")546 return c547 548 escaped_string = "".join(map(escape_char, string))549 possible_quotes = quote_types550 if "\n" in escaped_string:551 possible_quotes = [q for q in possible_quotes if q in _MULTI_QUOTES]552 possible_quotes = [q for q in possible_quotes if q not in escaped_string]553 if not possible_quotes:554 # If there aren't any possible_quotes, fallback to using repr555 # on the original string. Try to use a quote from quote_types,556 # e.g., so that we use triple quotes for docstrings.557 string = repr(string)558 quote = next((q for q in quote_types if string[0] in q), string[0])559 return string[1:-1], [quote]560 if escaped_string:561 # Sort so that we prefer '''"''' over """\""""562 possible_quotes.sort(key=lambda q: q[0] == escaped_string[-1])563 # If we're using triple quotes and we'd need to escape a final564 # quote, escape it565 if possible_quotes[0][0] == escaped_string[-1]:566 assert len(possible_quotes[0]) == 3567 escaped_string = escaped_string[:-1] + "\\" + escaped_string[-1]568 return escaped_string, possible_quotes569 570 def _write_str_avoiding_backslashes(self, string, *, quote_types=_ALL_QUOTES):571 """Write string literal value with a best effort attempt to avoid backslashes."""572 string, quote_types = self._str_literal_helper(string, quote_types=quote_types)573 quote_type = quote_types[0]574 self.write(f"{quote_type}{string}{quote_type}")575 576 def _ftstring_helper(self, parts):577 new_parts = []578 quote_types = list(_ALL_QUOTES)579 fallback_to_repr = False580 for value, is_constant in parts:581 if is_constant:582 value, new_quote_types = self._str_literal_helper(583 value,584 quote_types=quote_types,585 escape_special_whitespace=True,586 )587 if set(new_quote_types).isdisjoint(quote_types):588 fallback_to_repr = True589 break590 quote_types = new_quote_types591 else:592 if "\n" in value:593 quote_types = [q for q in quote_types if q in _MULTI_QUOTES]594 assert quote_types595 596 new_quote_types = [q for q in quote_types if q not in value]597 if new_quote_types:598 quote_types = new_quote_types599 new_parts.append(value)600 601 if fallback_to_repr:602 # If we weren't able to find a quote type that works for all parts603 # of the JoinedStr, fallback to using repr and triple single quotes.604 quote_types = ["'''"]605 new_parts.clear()606 for value, is_constant in parts:607 if is_constant:608 value = repr('"' + value) # force repr to use single quotes609 expected_prefix = "'\""610 assert value.startswith(expected_prefix), repr(value)611 value = value[len(expected_prefix):-1]612 new_parts.append(value)613 614 value = "".join(new_parts)615 quote_type = quote_types[0]616 self.write(f"{quote_type}{value}{quote_type}")617 618 def _write_ftstring(self, values, prefix):619 self.write(prefix)620 fstring_parts = []621 for value in values:622 with self.buffered() as buffer:623 self._write_ftstring_inner(value)624 fstring_parts.append(625 ("".join(buffer), isinstance(value, Constant))626 )627 self._ftstring_helper(fstring_parts)628 629 def visit_JoinedStr(self, node):630 self._write_ftstring(node.values, "f")631 632 def visit_TemplateStr(self, node):633 self._write_ftstring(node.values, "t")634 635 def _write_ftstring_inner(self, node, is_format_spec=False):636 if isinstance(node, JoinedStr):637 # for both the f-string itself, and format_spec638 for value in node.values:639 self._write_ftstring_inner(value, is_format_spec=is_format_spec)640 elif isinstance(node, Constant) and isinstance(node.value, str):641 value = node.value.replace("{", "{{").replace("}", "}}")642 643 if is_format_spec:644 value = value.replace("\\", "\\\\")645 value = value.replace("'", "\\'")646 value = value.replace('"', '\\"')647 value = value.replace("\n", "\\n")648 self.write(value)649 elif isinstance(node, FormattedValue):650 self.visit_FormattedValue(node)651 elif isinstance(node, Interpolation):652 self.visit_Interpolation(node)653 else:654 raise ValueError(f"Unexpected node inside JoinedStr, {node!r}")655 656 def _unparse_interpolation_value(self, inner):657 unparser = type(self)()658 unparser.set_precedence(_Precedence.TEST.next(), inner)659 return unparser.visit(inner)660 661 def _write_interpolation(self, node, use_str_attr=False):662 with self.delimit("{", "}"):663 if use_str_attr:664 expr = node.str665 else:666 expr = self._unparse_interpolation_value(node.value)667 if expr.startswith("{"):668 # Separate pair of opening brackets as "{ {"669 self.write(" ")670 self.write(expr)671 if node.conversion != -1:672 self.write(f"!{chr(node.conversion)}")673 if node.format_spec:674 self.write(":")675 self._write_ftstring_inner(node.format_spec, is_format_spec=True)676 677 def visit_FormattedValue(self, node):678 self._write_interpolation(node)679 680 def visit_Interpolation(self, node):681 # If `str` is set to `None`, use the `value` to generate the source code.682 self._write_interpolation(node, use_str_attr=node.str is not None)683 684 def visit_Name(self, node):685 self.write(node.id)686 687 def _write_docstring(self, node):688 self.fill(allow_semicolon=False)689 if node.kind == "u":690 self.write("u")691 self._write_str_avoiding_backslashes(node.value, quote_types=_MULTI_QUOTES)692 693 def _write_constant(self, value):694 if isinstance(value, (float, complex)):695 # Substitute overflowing decimal literal for AST infinities,696 # and inf - inf for NaNs.697 self.write(698 repr(value)699 .replace("inf", _INFSTR)700 .replace("nan", f"({_INFSTR}-{_INFSTR})")701 )702 else:703 self.write(repr(value))704 705 def visit_Constant(self, node):706 value = node.value707 if isinstance(value, tuple):708 with self.delimit("(", ")"):709 self.items_view(self._write_constant, value)710 elif value is ...:711 self.write("...")712 else:713 if node.kind == "u":714 self.write("u")715 self._write_constant(node.value)716 717 def visit_List(self, node):718 with self.delimit("[", "]"):719 self.interleave(lambda: self.write(", "), self.traverse, node.elts)720 721 def visit_ListComp(self, node):722 with self.delimit("[", "]"):723 self.traverse(node.elt)724 for gen in node.generators:725 self.traverse(gen)726 727 def visit_GeneratorExp(self, node):728 with self.delimit("(", ")"):729 self.traverse(node.elt)730 for gen in node.generators:731 self.traverse(gen)732 733 def visit_SetComp(self, node):734 with self.delimit("{", "}"):735 self.traverse(node.elt)736 for gen in node.generators:737 self.traverse(gen)738 739 def visit_DictComp(self, node):740 with self.delimit("{", "}"):741 self.traverse(node.key)742 self.write(": ")743 self.traverse(node.value)744 for gen in node.generators:745 self.traverse(gen)746 747 def visit_comprehension(self, node):748 if node.is_async:749 self.write(" async for ")750 else:751 self.write(" for ")752 self.set_precedence(_Precedence.TUPLE, node.target)753 self.traverse(node.target)754 self.write(" in ")755 self.set_precedence(_Precedence.TEST.next(), node.iter, *node.ifs)756 self.traverse(node.iter)757 for if_clause in node.ifs:758 self.write(" if ")759 self.traverse(if_clause)760 761 def visit_IfExp(self, node):762 with self.require_parens(_Precedence.TEST, node):763 self.set_precedence(_Precedence.TEST.next(), node.body, node.test)764 self.traverse(node.body)765 self.write(" if ")766 self.traverse(node.test)767 self.write(" else ")768 self.set_precedence(_Precedence.TEST, node.orelse)769 self.traverse(node.orelse)770 771 def visit_Set(self, node):772 if node.elts:773 with self.delimit("{", "}"):774 self.interleave(lambda: self.write(", "), self.traverse, node.elts)775 else:776 # `{}` would be interpreted as a dictionary literal, and777 # `set` might be shadowed. Thus:778 self.write('{*()}')779 780 def visit_Dict(self, node):781 def write_key_value_pair(k, v):782 self.traverse(k)783 self.write(": ")784 self.traverse(v)785 786 def write_item(item):787 k, v = item788 if k is None:789 # for dictionary unpacking operator in dicts {**{'y': 2}}790 # see PEP 448 for details791 self.write("**")792 self.set_precedence(_Precedence.EXPR, v)793 self.traverse(v)794 else:795 write_key_value_pair(k, v)796 797 with self.delimit("{", "}"):798 self.interleave(799 lambda: self.write(", "), write_item, zip(node.keys, node.values)800 )801 802 def visit_Tuple(self, node):803 with self.delimit_if(804 "(",805 ")",806 len(node.elts) == 0 or self.get_precedence(node) > _Precedence.TUPLE807 ):808 self.items_view(self.traverse, node.elts)809 810 unop = {"Invert": "~", "Not": "not", "UAdd": "+", "USub": "-"}811 unop_precedence = {812 "not": _Precedence.NOT,813 "~": _Precedence.FACTOR,814 "+": _Precedence.FACTOR,815 "-": _Precedence.FACTOR,816 }817 818 def visit_UnaryOp(self, node):819 operator = self.unop[node.op.__class__.__name__]820 operator_precedence = self.unop_precedence[operator]821 with self.require_parens(operator_precedence, node):822 self.write(operator)823 # factor prefixes (+, -, ~) shouldn't be separated824 # from the value they belong, (e.g: +1 instead of + 1)825 if operator_precedence is not _Precedence.FACTOR:826 self.write(" ")827 self.set_precedence(operator_precedence, node.operand)828 self.traverse(node.operand)829 830 binop = {831 "Add": "+",832 "Sub": "-",833 "Mult": "*",834 "MatMult": "@",835 "Div": "/",836 "Mod": "%",837 "LShift": "<<",838 "RShift": ">>",839 "BitOr": "|",840 "BitXor": "^",841 "BitAnd": "&",842 "FloorDiv": "//",843 "Pow": "**",844 }845 846 binop_precedence = {847 "+": _Precedence.ARITH,848 "-": _Precedence.ARITH,849 "*": _Precedence.TERM,850 "@": _Precedence.TERM,851 "/": _Precedence.TERM,852 "%": _Precedence.TERM,853 "<<": _Precedence.SHIFT,854 ">>": _Precedence.SHIFT,855 "|": _Precedence.BOR,856 "^": _Precedence.BXOR,857 "&": _Precedence.BAND,858 "//": _Precedence.TERM,859 "**": _Precedence.POWER,860 }861 862 binop_rassoc = frozenset(("**",))863 def visit_BinOp(self, node):864 operator = self.binop[node.op.__class__.__name__]865 operator_precedence = self.binop_precedence[operator]866 with self.require_parens(operator_precedence, node):867 if operator in self.binop_rassoc:868 left_precedence = operator_precedence.next()869 right_precedence = operator_precedence870 else:871 left_precedence = operator_precedence872 right_precedence = operator_precedence.next()873 874 self.set_precedence(left_precedence, node.left)875 self.traverse(node.left)876 self.write(f" {operator} ")877 self.set_precedence(right_precedence, node.right)878 self.traverse(node.right)879 880 cmpops = {881 "Eq": "==",882 "NotEq": "!=",883 "Lt": "<",884 "LtE": "<=",885 "Gt": ">",886 "GtE": ">=",887 "Is": "is",888 "IsNot": "is not",889 "In": "in",890 "NotIn": "not in",891 }892 893 def visit_Compare(self, node):894 with self.require_parens(_Precedence.CMP, node):895 self.set_precedence(_Precedence.CMP.next(), node.left, *node.comparators)896 self.traverse(node.left)897 for o, e in zip(node.ops, node.comparators):898 self.write(" " + self.cmpops[o.__class__.__name__] + " ")899 self.traverse(e)900 901 boolops = {"And": "and", "Or": "or"}902 boolop_precedence = {"and": _Precedence.AND, "or": _Precedence.OR}903 904 def visit_BoolOp(self, node):905 operator = self.boolops[node.op.__class__.__name__]906 operator_precedence = self.boolop_precedence[operator]907 908 def increasing_level_traverse(node):909 nonlocal operator_precedence910 operator_precedence = operator_precedence.next()911 self.set_precedence(operator_precedence, node)912 self.traverse(node)913 914 with self.require_parens(operator_precedence, node):915 s = f" {operator} "916 self.interleave(lambda: self.write(s), increasing_level_traverse, node.values)917 918 def visit_Attribute(self, node):919 self.set_precedence(_Precedence.ATOM, node.value)920 self.traverse(node.value)921 # Special case: 3.__abs__() is a syntax error, so if node.value922 # is an integer literal then we need to either parenthesize923 # it or add an extra space to get 3 .__abs__().924 if isinstance(node.value, Constant) and isinstance(node.value.value, int):925 self.write(" ")926 self.write(".")927 self.write(node.attr)928 929 def visit_Call(self, node):930 self.set_precedence(_Precedence.ATOM, node.func)931 self.traverse(node.func)932 with self.delimit("(", ")"):933 comma = False934 for e in node.args:935 if comma:936 self.write(", ")937 else:938 comma = True939 self.traverse(e)940 for e in node.keywords:941 if comma:942 self.write(", ")943 else:944 comma = True945 self.traverse(e)946 947 def visit_Subscript(self, node):948 def is_non_empty_tuple(slice_value):949 return (950 isinstance(slice_value, Tuple)951 and slice_value.elts952 )953 954 self.set_precedence(_Precedence.ATOM, node.value)955 self.traverse(node.value)956 with self.delimit("[", "]"):957 if is_non_empty_tuple(node.slice):958 # parentheses can be omitted if the tuple isn't empty959 self.items_view(self.traverse, node.slice.elts)960 else:961 self.traverse(node.slice)962 963 def visit_Starred(self, node):964 self.write("*")965 self.set_precedence(_Precedence.EXPR, node.value)966 self.traverse(node.value)967 968 def visit_Ellipsis(self, node):969 self.write("...")970 971 def visit_Slice(self, node):972 if node.lower:973 self.traverse(node.lower)974 self.write(":")975 if node.upper:976 self.traverse(node.upper)977 if node.step:978 self.write(":")979 self.traverse(node.step)980 981 def visit_Match(self, node):982 self.fill("match ", allow_semicolon=False)983 self.traverse(node.subject)984 with self.block():985 for case in node.cases:986 self.traverse(case)987 988 def visit_arg(self, node):989 self.write(node.arg)990 if node.annotation:991 self.write(": ")992 self.traverse(node.annotation)993 994 def visit_arguments(self, node):995 first = True996 # normal arguments997 all_args = node.posonlyargs + node.args998 defaults = [None] * (len(all_args) - len(node.defaults)) + node.defaults999 for index, elements in enumerate(zip(all_args, defaults), 1):1000 a, d = elements1001 if first:1002 first = False1003 else:1004 self.write(", ")1005 self.traverse(a)1006 if d:1007 self.write("=")1008 self.traverse(d)1009 if index == len(node.posonlyargs):1010 self.write(", /")1011 1012 # varargs, or bare '*' if no varargs but keyword-only arguments present1013 if node.vararg or node.kwonlyargs:1014 if first:1015 first = False1016 else:1017 self.write(", ")1018 self.write("*")1019 if node.vararg:1020 self.write(node.vararg.arg)1021 if node.vararg.annotation:1022 self.write(": ")1023 self.traverse(node.vararg.annotation)1024 1025 # keyword-only arguments1026 if node.kwonlyargs:1027 for a, d in zip(node.kwonlyargs, node.kw_defaults):1028 self.write(", ")1029 self.traverse(a)1030 if d:1031 self.write("=")1032 self.traverse(d)1033 1034 # kwargs1035 if node.kwarg:1036 if first:1037 first = False1038 else:1039 self.write(", ")1040 self.write("**" + node.kwarg.arg)1041 if node.kwarg.annotation:1042 self.write(": ")1043 self.traverse(node.kwarg.annotation)1044 1045 def visit_keyword(self, node):1046 if node.arg is None:1047 self.write("**")1048 else:1049 self.write(node.arg)1050 self.write("=")1051 self.traverse(node.value)1052 1053 def visit_Lambda(self, node):1054 with self.require_parens(_Precedence.TEST, node):1055 self.write("lambda")1056 with self.buffered() as buffer:1057 self.traverse(node.args)1058 if buffer:1059 self.write(" ", *buffer)1060 self.write(": ")1061 self.set_precedence(_Precedence.TEST, node.body)1062 self.traverse(node.body)1063 1064 def visit_alias(self, node):1065 self.write(node.name)1066 if node.asname:1067 self.write(" as " + node.asname)1068 1069 def visit_withitem(self, node):1070 self.traverse(node.context_expr)1071 if node.optional_vars:1072 self.write(" as ")1073 self.traverse(node.optional_vars)1074 1075 def visit_match_case(self, node):1076 self.fill("case ", allow_semicolon=False)1077 self.traverse(node.pattern)1078 if node.guard:1079 self.write(" if ")1080 self.traverse(node.guard)1081 with self.block():1082 self.traverse(node.body)1083 1084 def visit_MatchValue(self, node):1085 self.traverse(node.value)1086 1087 def visit_MatchSingleton(self, node):1088 self._write_constant(node.value)1089 1090 def visit_MatchSequence(self, node):1091 with self.delimit("[", "]"):1092 self.interleave(1093 lambda: self.write(", "), self.traverse, node.patterns1094 )1095 1096 def visit_MatchStar(self, node):1097 name = node.name1098 if name is None:1099 name = "_"1100 self.write(f"*{name}")1101 1102 def visit_MatchMapping(self, node):1103 def write_key_pattern_pair(pair):1104 k, p = pair1105 self.traverse(k)1106 self.write(": ")1107 self.traverse(p)1108 1109 with self.delimit("{", "}"):1110 keys = node.keys1111 self.interleave(1112 lambda: self.write(", "),1113 write_key_pattern_pair,1114 zip(keys, node.patterns, strict=True),1115 )1116 rest = node.rest1117 if rest is not None:1118 if keys:1119 self.write(", ")1120 self.write(f"**{rest}")1121 1122 def visit_MatchClass(self, node):1123 self.set_precedence(_Precedence.ATOM, node.cls)1124 self.traverse(node.cls)1125 with self.delimit("(", ")"):1126 patterns = node.patterns1127 self.interleave(1128 lambda: self.write(", "), self.traverse, patterns1129 )1130 attrs = node.kwd_attrs1131 if attrs:1132 def write_attr_pattern(pair):1133 attr, pattern = pair1134 self.write(f"{attr}=")1135 self.traverse(pattern)1136 1137 if patterns:1138 self.write(", ")1139 self.interleave(1140 lambda: self.write(", "),1141 write_attr_pattern,1142 zip(attrs, node.kwd_patterns, strict=True),1143 )1144 1145 def visit_MatchAs(self, node):1146 name = node.name1147 pattern = node.pattern1148 if name is None:1149 self.write("_")1150 elif pattern is None:1151 self.write(node.name)1152 else:1153 with self.require_parens(_Precedence.TEST, node):1154 self.set_precedence(_Precedence.BOR, node.pattern)1155 self.traverse(node.pattern)1156 self.write(f" as {node.name}")1157 1158 def visit_MatchOr(self, node):1159 with self.require_parens(_Precedence.BOR, node):1160 self.set_precedence(_Precedence.BOR.next(), *node.patterns)1161 self.interleave(lambda: self.write(" | "), self.traverse, node.patterns)1162 