16
16
from pylint .reporters import BaseReporter
17
17
import pytest
18
18
19
+ HISTKEY = 'pylint/mtimes'
20
+
19
21
20
22
class PyLintException (Exception ):
21
23
"""Exception to raise if a file has a specified pylint error"""
@@ -152,6 +154,40 @@ def include_file(path, ignore_list, ignore_patterns=None):
152
154
return not set (parts ) & set (ignore_list )
153
155
154
156
157
+ def pytest_configure (config ):
158
+ """
159
+ Add a plugin to cache file mtimes.
160
+
161
+ :param _pytest.config.Config config: pytest config object
162
+ """
163
+ if config .option .pylint :
164
+ config .pylint = PylintPlugin (config )
165
+ config .pluginmanager .register (config .pylint )
166
+ config .addinivalue_line ('markers' , "pylint: Tests which run pylint." )
167
+
168
+
169
+ # There will be an old-style-class error in Python 2.7,
170
+ # or a useless-object-inheritance warning in Python 3.
171
+ # If disable any, pylint will have a bad-option-value error in 2.7 or 3.
172
+ # Finally I have to disable useless-object-inheritance locally
173
+ # and bad-option-value globally.
174
+ # pylint: disable=too-few-public-methods, useless-object-inheritance
175
+ class PylintPlugin (object ):
176
+ """
177
+ A Plugin object for pylint, which loads and records file mtimes.
178
+ """
179
+ def __init__ (self , config ):
180
+ self .mtimes = config .cache .get (HISTKEY , {})
181
+
182
+ def pytest_sessionfinish (self , session ):
183
+ """
184
+ Save file mtimes to pytest cache.
185
+
186
+ :param _pytest.main.Session session: the pytest session object
187
+ """
188
+ session .config .cache .set (HISTKEY , self .mtimes )
189
+
190
+
155
191
def pytest_collect_file (path , parent ):
156
192
"""Collect files on which pylint should run"""
157
193
config = parent .session .config
@@ -162,17 +198,18 @@ def pytest_collect_file(path, parent):
162
198
rel_path = get_rel_path (path .strpath , parent .session .fspath .strpath )
163
199
session = parent .session
164
200
if session .pylint_config is None :
165
- session .pylint_files .add (rel_path )
166
201
# No pylintrc, therefore no ignores, so return the item.
167
- return PyLintItem (path , parent )
168
-
169
- if include_file (rel_path , session .pylint_ignore ,
170
- session .pylint_ignore_patterns ):
171
- session .pylint_files .add (rel_path )
172
- return PyLintItem (
202
+ item = PyLintItem (path , parent )
203
+ elif include_file (rel_path , session .pylint_ignore ,
204
+ session .pylint_ignore_patterns ):
205
+ item = PyLintItem (
173
206
path , parent , session .pylint_msg_template , session .pylintrc_file
174
207
)
175
- return None
208
+ else :
209
+ return None
210
+ if not item .should_skip :
211
+ session .pylint_files .add (rel_path )
212
+ return item
176
213
177
214
178
215
def pytest_collection_finish (session ):
@@ -231,6 +268,14 @@ def __init__(self, fspath, parent, msg_format=None, pylintrc_file=None):
231
268
self ._msg_format = msg_format
232
269
233
270
self .pylintrc_file = pylintrc_file
271
+ self .__mtime = self .fspath .mtime ()
272
+ prev_mtime = self .config .pylint .mtimes .get (self .nodeid , 0 )
273
+ self .should_skip = (prev_mtime == self .__mtime )
274
+
275
+ def setup (self ):
276
+ """Mark unchanged files as SKIPPED."""
277
+ if self .should_skip :
278
+ pytest .skip ("file(s) previously passed pylint checks" )
234
279
235
280
def runtest (self ):
236
281
"""Check the pylint messages to see if any errors were reported."""
@@ -243,6 +288,9 @@ def runtest(self):
243
288
if reported_errors :
244
289
raise PyLintException ('\n ' .join (reported_errors ))
245
290
291
+ # Update the cache if the item passed pylint.
292
+ self .config .pylint .mtimes [self .nodeid ] = self .__mtime
293
+
246
294
def repr_failure (self , excinfo ):
247
295
"""Handle any test failures by checkint that they were ours."""
248
296
if excinfo .errisinstance (PyLintException ):
0 commit comments