05Jan
How To Use Prospector For Python Static Code Analysis
How To Use Prospector For Python Static Code Analysis

Code testing is a very essential part of software development, testing your code while developing is referred to as TDD(Test Driven Development). Unfortunately, a lot of developers neglect its usefulness and go on to just label it time-consuming, irrelevant, and cumbersome. However, the advantages of testing your code before deployment cannot be overemphasized. Below are some of the advantages of testing your code:

Reasons to start testing your code

  •  Testing of code will help prevent you from deploying sub-standard code and in time help improve your coding skills. Keeping your code clean and efficient is a very important skill when working on large projects in a large company with a large team of developers.
  •  Testing of code will definitely improve your code stability. When every piece of code for each version or commit is properly tested, a stable build is a result of these processes and there will be some certainty your code will not end up crashing or develop issues on some devices.
  •  Testing when developing a project or software that is going to be open-source greatly helps to familiarize your contributors with all the test cases they should keep in mind when making changes to the software. This way you ensure everyone is on the same page.
  •  Testing can also help in discovering potential security threats early. Every year tech companies around the world lose a lot of money from cyber attacks

Now that we know the advantages of testing our code, we will now look at the two methods for testing our code during development.

Methods of testing code

There are two methods of analyzing and testing our code; dynamic code analysis and static code analysis. Dynamic code analysis simply involves testing our code by running it and evaluating the errors that may arise. This method is popular among developers and beginners.

The second method is static code analysis which simply put is the opposite of the first method; dynamic code analysis. Static code analysis involves evaluating our code for errors without running it. This is method is called “white box testing” because the code is available to the testers to see unlike in “black box testing”. Major software testing involves static code analysis, where the developers look for bugs. We are going to be focusing on the second method; Static Code Analysis, while also applying it with a tool called “Prospector”.

The Prospector Tool

The Prospector Tool
The Prospector Tool

Prospector is a tool that analyzes Python code, outputs information about the errors, potential problems, convention violation, and complexity of the program. It possesses the functionalities of other python code analysis tools such as Pylint, Pep 8, and McCabe complexity. All these other tools are amazing on their possessing unique functionalities, prospector equips you with all their functions, just at the tip of your finger. We have been doing a lot of explaining lets finally get to do the dirty work.

Writing and Analyzing our Program

We will be writing an API to collect names and emails of users with Flask and storing them in the database.

Prerequisites:
Python, Flask, SQLAlchemy, and Prospector.

Installing our dependencies with our command shell

# run this on your command line interface
$ pip install prospector
$ pip install flask
$ pip install flask-sqlalchemy
$ pip install prospector
# to check if prospector installed successfully
$ prospector --version$ prospector 1.3.1

Now that we have successfully installed our dependencies we can write our flask API that we are going to be testing.

Our Flask Program

Let us begin importing the required modules or libraries in our program.

#Importing Libraries
import os
from flask import Flask,send_from_directory,request,url_for,jsonify,render_template,redirect
from flask_restful import reqparse, abort, Api, Resource
from flask_sqlalchemy 
import SQLAlchemy
import prospect

Next, we initialize our Flask and configure our database using our app config and SQLAlchemy. Our database filename is data.sqlite3

