Phân tích tệp .py, đọc AST, sửa đổi tệp, sau đó ghi lại mã nguồn đã sửa đổi


167

Tôi muốn lập trình chỉnh sửa mã nguồn python. Về cơ bản tôi muốn đọc một .pytệp, tạo AST và sau đó viết lại mã nguồn python đã sửa đổi (tức là một .pytệp khác ).

Có nhiều cách để phân tích / biên dịch mã nguồn python bằng cách sử dụng các mô-đun python tiêu chuẩn, chẳng hạn như asthoặc compiler. Tuy nhiên, tôi không nghĩ bất kỳ ai trong số họ hỗ trợ các cách để sửa đổi mã nguồn (ví dụ: xóa khai báo hàm này) và sau đó viết lại mã nguồn python sửa đổi.

CẬP NHẬT: Lý do tôi muốn làm điều này là tôi muốn viết thư viện thử nghiệm Đột biến cho python, chủ yếu bằng cách xóa các câu lệnh / biểu thức, kiểm tra lại và xem những gì phá vỡ.


4
Không dùng nữa kể từ phiên bản 2.6: Gói trình biên dịch đã bị xóa trong Python 3.0.
dfa

1
Bạn không thể chỉnh sửa nguồn nào? Tại sao bạn không thể viết một trang trí?
S.Lott

3
Bò thần! Tôi muốn tạo một trình kiểm tra đột biến cho python bằng cách sử dụng kỹ thuật tương tự (cụ thể là tạo một plugin mũi), bạn có định mở nguồn không?
Ryan

2
@Ryan Vâng tôi sẽ mở bất cứ thứ gì tôi tạo ra. Chúng ta nên giữ liên lạc về điều này
Rory

1
Chắc chắn, tôi đã gửi cho bạn một email thông qua Launchpad.
Ryan

Câu trả lời:


73

Pythcop thực hiện điều này với các trường hợp thử nghiệm mà nó tự động tạo ra cũng như công cụ 2to3 cho python 2.6 (nó chuyển đổi nguồn python 2.x thành nguồn python 3.x).

Cả hai công cụ này đều sử dụng thư viện lib2to3 , đây là một triển khai của bộ phân tích / trình biên dịch python có thể lưu giữ các bình luận trong nguồn khi nó bị vấp từ nguồn -> AST -> nguồn.

Các dự án dây có thể đáp ứng nhu cầu của bạn nếu bạn muốn làm refactoring giống như biến đổi.

Các ast Module này là tùy chọn khác của bạn, và có một ví dụ cũ của làm thế nào để cây cú pháp "unparse" sao vào mã (sử dụng các mô-đun phân tích cú pháp). Nhưng astmô-đun hữu ích hơn khi thực hiện chuyển đổi AST trên mã sau đó được chuyển đổi thành đối tượng mã.

Các redbaron dự án cũng có thể phù hợp (ht Xavier Combelle)


5
ví dụ khác biệt vẫn được duy trì, đây là phiên bản py3k được cập nhật: hg.python.org/cpython/log/tip/Tools/parser/unparse.py
Janus Troelsen

2
Đối với unparse.pytập lệnh - có thể thực sự cồng kềnh khi sử dụng nó từ tập lệnh khác. Nhưng, có một gói được gọi là astunparse ( trên github , trên pypi ) về cơ bản là một phiên bản được đóng gói đúng cách unparse.py.
mbdevpl

Bạn có thể cập nhật câu trả lời của mình bằng cách thêm parso làm tùy chọn ưa thích không? Nó rất tốt và cập nhật.
đóng hộp

59

Mô-đun ast dựng sẵn dường như không có phương pháp để chuyển đổi trở lại nguồn. Tuy nhiên, mô-đun codegen ở đây cung cấp một máy in đẹp cho ast cho phép bạn làm như vậy. ví dụ.

import ast
import codegen

expr="""
def foo():
   print("hello world")
"""
p=ast.parse(expr)

p.body[0].body = [ ast.parse("return 42").body[0] ] # Replace function body with "return 42"

print(codegen.to_source(p))

Điều này sẽ in:

def foo():
    return 42

Lưu ý rằng bạn có thể mất định dạng và nhận xét chính xác, vì chúng không được bảo tồn.

