import os import re import sys from subprocess import Popen, PIPE class LintRunner(object): """ Base class provides common functionality to run python code checkers. """ sane_default_ignore_codes = set([]) command = None output_matcher = None #flymake: ("\\(.*\\) at \\([^ \n]+\\) line \\([0-9]+\\)[,.\n]" 2 3 nil 1) #or in non-retardate: r'(.*) at ([^ \n]) line ([0-9])[,.\n]' output_format = "%(level)s %(error_type)s%(error_number)s:" \ "%(description)s at %(filename)s line %(line_number)s." def __init__(self, virtualenv=None, ignore_codes=(), use_sane_defaults=True): if virtualenv: # This is the least we can get away with (hopefully). self.env = {'VIRTUAL_ENV': virtualenv, 'PATH': virtualenv + '/bin:' + os.environ['PATH']} else: self.env = None self.virtualenv = virtualenv self.ignore_codes = set(ignore_codes) self.use_sane_defaults = use_sane_defaults @property def operative_ignore_codes(self): if self.use_sane_defaults: return self.ignore_codes ^ self.sane_default_ignore_codes else: return self.ignore_codes @property def run_flags(self): return () @classmethod def fixup_data(cls, line, data): return data @classmethod def process_output(cls, line): m = cls.output_matcher.match(line) if m: fixed_data = dict.fromkeys(('level', 'error_type', 'error_number', 'description', 'filename', 'line_number'), '') fixed_data.update(cls.fixup_data(line, m.groupdict())) print cls.output_format % fixed_data def run(self, filename): args = [self.command] args.extend(self.run_flags) args.append(filename) process = Popen(args, stdout=PIPE, stderr=PIPE, env=self.env) for line in process.stdout: self.process_output(line) class PylintRunner(LintRunner): """ Run pylint, producing flymake readable output. The raw output looks like: render.py:49: [C0301] Line too long (82/80) render.py:1: [C0111] Missing docstring render.py:3: [E0611] No name 'Response' in module 'werkzeug' render.py:32: [C0111, render] Missing docstring """ output_matcher = re.compile( r'(?P[^:]+):' r'(?P\d+):' r'\s*\[(?P[WECR])(?P[^,]+),' r'\s*(?P[^\]]+)\]' r'\s*(?P.*)$') command = 'pylint' sane_default_ignore_codes = set([ "C0103", # Naming convention "C0111", # Missing Docstring "E1002", # Use super on old-style class "W0232", # No __init__ #"I0011", # Warning locally suppressed using disable-msg #"I0012", # Warning locally suppressed using disable-msg #"W0511", # FIXME/TODO #"W0142", # *args or **kwargs magic. "R0904", # Too many public methods "R0903", # Too few public methods "R0201", # Method could be a function ]) @classmethod def fixup_data(cls, line, data): if data['error_type'].startswith('E'): data['level'] = 'ERROR' else: data['level'] = 'WARNING' return data @property def run_flags(self): return ('--output-format', 'parseable', '--include-ids', 'y', '--reports', 'n', '--disable-msg=' + ','.join(self.operative_ignore_codes)) class PycheckerRunner(LintRunner): """ Run pychecker, producing flymake readable output. The raw output looks like: render.py:49: Parameter (maptype) not used render.py:49: Parameter (markers) not used render.py:49: Parameter (size) not used render.py:49: Parameter (zoom) not used """ command = 'pychecker' output_matcher = re.compile( r'(?P[^:]+):' r'(?P\d+):' r'\s+(?P.*)$') @classmethod def fixup_data(cls, line, data): #XXX: doesn't seem to give the level data['level'] = 'WARNING' return data @property def run_flags(self): return '--no-deprecated', '-0186', '--only', '-#0' class Pep8Runner(LintRunner): """ Run pep8.py, producing flymake readable output. The raw output looks like: spiders/structs.py:3:80: E501 line too long (80 characters) spiders/structs.py:7:1: W291 trailing whitespace spiders/structs.py:25:33: W602 deprecated form of raising exception spiders/structs.py:51:9: E301 expected 1 blank line, found 0 """ command = 'pep8.py' # sane_default_ignore_codes = set([ # 'RW29', 'W391', # 'W291', 'WO232']) output_matcher = re.compile( r'(?P[^:]+):' r'(?P[^:]+):' r'[^:]+:' r' (?P\w+) ' r'(?P.+)$') @classmethod def fixup_data(cls, line, data): if 'W' in data['error_number']: data['level'] = 'WARNING' else: data['level'] = 'ERROR' return data @property def run_flags(self): return '--repeat', '--ignore=' + ','.join(self.ignore_codes) if __name__ == '__main__': from optparse import OptionParser parser = OptionParser() parser.add_option("-e", "--virtualenv", dest="virtualenv", default=None, help="virtualenv directory") parser.add_option("-i", "--ignore_codes", dest="ignore_codes", default=(), help="error codes to ignore") options, args = parser.parse_args() pylint = PylintRunner(virtualenv=options.virtualenv, ignore_codes=options.ignore_codes) pylint.run(args[0]) pychecker = PycheckerRunner(virtualenv=options.virtualenv, ignore_codes=options.ignore_codes) pychecker.run(args[0]) pep8 = Pep8Runner(virtualenv=options.virtualenv, ignore_codes=options.ignore_codes) pep8.run(args[0]) sys.exit()