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是否保留了评论(以及空白)?
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)
小智 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)
写任何Python代码美化的,PEP-8检查等.在这种情况下,当这个问题自然就出现了,你正在做一个源到源转换,你做的期待只希望输入被人写,而不是输出是人类可读的,但另外期望它:
这与ast模块相比并不容易.你可以把它称为api中的一个洞,但似乎没有简单的方法来扩展api以轻松地做1和2.
Andrei建议同时使用ast和tokenize是一个很好的解决方法.在向Coffeescript转换器编写Python时,我也想到了这个想法,但代码远非微不足道.
TokenSync从py2cs.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_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)
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
一些人已经提到过lib2to3,但是我想创建一个更完整的答案,因为该工具是一个未被充分认识的工具。不用理会redbaron。
lib2to3 由几个部分组成:
以下是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以查看选项。
如果您的目标是收集数据而不是转换数据,那么您需要做更多的工作。这是我整理来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)
如果您使用的是 python 3,则可以使用bowler,它基于 lib2to3,但提供了更好的 API 和 CLI 来创建转换脚本。