Tuy nhiên, bạn có thể không cần. Nếu tất cả những gì bạn yêu cầu là thực thi AST được thay thế, bạn có thể làm điều đó đơn giản bằng cách gọi compile () trên ast và thực thi đối tượng mã kết quả.


20
Chỉ dành cho bất cứ ai sử dụng điều này trong tương lai, codegen phần lớn đã lỗi thời và có một vài lỗi. Tôi đã sửa một vài trong số chúng; Tôi có cái này như một ý chính trên github: gist.github.com/791312
mattbasta

Lưu ý rằng codegen mới nhất được cập nhật vào năm 2012, sau phần bình luận ở trên, vì vậy tôi đoán codegen đã được cập nhật. @mattbasta
zjffdu

3
astor dường như là một sự kế thừa duy trì cho codegen
medmunds

19

Bạn có thể không cần phải tạo lại mã nguồn. Tất nhiên, điều đó hơi nguy hiểm đối với tôi, vì bạn chưa thực sự giải thích lý do tại sao bạn nghĩ rằng bạn cần phải tạo một tệp .py đầy mã; nhưng:

  • Nếu bạn muốn tạo tệp .py mà mọi người thực sự sẽ sử dụng, có thể để họ có thể điền vào biểu mẫu và nhận tệp .py hữu ích để chèn vào dự án của họ, thì bạn không muốn thay đổi tệp đó thành AST và trở lại bởi vì bạn sẽ mất tất cả định dạng (nghĩ về các dòng trống khiến Python dễ đọc bằng cách nhóm các nhóm dòng liên quan lại với nhau) ( các nút ast linenocol_offsetthuộc tính ) nhận xét. Thay vào đó, có lẽ bạn sẽ muốn sử dụng một công cụ tạo khuôn mẫu ( ví dụ, ngôn ngữ mẫu Django , được thiết kế để dễ dàng tạo khuôn mẫu cho các tệp văn bản) để tùy chỉnh tệp .py hoặc sử dụng tiện ích mở rộng MetaPython của Rick Copeland .

  • Nếu bạn đang cố gắng thực hiện thay đổi trong quá trình biên dịch mô-đun, lưu ý rằng bạn không cần phải quay lại văn bản; bạn chỉ có thể biên dịch AST trực tiếp thay vì biến nó trở lại thành tệp .py.

  • Nhưng trong hầu hết mọi trường hợp, có lẽ bạn đang cố gắng làm một điều gì đó năng động mà một ngôn ngữ như Python thực sự làm rất dễ dàng, mà không cần viết các tệp .py mới! Nếu bạn mở rộng câu hỏi của mình để cho chúng tôi biết những gì bạn thực sự muốn thực hiện, các tệp .py mới có thể sẽ không liên quan đến câu trả lời; Tôi đã thấy hàng trăm dự án Python thực hiện hàng trăm thứ trong thế giới thực và không phải là một trong số chúng cần thiết để viết một tệp .py. Vì vậy, tôi phải thừa nhận, tôi có một chút hoài nghi rằng bạn đã tìm thấy trường hợp sử dụng tốt đầu tiên. :-)

Cập nhật: bây giờ bạn đã giải thích những gì bạn đang cố gắng thực hiện, dù sao tôi cũng muốn hoạt động trên AST. Bạn sẽ muốn thay đổi bằng cách xóa, không phải các dòng của tệp (điều này có thể dẫn đến một nửa câu lệnh đơn giản là chết với SyntaxError), nhưng toàn bộ câu lệnh - và nơi nào tốt hơn để làm điều đó hơn trong AST?


Tổng quan tốt về giải pháp có thể và các lựa chọn thay thế.
Ryan

1
Trường hợp sử dụng trong thế giới thực để tạo mã: Kid và Genshi (tôi tin) tạo Python từ các mẫu XML để hiển thị nhanh các trang động.
Rick Copeland

19

Trong một câu trả lời khác, tôi đề nghị sử dụng astorgói, nhưng từ đó tôi đã tìm thấy một gói không phân tích cú pháp AST cập nhật hơn được gọi là astunparse:

>>> import ast
>>> import astunparse
>>> print(astunparse.unparse(ast.parse('def foo(x): return 2 * x')))


def foo(x):
    return (2 * x)

