Coverage for /usr/local/lib/python3.7/site-packages/pytest_pylint.py : 11%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Pylint plugin for py.test"""
2from __future__ import absolute_import, print_function, unicode_literals
3import re
4from os import sep
5from os.path import exists, join, dirname
6import sys
7from six.moves.configparser import ( # pylint: disable=import-error
8 ConfigParser,
9 NoSectionError,
10 NoOptionError
11)
13from pylint import lint
14from pylint.config import PYLINTRC
15from pylint.interfaces import IReporter
16from pylint.reporters import BaseReporter
17import pytest
19HISTKEY = 'pylint/mtimes'
22class PyLintException(Exception):
23 """Exception to raise if a file has a specified pylint error"""
26class ProgrammaticReporter(BaseReporter):
27 """Reporter that replaces output with storage in list of dictionaries"""
29 __implements__ = IReporter
30 extension = 'prog'
32 def __init__(self, output=None):
33 BaseReporter.__init__(self, output)
34 self.current_module = None
35 self.data = []
37 def add_message(self, msg_id, location, msg):
38 """Deprecated, but required"""
39 raise NotImplementedError
41 def handle_message(self, msg):
42 """Get message and append to our data structure"""
43 self.data.append(msg)
45 def _display(self, layout):
46 """launch layouts display"""
48 def on_set_current_module(self, module, filepath):
49 """Hook called when a module starts to be analysed."""
50 print('.', end='')
51 sys.stdout.flush()
53 def on_close(self, stats, previous_stats):
54 """Hook called when all modules finished analyzing."""
55 # print a new line when pylint is finished
56 print('')
59def get_rel_path(path, parent_path):
60 """
61 Give the path to object relative to ``parent_path``.
62 """
63 replaced_path = path.replace(parent_path, '', 1)
64 if replaced_path[0] == sep:
65 rel_path = replaced_path[1:]
66 else:
67 rel_path = replaced_path
68 return rel_path
71def pytest_addoption(parser):
72 """Add all our command line options"""
73 group = parser.getgroup("general")
74 group.addoption(
75 "--pylint",
76 action="store_true", default=False,
77 help="run pylint on all"
78 )
79 group.addoption(
80 "--no-pylint",
81 action="store_true", default=False,
82 help="disable running pylint "
83 )
85 group.addoption(
86 '--pylint-rcfile',
87 default=None,
88 help='Location of RC file if not pylintrc'
89 )
90 group.addoption(
91 '--pylint-error-types',
92 default='CRWEF',
93 help='The types of pylint errors to consider failures by letter'
94 ', default is all of them (CRWEF).'
95 )
96 group.addoption(
97 '--pylint-jobs',
98 default=None,
99 help='Specify number of processes to use for pylint'
100 )
103def pytest_sessionstart(session):
104 """Storing pylint settings on the session"""
105 session.pylint_files = set()
106 session.pylint_messages = {}
107 session.pylint_config = None
108 session.pylintrc_file = None
109 session.pylint_ignore = []
110 session.pylint_ignore_patterns = []
111 session.pylint_msg_template = None
112 config = session.config
114 # Find pylintrc to check ignore list
115 pylintrc_file = config.option.pylint_rcfile or PYLINTRC
117 if pylintrc_file and not exists(pylintrc_file):
118 # The directory of pytest.ini got a chance
119 pylintrc_file = join(dirname(str(config.inifile)), pylintrc_file)
121 if pylintrc_file and exists(pylintrc_file):
122 session.pylintrc_file = pylintrc_file
123 session.pylint_config = ConfigParser()
124 session.pylint_config.read(pylintrc_file)
126 try:
127 ignore_string = session.pylint_config.get('MASTER', 'ignore')
128 if ignore_string:
129 session.pylint_ignore = ignore_string.split(',')
130 except (NoSectionError, NoOptionError):
131 pass
133 try:
134 session.pylint_ignore_patterns = session.pylint_config.get(
135 'MASTER', 'ignore-patterns')
136 except (NoSectionError, NoOptionError):
137 pass
139 try:
140 session.pylint_msg_template = session.pylint_config.get(
141 'REPORTS', 'msg-template'
142 )
143 except (NoSectionError, NoOptionError):
144 pass
147def include_file(path, ignore_list, ignore_patterns=None):
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
153 parts = path.split(sep)
154 return not set(parts) & set(ignore_list)
157def pytest_configure(config):
158 """
159 Add a plugin to cache file mtimes.
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.")
169class PylintPlugin(object):
170 """
171 A Plugin object for pylint, which loads and records file mtimes.
172 """
173 # pylint: disable=too-few-public-methods
175 def __init__(self, config):
176 if hasattr(config, 'cache'):
177 self.mtimes = config.cache.get(HISTKEY, {})
178 else:
179 self.mtimes = {}
181 def pytest_sessionfinish(self, session):
182 """
183 Save file mtimes to pytest cache.
185 :param _pytest.main.Session session: the pytest session object
186 """
187 if hasattr(session.config, 'cache'):
188 session.config.cache.set(HISTKEY, self.mtimes)
191def pytest_collect_file(path, parent):
192 """Collect files on which pylint should run"""
193 config = parent.session.config
194 if not config.option.pylint or config.option.no_pylint:
195 return None
196 if path.ext != ".py":
197 return None
198 rel_path = get_rel_path(path.strpath, parent.session.fspath.strpath)
199 session = parent.session
200 if session.pylint_config is None:
201 # No pylintrc, therefore no ignores, so return the item.
202 item = PyLintItem(path, parent)
203 elif include_file(rel_path, session.pylint_ignore,
204 session.pylint_ignore_patterns):
205 item = PyLintItem(
206 path, parent, session.pylint_msg_template, session.pylintrc_file
207 )
208 else:
209 return None
210 if not item.should_skip:
211 session.pylint_files.add(rel_path)
212 return item
215def pytest_collection_finish(session):
216 """Lint collected files and store messages on session."""
217 if not session.pylint_files:
218 return
220 jobs = session.config.option.pylint_jobs
221 reporter = ProgrammaticReporter()
222 # Build argument list for pylint
223 args_list = list(session.pylint_files)
224 if session.pylintrc_file:
225 args_list.append('--rcfile={0}'.format(
226 session.pylintrc_file
227 ))
228 if jobs is not None:
229 args_list.append('-j')
230 args_list.append(jobs)
231 print('-' * 65)
232 print('Linting files')
233 # Disabling keyword arg to handle both 1.x and 2.x pylint API calls
234 # pylint: disable=unexpected-keyword-arg
236 # Run pylint over the collected files.
237 try:
238 result = lint.Run(args_list, reporter=reporter, exit=False)
239 except TypeError: # Handle pylint 2.0 API
240 result = lint.Run(args_list, reporter=reporter, do_exit=False)
241 except RuntimeError:
242 return
243 messages = result.linter.reporter.data
244 # Stores the messages in a dictionary for lookup in tests.
245 for message in messages:
246 if message.path not in session.pylint_messages:
247 session.pylint_messages[message.path] = []
248 session.pylint_messages[message.path].append(message)
249 print('-' * 65)
252class PyLintItem(pytest.Item, pytest.File):
253 """pylint test running class."""
254 # pylint doesn't deal well with dynamic modules and there isn't an
255 # astng plugin for pylint in pypi yet, so we'll have to disable
256 # the checks.
257 # pylint: disable=no-member,abstract-method
258 def __init__(self, fspath, parent, msg_format=None, pylintrc_file=None):
259 super(PyLintItem, self).__init__(fspath, parent)
261 self.add_marker("pylint")
262 self.rel_path = get_rel_path(
263 fspath.strpath,
264 parent.session.fspath.strpath
265 )
267 if msg_format is None:
268 self._msg_format = '{C}:{line:3d},{column:2d}: {msg} ({symbol})'
269 else:
270 self._msg_format = msg_format
272 self.pylintrc_file = pylintrc_file
273 self.__mtime = self.fspath.mtime()
274 prev_mtime = self.config.pylint.mtimes.get(self.nodeid, 0)
275 self.should_skip = (prev_mtime == self.__mtime)
277 def setup(self):
278 """Mark unchanged files as SKIPPED."""
279 if self.should_skip:
280 pytest.skip("file(s) previously passed pylint checks")
282 def runtest(self):
283 """Check the pylint messages to see if any errors were reported."""
284 reported_errors = []
285 for error in self.session.pylint_messages.get(self.rel_path, []):
286 if error.C in self.config.option.pylint_error_types:
287 reported_errors.append(
288 error.format(self._msg_format)
289 )
290 if reported_errors:
291 raise PyLintException('\n'.join(reported_errors))
293 # Update the cache if the item passed pylint.
294 self.config.pylint.mtimes[self.nodeid] = self.__mtime
296 def repr_failure(self, excinfo):
297 """Handle any test failures by checkint that they were ours."""
298 if excinfo.errisinstance(PyLintException):
299 return excinfo.value.args[0]
300 return super(PyLintItem, self).repr_failure(excinfo)
302 def reportinfo(self):
303 """Generate our test report"""
304 return self.fspath, None, "[pylint] {0}".format(self.rel_path)