Minimal Pylint

14 Jul 2018

Pylint is a great linter for Python. But sometimes you might want to write your own checks without using pylint's checks.

The below example shows a mini pylint which only has one checker MyChecker.

It implements a check called consider-using-extend which checks to see if += (augmented assign or augassign) nodes are being used on lists. Since augassign creates an unneeded churn of lists, this message will tell the user they can use list.extend to just mutate the list inplace.

from pylint import lint, reporters, interfaces
from pylint import checkers
from pylint.reporters import text

import astroid

class MyChecker(checkers.BaseChecker):
    __implements__ = interfaces.IAstroidChecker
    msgs = {
        'R9991': ("Consider Using %s.extend(%s)",
                  "consider-using-extend",
                  "Consider using list.extend instead of '+=' ",
                  ),
    }

    def visit_augassign(self, node):
        try:
            for inferred in node.target.infer():
                if inferred.qname() == 'builtins.list':
                    args = (node.target.name,
                            node.value.as_string())
                    self.add_message(
                        'consider-using-extend',
                        node=node, args=args)
        except astroid.InferenceError:
            pass

linter = lint.PyLinter()
linter.register_checker(MyChecker(linter))
args = linter.load_command_line_configuration()
linter.set_reporter(text.TextReporter())
linter.disable('bad-option-value')
linter.check(args)
linter.generate_reports()

Once this code is saved to a file, it can be executed on a python file or module:

(ast) [user@self mylint]$ python consider_extend.py ./astroid
************* Module astroid.scoped_nodes
./astroid/scoped_nodes.py:1514:8: R9991: Consider Using decoratornodes.extend(self.extra_decorators) (consider-using-extend)
./astroid/scoped_nodes.py:1514:8: R9991: Consider Using decoratornodes.extend(self.extra_decorators) (consider-using-extend)
************* Module astroid.tests.unittest_brain
./astroid/tests/unittest_brain.py:669:12: R9991: Consider Using attrs.extend(['approx', 'register_assert_rewrite']) (consider-using-extend)

Having your own mini pylint can help speed development of new messages since pylint tends to be on the slow side. Or maybe you want to only run specific messages in batch at separate times. Instead of explicitly loading plugins or changing pylint's source code you can just execute a file (allowing for a downstream repo).

Code Overview

lint.Pylinter is a single threaded entry point for pylint execution. It keeps tracks of what checkers and reporters are registered, loads cli arguments, and does the execution as well.

MyChecker is an astroid [1] checker. pylint knows this because we:

  • Assign the __implements__ field the value of IAstroidChecker
  • register the checker with the linter

The TextReporter shows pylint messages as lines in the command-line.

linter.check(args) executes the registered checkers upon the given modules or files, and collects the messages for the reporters.

linter.generate_reports() applies the reporters to collected messages, in this case printing the messages to stdout.

visit_augassign

augassign is one of the available astroid nodes. There is some magic happening where, for astroid checkers, the linter checks and supplies visit_<nodename> methods. The target of a augassign node is the left-hand name being changed.

The infer method is available for most astroid nodes. It will try to determine what possible values that node can take, hence the iteration. The above code will add a message if any one of the possible values are an instance of a dictionary.

In pylint, howver, we might use safe_infer to avoid values which have multiple results to avoid false-positives. Since we are writing our own checker however, we can have as many false-positives as we want.

[1]The astroid library builds on Python's abstract syntax tree model to allow for inference of additional information upon regular ast nodes.

Comments !