Tôi đã thử nghiệm điều này trên Python 3.5.


10

Phân tích cú pháp và sửa đổi cấu trúc mã chắc chắn là có thể với sự trợ giúp của astmô-đun và tôi sẽ hiển thị nó trong một ví dụ trong giây lát. Tuy nhiên, không thể viết lại mã nguồn đã sửa đổi chỉ với astmô-đun. Có các mô-đun khác có sẵn cho công việc này, chẳng hạn như một ở đây .

LƯU Ý: Ví dụ dưới đây có thể được coi là hướng dẫn giới thiệu về cách sử dụng astmô-đun nhưng hướng dẫn toàn diện hơn về sử dụng astmô-đun có sẵn ở đây tại hướng dẫn rắn Green Treetài liệu chính thức về astmô-đun .

Giới thiệu về ast:

>>> import ast
>>> tree = ast.parse("print 'Hello Python!!'")
>>> exec(compile(tree, filename="<ast>", mode="exec"))
Hello Python!!

Bạn có thể phân tích mã python (được biểu thị bằng chuỗi) bằng cách gọi API ast.parse(). Điều này trả về tay cầm cho cấu trúc Tóm tắt Cú pháp Cây (AST). Điều thú vị là bạn có thể biên dịch lại cấu trúc này và thực hiện nó như được hiển thị ở trên.

Một API rất hữu ích khác là ast.dump()kết xuất toàn bộ AST ở dạng chuỗi. Nó có thể được sử dụng để kiểm tra cấu trúc cây và rất hữu ích trong việc gỡ lỗi. Ví dụ,

Trên Python 2.7:

>>> import ast
>>> tree = ast.parse("print 'Hello Python!!'")
>>> ast.dump(tree)
"Module(body=[Print(dest=None, values=[Str(s='Hello Python!!')], nl=True)])"

Trên Python 3.5:

>>> import ast
>>> tree = ast.parse("print ('Hello Python!!')")
>>> ast.dump(tree)
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello Python!!')], keywords=[]))])"

Lưu ý sự khác biệt về cú pháp cho câu lệnh in trong Python 2.7 so với Python 3.5 và sự khác biệt về loại nút AST trong các cây tương ứng.


Cách sửa đổi mã bằng cách sử dụng ast:

Bây giờ, chúng ta hãy xem một ví dụ về sửa đổi mã python theo astmô-đun. Công cụ chính để sửa đổi cấu trúc AST là ast.NodeTransformerlớp. Bất cứ khi nào cần sửa đổi AST, anh ấy / cô ấy cần phải phân lớp từ nó và viết (các) Biến đổi nút tương ứng.

Ví dụ của chúng tôi, hãy thử viết một tiện ích đơn giản để chuyển đổi Python 2, in các câu lệnh thành các lệnh gọi hàm Python 3.

Tuyên bố in sang tiện ích chuyển đổi cuộc gọi vui vẻ: print2to3.py:

#!/usr/bin/env python
'''
This utility converts the python (2.7) statements to Python 3 alike function calls before running the code.

USAGE:
     python print2to3.py <filename>
'''
import ast
import sys

class P2to3(ast.NodeTransformer):
    def visit_Print(self, node):
        new_node = ast.Expr(value=ast.Call(func=ast.Name(id='print', ctx=ast.Load()),
            args=node.values,
            keywords=[], starargs=None, kwargs=None))
        ast.copy_location(new_node, node)
        return new_node

def main(filename=None):
    if not filename:
        return

    with open(filename, 'r') as fp:
        data = fp.readlines()
    data = ''.join(data)
    tree = ast.parse(data)

    print "Converting python 2 print statements to Python 3 function calls"
    print "-" * 35
    P2to3().visit(tree)
    ast.fix_missing_locations(tree)
    # print ast.dump(tree)

    exec(compile(tree, filename="p23", mode="exec"))

if __name__ == '__main__':
    if len(sys.argv) <=1:
        print ("\nUSAGE:\n\t print2to3.py <filename>")
        sys.exit(1)
    else:
        main(sys.argv[1])

Tiện ích này có thể được thử trên tệp ví dụ nhỏ, chẳng hạn như dưới đây, và nó sẽ hoạt động tốt.

Kiểm tra tệp đầu vào: py2.py

