1# Copyright (c) 2009-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr> 2# Copyright (c) 2010 Daniel Harding <dharding@gmail.com> 3# Copyright (c) 2013-2016, 2018-2020 Claudiu Popa <pcmanticore@gmail.com> 4# Copyright (c) 2013-2014 Google, Inc. 5# Copyright (c) 2015-2016 Ceridwen <ceridwenv@gmail.com> 6# Copyright (c) 2016 Jared Garst <jgarst@users.noreply.github.com> 7# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net> 8# Copyright (c) 2017, 2019 Łukasz Rogalski <rogalski.91@gmail.com> 9# Copyright (c) 2017 rr- <rr-@sakuya.pl> 10# Copyright (c) 2018 Serhiy Storchaka <storchaka@gmail.com> 11# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi> 12# Copyright (c) 2018 brendanator <brendan.maginnis@gmail.com> 13# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com> 14# Copyright (c) 2019 Alex Hall <alex.mojaki@gmail.com> 15# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com> 16# Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> 17# Copyright (c) 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com> 18# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com> 19# Copyright (c) 2021 pre-commit-ci[bot] <bot@noreply.github.com> 20 21# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html 22# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE 23 24"""This module renders Astroid nodes as string""" 25from typing import TYPE_CHECKING, List 26 27if TYPE_CHECKING: 28 from astroid.nodes.node_classes import ( 29 Match, 30 MatchAs, 31 MatchCase, 32 MatchClass, 33 MatchMapping, 34 MatchOr, 35 MatchSequence, 36 MatchSingleton, 37 MatchStar, 38 MatchValue, 39 Unknown, 40 ) 41 42# pylint: disable=unused-argument 43 44DOC_NEWLINE = "\0" 45 46 47# Visitor pattern require argument all the time and is not better with staticmethod 48# noinspection PyUnusedLocal,PyMethodMayBeStatic 49class AsStringVisitor: 50 """Visitor to render an Astroid node as a valid python code string""" 51 52 def __init__(self, indent=" "): 53 self.indent = indent 54 55 def __call__(self, node): 56 """Makes this visitor behave as a simple function""" 57 return node.accept(self).replace(DOC_NEWLINE, "\n") 58 59 def _docs_dedent(self, doc): 60 """Stop newlines in docs being indented by self._stmt_list""" 61 return '\n{}"""{}"""'.format(self.indent, doc.replace("\n", DOC_NEWLINE)) 62 63 def _stmt_list(self, stmts, indent=True): 64 """return a list of nodes to string""" 65 stmts = "\n".join(nstr for nstr in [n.accept(self) for n in stmts] if nstr) 66 if indent: 67 return self.indent + stmts.replace("\n", "\n" + self.indent) 68 69 return stmts 70 71 def _precedence_parens(self, node, child, is_left=True): 72 """Wrap child in parens only if required to keep same semantics""" 73 if self._should_wrap(node, child, is_left): 74 return f"({child.accept(self)})" 75 76 return child.accept(self) 77 78 def _should_wrap(self, node, child, is_left): 79 """Wrap child if: 80 - it has lower precedence 81 - same precedence with position opposite to associativity direction 82 """ 83 node_precedence = node.op_precedence() 84 child_precedence = child.op_precedence() 85 86 if node_precedence > child_precedence: 87 # 3 * (4 + 5) 88 return True 89 90 if ( 91 node_precedence == child_precedence 92 and is_left != node.op_left_associative() 93 ): 94 # 3 - (4 - 5) 95 # (2**3)**4 96 return True 97 98 return False 99 100 # visit_<node> methods ########################################### 101 102 def visit_await(self, node): 103 return f"await {node.value.accept(self)}" 104 105 def visit_asyncwith(self, node): 106 return f"async {self.visit_with(node)}" 107 108 def visit_asyncfor(self, node): 109 return f"async {self.visit_for(node)}" 110 111 def visit_arguments(self, node): 112 """return an astroid.Function node as string""" 113 return node.format_args() 114 115 def visit_assignattr(self, node): 116 """return an astroid.AssAttr node as string""" 117 return self.visit_attribute(node) 118 119 def visit_assert(self, node): 120 """return an astroid.Assert node as string""" 121 if node.fail: 122 return f"assert {node.test.accept(self)}, {node.fail.accept(self)}" 123 return f"assert {node.test.accept(self)}" 124 125 def visit_assignname(self, node): 126 """return an astroid.AssName node as string""" 127 return node.name 128 129 def visit_assign(self, node): 130 """return an astroid.Assign node as string""" 131 lhs = " = ".join(n.accept(self) for n in node.targets) 132 return f"{lhs} = {node.value.accept(self)}" 133 134 def visit_augassign(self, node): 135 """return an astroid.AugAssign node as string""" 136 return f"{node.target.accept(self)} {node.op} {node.value.accept(self)}" 137 138 def visit_annassign(self, node): 139 """Return an astroid.AugAssign node as string""" 140 141 target = node.target.accept(self) 142 annotation = node.annotation.accept(self) 143 if node.value is None: 144 return f"{target}: {annotation}" 145 return f"{target}: {annotation} = {node.value.accept(self)}" 146 147 def visit_binop(self, node): 148 """return an astroid.BinOp node as string""" 149 left = self._precedence_parens(node, node.left) 150 right = self._precedence_parens(node, node.right, is_left=False) 151 if node.op == "**": 152 return f"{left}{node.op}{right}" 153 154 return f"{left} {node.op} {right}" 155 156 def visit_boolop(self, node): 157 """return an astroid.BoolOp node as string""" 158 values = [f"{self._precedence_parens(node, n)}" for n in node.values] 159 return (f" {node.op} ").join(values) 160 161 def visit_break(self, node): 162 """return an astroid.Break node as string""" 163 return "break" 164 165 def visit_call(self, node): 166 """return an astroid.Call node as string""" 167 expr_str = self._precedence_parens(node, node.func) 168 args = [arg.accept(self) for arg in node.args] 169 if node.keywords: 170 keywords = [kwarg.accept(self) for kwarg in node.keywords] 171 else: 172 keywords = [] 173 174 args.extend(keywords) 175 return f"{expr_str}({', '.join(args)})" 176 177 def visit_classdef(self, node): 178 """return an astroid.ClassDef node as string""" 179 decorate = node.decorators.accept(self) if node.decorators else "" 180 args = [n.accept(self) for n in node.bases] 181 if node._metaclass and not node.has_metaclass_hack(): 182 args.append("metaclass=" + node._metaclass.accept(self)) 183 args += [n.accept(self) for n in node.keywords] 184 args = f"({', '.join(args)})" if args else "" 185 docs = self._docs_dedent(node.doc) if node.doc else "" 186 return "\n\n{}class {}{}:{}\n{}\n".format( 187 decorate, node.name, args, docs, self._stmt_list(node.body) 188 ) 189 190 def visit_compare(self, node): 191 """return an astroid.Compare node as string""" 192 rhs_str = " ".join( 193 f"{op} {self._precedence_parens(node, expr, is_left=False)}" 194 for op, expr in node.ops 195 ) 196 return f"{self._precedence_parens(node, node.left)} {rhs_str}" 197 198 def visit_comprehension(self, node): 199 """return an astroid.Comprehension node as string""" 200 ifs = "".join(f" if {n.accept(self)}" for n in node.ifs) 201 generated = f"for {node.target.accept(self)} in {node.iter.accept(self)}{ifs}" 202 return f"{'async ' if node.is_async else ''}{generated}" 203 204 def visit_const(self, node): 205 """return an astroid.Const node as string""" 206 if node.value is Ellipsis: 207 return "..." 208 return repr(node.value) 209 210 def visit_continue(self, node): 211 """return an astroid.Continue node as string""" 212 return "continue" 213 214 def visit_delete(self, node): # XXX check if correct 215 """return an astroid.Delete node as string""" 216 return f"del {', '.join(child.accept(self) for child in node.targets)}" 217 218 def visit_delattr(self, node): 219 """return an astroid.DelAttr node as string""" 220 return self.visit_attribute(node) 221 222 def visit_delname(self, node): 223 """return an astroid.DelName node as string""" 224 return node.name 225 226 def visit_decorators(self, node): 227 """return an astroid.Decorators node as string""" 228 return "@%s\n" % "\n@".join(item.accept(self) for item in node.nodes) 229 230 def visit_dict(self, node): 231 """return an astroid.Dict node as string""" 232 return "{%s}" % ", ".join(self._visit_dict(node)) 233 234 def _visit_dict(self, node): 235 for key, value in node.items: 236 key = key.accept(self) 237 value = value.accept(self) 238 if key == "**": 239 # It can only be a DictUnpack node. 240 yield key + value 241 else: 242 yield f"{key}: {value}" 243 244 def visit_dictunpack(self, node): 245 return "**" 246 247 def visit_dictcomp(self, node): 248 """return an astroid.DictComp node as string""" 249 return "{{{}: {} {}}}".format( 250 node.key.accept(self), 251 node.value.accept(self), 252 " ".join(n.accept(self) for n in node.generators), 253 ) 254 255 def visit_expr(self, node): 256 """return an astroid.Discard node as string""" 257 return node.value.accept(self) 258 259 def visit_emptynode(self, node): 260 """dummy method for visiting an Empty node""" 261 return "" 262 263 def visit_excepthandler(self, node): 264 if node.type: 265 if node.name: 266 excs = f"except {node.type.accept(self)} as {node.name.accept(self)}" 267 else: 268 excs = f"except {node.type.accept(self)}" 269 else: 270 excs = "except" 271 return f"{excs}:\n{self._stmt_list(node.body)}" 272 273 def visit_empty(self, node): 274 """return an Empty node as string""" 275 return "" 276 277 def visit_for(self, node): 278 """return an astroid.For node as string""" 279 fors = "for {} in {}:\n{}".format( 280 node.target.accept(self), node.iter.accept(self), self._stmt_list(node.body) 281 ) 282 if node.orelse: 283 fors = f"{fors}\nelse:\n{self._stmt_list(node.orelse)}" 284 return fors 285 286 def visit_importfrom(self, node): 287 """return an astroid.ImportFrom node as string""" 288 return "from {} import {}".format( 289 "." * (node.level or 0) + node.modname, _import_string(node.names) 290 ) 291 292 def visit_joinedstr(self, node): 293 string = "".join( 294 # Use repr on the string literal parts 295 # to get proper escapes, e.g. \n, \\, \" 296 # But strip the quotes off the ends 297 # (they will always be one character: ' or ") 298 repr(value.value)[1:-1] 299 # Literal braces must be doubled to escape them 300 .replace("{", "{{").replace("}", "}}") 301 # Each value in values is either a string literal (Const) 302 # or a FormattedValue 303 if type(value).__name__ == "Const" else value.accept(self) 304 for value in node.values 305 ) 306 307 # Try to find surrounding quotes that don't appear at all in the string. 308 # Because the formatted values inside {} can't contain backslash (\) 309 # using a triple quote is sometimes necessary 310 for quote in ("'", '"', '"""', "'''"): 311 if quote not in string: 312 break 313 314 return "f" + quote + string + quote 315 316 def visit_formattedvalue(self, node): 317 result = node.value.accept(self) 318 if node.conversion and node.conversion >= 0: 319 # e.g. if node.conversion == 114: result += "!r" 320 result += "!" + chr(node.conversion) 321 if node.format_spec: 322 # The format spec is itself a JoinedString, i.e. an f-string 323 # We strip the f and quotes of the ends 324 result += ":" + node.format_spec.accept(self)[2:-1] 325 return "{%s}" % result 326 327 def handle_functiondef(self, node, keyword): 328 """return a (possibly async) function definition node as string""" 329 decorate = node.decorators.accept(self) if node.decorators else "" 330 docs = self._docs_dedent(node.doc) if node.doc else "" 331 trailer = ":" 332 if node.returns: 333 return_annotation = " -> " + node.returns.as_string() 334 trailer = return_annotation + ":" 335 def_format = "\n%s%s %s(%s)%s%s\n%s" 336 return def_format % ( 337 decorate, 338 keyword, 339 node.name, 340 node.args.accept(self), 341 trailer, 342 docs, 343 self._stmt_list(node.body), 344 ) 345 346 def visit_functiondef(self, node): 347 """return an astroid.FunctionDef node as string""" 348 return self.handle_functiondef(node, "def") 349 350 def visit_asyncfunctiondef(self, node): 351 """return an astroid.AsyncFunction node as string""" 352 return self.handle_functiondef(node, "async def") 353 354 def visit_generatorexp(self, node): 355 """return an astroid.GeneratorExp node as string""" 356 return "({} {})".format( 357 node.elt.accept(self), " ".join(n.accept(self) for n in node.generators) 358 ) 359 360 def visit_attribute(self, node): 361 """return an astroid.Getattr node as string""" 362 left = self._precedence_parens(node, node.expr) 363 if left.isdigit(): 364 left = f"({left})" 365 return f"{left}.{node.attrname}" 366 367 def visit_global(self, node): 368 """return an astroid.Global node as string""" 369 return f"global {', '.join(node.names)}" 370 371 def visit_if(self, node): 372 """return an astroid.If node as string""" 373 ifs = [f"if {node.test.accept(self)}:\n{self._stmt_list(node.body)}"] 374 if node.has_elif_block(): 375 ifs.append(f"el{self._stmt_list(node.orelse, indent=False)}") 376 elif node.orelse: 377 ifs.append(f"else:\n{self._stmt_list(node.orelse)}") 378 return "\n".join(ifs) 379 380 def visit_ifexp(self, node): 381 """return an astroid.IfExp node as string""" 382 return "{} if {} else {}".format( 383 self._precedence_parens(node, node.body, is_left=True), 384 self._precedence_parens(node, node.test, is_left=True), 385 self._precedence_parens(node, node.orelse, is_left=False), 386 ) 387 388 def visit_import(self, node): 389 """return an astroid.Import node as string""" 390 return f"import {_import_string(node.names)}" 391 392 def visit_keyword(self, node): 393 """return an astroid.Keyword node as string""" 394 if node.arg is None: 395 return f"**{node.value.accept(self)}" 396 return f"{node.arg}={node.value.accept(self)}" 397 398 def visit_lambda(self, node): 399 """return an astroid.Lambda node as string""" 400 args = node.args.accept(self) 401 body = node.body.accept(self) 402 if args: 403 return f"lambda {args}: {body}" 404 405 return f"lambda: {body}" 406 407 def visit_list(self, node): 408 """return an astroid.List node as string""" 409 return f"[{', '.join(child.accept(self) for child in node.elts)}]" 410 411 def visit_listcomp(self, node): 412 """return an astroid.ListComp node as string""" 413 return "[{} {}]".format( 414 node.elt.accept(self), " ".join(n.accept(self) for n in node.generators) 415 ) 416 417 def visit_module(self, node): 418 """return an astroid.Module node as string""" 419 docs = f'"""{node.doc}"""\n\n' if node.doc else "" 420 return docs + "\n".join(n.accept(self) for n in node.body) + "\n\n" 421 422 def visit_name(self, node): 423 """return an astroid.Name node as string""" 424 return node.name 425 426 def visit_namedexpr(self, node): 427 """Return an assignment expression node as string""" 428 target = node.target.accept(self) 429 value = node.value.accept(self) 430 return f"{target} := {value}" 431 432 def visit_nonlocal(self, node): 433 """return an astroid.Nonlocal node as string""" 434 return f"nonlocal {', '.join(node.names)}" 435 436 def visit_pass(self, node): 437 """return an astroid.Pass node as string""" 438 return "pass" 439 440 def visit_raise(self, node): 441 """return an astroid.Raise node as string""" 442 if node.exc: 443 if node.cause: 444 return f"raise {node.exc.accept(self)} from {node.cause.accept(self)}" 445 return f"raise {node.exc.accept(self)}" 446 return "raise" 447 448 def visit_return(self, node): 449 """return an astroid.Return node as string""" 450 if node.is_tuple_return() and len(node.value.elts) > 1: 451 elts = [child.accept(self) for child in node.value.elts] 452 return f"return {', '.join(elts)}" 453 454 if node.value: 455 return f"return {node.value.accept(self)}" 456 457 return "return" 458 459 def visit_set(self, node): 460 """return an astroid.Set node as string""" 461 return "{%s}" % ", ".join(child.accept(self) for child in node.elts) 462 463 def visit_setcomp(self, node): 464 """return an astroid.SetComp node as string""" 465 return "{{{} {}}}".format( 466 node.elt.accept(self), " ".join(n.accept(self) for n in node.generators) 467 ) 468 469 def visit_slice(self, node): 470 """return an astroid.Slice node as string""" 471 lower = node.lower.accept(self) if node.lower else "" 472 upper = node.upper.accept(self) if node.upper else "" 473 step = node.step.accept(self) if node.step else "" 474 if step: 475 return f"{lower}:{upper}:{step}" 476 return f"{lower}:{upper}" 477 478 def visit_subscript(self, node): 479 """return an astroid.Subscript node as string""" 480 idx = node.slice 481 if idx.__class__.__name__.lower() == "index": 482 idx = idx.value 483 idxstr = idx.accept(self) 484 if idx.__class__.__name__.lower() == "tuple" and idx.elts: 485 # Remove parenthesis in tuple and extended slice. 486 # a[(::1, 1:)] is not valid syntax. 487 idxstr = idxstr[1:-1] 488 return f"{self._precedence_parens(node, node.value)}[{idxstr}]" 489 490 def visit_tryexcept(self, node): 491 """return an astroid.TryExcept node as string""" 492 trys = [f"try:\n{self._stmt_list(node.body)}"] 493 for handler in node.handlers: 494 trys.append(handler.accept(self)) 495 if node.orelse: 496 trys.append(f"else:\n{self._stmt_list(node.orelse)}") 497 return "\n".join(trys) 498 499 def visit_tryfinally(self, node): 500 """return an astroid.TryFinally node as string""" 501 return "try:\n{}\nfinally:\n{}".format( 502 self._stmt_list(node.body), self._stmt_list(node.finalbody) 503 ) 504 505 def visit_tuple(self, node): 506 """return an astroid.Tuple node as string""" 507 if len(node.elts) == 1: 508 return f"({node.elts[0].accept(self)}, )" 509 return f"({', '.join(child.accept(self) for child in node.elts)})" 510 511 def visit_unaryop(self, node): 512 """return an astroid.UnaryOp node as string""" 513 if node.op == "not": 514 operator = "not " 515 else: 516 operator = node.op 517 return f"{operator}{self._precedence_parens(node, node.operand)}" 518 519 def visit_while(self, node): 520 """return an astroid.While node as string""" 521 whiles = f"while {node.test.accept(self)}:\n{self._stmt_list(node.body)}" 522 if node.orelse: 523 whiles = f"{whiles}\nelse:\n{self._stmt_list(node.orelse)}" 524 return whiles 525 526 def visit_with(self, node): # 'with' without 'as' is possible 527 """return an astroid.With node as string""" 528 items = ", ".join( 529 f"{expr.accept(self)}" + (v and f" as {v.accept(self)}" or "") 530 for expr, v in node.items 531 ) 532 return f"with {items}:\n{self._stmt_list(node.body)}" 533 534 def visit_yield(self, node): 535 """yield an ast.Yield node as string""" 536 yi_val = (" " + node.value.accept(self)) if node.value else "" 537 expr = "yield" + yi_val 538 if node.parent.is_statement: 539 return expr 540 541 return f"({expr})" 542 543 def visit_yieldfrom(self, node): 544 """Return an astroid.YieldFrom node as string.""" 545 yi_val = (" " + node.value.accept(self)) if node.value else "" 546 expr = "yield from" + yi_val 547 if node.parent.is_statement: 548 return expr 549 550 return f"({expr})" 551 552 def visit_starred(self, node): 553 """return Starred node as string""" 554 return "*" + node.value.accept(self) 555 556 def visit_match(self, node: "Match") -> str: 557 """Return an astroid.Match node as string.""" 558 return f"match {node.subject.accept(self)}:\n{self._stmt_list(node.cases)}" 559 560 def visit_matchcase(self, node: "MatchCase") -> str: 561 """Return an astroid.MatchCase node as string.""" 562 guard_str = f" if {node.guard.accept(self)}" if node.guard else "" 563 return ( 564 f"case {node.pattern.accept(self)}{guard_str}:\n" 565 f"{self._stmt_list(node.body)}" 566 ) 567 568 def visit_matchvalue(self, node: "MatchValue") -> str: 569 """Return an astroid.MatchValue node as string.""" 570 return node.value.accept(self) 571 572 @staticmethod 573 def visit_matchsingleton(node: "MatchSingleton") -> str: 574 """Return an astroid.MatchSingleton node as string.""" 575 return str(node.value) 576 577 def visit_matchsequence(self, node: "MatchSequence") -> str: 578 """Return an astroid.MatchSequence node as string.""" 579 if node.patterns is None: 580 return "[]" 581 return f"[{', '.join(p.accept(self) for p in node.patterns)}]" 582 583 def visit_matchmapping(self, node: "MatchMapping") -> str: 584 """Return an astroid.MatchMapping node as string.""" 585 mapping_strings: List[str] = [] 586 if node.keys and node.patterns: 587 mapping_strings.extend( 588 f"{key.accept(self)}: {p.accept(self)}" 589 for key, p in zip(node.keys, node.patterns) 590 ) 591 if node.rest: 592 mapping_strings.append(f"**{node.rest.accept(self)}") 593 return f"{'{'}{', '.join(mapping_strings)}{'}'}" 594 595 def visit_matchclass(self, node: "MatchClass") -> str: 596 """Return an astroid.MatchClass node as string.""" 597 if node.cls is None: 598 raise Exception(f"{node} does not have a 'cls' node") 599 class_strings: List[str] = [] 600 if node.patterns: 601 class_strings.extend(p.accept(self) for p in node.patterns) 602 if node.kwd_attrs and node.kwd_patterns: 603 for attr, pattern in zip(node.kwd_attrs, node.kwd_patterns): 604 class_strings.append(f"{attr}={pattern.accept(self)}") 605 return f"{node.cls.accept(self)}({', '.join(class_strings)})" 606 607 def visit_matchstar(self, node: "MatchStar") -> str: 608 """Return an astroid.MatchStar node as string.""" 609 return f"*{node.name.accept(self) if node.name else '_'}" 610 611 def visit_matchas(self, node: "MatchAs") -> str: 612 """Return an astroid.MatchAs node as string.""" 613 # pylint: disable=import-outside-toplevel 614 # Prevent circular dependency 615 from astroid.nodes.node_classes import MatchClass, MatchMapping, MatchSequence 616 617 if isinstance(node.parent, (MatchSequence, MatchMapping, MatchClass)): 618 return node.name.accept(self) if node.name else "_" 619 return ( 620 f"{node.pattern.accept(self) if node.pattern else '_'}" 621 f"{f' as {node.name.accept(self)}' if node.name else ''}" 622 ) 623 624 def visit_matchor(self, node: "MatchOr") -> str: 625 """Return an astroid.MatchOr node as string.""" 626 if node.patterns is None: 627 raise Exception(f"{node} does not have pattern nodes") 628 return " | ".join(p.accept(self) for p in node.patterns) 629 630 # These aren't for real AST nodes, but for inference objects. 631 632 def visit_frozenset(self, node): 633 return node.parent.accept(self) 634 635 def visit_super(self, node): 636 return node.parent.accept(self) 637 638 def visit_uninferable(self, node): 639 return str(node) 640 641 def visit_property(self, node): 642 return node.function.accept(self) 643 644 def visit_evaluatedobject(self, node): 645 return node.original.accept(self) 646 647 def visit_unknown(self, node: "Unknown") -> str: 648 return str(node) 649 650 651def _import_string(names): 652 """return a list of (name, asname) formatted as a string""" 653 _names = [] 654 for name, asname in names: 655 if asname is not None: 656 _names.append(f"{name} as {asname}") 657 else: 658 _names.append(name) 659 return ", ".join(_names) 660 661 662# This sets the default indent to 4 spaces. 663to_code = AsStringVisitor(" ") 664