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