Building a Python Code Review Scheduler: Keeping the Review Information

In the second part of this series, you saw how to collect the commit information from the git logs and send review requests to random developers selected from the project members list.

Building a Python Code Review Scheduler: Keeping the Review Information

In this part, you’ll see how to save the code review information to follow up each time the code scheduler is run. You’ll also see how to read emails to check if the reviewer has responded to the review request.

Getting Started

Start by cloning the source code from the second part of the tutorial series.

git clone https://github.com/royagasthyan/CodeReviewer-Part2 CodeReviewer

Modify the config.json file to include some relevant email addresses, keeping the [email protected] email address. It’s because the git has commits related to the particular email address which is required for the code to execute as expected. Modify the SMTP credentials in the schedule.py file:

FROM_EMAIL      = "[email protected]"
FROM_PWD        = "your_password"

Navigate to the project directory CodeReviewer and try to execute the following command in the terminal.

python scheduler.py -n 20 -p "project_x"

It should send the code review request to random developers for review.

Keeping the Review Request Information

To follow up on the review request information, you need to keep it somewhere for reference. You can select where you want to keep the code review request information. It can be any database or may be a file. For the sake of this tutorial, we’ll keep the review request information inside a reviewer.json file. Each time the scheduler is run, it’ll check the info file to follow up on the review requests that haven’t been responded to.

Create a method called save_review_info which will save the review request information inside a file. Inside the save_review_info method, create an info object with the reviewer, subject, and a unique Id.

def save_review_info(reviewer, subject):
    info = {'reviewer':reviewer,'subject':subject,'id':str(uuid.uuid4()),'sendDate':str(datetime.date.today())}

For a unique Id, import the uuid Python module.

import uuid

You also need the datetime Python module to get the current date. Import the datetime Python module.

import datetime

You need to initialize the reviewer.json file when the program starts if it doesn’t already exist.

if not os.path.exists('reviewer.json'):
    with open('reviewer.json','w+') as outfile:
        json.dump([],outfile)

If the file doesn’t exist, you need to create a file called reviewer.json and fill it with an empty JSON array as seen in the above code.

This method will be called each time a review request is sent. So, inside the save_review_info method, open the reviewer.json file in read mode and read the contents. Append the new content information into the existing content and write it back to the reviewer.json file. Here is how the code would look:

def save_review_info(reviewer, subject):
    info = {'reviewer':reviewer,'subject':subject,'id':str(uuid.uuid4()),'sendDate':str(datetime.date.today())}

    with open('reviewer.json','r') as infile:
        review_data = json.load(infile)

    review_data.append(info)

    with open('reviewer.json','w') as outfile:
        json.dump(review_data,outfile)

Inside the schedule_review_request method, before sending the code review request mail, call the save_review_info method to save the review information.

def schedule_review_request(commits):
    date = time.strftime("%Y-%m-%d")
    
    for commit in commits:
        reviewer = select_reviewer(commit.Author, project_members)
        subject = date + " Code Review [commit:" + commit.Id + "]"
        body = "Hello '" + reviewer + "', you have been selected to review the code for commitn"
        body += "done by '" + commit.Author + "'.n"
        body += "n"
        
        body += format_review_commit(commit)

        save_review_info(reviewer,subject);

        send_email(reviewer,subject,body)

Save the above changes and execute the scheduler program. Once the scheduler has been run, you should be able to view the reviewer.json file inside the project directory with the code review request information. Here is how it would look:

[{
    "reviewer": "[email protected]",
    "id": "8ca7da84-9da7-4a17-9843-be293ea8202c",
    "sendDate": "2017-02-24",
    "subject": "2017-02-24 Code Review [commit:16393106c944981f57b2b48a9180a33e217faacc]"
}, {
    "reviewer": "[email protected]",
    "id": "68765291-1891-4b50-886e-e30ab41a8810",
    "sendDate": "2017-02-24",
    "subject": "2017-02-24 Code Review [commit:04d11e21fb625215c5e672a93d955f4a176e16e4]"
}]

Reading the Email Data

You have collected all the code review request information and saved it in the reviewer.json file. Now, each time the scheduler is run, you need to check your mail inbox to see if the reviewer has responded to the code review request. So first you need to define a method to read your Gmail inbox.

Create a method called read_email which takes the number of days to check the inbox as a parameter. You’ll make use of the imaplib Python module to read the email inbox. Import the imaplib Python module:

import imaplib

To read the email using the imaplib module, you first need to create the server.

email_server = imaplib.IMAP4_SSL(SERVER)

Log in to the server using the email address and password:

email_server.login(FROM_EMAIL,FROM_PWD)

Once logged in, select the inbox to read the emails:

email_server.select('inbox')

You’ll be reading the emails for the past n number of days since the code review request was sent. Import the timedelta Python module.

import timedelta

Create the email date as shown:

email_date = datetime.date.today() - timedelta(days=num_days)
formatted_date = email_date.strftime('%d-%b-%Y')

Using the formatted_date, search the email server for emails.

typ, data = email_server.search(None, '(SINCE "' + formatted_date + '")')

It will return the unique IDs for each email, and using the unique IDs you can get the email details.

ids = data[0]

id_list = ids.split()

