1# https://docs.pytest.org/en/latest/writing_plugins.html 2# https://docs.pytest.org/en/latest/example/nonpython.html#yaml-plugin 3 4import optparse 5 6import py.io 7import pycodestyle 8import pytest 9 10 11def pytest_addoption(parser): 12 group = parser.getgroup('pycodestyle') 13 group.addoption('--pycodestyle', action='store_true', 14 default=False, help='run pycodestyle') 15 16 17def pytest_configure(config): 18 config.addinivalue_line('markers', 'pycodestyle: mark tests to be checked by pycodestyle.') 19 20 21def pytest_collect_file(parent, path): 22 config = parent.config 23 if config.getoption('pycodestyle') and path.ext == '.py': 24 # https://github.com/PyCQA/pycodestyle/blob/2.5.0/pycodestyle.py#L2295 25 style_guide = pycodestyle.StyleGuide(paths=[str(path)], verbose=False) 26 if not style_guide.excluded(filename=str(path)): 27 # https://github.com/pytest-dev/pytest/blob/ee1950af7793624793ee297e5f48b49c8bdf2065/src/_pytest/nodes.py#L477 28 return File.from_parent(parent=parent, fspath=path, style_guide_options=style_guide.options) 29 30 31class File(pytest.File): 32 33 @classmethod 34 def from_parent(cls, parent, fspath, style_guide_options: optparse.Values): 35 _file = super().from_parent(parent=parent, fspath=fspath) 36 # store options of pycodestyle 37 _file.style_guide_options = style_guide_options 38 return _file 39 40 def collect(self): 41 # https://github.com/pytest-dev/pytest/blob/ee1950af7793624793ee297e5f48b49c8bdf2065/src/_pytest/nodes.py#L399 42 yield Item.from_parent(parent=self, name=self.name, nodeid=self.nodeid) 43 44 45class Item(pytest.Item): 46 CACHE_KEY = 'pycodestyle/mtimes' 47 48 def __init__(self, name, parent, nodeid): 49 # https://github.com/pytest-dev/pytest/blob/ee1950af7793624793ee297e5f48b49c8bdf2065/src/_pytest/nodes.py#L544 50 super().__init__(name, parent=parent, nodeid=f"{nodeid}::PYCODESTYLE") 51 self.add_marker('pycodestyle') 52 # load options of pycodestyle 53 self.options: optparse.Values = self.parent.style_guide_options 54 55 def setup(self): 56 if not hasattr(self.config, 'cache'): 57 return 58 59 old_mtime = self.config.cache.get(self.CACHE_KEY, {}).get(str(self.fspath), -1) 60 mtime = self.fspath.mtime() 61 if old_mtime == mtime: 62 pytest.skip('previously passed pycodestyle checks') 63 64 def runtest(self): 65 # http://pycodestyle.pycqa.org/en/latest/api.html#pycodestyle.Checker 66 # http://pycodestyle.pycqa.org/en/latest/advanced.html 67 checker = pycodestyle.Checker(filename=str(self.fspath), 68 options=self.options) 69 file_errors, out, err = py.io.StdCapture.call(checker.check_all) 70 if file_errors > 0: 71 raise PyCodeStyleError(out) 72 elif hasattr(self.config, 'cache'): 73 # update cache 74 # http://pythonhosted.org/pytest-cache/api.html 75 cache = self.config.cache.get(self.CACHE_KEY, {}) 76 cache[str(self.fspath)] = self.fspath.mtime() 77 self.config.cache.set(self.CACHE_KEY, cache) 78 79 def repr_failure(self, excinfo): 80 if excinfo.errisinstance(PyCodeStyleError): 81 return excinfo.value.args[0] 82 else: 83 return super().repr_failure(excinfo) 84 85 def reportinfo(self): 86 # https://github.com/pytest-dev/pytest/blob/4678cbeb913385f00cc21b79662459a8c9fafa87/_pytest/main.py#L550 87 # https://github.com/pytest-dev/pytest/blob/4678cbeb913385f00cc21b79662459a8c9fafa87/_pytest/doctest.py#L149 88 return self.fspath, None, 'pycodestyle-check' 89 90 91class PyCodeStyleError(Exception): 92 """custom exception for error reporting.""" 93