我可以使用以下方法获得没有注释的AST:
import ast
module = ast.parse(open('/path/to/module.py').read())
你能展示一个获取保留注释(和空格)的 AST 的例子吗?
我可以使用以下方法获得没有注释的AST:
import ast
module = ast.parse(open('/path/to/module.py').read())
你能展示一个获取保留注释(和空格)的 AST 的例子吗?
保留格式、注释等信息的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())
在编写任何Python代码美化器、PEP-8检查器等时,这个问题自然而然地出现了。在这种情况下,您正在进行源到源的转换,您希望输入由人类编写,并且不仅希望输出可读性强,而且还期望它:
使用ast模块实现这一点绝非易事。您可以将其视为API中的漏洞,但似乎没有简单的方法来扩展API以轻松完成1和2。
安德烈的建议是同时使用ast和tokenize,这是一个聪明的解决方法。当我编写Python到Coffeescript转换器时,我也想到了这个思路,但这段代码远非简单。
TokenSync
(ts)类从py2cs.py第1305行开始,协调基于标记的数据和ast遍历之间的通信。给定源字符串s,TokenSync
类标记化s并初始化内部数据结构,支持几个接口方法:
ts.leading_lines(node)
:返回前面的注释和空行列表。
ts.trailing_comment(node)
:如果有,则返回包含节点尾部注释的字符串。
ts.sync_string(node)
:返回给定节点处字符串的拼写。
对于ast访问者来说,使用这些方法是很简单的,但有点笨拙。以下是py2cs.py中CoffeeScriptTraverser
(cst)类的一些示例:
def do_Str(self, node):
'''A string constant, including docstrings.'''
if hasattr(node, 'lineno'):
return self.sync_string(node)
只要以它们在源代码中出现的顺序访问ast.Str节点,就可以正常工作。在大多数遍历中,这种情况会自然发生。
这里是ast.If访问器的例子。它展示了如何使用ts.leading_lines
和ts.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)
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
请注意,cst.tail_after_body
只是调用了ts.tail_after_body
。ts.leading_lines
、ts.trailing_comment
和ts.sync_string
的调用。此外,需要使用ts.tail_after_body
来处理“缺失”的ast节点。redbaron
。
lib2to3
由几个部分组成:
lib2to3
进行转换和抓取数据(即提取)。
如果您想要转换Python文件(即复杂的查找/替换),则lib2to3
提供的CLI功能齐全,并且可以并行转换文件。
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()
运行yourscript -h
以查看选项。
如果您的目标是收集数据而不是转换数据,那么您需要做更多的工作。这里是我为使用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]
下面是一个简单的爬虫程序,它可以找到函数定义后的第一条评论:
# 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')]
PATTERN
变量的目的是什么?是否有文档记录其中使用的语法? - Nils Lindemannfoo<whitespace>()
的出现,但不能找到foo()
的出现? - Nils LindemannPATTERN
是fixer_base.BaseFix
的抽象属性。我从未找到过任何关于它的文档,但在修复模块中有很多示例。如果需要更多示例,可以查看future库中的修复。关键字对应于Python语法中的符号。PATTERN
使用的树匹配语言的语法在这里。 - chadrikLibCST为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,
),
),
)
ast-comments
来处理特定情况(https://pypi.org/project/ast-comments/)。ast
和 tokenize
,如在https://dev59.com/l2s05IYBdhLWcg3wFOC8#7457047中所述。>>> import ast_comments as astcom
>>> source = """
... # some comments (1)
... some_variable = 'value' # inline comments (2)
... """
>>> tree = astcom.parse(source)
>>> node = tree.body[0]
>>> node.comments
('some comments (1)', 'inline comments (2)')
>>> astcom.dump(tree)
"Module(body=[Assign(targets=[Name(id='some_variable', ctx=Store())], value=Constant(value='value', kind=None), type_comment=None, comments=('some comments (1)', 'inline comments (2)'))], type_ignores=[])"
其他专家似乎认为Python AST模块会剥离注释,这意味着该方法对您来说根本行不通。
我们的DMS软件重构工具包及其Python前端将解析Python并构建AST,以捕获所有注释(请参阅此SO示例)。 Python前端包括一个漂亮的打印机,可以直接从AST中重新生成Python代码(包括注释!)。 DMS本身提供了低级解析机制和源到源转换功能,可使用目标语言(例如Python)表面语法编写的模式进行操作。
lib2to3
。 - Andreiimport libcst as cst
和cst.parse_module(some_python_filecontent)
,可以得到带有注释的 CST。 - a.t.