Python 仅使用内置库进行保留注释的解析?

Python comment-preserving parsing using only builtin libraries?

我只使用 ast and inspect libraries to parse and emit [uses astor 在 Python < 3.9] 内部 Python 构造上编写了一个库。

刚刚意识到我真的需要保留评论。最好不要诉诸 RedBaron or LibCST;因为我只需要发出未改变的评论;是否有一种简洁明了的 注释保留方式 parsing/emitting Python 仅使用 stdlib 的源代码?

可以通过使用分词器捕获评论并将其合并回生成的源代码来保留评论。

给定一个程序变量中的玩具程序,我们可以演示注释如何在 AST 中丢失:

import ast

program = """
# This comment lost
p1v = 4 + 4
p1l = ['a', # Implicit line joining comment for a lost
       'b'] # Ending comment for b lost
def p1f(x):
    "p1f docstring"
    # Comment in function p1f lost
    return x
print(p1f(p1l), p1f(p1v))
"""
tree = ast.parse(program)
print('== Full program code:')
print(ast.unparse(tree))

输出显示所有评论都消失了:

== Full program code:
p1v = 4 + 4
p1l = ['a', 'b']

def p1f(x):
    """p1f docstring"""
    return x
print(p1f(p1l), p1f(p1v))

但是,如果我们用分词器扫描评论,我们可以 用它来合并评论:

from io import StringIO
import tokenize

def scan_comments(source):
    """ Scan source code file for relevant comments
    """
    # Find token for comments
    for k,v in tokenize.tok_name.items():
        if v == 'COMMENT':
            comment = k
            break
    comtokens = []
    with StringIO(source) as f:
        tokens = tokenize.generate_tokens(f.readline)
        for token in tokens:
            if token.type != comment:
                continue
            comtokens += [token]
    return comtokens

comtokens = scan_comments(program)
print('== Comment after p1l[0]\n\t', comtokens[1])

输出(编辑为拆分长行):

== Comment after p1l[0]
     TokenInfo(type=60 (COMMENT),
               string='# Implicit line joining comment for a lost',
               start=(4, 12), end=(4, 54),
               line="p1l = ['a', # Implicit line joining comment for a lost\n")

使用 ast.unparse() 的略微修改版本,替换 方法 maybe_newline()traverse() 修改后的版本, 您应该能够合并回他们的所有评论 大概位置,使用评论中的位置信息 扫描仪(起始变量),结合来自的位置信息 助攻;大多数节点都有 lineno 属性。

不完全是。参见例如列表变量赋值。这 源代码分为两行,但是 ast.unparse() 仅生成一行(请参阅第二个代码段中的输出)。

此外,您需要确保更新 AST 中的位置信息 添加代码后使用 ast.increment_lineno()

好像多了一些电话 maybe_newline() 可能需要在库代码(或其 更换)。

我最终做的是编写一个简单的解析器,在 339 行源代码中没有 meta-language: https://github.com/offscale/cdd-python/blob/master/cdd/cst_utils.py

具体语法树的实现 [List!]

  1. 逐字符读取源文件;
  2. 一旦检测到语句结束†,将statement-type添加到一维列表中;
    • †行尾 if line.lstrip().startswith("#") or line not endswith('\') and balanced_parens(line) 否则继续咀嚼直到该条件为真……加上一些 edge-cases 围绕多行字符串等;
  3. 完成后会有一个大的 (1D) 列表,其中每个元素都是 namedtuplevalue 属性.

与内置抽象语法树集成ast

  1. ast 个节点限制为修改而不是删除:{ClassDef,AsyncFunctionDef,FunctionDef} docstring (first body element Constant|Str), AssignAnnAssign;
  2. cst_idx, cst_node = find_cst_at_ast(cst_list, _node);
  3. if doc_str 节点 then maybe_replace_doc_str_in_function_or_class(_node, cst_idx, cst_list)
  4. 现在 cst_list 仅包含对上述节点的更改,并且仅当更改超过空格时,才能创建为带有 "".join(map(attrgetter("value"), cst_list)) 的字符串以输出到 eval 或直接转到源文件(例如,in-place 覆盖)。

质量控制

  1. 100% 测试覆盖率
  2. 100% 文档覆盖率
  3. 支持 Python 的最后 6 个版本(包括最新的 alpha)
  4. CI/CD
  5. (Apache-2.0 或 MIT)许可

限制

  1. 缺少 meta-language,特别是缺少使用 Python's provided grammar 意味着不会自动支持新语法元素(支持 match/case,但如果有自此引入了新语法,[还?] 不支持……至少不自动支持);
  2. 不是内置于 stdlib,因此 stdlib 可能会破坏兼容性;
  3. [可能]不支持删除节点;
  4. 如果存在影子变量或 linters 应该指出的类似问题,则可能会错误地识别节点。