1import re
2import py
3import pytest
4import pep8
5import os
6
7__version__ = '1.0.6'
8
9HISTKEY = "pep8/mtimes"
10
11
12def pytest_addoption(parser):
13    group = parser.getgroup("general")
14    group.addoption('--pep8', action='store_true',
15        help="perform some pep8 sanity checks on .py files")
16    parser.addini("pep8ignore", type="linelist",
17        help="each line specifies a glob pattern and whitespace "
18             "separated PEP8 errors or warnings which will be ignored, "
19             "example: *.py W293")
20    parser.addini("pep8maxlinelength",
21        help="max. line length (default: %d)" % pep8.MAX_LINE_LENGTH)
22
23
24def pytest_sessionstart(session):
25    config = session.config
26    if config.option.pep8:
27        config._pep8ignore = Ignorer(config.getini("pep8ignore"))
28        config._pep8mtimes = config.cache.get(HISTKEY, {})
29        config._max_line_length = int(config.getini("pep8maxlinelength")
30                                      or pep8.MAX_LINE_LENGTH)
31
32
33def pytest_collect_file(path, parent):
34    config = parent.config
35    if config.option.pep8 and path.ext == '.py':
36        pep8ignore = config._pep8ignore(path)
37        if pep8ignore is not None:
38            return Pep8Item(path, parent, pep8ignore, config._max_line_length)
39
40
41def pytest_sessionfinish(session):
42    config = session.config
43    if hasattr(config, "_pep8mtimes"):
44        config.cache.set(HISTKEY, config._pep8mtimes)
45
46
47class Pep8Error(Exception):
48    """ indicates an error during pep8 checks. """
49
50
51class Pep8Item(pytest.Item, pytest.File):
52
53    def __init__(self, path, parent, pep8ignore, max_line_length):
54        super(Pep8Item, self).__init__(path, parent)
55        self.add_marker("pep8")
56        self.pep8ignore = pep8ignore
57        self.max_line_length = max_line_length
58
59    def setup(self):
60        pep8mtimes = self.config._pep8mtimes
61        self._pep8mtime = self.fspath.mtime()
62        old = pep8mtimes.get(str(self.fspath), (0, []))
63        if old == (self._pep8mtime, self.pep8ignore):
64            pytest.skip("file(s) previously passed PEP8 checks")
65
66    def runtest(self):
67        call = py.io.StdCapture.call
68        found_errors, out, err = call(check_file, self.fspath, self.pep8ignore,
69                                      self.max_line_length)
70        if found_errors:
71            raise Pep8Error(out, err)
72        # update mtime only if test passed
73        # otherwise failures would not be re-run next time
74        self.config._pep8mtimes[str(self.fspath)] = (self._pep8mtime,
75                                                     self.pep8ignore)
76
77    def repr_failure(self, excinfo):
78        if excinfo.errisinstance(Pep8Error):
79            return excinfo.value.args[0]
80        return super(Pep8Item, self).repr_failure(excinfo)
81
82    def reportinfo(self):
83        if self.pep8ignore:
84            ignores = "(ignoring %s)" % " ".join(self.pep8ignore)
85        else:
86            ignores = ""
87        return (self.fspath, -1, "PEP8-check%s" % ignores)
88
89
90class Ignorer:
91    def __init__(self, ignorelines, coderex=re.compile("[EW]\d\d\d")):
92        self.ignores = ignores = []
93        for line in ignorelines:
94            i = line.find("#")
95            if i != -1:
96                line = line[:i]
97            try:
98                glob, ign = line.split(None, 1)
99            except ValueError:
100                glob, ign = None, line
101            if glob and coderex.match(glob):
102                glob, ign = None, line
103            ign = ign.split()
104            if "ALL" in ign:
105                ign = None
106            if glob and "/" != os.sep and "/" in glob:
107                glob = glob.replace("/", os.sep)
108            ignores.append((glob, ign))
109
110    def __call__(self, path):
111        l = []
112        for (glob, ignlist) in self.ignores:
113            if not glob or path.fnmatch(glob):
114                if ignlist is None:
115                    return None
116                l.extend(ignlist)
117        return l
118
119
120def check_file(path, pep8ignore, max_line_length):
121    checker = pep8.Checker(str(path), ignore=pep8ignore, show_source=1,
122                           max_line_length=max_line_length)
123    return checker.check_all()
124