app = Flask(__name__)
application=app
api = Api(app)
db = SQLAlchemy(app)app.config
['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.sqlite3'

Now we would be creating our database tables and their structure using ORM(Object Relational Mapping) done with SQLAlchemy, whereas our table names are the class names and the class attributes are the table fields. We then use the command create_all to create our database file.

class User(db.Model):id = db.Column('id', db.Integer,primary_key=True)
name = db.Column('name', db.String(100), nullable=True)
email = db.Column('email', db.String(100), nullable=True)
    def __init__(self,id,student_id,pin,school_id,date,expire):
        self.id = id
        self.name = name
        self.email = email
db.create_all()

At this point, we will be defining our API endpoint save which would save the user’s name and email to the database. Before saving we simply check if the email provided by the user already exists in the database. To save we define an object of the database class “User” called “user” and add this object instance to the DB.

class save(Resource):
    def get(self):
        name=request.form.get("name")
        if name:
            pass
        else:
            name= request.args.get("name")
        email=request.form.get("email")
        if email:
            pass
        else:
            email=request.args.get("email")
        if User.query.filter_by(email=email).first():
            return jsonify({"success": False, "message": "Email already saved"})
        else:
            user = User(name=name,email=email)
            db.session.add(user)
            db.session.commit()
            return jsonify({"success": True, "message": "data saved"})

Finally, we add a route for the save resource we just created above

#adding resouce with route
api.add_resource(save, '/save')
if __name__ == '__main__':
    app.run(debug=False)

If you successfully followed the previous steps, congratulations !!!, we can now test our code with the “Prospector” tool.

Using Prospector without customizing configurations

NOTE: Currently prospector supports just Celery, Django and Flask. So let’s try to stick to these frameworks when utilizing them. We are going to try out our prospector tool without customizing any configuration for coding style or strictness levels.

# run this on your command line interface
prospector collect.py

Below are the results of running prospector on our Flask app:

Prospector results
Prospector results

In the image above, we can see all the 22 error messages, the tools used, strictness levels, and other settings on our prospector tool.
The prospector tool highlighted that from line 2 to line 4, there were some packages we imported in the program that we didn’t utilize. The packages are os, send_from_directory, and reqparse. With this, we can now go-ahead to remove these packages from our code.

We can also see that prospector generated an indentation error message because in our User class we indented with three spaces instead of four spaces according to the pep8 writing style. With this we can also make the pointed out writing correction, however, this is a less severe case and can be handled when we reduce the strictness levels of the prospector and customize it to your coding style.

Next on lines 20 to 23, we have error messages generated with the Pylint library. On line 20 we have a “too many arguments” error which is caused by exceeding the default maximum value of arguments __init__ function can possess. This maximum value can also be customized to whatever maximum number you desire. On lines 21 to 23, we have indentation error messages we talked about. However, for this case, some of the arguments defined were unused therefore irrelevant to our code. On line 25 we have a pep8 error message which was generated because according to the pep8 writing style we were meant to leave blank lines after our class definition before our next line of code. Like the indentation error, this can also be handled by reducing the strictness levels and customizing to the writing style of your choice. Still, on our user class, we can see the prospector raised an “undefined variable” error message for name and email because we had them used in our __init__ function but not defined as arguments for it. We can debug this by adding names and email to the arguments.

On line 46 we have another error message;” no-else-return” which is generated by Pylint. This error message was raised because Pylint recognizes that for the case where the condition in the if statement a return statement is executed and anything, after it is ignored, therefore an else statement after this, will be ignored too and be irrelevant, but if the condition is false then the return statement in it would be ignored and the return statement after it will be executed instead. Below is an example

no-return-else
no-return-else

The last error message is a pep8 indentation error located at line 55. This error is generated because our comment did not start at the beginning of the line and did not have four spaces but three instead. This comment was irrelevant in this code anyway so we would just remove it.

We are finally done with making necessary corrections to our code based on the prospector’s error messages. Our code should now look like this

#Importing Libraries
from flask import Flask,jsonify
from flask_restful 
import Api, Resource
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
application=app
api = Api(app)
db = SQLAlchemy(app)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.sqlite3'

class User(db.Model):
    id = db.Column('id', db.Integer,primary_key=True)
    name = db.Column('name', db.String(100), nullable=True)
    email = db.Column('email', db.String(100), nullable=True)
    def __init__(self,id,name,email):
        self.id = id
        self.name = name
        self.email = email
    db.create_all()

    class save(Resource):
        def get(self):
            name=request.form.get("name")
            if name:
                pass
            else:
                name= request.args.get("name")
            email=request.form.get("email")
            if email:
                pass
            else:
                email=request.args.get("email")
            if User.query.filter_by(email=email).first():
                return jsonify({"success": False, "message": "Email already saved"})
                user = User(name=name,email=email)
                db.session.add(user)db.session.commit()
                return jsonify({"success": True, "message": "data saved"})
 
#adding resouce with route
api.add_resource(save, '/save')
if __name__ == '__main__':
    app.run(debug=False)

Customizing Prospector

The prospector can be configured by creating a profile. A profile is a YAML file containing several sections as described below. The prospector will search for a .prospector.yaml file (and several others) in the path that it is checking. If found, it will automatically be loaded. You can also pass in the profile path as an argument as shown below

$ prospector --profile /path/to/your/profile.yaml

An example of a YAML file with some customized configurations can be seen below

output-format: json
output-format: json
strictness: medium
test-warnings: true
doc-warnings: false
member-warnings: false
inherits:
    - default
ignore-paths:
    - docs
ignore-patterns:
    - (^|/)skip(this)?(/|$)
autodetect: true
max-line-length: 88

bandit:
    run: true
    options:
        config: .bandit.yml
dodgy:
    run: true
    
frosted:
    disable:
        - E103
        - E306
mccabe:
    run: false
    options:
        max-complexity: 10

pep8:
    disable:
        - W602
        - W603
    enable:
        - W601
    options:
        max-line-length: 79
pep257:
    disable:
        - D100
        - D101py

flakes:
    disable:
        - F403
        - F810

pylint:
    disable:
        - bad-builtin
        - too-few-public-methods
    options:
        max-locals: 15
        max-returns: 6
        max-branches: 15 
        max-statements: 60
        max-parents: 7
        max-attributes: 7
        min-public-methods: 1
        max-public-methods: 20
        max-module-lines: 1000
        max-line-length: 99
        max-args: 6

pyroma:
     disable:
        - PYR15
        - PYR18

mypy:
     run: true
     options:
         ignore-missing-imports: true
         follow-imports: skip

vulture:
     run: true

After saving your configurations and adding the path to your profile, the prospector tools will now work with the configurations you provided. Configuring before use is very important especially when testing or evaluating large projects. Without configuring, one can be overwhelmed with the number of error messages and ultimately will be unable to achieve anything. So setting up the configurations that work for you is advised.

You could see more about prospector and their configurations in their official documentation

Conclusion

  •  We learned about code testing, its importance, and the methods of code testing.
  •  We learned about static code analysis
  • We learned about the Prospector tool and what makes it unique among other static analysis tools is its ability to combine several static analysis tools in evaluating a piece of code.
  • We learned how to use prospector to test a piece of code.

After reading this article, readers should now be able to understand static code analysis and how to use prospector in analyzing their python codes and projects. Thank you for your time and see you soon😃. For any questions don’t hesitate to contact me on Twitter: @LordChuks3.

Leave a Reply