1from pyflakes.checker import Binding, Assignment, Checker 2from pyflakes.api import isPythonFile 3import _ast 4import re 5import py 6import pytest 7import sys 8import tokenize 9 10 11def assignment_monkeypatched_init(self, name, source): 12 Binding.__init__(self, name, source) 13 if name == '__tracebackhide__': 14 self.used = True 15 16Assignment.__init__ = assignment_monkeypatched_init 17 18 19HISTKEY = "flakes/mtimes" 20 21 22def pytest_addoption(parser): 23 group = parser.getgroup("general") 24 group.addoption( 25 '--flakes', action='store_true', 26 help="run pyflakes on .py files") 27 parser.addini( 28 "flakes-ignore", type="linelist", 29 help="each line specifies a glob pattern and whitespace " 30 "separated pyflakes errors which will be ignored, " 31 "example: *.py UnusedImport") 32 33 34def pytest_configure(config): 35 if config.option.flakes: 36 config._flakes = FlakesPlugin(config) 37 config.pluginmanager.register(config._flakes) 38 config.addinivalue_line('markers', "flakes: Tests which run flake.") 39 40 41class FlakesPlugin(object): 42 def __init__(self, config): 43 self.ignore = Ignorer(config.getini("flakes-ignore")) 44 self.mtimes = config.cache.get(HISTKEY, {}) 45 46 def pytest_collect_file(self, path, parent): 47 config = parent.config 48 if config.option.flakes and isPythonFile(path.strpath): 49 flakesignore = self.ignore(path) 50 if flakesignore is not None: 51 if hasattr(FlakesItem, 'from_parent'): 52 item = FlakesItem.from_parent(parent, 53 fspath=path, 54 flakesignore=flakesignore) 55 else: 56 item = FlakesItem(path, parent, flakesignore) 57 return item 58 59 def pytest_sessionfinish(self, session): 60 session.config.cache.set(HISTKEY, self.mtimes) 61 62 63class FlakesError(Exception): 64 """ indicates an error during pyflakes checks. """ 65 66 67class FlakesItem(pytest.Item, pytest.File): 68 69 def __init__(self, fspath, parent, flakesignore): 70 super(FlakesItem, self).__init__(fspath, parent) 71 if hasattr(self, 'add_marker'): 72 self.add_marker("flakes") 73 else: 74 self.keywords["flakes"] = True 75 self.flakesignore = flakesignore 76 77 def setup(self): 78 flakesmtimes = self.config._flakes.mtimes 79 self._flakesmtime = self.fspath.mtime() 80 old = flakesmtimes.get(self.nodeid, 0) 81 if old == [self._flakesmtime, self.flakesignore]: 82 pytest.skip("file(s) previously passed pyflakes checks") 83 84 def runtest(self): 85 found_errors, out = check_file(self.fspath, self.flakesignore) 86 if found_errors: 87 raise FlakesError("\n".join(out)) 88 # update mtime only if test passed 89 # otherwise failures would not be re-run next time 90 self.config._flakes.mtimes[self.nodeid] = [self._flakesmtime, self.flakesignore] 91 92 def repr_failure(self, excinfo): 93 if excinfo.errisinstance(FlakesError): 94 return excinfo.value.args[0] 95 return super(FlakesItem, self).repr_failure(excinfo) 96 97 def reportinfo(self): 98 if self.flakesignore: 99 ignores = "(ignoring %s)" % " ".join(self.flakesignore) 100 else: 101 ignores = "" 102 return (self.fspath, -1, "pyflakes-check%s" % ignores) 103 104 105class Ignorer: 106 def __init__(self, ignorelines, coderex=re.compile(r"[EW]\d\d\d")): 107 self.ignores = ignores = [] 108 for line in ignorelines: 109 i = line.find("#") 110 if i != -1: 111 line = line[:i] 112 try: 113 glob, ign = line.split(None, 1) 114 except ValueError: 115 glob, ign = None, line 116 if glob and coderex.match(glob): 117 glob, ign = None, line 118 ign = ign.split() 119 if "ALL" in ign: 120 ign = None 121 ignores.append((glob, ign)) 122 123 def __call__(self, path): 124 l = set() 125 for (glob, ignlist) in self.ignores: 126 if not glob or path.fnmatch(glob): 127 if ignlist is None: 128 return None 129 l.update(set(ignlist)) 130 return sorted(l) 131 132 133def check_file(path, flakesignore): 134 if not hasattr(tokenize, 'open'): 135 codeString = path.read() 136 else: 137 with tokenize.open(path.strpath) as f: 138 codeString = f.read() 139 filename = py.builtin._totext(path) 140 errors = [] 141 try: 142 tree = compile(codeString, filename, "exec", _ast.PyCF_ONLY_AST) 143 except SyntaxError: 144 value = sys.exc_info()[1] 145 (lineno, offset, text) = value.lineno, value.offset, value.text 146 if text is None: 147 errors.append("%s: problem decoding source" % filename) 148 else: 149 line = text.splitlines()[-1] 150 151 if offset is not None: 152 offset = offset - (len(text) - len(line)) 153 154 msg = '%s:%d: %s' % (filename, lineno, value.args[0]) 155 msg = "%s\n%s" % (msg, line) 156 157 if offset is not None: 158 msg = "%s\n%s" % (msg, "%s^" % (" " * offset)) 159 errors.append(msg) 160 return 1, errors 161 else: 162 # Okay, it's syntactically valid. Now check it. 163 w = Checker(tree, filename) 164 w.messages.sort(key=lambda m: m.lineno) 165 lines = codeString.split('\n') 166 for warning in w.messages: 167 if warning.__class__.__name__ in flakesignore or is_ignored_line(lines[warning.lineno - 1].strip()): 168 continue 169 errors.append( 170 '%s:%s: %s\n%s' % ( 171 warning.filename, 172 warning.lineno, 173 warning.__class__.__name__, 174 warning.message % warning.message_args)) 175 return len(errors), errors 176 177 178def is_ignored_line(line): 179 if line.endswith('# noqa') or line.endswith('# pragma: no flakes'): 180 return True 181 return False 182