1# Code copied from Lib/ast.py from cpython 3.10 and slightly adjusted for gast
2#
3# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
4# --------------------------------------------
5#
6# 1. This LICENSE AGREEMENT is between the Python Software Foundation
7# ("PSF"), and the Individual or Organization ("Licensee") accessing and
8# otherwise using this software ("Python") in source or binary form and
9# its associated documentation.
10#
11# 2. Subject to the terms and conditions of this License Agreement, PSF hereby
12# grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
13# analyze, test, perform and/or display publicly, prepare derivative works,
14# distribute, and otherwise use Python alone or in any derivative version,
15# provided, however, that PSF's License Agreement and PSF's notice of copyright,
16# i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
17# 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation;
18# All Rights Reserved" are retained in Python alone or in any derivative version
19# prepared by Licensee.
20#
21# 3. In the event Licensee prepares a derivative work that is based on
22# or incorporates Python or any part thereof, and wants to make
23# the derivative work available to others as provided herein, then
24# Licensee hereby agrees to include in any such work a brief summary of
25# the changes made to Python.
26#
27# 4. PSF is making Python available to Licensee on an "AS IS"
28# basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
29# IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
30# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
31# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
32# INFRINGE ANY THIRD PARTY RIGHTS.
33#
34# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
35# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
36# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
37# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
38#
39# 6. This License Agreement will automatically terminate upon a material
40# breach of its terms and conditions.
41#
42# 7. Nothing in this License Agreement shall be deemed to create any
43# relationship of agency, partnership, or joint venture between PSF and
44# Licensee.  This License Agreement does not grant permission to use PSF
45# trademarks or trade name in a trademark sense to endorse or promote
46# products or services of Licensee, or any third party.
47#
48# 8. By copying, installing or otherwise using Python, Licensee
49# agrees to be bound by the terms and conditions of this License
50# Agreement.
51
52import sys
53from gast import *
54from contextlib import contextmanager
55from enum import auto, Enum
56
57
58class nullcontext(object):
59    def __init__(self, enter_result=None):
60        self.enter_result = enter_result
61
62    def __enter__(self):
63        return self.enter_result
64
65    def __exit__(self, *excinfo):
66        pass
67
68
69# Large float and imaginary literals get turned into infinities in the AST.
70# We unparse those infinities to INFSTR.
71_INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1)
72
73class _Precedence(object):
74    """Precedence table that originated from python grammar."""
75
76    TUPLE = 1
77    YIELD = 2           # 'yield', 'yield from'
78    TEST = 3            # 'if'-'else', 'lambda'
79    OR = 4              # 'or'
80    AND = 5             # 'and'
81    NOT = 6             # 'not'
82    CMP = 7             # '<', '>', '==', '>=', '<=', '!=',
83                             # 'in', 'not in', 'is', 'is not'
84    EXPR = 8
85    BOR = EXPR               # '|'
86    BXOR = 9            # '^'
87    BAND = 10            # '&'
88    SHIFT = 11           # '<<', '>>'
89    ARITH = 12           # '+', '-'
90    TERM = 13            # '*', '@', '/', '%', '//'
91    FACTOR = 14          # unary '+', '-', '~'
92    POWER = 15           # '**'
93    AWAIT = 16           # 'await'
94    ATOM = 17
95
96
97_SINGLE_QUOTES = ("'", '"')
98_MULTI_QUOTES = ('"""', "'''")
99_ALL_QUOTES = (*_SINGLE_QUOTES, *_MULTI_QUOTES)
100
101class _Unparser(NodeVisitor):
102    """Methods in this class recursively traverse an AST and
103    output source code for the abstract syntax; original formatting
104    is disregarded."""
105
106    def __init__(self, *, _avoid_backslashes=False):
107        self._source = []
108        self._buffer = []
109        self._precedences = {}
110        self._type_ignores = {}
111        self._indent = 0
112        self._avoid_backslashes = _avoid_backslashes
113
114    def interleave(self, inter, f, seq):
115        """Call f on each item in seq, calling inter() in between."""
116        seq = iter(seq)
117        try:
118            f(next(seq))
119        except StopIteration:
120            pass
121        else:
122            for x in seq:
123                inter()
124                f(x)
125
126    def items_view(self, traverser, items):
127        """Traverse and separate the given *items* with a comma and append it to
128        the buffer. If *items* is a single item sequence, a trailing comma
129        will be added."""
130        if len(items) == 1:
131            traverser(items[0])
132            self.write(",")
133        else:
134            self.interleave(lambda: self.write(", "), traverser, items)
135
136    def maybe_newline(self):
137        """Adds a newline if it isn't the start of generated source"""
138        if self._source:
139            self.write("\n")
140
141    def fill(self, text=""):
142        """Indent a piece of text and append it, according to the current
143        indentation level"""
144        self.maybe_newline()
145        self.write("    " * self._indent + text)
146
147    def write(self, text):
148        """Append a piece of text"""
149        self._source.append(text)
150
151    def buffer_writer(self, text):
152        self._buffer.append(text)
153
154    @property
155    def buffer(self):
156        value = "".join(self._buffer)
157        self._buffer.clear()
158        return value
159
160    @contextmanager
161    def block(self, *, extra = None):
162        """A context manager for preparing the source for blocks. It adds
163        the character':', increases the indentation on enter and decreases
164        the indentation on exit. If *extra* is given, it will be directly
165        appended after the colon character.
166        """
167        self.write(":")
168        if extra:
169            self.write(extra)
170        self._indent += 1
171        yield
172        self._indent -= 1
173
174    @contextmanager
175    def delimit(self, start, end):
176        """A context manager for preparing the source for expressions. It adds
177        *start* to the buffer and enters, after exit it adds *end*."""
178
179        self.write(start)
180        yield
181        self.write(end)
182
183    def delimit_if(self, start, end, condition):
184        if condition:
185            return self.delimit(start, end)
186        else:
187            return nullcontext()
188
189    def require_parens(self, precedence, node):
190        """Shortcut to adding precedence related parens"""
191        return self.delimit_if("(", ")", self.get_precedence(node) > precedence)
192
193    def get_precedence(self, node):
194        return self._precedences.get(node, _Precedence.TEST)
195
196    def set_precedence(self, precedence, *nodes):
197        for node in nodes:
198            self._precedences[node] = precedence
199
200    def get_raw_docstring(self, node):
201        """If a docstring node is found in the body of the *node* parameter,
202        return that docstring node, None otherwise.
203
204        Logic mirrored from ``_PyAST_GetDocString``."""
205        if not isinstance(
206            node, (AsyncFunctionDef, FunctionDef, ClassDef, Module)
207        ) or len(node.body) < 1:
208            return None
209        node = node.body[0]
210        if not isinstance(node, Expr):
211            return None
212        node = node.value
213        if isinstance(node, Constant) and isinstance(node.value, str):
214            return node
215
216    def get_type_comment(self, node):
217        comment = self._type_ignores.get(node.lineno) or node.type_comment
218        if comment is not None:
219            return f" # type: {comment}"
220
221    def traverse(self, node):
222        if isinstance(node, list):
223            for item in node:
224                self.traverse(item)
225        else:
226            super().visit(node)
227
228    # Note: as visit() resets the output text, do NOT rely on
229    # NodeVisitor.generic_visit to handle any nodes (as it calls back in to
230    # the subclass visit() method, which resets self._source to an empty list)
231    def visit(self, node):
232        """Outputs a source code string that, if converted back to an ast
233        (using ast.parse) will generate an AST equivalent to *node*"""
234        self._source = []
235        self.traverse(node)
236        return "".join(self._source)
237
238    def _write_docstring_and_traverse_body(self, node):
239        docstring = self.get_raw_docstring(node)
240        if docstring:
241            self._write_docstring(docstring)
242            self.traverse(node.body[1:])
243        else:
244            self.traverse(node.body)
245
246    def visit_Module(self, node):
247        self._type_ignores = {
248            ignore.lineno: f"ignore{ignore.tag}"
249            for ignore in node.type_ignores
250        }
251        self._write_docstring_and_traverse_body(node)
252        self._type_ignores.clear()
253
254    def visit_FunctionType(self, node):
255        with self.delimit("(", ")"):
256            self.interleave(
257                lambda: self.write(", "), self.traverse, node.argtypes
258            )
259
260        self.write(" -> ")
261        self.traverse(node.returns)
262
263    def visit_Expr(self, node):
264        self.fill()
265        self.set_precedence(_Precedence.YIELD, node.value)
266        self.traverse(node.value)
267
268    def visit_NamedExpr(self, node):
269        with self.require_parens(_Precedence.TUPLE, node):
270            self.set_precedence(_Precedence.ATOM, node.target, node.value)
271            self.traverse(node.target)
272            self.write(" := ")
273            self.traverse(node.value)
274
275    def visit_Import(self, node):
276        self.fill("import ")
277        self.interleave(lambda: self.write(", "), self.traverse, node.names)
278
279    def visit_ImportFrom(self, node):
280        self.fill("from ")
281        self.write("." * node.level)
282        if node.module:
283            self.write(node.module)
284        self.write(" import ")
285        self.interleave(lambda: self.write(", "), self.traverse, node.names)
286
287    def visit_Assign(self, node):
288        self.fill()
289        for target in node.targets:
290            self.traverse(target)
291            self.write(" = ")
292        self.traverse(node.value)
293        type_comment = self.get_type_comment(node)
294        if type_comment:
295            self.write(type_comment)
296
297    def visit_AugAssign(self, node):
298        self.fill()
299        self.traverse(node.target)
300        self.write(" " + self.binop[node.op.__class__.__name__] + "= ")
301        self.traverse(node.value)
302
303    def visit_AnnAssign(self, node):
304        self.fill()
305        with self.delimit_if("(", ")", not node.simple and isinstance(node.target, Name)):
306            self.traverse(node.target)
307        self.write(": ")
308        self.traverse(node.annotation)
309        if node.value:
310            self.write(" = ")
311            self.traverse(node.value)
312
313    def visit_Return(self, node):
314        self.fill("return")
315        if node.value:
316            self.write(" ")
317            self.traverse(node.value)
318
319    def visit_Pass(self, node):
320        self.fill("pass")
321
322    def visit_Break(self, node):
323        self.fill("break")
324
325    def visit_Continue(self, node):
326        self.fill("continue")
327
328    def visit_Delete(self, node):
329        self.fill("del ")
330        self.interleave(lambda: self.write(", "), self.traverse, node.targets)
331
332    def visit_Assert(self, node):
333        self.fill("assert ")
334        self.traverse(node.test)
335        if node.msg:
336            self.write(", ")
337            self.traverse(node.msg)
338
339    def visit_Global(self, node):
340        self.fill("global ")
341        self.interleave(lambda: self.write(", "), self.write, node.names)
342
343    def visit_Nonlocal(self, node):
344        self.fill("nonlocal ")
345        self.interleave(lambda: self.write(", "), self.write, node.names)
346
347    def visit_Await(self, node):
348        with self.require_parens(_Precedence.AWAIT, node):
349            self.write("await")
350            if node.value:
351                self.write(" ")
352                self.set_precedence(_Precedence.ATOM, node.value)
353                self.traverse(node.value)
354
355    def visit_Yield(self, node):
356        with self.require_parens(_Precedence.YIELD, node):
357            self.write("yield")
358            if node.value:
359                self.write(" ")
360                self.set_precedence(_Precedence.ATOM, node.value)
361                self.traverse(node.value)
362
363    def visit_YieldFrom(self, node):
364        with self.require_parens(_Precedence.YIELD, node):
365            self.write("yield from ")
366            if not node.value:
367                raise ValueError("Node can't be used without a value attribute.")
368            self.set_precedence(_Precedence.ATOM, node.value)
369            self.traverse(node.value)
370
371    def visit_Raise(self, node):
372        self.fill("raise")
373        if not node.exc:
374            if node.cause:
375                raise ValueError(f"Node can't use cause without an exception.")
376            return
377        self.write(" ")
378        self.traverse(node.exc)
379        if node.cause:
380            self.write(" from ")
381            self.traverse(node.cause)
382
383    def visit_Try(self, node):
384        self.fill("try")
385        with self.block():
386            self.traverse(node.body)
387        for ex in node.handlers:
388            self.traverse(ex)
389        if node.orelse:
390            self.fill("else")
391            with self.block():
392                self.traverse(node.orelse)
393        if node.finalbody:
394            self.fill("finally")
395            with self.block():
396                self.traverse(node.finalbody)
397
398    def visit_ExceptHandler(self, node):
399        self.fill("except")
400        if node.type:
401            self.write(" ")
402            self.traverse(node.type)
403        if node.name:
404            self.write(" as ")
405            self.write(node.name.id)
406        with self.block():
407            self.traverse(node.body)
408
409    def visit_ClassDef(self, node):
410        self.maybe_newline()
411        for deco in node.decorator_list:
412            self.fill("@")
413            self.traverse(deco)
414        self.fill("class " + node.name)
415        with self.delimit_if("(", ")", condition = node.bases or node.keywords):
416            comma = False
417            for e in node.bases:
418                if comma:
419                    self.write(", ")
420                else:
421                    comma = True
422                self.traverse(e)
423            for e in node.keywords:
424                if comma:
425                    self.write(", ")
426                else:
427                    comma = True
428                self.traverse(e)
429
430        with self.block():
431            self._write_docstring_and_traverse_body(node)
432
433    def visit_FunctionDef(self, node):
434        self._function_helper(node, "def")
435
436    def visit_AsyncFunctionDef(self, node):
437        self._function_helper(node, "async def")
438
439    def _function_helper(self, node, fill_suffix):
440        self.maybe_newline()
441        for deco in node.decorator_list:
442            self.fill("@")
443            self.traverse(deco)
444        def_str = fill_suffix + " " + node.name
445        self.fill(def_str)
446        with self.delimit("(", ")"):
447            self.traverse(node.args)
448        if node.returns:
449            self.write(" -> ")
450            self.traverse(node.returns)
451        with self.block(extra=self.get_type_comment(node)):
452            self._write_docstring_and_traverse_body(node)
453
454    def visit_For(self, node):
455        self._for_helper("for ", node)
456
457    def visit_AsyncFor(self, node):
458        self._for_helper("async for ", node)
459
460    def _for_helper(self, fill, node):
461        self.fill(fill)
462        self.traverse(node.target)
463        self.write(" in ")
464        self.traverse(node.iter)
465        with self.block(extra=self.get_type_comment(node)):
466            self.traverse(node.body)
467        if node.orelse:
468            self.fill("else")
469            with self.block():
470                self.traverse(node.orelse)
471
472    def visit_If(self, node):
473        self.fill("if ")
474        self.traverse(node.test)
475        with self.block():
476            self.traverse(node.body)
477        # collapse nested ifs into equivalent elifs.
478        while node.orelse and len(node.orelse) == 1 and isinstance(node.orelse[0], If):
479            node = node.orelse[0]
480            self.fill("elif ")
481            self.traverse(node.test)
482            with self.block():
483                self.traverse(node.body)
484        # final else
485        if node.orelse:
486            self.fill("else")
487            with self.block():
488                self.traverse(node.orelse)
489
490    def visit_While(self, node):
491        self.fill("while ")
492        self.traverse(node.test)
493        with self.block():
494            self.traverse(node.body)
495        if node.orelse:
496            self.fill("else")
497            with self.block():
498                self.traverse(node.orelse)
499
500    def visit_With(self, node):
501        self.fill("with ")
502        self.interleave(lambda: self.write(", "), self.traverse, node.items)
503        with self.block(extra=self.get_type_comment(node)):
504            self.traverse(node.body)
505
506    def visit_AsyncWith(self, node):
507        self.fill("async with ")
508        self.interleave(lambda: self.write(", "), self.traverse, node.items)
509        with self.block(extra=self.get_type_comment(node)):
510            self.traverse(node.body)
511
512    def _str_literal_helper(
513        self, string, *, quote_types=_ALL_QUOTES, escape_special_whitespace=False
514    ):
515        """Helper for writing string literals, minimizing escapes.
516        Returns the tuple (string literal to write, possible quote types).
517        """
518        def escape_char(c):
519            # \n and \t are non-printable, but we only escape them if
520            # escape_special_whitespace is True
521            if not escape_special_whitespace and c in "\n\t":
522                return c
523            # Always escape backslashes and other non-printable characters
524            if c == "\\" or not c.isprintable():
525                return c.encode("unicode_escape").decode("ascii")
526            return c
527
528        escaped_string = "".join(map(escape_char, string))
529        possible_quotes = quote_types
530        if "\n" in escaped_string:
531            possible_quotes = [q for q in possible_quotes if q in _MULTI_QUOTES]
532        possible_quotes = [q for q in possible_quotes if q not in escaped_string]
533        if not possible_quotes:
534            # If there aren't any possible_quotes, fallback to using repr
535            # on the original string. Try to use a quote from quote_types,
536            # e.g., so that we use triple quotes for docstrings.
537            string = repr(string)
538            quote = next((q for q in quote_types if string[0] in q), string[0])
539            return string[1:-1], [quote]
540        if escaped_string:
541            # Sort so that we prefer '''"''' over """\""""
542            possible_quotes.sort(key=lambda q: q[0] == escaped_string[-1])
543            # If we're using triple quotes and we'd need to escape a final
544            # quote, escape it
545            if possible_quotes[0][0] == escaped_string[-1]:
546                assert len(possible_quotes[0]) == 3
547                escaped_string = escaped_string[:-1] + "\\" + escaped_string[-1]
548        return escaped_string, possible_quotes
549
550    def _write_str_avoiding_backslashes(self, string, *, quote_types=_ALL_QUOTES):
551        """Write string literal value with a best effort attempt to avoid backslashes."""
552        string, quote_types = self._str_literal_helper(string, quote_types=quote_types)
553        quote_type = quote_types[0]
554        self.write(f"{quote_type}{string}{quote_type}")
555
556    def visit_JoinedStr(self, node):
557        self.write("f")
558        if self._avoid_backslashes:
559            self._fstring_JoinedStr(node, self.buffer_writer)
560            self._write_str_avoiding_backslashes(self.buffer)
561            return
562
563        # If we don't need to avoid backslashes globally (i.e., we only need
564        # to avoid them inside FormattedValues), it's cosmetically preferred
565        # to use escaped whitespace. That is, it's preferred to use backslashes
566        # for cases like: f"{x}\n". To accomplish this, we keep track of what
567        # in our buffer corresponds to FormattedValues and what corresponds to
568        # Constant parts of the f-string, and allow escapes accordingly.
569        buffer = []
570        for value in node.values:
571            meth = getattr(self, "_fstring_" + type(value).__name__)
572            meth(value, self.buffer_writer)
573            buffer.append((self.buffer, isinstance(value, Constant)))
574        new_buffer = []
575        quote_types = _ALL_QUOTES
576        for value, is_constant in buffer:
577            # Repeatedly narrow down the list of possible quote_types
578            value, quote_types = self._str_literal_helper(
579                value, quote_types=quote_types,
580                escape_special_whitespace=is_constant
581            )
582            new_buffer.append(value)
583        value = "".join(new_buffer)
584        quote_type = quote_types[0]
585        self.write(f"{quote_type}{value}{quote_type}")
586
587    def visit_FormattedValue(self, node):
588        self.write("f")
589        self._fstring_FormattedValue(node, self.buffer_writer)
590        self._write_str_avoiding_backslashes(self.buffer)
591
592    def _fstring_JoinedStr(self, node, write):
593        for value in node.values:
594            meth = getattr(self, "_fstring_" + type(value).__name__)
595            meth(value, write)
596
597    def _fstring_Constant(self, node, write):
598        if not isinstance(node.value, str):
599            raise ValueError("Constants inside JoinedStr should be a string.")
600        value = node.value.replace("{", "{{").replace("}", "}}")
601        write(value)
602
603    def _fstring_FormattedValue(self, node, write):
604        write("{")
605        unparser = type(self)(_avoid_backslashes=True)
606        unparser.set_precedence(_Precedence.TEST + 1, node.value)
607        expr = unparser.visit(node.value)
608        if expr.startswith("{"):
609            write(" ")  # Separate pair of opening brackets as "{ {"
610        if "\\" in expr:
611            raise ValueError("Unable to avoid backslash in f-string expression part")
612        write(expr)
613        if node.conversion != -1:
614            conversion = chr(node.conversion)
615            if conversion not in "sra":
616                raise ValueError("Unknown f-string conversion.")
617            write(f"!{conversion}")
618        if node.format_spec:
619            write(":")
620            meth = getattr(self, "_fstring_" + type(node.format_spec).__name__)
621            meth(node.format_spec, write)
622        write("}")
623
624    def visit_Name(self, node):
625        self.write(node.id)
626
627    def _write_docstring(self, node):
628        self.fill()
629        if node.kind == "u":
630            self.write("u")
631        self._write_str_avoiding_backslashes(node.value, quote_types=_MULTI_QUOTES)
632
633    def _write_constant(self, value):
634        if isinstance(value, (float, complex)):
635            # Substitute overflowing decimal literal for AST infinities,
636            # and inf - inf for NaNs.
637            self.write(
638                repr(value)
639                .replace("inf", _INFSTR)
640                .replace("nan", f"({_INFSTR}-{_INFSTR})")
641            )
642        elif self._avoid_backslashes and isinstance(value, str):
643            self._write_str_avoiding_backslashes(value)
644        else:
645            self.write(repr(value))
646
647    def visit_Constant(self, node):
648        value = node.value
649        if isinstance(value, tuple):
650            with self.delimit("(", ")"):
651                self.items_view(self._write_constant, value)
652        elif value is ...:
653            self.write("...")
654        else:
655            if node.kind == "u":
656                self.write("u")
657            self._write_constant(node.value)
658
659    def visit_List(self, node):
660        with self.delimit("[", "]"):
661            self.interleave(lambda: self.write(", "), self.traverse, node.elts)
662
663    def visit_ListComp(self, node):
664        with self.delimit("[", "]"):
665            self.traverse(node.elt)
666            for gen in node.generators:
667                self.traverse(gen)
668
669    def visit_GeneratorExp(self, node):
670        with self.delimit("(", ")"):
671            self.traverse(node.elt)
672            for gen in node.generators:
673                self.traverse(gen)
674
675    def visit_SetComp(self, node):
676        with self.delimit("{", "}"):
677            self.traverse(node.elt)
678            for gen in node.generators:
679                self.traverse(gen)
680
681    def visit_DictComp(self, node):
682        with self.delimit("{", "}"):
683            self.traverse(node.key)
684            self.write(": ")
685            self.traverse(node.value)
686            for gen in node.generators:
687                self.traverse(gen)
688
689    def visit_comprehension(self, node):
690        if node.is_async:
691            self.write(" async for ")
692        else:
693            self.write(" for ")
694        self.set_precedence(_Precedence.TUPLE, node.target)
695        self.traverse(node.target)
696        self.write(" in ")
697        self.set_precedence(_Precedence.TEST + 1, node.iter, *node.ifs)
698        self.traverse(node.iter)
699        for if_clause in node.ifs:
700            self.write(" if ")
701            self.traverse(if_clause)
702
703    def visit_IfExp(self, node):
704        with self.require_parens(_Precedence.TEST, node):
705            self.set_precedence(_Precedence.TEST + 1, node.body, node.test)
706            self.traverse(node.body)
707            self.write(" if ")
708            self.traverse(node.test)
709            self.write(" else ")
710            self.set_precedence(_Precedence.TEST, node.orelse)
711            self.traverse(node.orelse)
712
713    def visit_Set(self, node):
714        if node.elts:
715            with self.delimit("{", "}"):
716                self.interleave(lambda: self.write(", "), self.traverse, node.elts)
717        else:
718            # `{}` would be interpreted as a dictionary literal, and
719            # `set` might be shadowed. Thus:
720            self.write('{*()}')
721
722    def visit_Dict(self, node):
723        def write_key_value_pair(k, v):
724            self.traverse(k)
725            self.write(": ")
726            self.traverse(v)
727
728        def write_item(item):
729            k, v = item
730            if k is None:
731                # for dictionary unpacking operator in dicts {**{'y': 2}}
732                # see PEP 448 for details
733                self.write("**")
734                self.set_precedence(_Precedence.EXPR, v)
735                self.traverse(v)
736            else:
737                write_key_value_pair(k, v)
738
739        with self.delimit("{", "}"):
740            self.interleave(
741                lambda: self.write(", "), write_item, zip(node.keys, node.values)
742            )
743
744    def visit_Tuple(self, node):
745        with self.delimit("(", ")"):
746            self.items_view(self.traverse, node.elts)
747
748    unop = {"Invert": "~", "Not": "not", "UAdd": "+", "USub": "-"}
749    unop_precedence = {
750        "not": _Precedence.NOT,
751        "~": _Precedence.FACTOR,
752        "+": _Precedence.FACTOR,
753        "-": _Precedence.FACTOR,
754    }
755
756    def visit_UnaryOp(self, node):
757        operator = self.unop[node.op.__class__.__name__]
758        operator_precedence = self.unop_precedence[operator]
759        with self.require_parens(operator_precedence, node):
760            self.write(operator)
761            # factor prefixes (+, -, ~) shouldn't be seperated
762            # from the value they belong, (e.g: +1 instead of + 1)
763            if operator_precedence is not _Precedence.FACTOR:
764                self.write(" ")
765            self.set_precedence(operator_precedence, node.operand)
766            self.traverse(node.operand)
767
768    binop = {
769        "Add": "+",
770        "Sub": "-",
771        "Mult": "*",
772        "MatMult": "@",
773        "Div": "/",
774        "Mod": "%",
775        "LShift": "<<",
776        "RShift": ">>",
777        "BitOr": "|",
778        "BitXor": "^",
779        "BitAnd": "&",
780        "FloorDiv": "//",
781        "Pow": "**",
782    }
783
784    binop_precedence = {
785        "+": _Precedence.ARITH,
786        "-": _Precedence.ARITH,
787        "*": _Precedence.TERM,
788        "@": _Precedence.TERM,
789        "/": _Precedence.TERM,
790        "%": _Precedence.TERM,
791        "<<": _Precedence.SHIFT,
792        ">>": _Precedence.SHIFT,
793        "|": _Precedence.BOR,
794        "^": _Precedence.BXOR,
795        "&": _Precedence.BAND,
796        "//": _Precedence.TERM,
797        "**": _Precedence.POWER,
798    }
799
800    binop_rassoc = frozenset(("**",))
801    def visit_BinOp(self, node):
802        operator = self.binop[node.op.__class__.__name__]
803        operator_precedence = self.binop_precedence[operator]
804        with self.require_parens(operator_precedence, node):
805            if operator in self.binop_rassoc:
806                left_precedence = operator_precedence + 1
807                right_precedence = operator_precedence
808            else:
809                left_precedence = operator_precedence
810                right_precedence = operator_precedence + 1
811
812            self.set_precedence(left_precedence, node.left)
813            self.traverse(node.left)
814            self.write(f" {operator} ")
815            self.set_precedence(right_precedence, node.right)
816            self.traverse(node.right)
817
818    cmpops = {
819        "Eq": "==",
820        "NotEq": "!=",
821        "Lt": "<",
822        "LtE": "<=",
823        "Gt": ">",
824        "GtE": ">=",
825        "Is": "is",
826        "IsNot": "is not",
827        "In": "in",
828        "NotIn": "not in",
829    }
830
831    def visit_Compare(self, node):
832        with self.require_parens(_Precedence.CMP, node):
833            self.set_precedence(_Precedence.CMP + 1, node.left, *node.comparators)
834            self.traverse(node.left)
835            for o, e in zip(node.ops, node.comparators):
836                self.write(" " + self.cmpops[o.__class__.__name__] + " ")
837                self.traverse(e)
838
839    boolops = {"And": "and", "Or": "or"}
840    boolop_precedence = {"and": _Precedence.AND, "or": _Precedence.OR}
841
842    def visit_BoolOp(self, node):
843        operator = self.boolops[node.op.__class__.__name__]
844        operator_precedence = self.boolop_precedence[operator]
845
846        def increasing_level_traverse(node):
847            nonlocal operator_precedence
848            operator_precedence = operator_precedence + 1
849            self.set_precedence(operator_precedence, node)
850            self.traverse(node)
851
852        with self.require_parens(operator_precedence, node):
853            s = f" {operator} "
854            self.interleave(lambda: self.write(s), increasing_level_traverse, node.values)
855
856    def visit_Attribute(self, node):
857        self.set_precedence(_Precedence.ATOM, node.value)
858        self.traverse(node.value)
859        # Special case: 3.__abs__() is a syntax error, so if node.value
860        # is an integer literal then we need to either parenthesize
861        # it or add an extra space to get 3 .__abs__().
862        if isinstance(node.value, Constant) and isinstance(node.value.value, int):
863            self.write(" ")
864        self.write(".")
865        self.write(node.attr)
866
867    def visit_Call(self, node):
868        self.set_precedence(_Precedence.ATOM, node.func)
869        self.traverse(node.func)
870        with self.delimit("(", ")"):
871            comma = False
872            for e in node.args:
873                if comma:
874                    self.write(", ")
875                else:
876                    comma = True
877                self.traverse(e)
878            for e in node.keywords:
879                if comma:
880                    self.write(", ")
881                else:
882                    comma = True
883                self.traverse(e)
884
885    def visit_Subscript(self, node):
886        def is_simple_tuple(slice_value):
887            # when unparsing a non-empty tuple, the parentheses can be safely
888            # omitted if there aren't any elements that explicitly requires
889            # parentheses (such as starred expressions).
890            return (
891                isinstance(slice_value, Tuple)
892                and slice_value.elts
893                and not any(isinstance(elt, Starred) for elt in slice_value.elts)
894            )
895
896        self.set_precedence(_Precedence.ATOM, node.value)
897        self.traverse(node.value)
898        with self.delimit("[", "]"):
899            if is_simple_tuple(node.slice):
900                self.items_view(self.traverse, node.slice.elts)
901            else:
902                self.traverse(node.slice)
903
904    def visit_Starred(self, node):
905        self.write("*")
906        self.set_precedence(_Precedence.EXPR, node.value)
907        self.traverse(node.value)
908
909    def visit_Ellipsis(self, node):
910        self.write("...")
911
912    def visit_Slice(self, node):
913        if node.lower:
914            self.traverse(node.lower)
915        self.write(":")
916        if node.upper:
917            self.traverse(node.upper)
918        if node.step:
919            self.write(":")
920            self.traverse(node.step)
921
922    def visit_Match(self, node):
923        self.fill("match ")
924        self.traverse(node.subject)
925        with self.block():
926            for case in node.cases:
927                self.traverse(case)
928
929    def visit_arg(self, node):
930        self.write(node.arg.id)
931        if node.annotation:
932            self.write(": ")
933            self.traverse(node.annotation)
934
935    def visit_arguments(self, node):
936        first = True
937        # normal arguments
938        all_args = node.posonlyargs + node.args
939        defaults = [None] * (len(all_args) - len(node.defaults)) + node.defaults
940        for index, elements in enumerate(zip(all_args, defaults), 1):
941            a, d = elements
942            if first:
943                first = False
944            else:
945                self.write(", ")
946            self.traverse(a)
947            if d:
948                self.write("=")
949                self.traverse(d)
950            if index == len(node.posonlyargs):
951                self.write(", /")
952
953        # varargs, or bare '*' if no varargs but keyword-only arguments present
954        if node.vararg or node.kwonlyargs:
955            if first:
956                first = False
957            else:
958                self.write(", ")
959            self.write("*")
960            if node.vararg:
961                self.write(node.vararg.id)
962                if node.vararg.annotation:
963                    self.write(": ")
964                    self.traverse(node.vararg.annotation)
965
966        # keyword-only arguments
967        if node.kwonlyargs:
968            for a, d in zip(node.kwonlyargs, node.kw_defaults):
969                self.write(", ")
970                self.traverse(a)
971                if d:
972                    self.write("=")
973                    self.traverse(d)
974
975        # kwargs
976        if node.kwarg:
977            if first:
978                first = False
979            else:
980                self.write(", ")
981            self.write("**" + node.kwarg.id)
982            if node.kwarg.annotation:
983                self.write(": ")
984                self.traverse(node.kwarg.annotation)
985
986    def visit_keyword(self, node):
987        if node.arg is None:
988            self.write("**")
989        else:
990            self.write(node.arg)
991            self.write("=")
992        self.traverse(node.value)
993
994    def visit_Lambda(self, node):
995        with self.require_parens(_Precedence.TEST, node):
996            self.write("lambda ")
997            self.traverse(node.args)
998            self.write(": ")
999            self.set_precedence(_Precedence.TEST, node.body)
1000            self.traverse(node.body)
1001
1002    def visit_alias(self, node):
1003        self.write(node.name)
1004        if node.asname:
1005            self.write(" as " + node.asname)
1006
1007    def visit_withitem(self, node):
1008        self.traverse(node.context_expr)
1009        if node.optional_vars:
1010            self.write(" as ")
1011            self.traverse(node.optional_vars)
1012
1013    def visit_match_case(self, node):
1014        self.fill("case ")
1015        self.traverse(node.pattern)
1016        if node.guard:
1017            self.write(" if ")
1018            self.traverse(node.guard)
1019        with self.block():
1020            self.traverse(node.body)
1021
1022    def visit_MatchValue(self, node):
1023        self.traverse(node.value)
1024
1025    def visit_MatchSingleton(self, node):
1026        self._write_constant(node.value)
1027
1028    def visit_MatchSequence(self, node):
1029        with self.delimit("[", "]"):
1030            self.interleave(
1031                lambda: self.write(", "), self.traverse, node.patterns
1032            )
1033
1034    def visit_MatchStar(self, node):
1035        name = node.name
1036        if name is None:
1037            name = "_"
1038        self.write(f"*{name}")
1039
1040    def visit_MatchMapping(self, node):
1041        def write_key_pattern_pair(pair):
1042            k, p = pair
1043            self.traverse(k)
1044            self.write(": ")
1045            self.traverse(p)
1046
1047        with self.delimit("{", "}"):
1048            keys = node.keys
1049            self.interleave(
1050                lambda: self.write(", "),
1051                write_key_pattern_pair,
1052                zip(keys, node.patterns, strict=True),
1053            )
1054            rest = node.rest
1055            if rest is not None:
1056                if keys:
1057                    self.write(", ")
1058                self.write(f"**{rest}")
1059
1060    def visit_MatchClass(self, node):
1061        self.set_precedence(_Precedence.ATOM, node.cls)
1062        self.traverse(node.cls)
1063        with self.delimit("(", ")"):
1064            patterns = node.patterns
1065            self.interleave(
1066                lambda: self.write(", "), self.traverse, patterns
1067            )
1068            attrs = node.kwd_attrs
1069            if attrs:
1070                def write_attr_pattern(pair):
1071                    attr, pattern = pair
1072                    self.write(f"{attr}=")
1073                    self.traverse(pattern)
1074
1075                if patterns:
1076                    self.write(", ")
1077                self.interleave(
1078                    lambda: self.write(", "),
1079                    write_attr_pattern,
1080                    zip(attrs, node.kwd_patterns, strict=True),
1081                )
1082
1083    def visit_MatchAs(self, node):
1084        name = node.name
1085        pattern = node.pattern
1086        if name is None:
1087            self.write("_")
1088        elif pattern is None:
1089            self.write(node.name)
1090        else:
1091            with self.require_parens(_Precedence.TEST, node):
1092                self.set_precedence(_Precedence.BOR, node.pattern)
1093                self.traverse(node.pattern)
1094                self.write(f" as {node.name}")
1095
1096    def visit_MatchOr(self, node):
1097        with self.require_parens(_Precedence.BOR, node):
1098            self.set_precedence(_Precedence.BOR + 1, *node.patterns)
1099            self.interleave(lambda: self.write(" | "), self.traverse, node.patterns)
1100
1101def unparse(ast_obj):
1102    unparser = _Unparser()
1103    return unparser.visit(ast_obj)
1104