Search code examples
pythonmetaprogrammingabstract-syntax-tree

Edit attribute in script string with AST


I'm unfamiliar with the AST module and would appreciate any insight. If, for example, I have a string that contains a valid python script such as

import sys #Just any module
class SomeClass:
    def __init__(self):
        self.x = 10
        self.b = 15
    def a_func(self):
        print(self.x)

I would like to be able to programmatically edit lines such as changing self.x = 10 to something like self.x = 20. I can break it down somewhat with ast via:

some_string = "..." #String of class above
for body_item in ast.parse(some_string):
    ...

But this doesn't feel like the "right" way(not that there is a right way since this is somewhat niche). I was hoping someone could correct me towards something cleaner, or just better.


Solution

  • You can start by using ast.dump to get an idea of the AST structure of the code you're dealing with:

    import ast
    
    code='self.x = 10'
    print(ast.dump(ast.parse(code), indent=2))
    

    This outputs:

    Module(
      body=[
        Assign(
          targets=[
            Attribute(
              value=Name(id='self', ctx=Load()),
              attr='x',
              ctx=Store())],
          value=Constant(value=10))],
      type_ignores=[])
    

    From which you can see what you want to look for is an Assign node where the first of targets is an Attribute node whose value is a Name node with an id of 'self' and an attr of 'x'.

    With this knowledge, you can then use ast.walk to traverse the AST nodes to look for a node with the aforementioned properties, modify its value to a Constant node with a value of 20, and finally use ast.unparse to convert AST back to a string of code:

    import ast
    
    code = '''
    import sys #Just any module
    class SomeClass:
        def __init__(self):
            self.x = 10
            self.b = 15
        def a_func(self):
            print(self.x)
    '''
    tree = ast.parse(code)
    for node in ast.walk(tree):
        if (
            isinstance(node, ast.Assign) and
            isinstance((target := node.targets[0]), ast.Attribute) and
            isinstance(target.value, ast.Name) and
            target.value.id == 'self' and
            target.attr == 'x'
        ):
            node.value = ast.Constant(value=20)
    print(ast.unparse(tree))
    

    This outputs:

    class SomeClass:
    
        def __init__(self):
            self.x = 20
            self.b = 15
    
        def a_func(self):
            print(self.x)
    

    Note that ast.unparse requires Python 3.10 or later. If you're using an earlier version, you can use astunparse.unparse from the astunparse package instead.

    Demo: https://trinket.io/python3/3b09901326