11"""Pylint plugin for py.test"""
22from __future__ import absolute_import , print_function , unicode_literals
3+ import re
34from os import sep
45from os .path import exists , join , dirname
56import sys
1516from pylint .reporters import BaseReporter
1617import pytest
1718
19+ HISTKEY = 'pylint/mtimes'
20+
1821
1922class PyLintException (Exception ):
2023 """Exception to raise if a file has a specified pylint error"""
@@ -104,6 +107,7 @@ def pytest_sessionstart(session):
104107 session .pylint_config = None
105108 session .pylintrc_file = None
106109 session .pylint_ignore = []
110+ session .pylint_ignore_patterns = []
107111 session .pylint_msg_template = None
108112 config = session .config
109113
@@ -118,12 +122,20 @@ def pytest_sessionstart(session):
118122 session .pylintrc_file = pylintrc_file
119123 session .pylint_config = ConfigParser ()
120124 session .pylint_config .read (pylintrc_file )
125+
121126 try :
122127 ignore_string = session .pylint_config .get ('MASTER' , 'ignore' )
123128 if ignore_string :
124129 session .pylint_ignore = ignore_string .split (',' )
125130 except (NoSectionError , NoOptionError ):
126131 pass
132+
133+ try :
134+ session .pylint_ignore_patterns = session .pylint_config .get (
135+ 'MASTER' , 'ignore-patterns' )
136+ except (NoSectionError , NoOptionError ):
137+ pass
138+
127139 try :
128140 session .pylint_msg_template = session .pylint_config .get (
129141 'REPORTS' , 'msg-template'
@@ -132,12 +144,46 @@ def pytest_sessionstart(session):
132144 pass
133145
134146
135- def include_file (path , ignore_list ):
147+ def include_file (path , ignore_list , ignore_patterns = None ):
136148 """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
137153 parts = path .split (sep )
138154 return not set (parts ) & set (ignore_list )
139155
140156
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+
141187def pytest_collect_file (path , parent ):
142188 """Collect files on which pylint should run"""
143189 config = parent .session .config
@@ -148,16 +194,18 @@ def pytest_collect_file(path, parent):
148194 rel_path = get_rel_path (path .strpath , parent .session .fspath .strpath )
149195 session = parent .session
150196 if session .pylint_config is None :
151- session .pylint_files .add (rel_path )
152197 # 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 (
158202 path , parent , session .pylint_msg_template , session .pylintrc_file
159203 )
160- return None
204+ else :
205+ return None
206+ if not item .should_skip :
207+ session .pylint_files .add (rel_path )
208+ return item
161209
162210
163211def pytest_collection_finish (session ):
@@ -216,6 +264,14 @@ def __init__(self, fspath, parent, msg_format=None, pylintrc_file=None):
216264 self ._msg_format = msg_format
217265
218266 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" )
219275
220276 def runtest (self ):
221277 """Check the pylint messages to see if any errors were reported."""
@@ -228,6 +284,9 @@ def runtest(self):
228284 if reported_errors :
229285 raise PyLintException ('\n ' .join (reported_errors ))
230286
287+ # Update the cache if the item passed pylint.
288+ self .config .pylint .mtimes [self .nodeid ] = self .__mtime
289+
231290 def repr_failure (self , excinfo ):
232291 """Handle any test failures by checkint that they were ours."""
233292 if excinfo .errisinstance (PyLintException ):
0 commit comments