#!/usr/bin/python # pylint: disable=simplifiable-if-statement # simplifiable-if-statement: if's a sometimes nicer for debugging '''Run pylint using a 2.x and a 3.x interpreter, and optionally ignore some messages''' import os import re import sys import subprocess IGNORE_MESSAGES = [] WHICH_2 = '/usr/local/cpython-2.7/bin/pylint' WHICH_3 = '/usr/local/cpython-3.4/bin/pylint' def usage(retval): '''Output a usage message''' write = sys.stderr.write write('Usage: %s\n' % sys.argv[0]) write(' --ignore-message re1 --ignore-message re2\n') write(' --verbose\n') write(' --which-2 %s\n' % (WHICH_2, )) write(' --which-3 %s\n' % (WHICH_3, )) write(' --to-pylint args\n') write('\n') write('--which-2 and --which-3 must specify the path to a pylint for python\n') write('2.x or 3.x respectively. They can also be specified as "None" to skip\n') write('checking 2.x or 3.x. Naturally, if you skip both, no checks are run.\n') sys.exit(retval) def get_output_ignore_exit_code(command): '''Run a subprocess. Return its stdout. Ignore the exit code''' process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout = process.stdout.read() dummy = process.wait() lines = stdout.split(b'\n') return lines def is_relevant(line): '''Classify a pylint line as relevant or irrelevant''' for prefix in [b'***', b'C:', b'E:', b'F:', b'I:', b'R:', b'W:']: if line.startswith(prefix): return True return False def is_traceback(line): '''Return True iff this is a traceback''' if line.startswith(b'Traceback (most recent call last):'): return True else: return False def has_traceback(lines): '''Return True iff there is a traceback present in lines''' if any(is_traceback(line) for line in lines): return True else: return False def remove_semi_relevant(lines): '''Remove semi-relevant lines''' for line in lines: if is_semi_relevant(line): pass else: yield line def is_semi_relevant(line): '''Return True iff line is semi-relevant''' if b'FIXME' in line: return True elif line.startswith(b'I:') and b'Locally disabling' in line: return True elif line.startswith(b'***'): return True else: for ignore_message in IGNORE_MESSAGES: # We compile these more than necessary, but this isn't really much of a bottleneck ignore_regex = re.compile(ignore_message, re.IGNORECASE) match = ignore_regex.match(line) if match: return True return False def to_bytes(string): '''Convert string to bytes''' try: result = bytes(string, 'ASCII') except TypeError: result = string return result class Options(object): # pylint: disable=too-few-public-methods '''Deal with command line options''' def __init__(self): # pylint: disable=global-statement,too-many-branches # too-many-branches: argument parsing almost always has "too many" branches global WHICH_2 global WHICH_3 if 'PYLINT_ARGS' in os.environ: self.to_pylint = os.environ['PYLINT_ARGS'].split(' ') else: self.to_pylint = [] self.verbose = False while sys.argv[1:]: if sys.argv[1] == '--ignore-message': IGNORE_MESSAGES.append(to_bytes(sys.argv[2])) del sys.argv[1] elif sys.argv[1] == '--verbose': self.verbose = True elif sys.argv[1] == '--which-2': lower_argv_2 = sys.argv[2].lower() if lower_argv_2 == 'none': WHICH_2 = None else: WHICH_2 = sys.argv[2] del sys.argv[1] elif sys.argv[1] == '--which-3': lower_argv_2 = sys.argv[2].lower() if lower_argv_2 == 'none': WHICH_3 = None else: WHICH_3 = sys.argv[2] del sys.argv[1] elif sys.argv[1] in '--to-pylint': self.to_pylint.extend(sys.argv[2:]) del sys.argv[2:] elif sys.argv[1] in ['--help', '-h']: usage(0) else: sys.stderr.write('%s: Unrecognized option: %s\n' % (sys.argv[0], sys.argv[1])) usage(1) del sys.argv[1] self.significant_found = False self.messages_of_interest = [] self.pylints = set() def check(self): '''Check command line options for suitability''' if WHICH_2 and WHICH_2.endswith('/python'): sys.stderr.write('{}: --which-2 needs a pylint, not a python'.format(sys.argv[0])) sys.exit(1) if WHICH_3 and (WHICH_3.endswith('/python') or WHICH_3.endswith('/python3')): sys.stderr.write('{}: --which-3 needs a pylint, not a python'.format(sys.argv[0])) sys.exit(1) if WHICH_2: self.pylints.add(WHICH_2) if WHICH_3: self.pylints.add(WHICH_3) if not self.pylints: sys.stderr.write('%s: No python 2.x /and/ no python 3.x. Nothing to do.\n' % (sys.argv[0], )) sys.exit(1) def check_one(options, pylint): '''Check one pylint''' traceback_count = 0 command = [ ('%s' % pylint), ('--init-hook=import sys; sys.path.append("%s"); sys.path.append(".")' % os.path.expanduser('~/lib')), '--max-line-length=133', "--indent-string= ", '--module-rgx=[A-Za-z_][-a-zA-Z0-9_]+$', '--class-rgx=[A-Za-z_][-a-zA-Z0-9_]+$', ] command.extend(options.to_pylint) if options.verbose: sys.stderr.write('\n{}\n\n'.format(command)) output_lines = get_output_ignore_exit_code(command) if options.verbose: sys.stderr.write('Output from {} was:\n'.format(pylint, )) for line in output_lines: sys.stderr.write(' {}\n'.format(line)) sys.stderr.write('\n') if len(output_lines) == 1 and output_lines[0] == '': sys.stderr.write('Error, {} returned no output\n'.format(pylint)) sys.exit(1) if has_traceback(output_lines): sys.stderr.write('\n%s: Detected %s traceback:\n' % (sys.argv[0], pylint)) sys.stderr.write('\n'.join(output_lines)) traceback_count += 1 else: relevant_lines = [output_line for output_line in output_lines if is_relevant(output_line)] pruned_lines = set(remove_semi_relevant(relevant_lines)) if pruned_lines: options.significant_found = True for relevant_line in relevant_lines: if relevant_line in pruned_lines: prefix = b'relevant ' else: prefix = b'semirelevant ' options.messages_of_interest.append(prefix + relevant_line) return traceback_count def main(): '''Main function''' options = Options() options.check() traceback_count = 0 for pylint in sorted(options.pylints): if not os.path.exists(pylint): sys.stderr.write('%s: pylint %s does not exist\n' % (sys.argv[0], pylint)) sys.exit(1) traceback_count += check_one(options, pylint) if traceback_count: sys.stderr.write('\n%s pylint tracebacks detected\n' % traceback_count) if traceback_count: sys.exit(1) else: if options.significant_found: for message_of_interest in options.messages_of_interest: sys.stderr.write('%s\n' % message_of_interest.decode('ISO-8859-1')) sys.exit(1) else: sys.exit(0) main()