1
1
"""Pylint plugin for py.test"""
2
2
from __future__ import absolute_import , print_function , unicode_literals
3
+ import re
3
4
from os import sep
4
5
from os .path import exists , join , dirname
5
6
import sys
15
16
from pylint .reporters import BaseReporter
16
17
import pytest
17
18
19
+ HISTKEY = 'pylint/mtimes'
20
+
18
21
19
22
class PyLintException (Exception ):
20
23
"""Exception to raise if a file has a specified pylint error"""
@@ -104,6 +107,7 @@ def pytest_sessionstart(session):
104
107
session .pylint_config = None
105
108
session .pylintrc_file = None
106
109
session .pylint_ignore = []
110
+ session .pylint_ignore_patterns = []
107
111
session .pylint_msg_template = None
108
112
config = session .config
109
113
@@ -118,12 +122,20 @@ def pytest_sessionstart(session):
118
122
session .pylintrc_file = pylintrc_file
119
123
session .pylint_config = ConfigParser ()
120
124
session .pylint_config .read (pylintrc_file )
125
+
121
126
try :
122
127
ignore_string = session .pylint_config .get ('MASTER' , 'ignore' )
123
128
if ignore_string :
124
129
session .pylint_ignore = ignore_string .split (',' )
125
130
except (NoSectionError , NoOptionError ):
126
131
pass
132
+
133
+ try :
134
+ session .pylint_ignore_patterns = session .pylint_config .get (
135
+ 'MASTER' , 'ignore-patterns' )
136
+ except (NoSectionError , NoOptionError ):
137
+ pass
138
+
127
139
try :
128
140
session .pylint_msg_template = session .pylint_config .get (
129
141
'REPORTS' , 'msg-template'
@@ -132,12 +144,46 @@ def pytest_sessionstart(session):
132
144
pass
133
145
134
146
135
- def include_file (path , ignore_list ):
147
+ def include_file (path , ignore_list , ignore_patterns = None ):
136
148
"""Checks if a file should be included in the collection."""
149
+ if ignore_patterns :
150
+ for pattern in ignore_patterns :
151
+ if re .match (pattern , path ):
152
+ return False
137
153
parts = path .split (sep )
138
154
return not set (parts ) & set (ignore_list )
139
155
140
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
+ class PylintPlugin (object ):
170
+ """
171
+ A Plugin object for pylint, which loads and records file mtimes.
172
+ """
173
+ # pylint: disable=too-few-public-methods
174
+
175
+ def __init__ (self , config ):
176
+ self .mtimes = config .cache .get (HISTKEY , {})
177
+
178
+ def pytest_sessionfinish (self , session ):
179
+ """
180
+ Save file mtimes to pytest cache.
181
+
182
+ :param _pytest.main.Session session: the pytest session object
183
+ """
184
+ session .config .cache .set (HISTKEY , self .mtimes )
185
+
186
+
141
187
def pytest_collect_file (path , parent ):
142
188
"""Collect files on which pylint should run"""
143
189
config = parent .session .config
@@ -148,16 +194,18 @@ def pytest_collect_file(path, parent):
148
194
rel_path = get_rel_path (path .strpath , parent .session .fspath .strpath )
149
195
session = parent .session
150
196
if session .pylint_config is None :
151
- session .pylint_files .add (rel_path )
152
197
# No pylintrc, therefore no ignores, so return the item.
153
- return PyLintItem (path , parent )
154
-
155
- if include_file (rel_path , session .pylint_ignore ):
156
- session .pylint_files .add (rel_path )
157
- return PyLintItem (
198
+ item = PyLintItem (path , parent )
199
+ elif include_file (rel_path , session .pylint_ignore ,
200
+ session .pylint_ignore_patterns ):
201
+ item = PyLintItem (
158
202
path , parent , session .pylint_msg_template , session .pylintrc_file
159
203
)
160
- return None
204
+ else :
205
+ return None
206
+ if not item .should_skip :
207
+ session .pylint_files .add (rel_path )
208
+ return item
161
209
162
210
163
211
def pytest_collection_finish (session ):
@@ -216,6 +264,14 @@ def __init__(self, fspath, parent, msg_format=None, pylintrc_file=None):
216
264
self ._msg_format = msg_format
217
265
218
266
self .pylintrc_file = pylintrc_file
267
+ self .__mtime = self .fspath .mtime ()
268
+ prev_mtime = self .config .pylint .mtimes .get (self .nodeid , 0 )
269
+ self .should_skip = (prev_mtime == self .__mtime )
270
+
271
+ def setup (self ):
272
+ """Mark unchanged files as SKIPPED."""
273
+ if self .should_skip :
274
+ pytest .skip ("file(s) previously passed pylint checks" )
219
275
220
276
def runtest (self ):
221
277
"""Check the pylint messages to see if any errors were reported."""
@@ -228,6 +284,9 @@ def runtest(self):
228
284
if reported_errors :
229
285
raise PyLintException ('\n ' .join (reported_errors ))
230
286
287
+ # Update the cache if the item passed pylint.
288
+ self .config .pylint .mtimes [self .nodeid ] = self .__mtime
289
+
231
290
def repr_failure (self , excinfo ):
232
291
"""Handle any test failures by checkint that they were ours."""
233
292
if excinfo .errisinstance (PyLintException ):
0 commit comments