带有保留注释的Python AST

And*_*rei 18 python comments abstract-syntax-tree

我可以在没有评论的情况下使用AST

import ast
module = ast.parse(open('/path/to/module.py').read())
Run Code Online (Sandbox Code Playgroud)

你能举例说明AST是否保留了评论(以及空白)?

Ned*_*der 11

ast模块不包含注释.该tokenize模块可以为您提供注释,但不提供其他程序结构.

  • AST不包括它们是有原因的.因为`ast.Str`有`col_offset = -1`而`lineno`是字符串的最后一行,所以我遇到了多行字符串的麻烦.所有这些问题都可以通过一起使用`ast`和`tokenize`来解决.谢谢 (2认同)

azm*_*euk 10

保存有关格式化,注释等信息的AST称为完整语法树.

redbaron能够做到这一点.安装pip install redbaron并尝试以下代码.

import redbaron

with open("/path/to/module.py", "r") as source_code:
    red = redbaron.RedBaron(source_code.read())

print (red.fst())
Run Code Online (Sandbox Code Playgroud)

  • 不幸的是,`redbaron` 和它所基于的`baron` 库严重损坏,至少`redbaron` 似乎基本上没有维护。我努力让它做基本的转换,在修复它的过程中发现测试还不够完整。我的 [PR](https://github.com/PyCQA/redbaron/pull/144) 已经 4 个月没有一个回复了。一旦我解决了这个问题,我就遇到了底层`baron` 库的基本源代码解析问题。那时我发现了`lib2to3`并且再也没有回头。 (5认同)

小智 9

LibCST 为 Python 提供了一个具体语法树,看起来和感觉都像 AST。大多数节点类型与 AST 相同,同时提供格式信息(注释、空格、逗号等)。 https://github.com/Instagram/LibCST/

In [1]: import libcst as cst

In [2]: cst.parse_statement("fn(1, 2)  # a comment")                                                                                                                
Out[2]:
SimpleStatementLine(
    body=[
        Expr(
            value=Call(
                func=Name(
                    value='fn',
                    lpar=[],
                    rpar=[],
                ),
                args=[
                    Arg(
                        value=Integer(
                            value='1',
                            lpar=[],
                            rpar=[],
                        ),
                        keyword=None,
                        equal=MaybeSentinel.DEFAULT,
                        comma=Comma(        # <--- a comma
                            whitespace_before=SimpleWhitespace(
                                value='',
                            ),
                            whitespace_after=SimpleWhitespace(
                                value=' ',  # <--- a white space
                            ),
                        ),
                        star='',
                        whitespace_after_star=SimpleWhitespace(
                            value='',
                        ),
                        whitespace_after_arg=SimpleWhitespace(
                            value='',
                        ),
                    ),
                    Arg(
                        value=Integer(
                            value='2',
                            lpar=[],
                            rpar=[],
                        ),
                        keyword=None,
                        equal=MaybeSentinel.DEFAULT,
                        comma=MaybeSentinel.DEFAULT,
                        star='',
                        whitespace_after_star=SimpleWhitespace(
                            value='',
                        ),
                        whitespace_after_arg=SimpleWhitespace(
                            value='',
                        ),
                    ),
                ],
                lpar=[],
                rpar=[],
                whitespace_after_func=SimpleWhitespace(
                    value='',
                ),
                whitespace_before_args=SimpleWhitespace(
                    value='',
                ),
            ),
            semicolon=MaybeSentinel.DEFAULT,
        ),
    ],
    leading_lines=[],
    trailing_whitespace=TrailingWhitespace(
        whitespace=SimpleWhitespace(
            value='  ',
        ),
        comment=Comment(
            value='# a comment',  # <--- comment
        ),
        newline=Newline(
            value=None,
        ),
    ),
)
Run Code Online (Sandbox Code Playgroud)


Edw*_*eam 5

写任何Python代码美化的,PEP-8检查等.在这种情况下,当这个问题自然就出现了,你正在做一个源到源转换,你做的期待只希望输入被人写,而不是输出是人类可读的,但另外期望它:

  1. 包括所有评论,确切地说它们出现在原文中的位置.
  2. 输出字符串的确切拼写,包括原始文档中的文档字符串.

这与ast模块相比并不容易.你可以把它称为api中的一个洞,但似乎没有简单的方法来扩展api以轻松地做1和2.

Andrei建议同时使用ast和tokenize是一个很好的解决方法.在向Coffeescript转换器编写Python时,我也想到了这个想法,但代码远非微不足道.

TokenSyncpy2cs.py中的第1305行开始的(ts)类协调基于令牌的数据和ast遍历之间的通信.给定源字符串s,TokenSync该类标记并支持多个接口方法的内部数据结构:

ts.leading_lines(node):返回前面注释和空行的列表.

ts.trailing_comment(node):返回包含节点的尾随注释的字符串(如果有).

ts.sync_string(node):返回给定节点处字符串的拼写.

对于访问者来说,使用这些方法很简单,但有点笨拙.以下是CoffeeScriptTraverserpy2cs.py中(cst)类的一些示例:

def do_Str(self, node):
    '''A string constant, including docstrings.'''
    if hasattr(node, 'lineno'):
        return self.sync_string(node)
Run Code Online (Sandbox Code Playgroud)

这项工作提供了ast.Str节点按它们在源中出现的顺序访问.这在大多数遍历中自然发生.

这是ast.If访客.它显示了如何使用ts.leading_linests.trailing_comment:

def do_If(self, node):

    result = self.leading_lines(node)
    tail = self.trailing_comment(node)
    s = 'if %s:%s' % (self.visit(node.test), tail)
    result.append(self.indent(s))
    for z in node.body:
        self.level += 1
        result.append(self.visit(z))
        self.level -= 1
    if node.orelse:
        tail = self.tail_after_body(node.body, node.orelse, result)
        result.append(self.indent('else:' + tail))
        for z in node.orelse:
            self.level += 1
            result.append(self.visit(z))
            self.level -= 1
    return ''.join(result)
Run Code Online (Sandbox Code Playgroud)

ts.tail_after_body方法补偿了没有表示'else'子句的ast节点的事实.这不是火箭科学,但它并不漂亮:

def tail_after_body(self, body, aList, result):
    '''
    Return the tail of the 'else' or 'finally' statement following the given body.
    aList is the node.orelse or node.finalbody list.
    '''
    node = self.last_node(body)
    if node:
        max_n = node.lineno
        leading = self.leading_lines(aList[0])
        if leading:
            result.extend(leading)
            max_n += len(leading)
        tail = self.trailing_comment_at_lineno(max_n + 1)
    else:
        tail = '\n'
    return tail
Run Code Online (Sandbox Code Playgroud)

请注意,cst.tail_after_body只需要通话ts.tail_after_body.

摘要

TokenSync类封装了使面向令牌的数据可用于ast遍历代码所涉及的大多数复杂性.使用TokenSync类很简单,但所有Python语句(和ast.Str)的访问者必须包含对ts.leading_lines,ts.trailing_comment和的调用ts.sync_string.此外,ts.tail_after_body需要hack来处理"丢失"的ast节点.

简而言之,代码运行良好,但有点笨拙.

@Andrei:你的简短回答可能暗示你知道一种更优雅的方式.如果是这样,我很乐意看到它.

Edward K. Ream


cha*_*rik 5

一些人已经提到过lib2to3,但是我想创建一个更完整的答案,因为该工具是一个未被充分认识的工具。不用理会redbaron

lib2to3 由几个部分组成:

  • 解析器:令牌,语法等
  • 固定器:转换库
  • 重构工具:将修复程序应用于已解析的ast
  • 命令行:选择要应用的修补程序,并使用多处理并行运行它们

以下是lib2to3用于转换和抓取数据(即提取)的简要介绍。

转变

如果您想转换python文件(即复杂的查找/替换),则提供的CLI lib2to3功能齐全,并且可以并行转换文件。

要使用它,请创建一个python包,其中的每个子模块都包含一个子类lib2to3.fixer_base.BaseFix。请参阅参考资料lib2to3.fixes

然后创建您的可执行脚本(用包名称替换“ myfixes”):

import sys
import lib2to3.main

def main(args=None):
    sys.exit(lib2to3.main.main("myfixes", args=args))

if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

运行yourscript -h以查看选项。

刮ing

如果您的目标是收集数据而不是转换数据,那么您需要做更多的工作。这是我整理来lib2to3用于数据抓取的食谱:

# file: basescraper.py
from __future__ import absolute_import, print_function

from lib2to3.pgen2 import token
from lib2to3.pgen2.parse import ParseError
from lib2to3.pygram import python_grammar
from lib2to3.refactor import RefactoringTool
from lib2to3 import fixer_base


def symbol_name(number):
    """
    Get a human-friendly name from a token or symbol

    Very handy for debugging.
    """
    try:
        return token.tok_name[number]
    except KeyError:
        return python_grammar.number2symbol[number]


class SimpleRefactoringTool(RefactoringTool):
    def __init__(self, scraper_classes, options=None, explicit=None):
        self.fixers = None
        self.scraper_classes = scraper_classes
        # first argument is a list of fixer paths, as strings. we override
        # get_fixers, so we don't need it.
        super(SimpleRefactoringTool, self).__init__(None, options, explicit)

    def get_fixers(self):
        """
        Override base method to get fixers from passed fixers classes instead
        of via dotted-module-paths.
        """
        self.fixers = [cls(self.options, self.fixer_log)
                       for cls in self.scraper_classes]
        return (self.fixers, [])

    def get_results(self):
        """
        Get the scraped results returned from `scraper_classes`
        """
        return {type(fixer): fixer.results for fixer in self.fixers}


class BaseScraper(fixer_base.BaseFix):
    """
    Base class for a fixer that stores results.

    lib2to3 was designed with transformation in mind, but if you just want
    to scrape results, you need a way to pass data back to the caller.
    """
    BM_compatible = True

    def __init__(self, options, log):
        self.results = []
        super(BaseScraper, self).__init__(options, log)

    def scrape(self, node, match):
        raise NotImplementedError

    def transform(self, node, match):
        result = self.scrape(node, match)
        if result is not None:
            self.results.append(result)


def scrape(code, scraper):
    """
    Simple interface when you have a single scraper class.
    """
    tool = SimpleRefactoringTool([scraper])
    tool.refactor_string(code, '<test.py>')
    return tool.get_results()[scraper]
Run Code Online (Sandbox Code Playgroud)

这是一个简单的刮板,可在函数def之后找到第一个注释:

# file: commentscraper.py
from basescraper import scrape, BaseScraper, ParseError

class FindComments(BaseScraper):

    PATTERN = """ 
    funcdef< 'def' name=any parameters< '(' [any] ')' >
           ['->' any] ':' suite=any+ >
    """

    def scrape(self, node, results):
        suite = results["suite"]
        name = results["name"]

        if suite[0].children[1].type == token.INDENT:
            indent_node = suite[0].children[1]
            return (str(name), indent_node.prefix.strip())
        else:
            # e.g. "def foo(...): x = 5; y = 7"
            # nothing to save
            return

# example usage:

code = '''\

@decorator
def foobar():
    # type: comment goes here
    """
    docstring
    """
    pass

'''
comments = scrape(code, FindTypeComments)
assert comments == [('foobar', '# type: comment goes here')]
Run Code Online (Sandbox Code Playgroud)


cha*_*rik 5

如果您使用的是 python 3,则可以使用bowler,它基于 lib2to3,但提供了更好的 API 和 CLI 来创建转换脚本。

https://pybowler.io/