In this chapter, we will learn how to use ASTs in Python to generate code for various applications. We will apply these abstract syntax trees to metaprogramming to implement automatic code generated in this chapter.
Automatic code generation is one way of making the life of a programmer easier. An abstract syntax tree is an excellent functionality that can help us generate code in a much simpler way.
The concept of AST is discussed with examples in Chapter 9 of this book. In this chapter, we will be tapping the advantages of ASTs to generate code automatically. Code generation can be implemented to enable no-code or limited coding while developing applications. In this chapter, we will continue to use the example of ABC Megamart to generate code from ASTs.
In this chapter, we will be looking at the following main topics:
By the end of this chapter, you should be able to understand how to use the existing methods of the ast library in Python to enable your application to generate its own code, how to avoid repeating yourself, and how to generate code dynamically.
The code examples shared in this chapter are available on GitHub under the code for this chapter here: https://github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter14.
In this section, we will be looking at how to generate code for a class without actually defining the class itself. We will be creating a string-based template with the structure of a class we want to develop but not the actual code that can be executed. To explain this further, let us look at an example where we will generate a class named VegCounter by parsing a series of strings using the ast module.
The sequence of steps to be followed to generate code for a class is represented in the following flow diagram:
Figure 14.1 – A code generation sequence for a simple class
Let us look at the implementation of this example:
import ast
classname = "VegCounter"
classtemplate = """class """ +classname+ """():pass"""
print(ast.dump(class_tree, indent = 4))
Module(
body=[
ClassDef(
name='VegCounter',
bases=[],
keywords=[],
body=[
Pass()],
decorator_list=[])],
type_ignores=[])
actualclass = compile(class_tree, 'vegctr_tree', 'exec')
actualclass
Thus, this leads to the following output:
<code object <module> at 0x0000028AAB0D2A80, file "vegctr_tree", line 1>
VegCounter.
print(ast.unparse(class_tree))
Executing the preceding code leads to the following output:
class VegCounter:
pass
code = open("classtemplate.py", "w")
script = code.write(ast.unparse(class_tree))
code.close()
Figure 14.2 – The classtemplate.py file
import classtemplate as c
vegc = c.VegCounter()
vegc
The output is as follows:
<classtemplate.VegCounter at 0x28aab1d6a30>
In this section, we have generated a simple class code using the ast module. This example helps us in understanding the steps to be followed to generate code for a custom class since it is easier to start understanding code generation by starting simple. With this understanding, let us generate code for multiple classes.
In this section, we will look at generating code for multiple classes dynamically using the ast module and its unparse method.
Generating the code for more than one class dynamically gives us a direction for implementing code generation for multiple functionalities of an application. The classes need not be for the same functionality and the class code thus generated can later be modified to include additional methods or attributes as required by the application. The skeletal class code will be generated through this example.
To understand this further, we will follow the sequence described in the following flow diagram.
Figure 14.3 – A code generation sequence for multiple classes
Let us now look at how to implement this scenario:
classnames = ["VegCounter", "ElectronicsCounter", "PasadenaBranch", "VegasBranch"]
classgenerator = []
for classname in classnames:
classcode = """class """ +classname+ """():pass"""
classgenerator.append(classcode)
classgenerator
The class templates are added to another list named classgenerator, and the list is as follows:
['class VegCounter():pass',
'class ElectronicsCounter():pass',
'class PasadenaBranch():pass',
'class VegasBranch():pass']
classtrees = []
for i in classgenerator:
classtree = ast.parse(i)
classtrees.append(classtree)
classtrees
The parsed class trees that are assigned to the classtrees list variable are displayed as follows:
[<ast.Module at 0x1efa91fde20>,
<ast.Module at 0x1efa91e6d30>,
<ast.Module at 0x1efa91e6220>,
<ast.Module at 0x1efa91e6370>]
print(ast.dump(classtrees[0], indent = 4))
The output is generated as follows:
Module(
body=[
ClassDef(
name='VegCounter',
bases=[],
keywords=[],
body=[
Pass()],
decorator_list=[])],
type_ignores=[])
print(ast.unparse(classtrees[1]))
An example output looks as follows:
class ElectronicsCounter:
pass
code = open("classtemplates.py", "w")
for i in classtrees:
code.write(ast.unparse(i))
code.write(" ")
code.write(" ")
code.close()
The generated classtemplates.py file looks as follows:
Figure 14.4 – The classtemplates.py file
import classtemplates as ct
print(ct.ElectronicsCounter())
print(ct.PasadenaBranch())
print(ct.VegasBranch())
print(ct.VegCounter())
The output of the preceding code is as follows:
<classtemplates.ElectronicsCounter object at 0x00000255C0760FA0>
<classtemplates.PasadenaBranch object at 0x00000255C0760F10>
<classtemplates.VegasBranch object at 0x00000255C0760FA0>
<classtemplates.VegCounter object at 0x00000255C0760F10>
In this section, we have generated code for multiple classes using the ast module. This example is the next step toward working on automatic code generation for multiple functionalities or modules of an application.
In this section, we will generate code for a class, along with a list of attributes, that will also be included dynamically in the class. Generating code for a class alone can give the initial skeletal structure for a module, whereas we need to add attributes if we want to make the class more specific. The following flow diagram represents the sequence of steps to be followed for this example:
Figure 14.5 – A code generation sequence for a class with multiple attributes
Let us look at the code for this example:
classname = "VegCounter"
classtemplate = '''class ''' +classname+ ''':'''+' '
attributename = ['items', 'countername', 'billamount']
for attr in attributename:
classtemplate = classtemplate + attr +''' =
None''' + ' '
class_tree = ast.parse(classtemplate)
print(ast.dump(class_tree, indent = 4))
Module(
body=[
ClassDef(
name='VegCounter',
bases=[],
keywords=[],
body=[
Assign(
targets=[
Name(id='items',
ctx=Store())],
value=Constant(value=None)),
Assign(
targets=[
Name(id='countername',
ctx=Store())],
value=Constant(value=None)),
Assign(
targets=[
Name(id='billamount',
ctx=Store())],
value=Constant(value=None))],
decorator_list=[])],
type_ignores=[])
All three variables – items, countername, and billamount added into the class template – are now part of the syntax tree. If we review the tree in detail, we can look at these variables under body | assign | targets | name | id.
print(ast.unparse(class_tree))
The output looks as follows:
class VegCounter:
items = None
countername = None
billamount = None
Let us write the code to a file and import it:
code = open("classtemplateattr.py", "w")
script = code.write(ast.unparse(class_tree))
code.close()
The generated code looks as follows:
Figure 14.6 –The classtemplateattr.py file
We can import the classtemplateattr.py file and the class can be accessed as follows:
import classtemplateattr as c
c.VegCounter()
vegc = c.VegCounter()
vegc.items = ['onions','tomatoes','carrots','lettuce']
vegc.countername = 'Veg Counter'
vegc.billamount = 200
The output is displayed as follows, with all the attributes and their corresponding values assigned:
['onions', 'tomatoes', 'carrots', 'lettuce']
Veg Counter
200
In this section, we have generated a class with multiple attributes without writing the code for the class. Instead, we have defined a template that takes in a class name and a list of attributes as input. With this understanding, we can look at generating a class with methods.
In this section, let us generate code for a class and its methods. Throughout this chapter, our goal is to generate code dynamically for building applications that can solve a specific purpose. Adding methods along with attributes makes the code generation for a class even more application-specific. We can look at two variations of this example:
Let’s discuss each in detail.
In this example, let us generate code for a class and add an init method to the class and also initialize attributes. In this example, we will define a class for the vegetable counter of ABC Megamart. In the init method, let us initialize cart items from the vegetable counter of ABC Megamart in this class:
classname = "VegCounter"
classtemplate = '''class ''' +classname+ ''':'''+' ' +''' def __init__(self,*items):
cartItems = []
for i in items:
cartItems.append(i)
self.items = cartItems'''
class_tree = ast.parse(classtemplate)
print(ast.unparse(class_tree))
The parsed class template generates the following code:
class VegCounter:
def __init__(self, *items):
cartItems = []
for i in items:
cartItems.append(i)
self.items = cartItems
The abstract syntax tree for this class is generated with the function definition, as represented in the following figure:
Figure 14.7 – The function definition of the init method
With this understanding, let us look at one more example of this same class by generating code for a user-defined method.
In this section, let us look at a variation of the class by creating a template that generates a user-defined method for the class:
classname = "VegCounter"
methodname = "returnCart"
classtemplate = '''class ''' +classname+ ''':'''+' ' +''' def '''+methodname+'''(self,*items):
cartItems = []
for i in items:
cartItems.append(i)
return cartItems'''
class_tree = ast.parse(classtemplate)
print(ast.unparse(class_tree))
The parsed classtemplate generates the following code:
class VegCounter:
def returnCart(self, *items):
cartItems = []
for i in items:
cartItems.append(i)
return cartItems
The abstract syntax tree for this class is generated with the function definition, as represented in the following figure:
Figure 14.8 – The function definition of the user-defined method
We can either use the init method when we want to initialize the cart items at the class level or use the attributes later. By contrast, the user-defined method can be used if we want to keep the attributes specific to the method and perform actions based on the attributes within the method.
With this understanding, let us look at defining a custom class factory.
In this section, let us define a function named classgenerator that generates a custom class, attribute, and method using a class template as follows:
def classgenerator(classname, attribute, method):
classtemplate = '''class ''' +classname+
''':'''+' ' +attribute+''' =
None def '''+method+'''(self,item,status):
if (status == 'Y'):
print('Test passed for', item)
else:
print('Get another', item)
'''
return classtemplate
In this section, we are making the code generation more dynamic by creating a function that can generate code with custom values for the class name, attribute name, and method name, respectively. This helps in creating custom code for multiple functionalities in an application.
Let us provide a custom class name, attribute name, and method name as input to the preceding function:
class_tree = ast.parse(classgenerator('ElectronicCounter', 'TestItem', 'verifyCart')
actualclass = compile(class_tree, 'elec_tree', 'exec')
print(ast.unparse(class_tree))
The generated class code is as follows:
class ElectronicCounter:
TestItem = None
def verifyCart(self, item, status):
if status == 'Y':
print('Test passed for', item)
else:
print('Get another', item)
We can expand this example further by developing a code generator library in the following section.
In this section, let us develop a simple code generator that generates code for a class with get, set, and delete properties for its custom attributes. The purpose of this section is to generate a complete library through automatic code generation. To fulfill this, let us write the following code:
class CodeGenerator:
def __init__(self, classname, attribute):
self.classname = classname
self.attribute = attribute
def generatecode(self):
classtemplate = '''class ''' +self.classname+ ''':'''+''' def __init__(self):''' + ' '+''' self._'''+self.attribute+''' = None @property
def test'''+self.attribute+'''(self): return self.test'''+self.attribute+''' @test'''+self.attribute+'''.getter
def test'''+self.attribute+'''(self): print("get test'''+self.attribute+'''") return self._test'''+self.attribute+'''
@test'''+self.attribute+'''.setter
def test'''+self.attribute+'''(self, value):
print("set test'''+self.attribute+'''")
self._test'''+self.attribute+''' = value
@test'''+self.attribute+'''.deleter
def test'''+self.attribute+'''(self):
print("del test'''+self.attribute+'''")
del self._test'''+self.attribute+'''
'''
class_tree = ast.parse(classtemplate)
print(ast.unparse(class_tree))
print(' ')
from codegenerator import CodeGenerator as c
classes = {'VegCounter' : 'items',
'ElectronicCounter' : 'goods',
'BranchManhattan' : 'Sales',
'BranchPasadena' : 'Products'
}
def generatelib(classes):
for key, value in classes.items():
codegen = c(key, value)
codegen.generatecode()
from contextlib import redirect_stdout
with open('abcmegamartlib.py', 'w') as code:
with redirect_stdout(code):
generatelib(classes)
code.close()
class VegCounter:
def __init__(self):
self._items = None
@property
def testitems(self):
return self.testitems
@testitems.getter
def testitems(self):
print('get testitems')
return self._testitems
@testitems.setter
def testitems(self, value):
print('set testitems')
self._testitems = value
@testitems.deleter
def testitems(self):
print('del testitems')
del self._testitems
import abcmegamartlib as abc
abc.BranchManhattan()
The preceding code returns the following output:
<abcmegamartlib.BranchManhattan at 0x21c4800c7f0>
These are various examples of code generation that can be implemented using Python’s metaprogramming ast module.
In this chapter, we have looked at various examples to generate code for a custom class and a class with custom attributes. We have also covered examples of generating code for a custom class with methods and attributes. Finally, we have developed a code generator that can be used to develop a custom library using the concept of abstract syntax trees in Python.
Overall, we have seen various scenarios that can help us utilize the abstract syntax tree within Python’s ast module and generate dynamic code using Python metaprogramming.
In the next chapter, we will be discussing a case study to which we can apply all the concepts of metaprogramming that we have covered so far in the book.