1"""py.test plugin to test with flake8.""" 2 3import os 4import re 5 6from flake8.main import application 7from flake8.options import config 8 9import py 10 11import pytest 12 13__version__ = '0.6' 14 15HISTKEY = "flake8/mtimes" 16 17 18def pytest_addoption(parser): 19 """Hook up additional options.""" 20 group = parser.getgroup("general") 21 group.addoption( 22 '--flake8', action='store_true', 23 help="perform some flake8 sanity checks on .py files") 24 parser.addini( 25 "flake8-ignore", type="linelist", 26 help="each line specifies a glob pattern and whitespace " 27 "separated FLAKE8 errors or warnings which will be ignored, " 28 "example: *.py W293") 29 parser.addini( 30 "flake8-max-line-length", 31 help="maximum line length") 32 parser.addini( 33 "flake8-max-complexity", 34 help="McCabe complexity threshold") 35 parser.addini( 36 "flake8-show-source", type="bool", 37 help="show the source generate each error or warning") 38 parser.addini( 39 "flake8-statistics", type="bool", 40 help="count errors and warnings") 41 parser.addini( 42 "flake8-extensions", type="args", default=[".py"], 43 help="a list of file extensions, for example: .py .pyx") 44 45 46def pytest_configure(config): 47 """Start a new session.""" 48 if config.option.flake8: 49 config._flake8ignore = Ignorer(config.getini("flake8-ignore")) 50 config._flake8maxlen = config.getini("flake8-max-line-length") 51 config._flake8maxcomplexity = config.getini("flake8-max-complexity") 52 config._flake8showshource = config.getini("flake8-show-source") 53 config._flake8statistics = config.getini("flake8-statistics") 54 config._flake8exts = config.getini("flake8-extensions") 55 config.addinivalue_line('markers', "flake8: Tests which run flake8.") 56 if hasattr(config, 'cache'): 57 config._flake8mtimes = config.cache.get(HISTKEY, {}) 58 59 60def pytest_collect_file(path, parent): 61 """Filter files down to which ones should be checked.""" 62 config = parent.config 63 if config.option.flake8 and path.ext in config._flake8exts: 64 flake8ignore = config._flake8ignore(path) 65 if flake8ignore is not None: 66 if hasattr(Flake8Item, "from_parent"): 67 item = Flake8Item.from_parent(parent, fspath=path) 68 item.flake8ignore = flake8ignore 69 item.maxlength = config._flake8maxlen 70 item.maxcomplexity = config._flake8maxcomplexity 71 item.showshource = config._flake8showshource 72 item.statistics = config._flake8statistics 73 return item 74 else: 75 return Flake8Item( 76 path, 77 parent, 78 flake8ignore=flake8ignore, 79 maxlength=config._flake8maxlen, 80 maxcomplexity=config._flake8maxcomplexity, 81 showshource=config._flake8showshource, 82 statistics=config._flake8statistics) 83 84 85def pytest_unconfigure(config): 86 """Flush cache at end of run.""" 87 if hasattr(config, "_flake8mtimes"): 88 config.cache.set(HISTKEY, config._flake8mtimes) 89 90 91class Flake8Error(Exception): 92 """ indicates an error during flake8 checks. """ 93 94 95class Flake8Item(pytest.Item, pytest.File): 96 97 def __init__(self, fspath, parent, flake8ignore=None, maxlength=None, 98 maxcomplexity=None, showshource=None, statistics=None): 99 super(Flake8Item, self).__init__(fspath, parent) 100 self._nodeid += "::FLAKE8" 101 self.add_marker("flake8") 102 self.flake8ignore = flake8ignore 103 self.maxlength = maxlength 104 self.maxcomplexity = maxcomplexity 105 self.showshource = showshource 106 self.statistics = statistics 107 108 def setup(self): 109 if hasattr(self.config, "_flake8mtimes"): 110 flake8mtimes = self.config._flake8mtimes 111 else: 112 flake8mtimes = {} 113 self._flake8mtime = self.fspath.mtime() 114 old = flake8mtimes.get(str(self.fspath), (0, [])) 115 if old == [self._flake8mtime, self.flake8ignore]: 116 pytest.skip("file(s) previously passed FLAKE8 checks") 117 118 def runtest(self): 119 call = py.io.StdCapture.call 120 found_errors, out, err = call( 121 check_file, 122 self.fspath, 123 self.flake8ignore, 124 self.maxlength, 125 self.maxcomplexity, 126 self.showshource, 127 self.statistics) 128 if found_errors: 129 raise Flake8Error(out, err) 130 # update mtime only if test passed 131 # otherwise failures would not be re-run next time 132 if hasattr(self.config, "_flake8mtimes"): 133 self.config._flake8mtimes[str(self.fspath)] = (self._flake8mtime, 134 self.flake8ignore) 135 136 def repr_failure(self, excinfo): 137 if excinfo.errisinstance(Flake8Error): 138 return excinfo.value.args[0] 139 return super(Flake8Item, self).repr_failure(excinfo) 140 141 def reportinfo(self): 142 if self.flake8ignore: 143 ignores = "(ignoring %s)" % " ".join(self.flake8ignore) 144 else: 145 ignores = "" 146 return (self.fspath, -1, "FLAKE8-check%s" % ignores) 147 148 def collect(self): 149 return iter((self,)) 150 151 152class Ignorer: 153 def __init__(self, ignorelines, coderex=re.compile(r"[EW]\d\d\d")): 154 self.ignores = ignores = [] 155 for line in ignorelines: 156 i = line.find("#") 157 if i != -1: 158 line = line[:i] 159 try: 160 glob, ign = line.split(None, 1) 161 except ValueError: 162 glob, ign = None, line 163 if glob and coderex.match(glob): 164 glob, ign = None, line 165 ign = ign.split() 166 if "ALL" in ign: 167 ign = None 168 if glob and "/" != os.sep and "/" in glob: 169 glob = glob.replace("/", os.sep) 170 ignores.append((glob, ign)) 171 172 def __call__(self, path): 173 l = [] # noqa: E741 174 for (glob, ignlist) in self.ignores: 175 if not glob or path.fnmatch(glob): 176 if ignlist is None: 177 return None 178 l.extend(ignlist) 179 return l 180 181 182def check_file(path, flake8ignore, maxlength, maxcomplexity, 183 showshource, statistics): 184 """Run flake8 over a single file, and return the number of failures.""" 185 args = [] 186 if maxlength: 187 args += ['--max-line-length', maxlength] 188 if maxcomplexity: 189 args += ['--max-complexity', maxcomplexity] 190 if showshource: 191 args += ['--show-source'] 192 if statistics: 193 args += ['--statistics'] 194 app = application.Application() 195 if not hasattr(app, 'parse_preliminary_options_and_args'): # flake8 >= 3.8 196 prelim_opts, remaining_args = app.parse_preliminary_options(args) 197 config_finder = config.ConfigFileFinder( 198 app.program, 199 prelim_opts.append_config, 200 config_file=prelim_opts.config, 201 ignore_config_files=prelim_opts.isolated, 202 ) 203 app.find_plugins(config_finder) 204 app.register_plugin_options() 205 app.parse_configuration_and_cli(config_finder, remaining_args) 206 else: 207 app.parse_preliminary_options_and_args(args) 208 app.make_config_finder() 209 app.find_plugins() 210 app.register_plugin_options() 211 app.parse_configuration_and_cli(args) 212 if flake8ignore: 213 app.options.ignore = flake8ignore 214 app.make_formatter() # fix this 215 if hasattr(app, 'make_notifier'): 216 # removed in flake8 3.7+ 217 app.make_notifier() 218 app.make_guide() 219 app.make_file_checker_manager() 220 app.run_checks([str(path)]) 221 app.formatter.start() 222 app.report_errors() 223 app.formatter.stop() 224 return app.result_count 225