class A(object):
    def __init__(self):
        pass

def good():
    print "I am good"

main = good

if __name__ == '__main__':
    print "I am in main"
    main()

Xin lưu ý rằng chuyển đổi ở trên chỉ dành cho astmục đích hướng dẫn và trong trường hợp thực tế, người ta sẽ phải xem xét tất cả các kịch bản khác nhau, chẳng hạn như print " x is %s" % ("Hello Python").


6

Tôi đã tạo gần đây khá ổn định (lõi thực sự được kiểm tra tốt) và đoạn mã có thể mở rộng tạo mã từ astcây: https://github.com/paluh/code-formatter .

Tôi đang sử dụng dự án của mình làm cơ sở cho một plugin vim nhỏ (mà tôi đang sử dụng hàng ngày), vì vậy mục tiêu của tôi là tạo mã python thực sự đẹp và dễ đọc.

PS Tôi đã cố gắng mở rộng codegennhưng kiến ​​trúc của nó dựa trên ast.NodeVisitorgiao diện, vì vậy các trình định dạng ( visitor_phương thức) chỉ là các hàm. Tôi thấy cấu trúc này khá hạn chế và khó tối ưu hóa (trong trường hợp các biểu thức dài và lồng nhau, việc giữ cây đối tượng và lưu trữ một số kết quả một phần dễ dàng hơn - theo cách khác bạn có thể đạt được độ phức tạp theo cấp số nhân nếu bạn muốn tìm kiếm bố cục tốt nhất). NHƯNG codegen như mọi tác phẩm của mitsuhiko (mà tôi đã đọc) được viết rất hay và súc tích.


4

Một trong những câu trả lời khác khuyến nghị codegen, dường như đã được áp dụng bởi astor. Phiên bản astortrên PyPI (phiên bản 0.5 khi viết bài này) dường như cũng hơi lỗi thời, vì vậy bạn có thể cài đặt phiên bản phát triển astornhư sau.

pip install git+https://github.com/berkerpeksag/astor.git#egg=astor

Sau đó, bạn có thể sử dụng astor.to_sourceđể chuyển đổi Python AST thành mã nguồn Python có thể đọc được:

>>> import ast
>>> import astor
>>> print(astor.to_source(ast.parse('def foo(x): return 2 * x')))
def foo(x):
    return 2 * x

Tôi đã thử nghiệm điều này trên Python 3.5.


4

Nếu bạn đang xem xét điều này vào năm 2019, thì bạn có thể sử dụng libcst này gói . Nó có cú pháp tương tự ast. Điều này hoạt động như một nét duyên dáng, và bảo tồn cấu trúc mã. Về cơ bản, nó hữu ích cho dự án nơi bạn phải giữ bình luận, khoảng trắng, dòng mới, v.v.

Nếu bạn không cần quan tâm đến việc lưu giữ các bình luận, khoảng trắng và các bình luận khác, thì sự kết hợp giữa ast và astor hoạt động tốt.


2

Chúng tôi có một nhu cầu tương tự, không được giải quyết bằng các câu trả lời khác ở đây. Vì vậy, chúng tôi đã tạo một thư viện cho cái này, ASTTokens , lấy một cây AST được tạo ra với các mô-đun ast hoặc astroid và đánh dấu nó bằng các phạm vi văn bản trong mã nguồn gốc.

Nó không thực hiện sửa đổi mã trực tiếp, nhưng điều đó không khó để thêm vào đầu, vì nó cho bạn biết phạm vi văn bản bạn cần sửa đổi.

Ví dụ, phần này kết thúc một lệnh gọi hàm WRAP(...), giữ nguyên các bình luận và mọi thứ khác:

example = """
def foo(): # Test
  '''My func'''
  log("hello world")  # Print
"""

import ast, asttokens
atok = asttokens.ASTTokens(example, parse=True)

call = next(n for n in ast.walk(atok.tree) if isinstance(n, ast.Call))
start, end = atok.get_text_range(call)
print(atok.text[:start] + ('WRAP(%s)' % atok.text[start:end])  + atok.text[end:])

Sản xuất:

def foo(): # Test
  '''My func'''
  WRAP(log("hello world"))  # Print

Hi vọng điêu nay co ich!


1