first_email_id = int(id_list[0])
last_email_id = int(id_list[-1])

Now you’ll make use of the first_email_id and the last_email_id to iterate through the emails and fetch the subject and the “from” address of the emails.

for i in range(last_email_id,first_email_id, -1):
    typ, data = email_server.fetch(i, '(RFC822)' )

data will contain the email content, so iterate the data part and check for a tuple. You’ll be making use of the email Python module to extract the details. So import the email Python module.

import email

You can extract the email subject and the “from” address as shown:

for response_part in data:
    if isinstance(response_part, tuple):
        msg = email.message_from_string(response_part[1])
        print 'From: ' + msg['from']
        print 'n'
        print 'Subject: ' + msg['subject']
        print 'n'
        print '------------------------------------------------'

Here is the complete read_email method:

def read_email(num_days):
    try:
        email_server = imaplib.IMAP4_SSL(SERVER)
        email_server.login(FROM_EMAIL,FROM_PWD)
        email_server.select('inbox')

        email_date = datetime.date.today() - timedelta(days=num_days)
        formatted_date = email_date.strftime('%d-%b-%Y')

        typ, data = email_server.search(None, '(SINCE "' + formatted_date + '")')
        ids = data[0]

        id_list = ids.split()

        first_email_id = int(id_list[0])
        last_email_id = int(id_list[-1])

        for i in range(last_email_id,first_email_id, -1):
            typ, data = email_server.fetch(i, '(RFC822)' )

            for response_part in data:
                if isinstance(response_part, tuple):
                    msg = email.message_from_string(response_part[1])
                    print 'From: ' + msg['from']
                    print 'n'
                    print 'Subject: ' + msg['subject']
                    print 'n'
                    print '------------------------------------------------'

    except Exception, e:
        print str(e)

Save the above changes and try running the above read_email method:

read_email(1)

It should print the email subject and “from” address on the terminal.

Building a Python Code Review Scheduler: Keeping the Review Information

Now let’s collect the “from” address and subject into an email_info list and return the data.

email_info = []

Instead of printing the subject and the “from” address, append the data to the email_info list and return the email_info list.

email_info.append({'From':msg['from'],'Subject':msg['subject'].replace("rn","")})

Here is the modified read_email method:

def read_email(num_days):
    try:
        email_info = []
        email_server = imaplib.IMAP4_SSL(SERVER)
        email_server.login(FROM_EMAIL,FROM_PWD)
        email_server.select('inbox')

        email_date = datetime.date.today() - timedelta(days=num_days)
        formatted_date = email_date.strftime('%d-%b-%Y')

        typ, data = email_server.search(None, '(SINCE "' + formatted_date + '")')
        ids = data[0]

        id_list = ids.split()

        first_email_id = int(id_list[0])
        last_email_id = int(id_list[-1])

        for i in range(last_email_id,first_email_id, -1):
            typ, data = email_server.fetch(i, '(RFC822)' )

            for response_part in data:
                if isinstance(response_part, tuple):
                    msg = email.message_from_string(response_part[1])
                    email_info.append({'From':msg['from'],'Subject':msg['subject'].replace("rn","")})

    except Exception, e:
        print str(e)

    return email_info

Adding Logging for Error Handling

Error handling is an important aspect of software development. It’s really useful during the debugging phase to trace bugs. If you have no error handling, then it gets really difficult to track the error. Since you’re growing with a couple of new methods, I think it’s the right time to add error handling to the scheduler code.

To get started with error handling, you’ll be needing the logging Python module and the RotatingFileHandler class. Import them as shown:

import logging
from logging.handlers import RotatingFileHandler

Once you have the required imports, initialize the logger as shown:

logger = logging.getLogger("Code Review Log")
logger.setLevel(logging.INFO)

In the above code, you initialized the logger and set the log level to INFO.

Create a rotating file log handler which will create a new file each time the log file has reached a maximum size.

logHandler = RotatingFileHandler('app.log',maxBytes=3000,backupCount=2)

Attach the logHandler to the logger object.

logger.addHandler(logHandler)

Let’s add the error logger to log errors when an exception is caught. In the read_email method’s exception part, add the following code:

logger.error(str(datetime.datetime.now()) + " - Error while reading mail : " + str(e) + "n")
logger.exception(str(e))

The first line logs the error message with the current date and time to the log file. The second line logs the stack trace to the error.

Similarly, you can add the error handling to the main part of the code. Here is how the code with error handling would look:

try:
    commits = process_commits()

    if len(commits) == 0:
        print 'No commits found '
    else:
        schedule_review_request(commits)
except Exception,e:
    print 'Error occurred. Check log for details.'
    logger.error(str(datetime.datetime.now()) + " - Error while reading mail : " + str(e) + "n")
    logger.exception(str(e))

Wrapping It Up

In this part of the series, you shelved the review request information in the reviewer.json file. You also created a method to read the emails. You’ll be using both of these functions to follow up on the code review requests in the final part of this series.

Additionally, don’t hesitate to see what we have available for sale and for study in the marketplace, and don’t hesitate to ask any questions and provide your valuable feedback using the feed below.

Source code from this tutorial is available on GitHub.

Do let us know your thoughts and suggestions in the comments below.