Một chuyển đổi chương trình Hệ thống là một công cụ phân tích nguồn văn bản, xây dựng ASTs, cho phép bạn chỉnh sửa chúng bằng nguồn-to-nguồn biến đổi ( "nếu bạn thấy mô hình này, thay thế nó bằng rằng mô hình"). Các công cụ như vậy rất lý tưởng để thực hiện đột biến mã nguồn hiện tại, đó chỉ là "nếu bạn thấy mẫu này, thay thế bằng một biến thể mẫu".

Tất nhiên, bạn cần một công cụ chuyển đổi chương trình có thể phân tích ngôn ngữ mà bạn quan tâm và vẫn thực hiện các chuyển đổi theo hướng mẫu. Bộ công cụ tái cấu trúc phần mềm DMS của chúng tôi là một hệ thống có thể làm điều đó và xử lý Python và nhiều ngôn ngữ khác.

Xem câu trả lời SO này để biết ví dụ về AST được phân tích cú pháp DMS để Python ghi nhận bình luận chính xác. DMS có thể thay đổi AST và tạo lại văn bản hợp lệ, bao gồm cả các nhận xét. Bạn có thể yêu cầu nó in ấn AST, sử dụng các quy ước định dạng riêng của nó (bạn có thể thay đổi các quy tắc này) hoặc thực hiện "in độ trung thực", sử dụng thông tin dòng và cột ban đầu để bảo toàn tối đa bố cục ban đầu (một số thay đổi trong cách bố trí mã mới được chèn là không thể tránh khỏi).

Để thực hiện quy tắc "đột biến" cho Python với DMS, bạn có thể viết như sau:

rule mutate_addition(s:sum, p:product):sum->sum =
  " \s + \p " -> " \s - \p"
 if mutate_this_place(s);

Quy tắc này thay thế "+" bằng "-" theo cách cú pháp chính xác; nó hoạt động trên AST và do đó sẽ không chạm vào chuỗi hoặc nhận xét có vẻ đúng. Điều kiện bổ sung trên "mutate_this_place" là cho phép bạn kiểm soát tần suất xảy ra; bạn không muốn đột biến mọi nơi trong chương trình.

Rõ ràng là bạn muốn có một loạt các quy tắc như thế này để phát hiện các cấu trúc mã khác nhau và thay thế chúng bằng các phiên bản bị đột biến. DMS rất vui khi áp dụng một bộ quy tắc. AST đột biến sau đó được in đẹp.


Tôi đã không nhìn vào câu trả lời này trong 4 năm. Wow, nó đã bị hạ cấp nhiều lần. Điều đó thực sự tuyệt vời, vì nó trả lời trực tiếp câu hỏi của OP, và thậm chí chỉ ra cách thực hiện các đột biến mà anh ấy muốn làm. Tôi không cho rằng bất kỳ người trong số những người downvoters sẽ quan tâm để giải thích lý do tại sao họ downvot.
Ira Baxter

4
Bởi vì nó thúc đẩy một công cụ nguồn đóng rất đắt tiền.
Zoran Pavlovic

@ZoranPavlovic: Vì vậy, bạn không phản đối bất kỳ độ chính xác hoặc tiện ích kỹ thuật nào của nó?
Ira Baxter

2
@Zoran: Anh ấy không nói rằng anh ấy có một thư viện mã nguồn mở. Ông nói rằng ông muốn sửa đổi mã nguồn Python (sử dụng AST) và các giải pháp ông có thể tìm thấy đã không làm điều đó. Đây là một giải pháp như vậy. Bạn không nghĩ mọi người sử dụng các công cụ thương mại trên các chương trình được viết bằng các ngôn ngữ như Python trên Java?
Ira Baxter

1
Tôi không phải là người bỏ phiếu, nhưng bài viết hơi giống quảng cáo. Để cải thiện câu trả lời, bạn có thể tiết lộ rằng bạn đã liên kết với sản phẩm
wim

0

Tôi đã từng sử dụng baron cho việc này, nhưng giờ đã chuyển sang parso vì nó cập nhật với python hiện đại. Nó hoạt động rất tốt.

Tôi cũng cần điều này cho một người thử nghiệm đột biến. Thực sự khá đơn giản để tạo một cái có parso, hãy xem mã của tôi tại https://github.com/boxed/mutmut

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.