gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / pylint / lint.py @ 1026
History | View | Annotate | Download (56.6 KB)
1 | 745 | jjdelcerro | # Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
|
---|---|---|---|
2 | # http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||
3 | #
|
||
4 | # This program is free software; you can redistribute it and/or modify it under
|
||
5 | # the terms of the GNU General Public License as published by the Free Software
|
||
6 | # Foundation; either version 2 of the License, or (at your option) any later
|
||
7 | # version.
|
||
8 | #
|
||
9 | # This program is distributed in the hope that it will be useful, but WITHOUT
|
||
10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||
11 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||
12 | #
|
||
13 | # You should have received a copy of the GNU General Public License along with
|
||
14 | # this program; if not, write to the Free Software Foundation, Inc.,
|
||
15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
16 | """ %prog [options] module_or_package
|
||
17 |
|
||
18 | Check that a module satisfies a coding standard (and more !).
|
||
19 |
|
||
20 | %prog --help
|
||
21 |
|
||
22 | Display this help message and exit.
|
||
23 |
|
||
24 | %prog --help-msg <msg-id>[,<msg-id>]
|
||
25 |
|
||
26 | Display help messages about given message identifiers and exit.
|
||
27 | """
|
||
28 | from __future__ import print_function |
||
29 | |||
30 | import collections |
||
31 | import contextlib |
||
32 | import operator |
||
33 | import os |
||
34 | try:
|
||
35 | import multiprocessing |
||
36 | except ImportError: |
||
37 | multiprocessing = None
|
||
38 | import sys |
||
39 | import tokenize |
||
40 | import warnings |
||
41 | |||
42 | import six |
||
43 | |||
44 | import astroid |
||
45 | from astroid.__pkginfo__ import version as astroid_version |
||
46 | from astroid import modutils |
||
47 | from pylint import checkers |
||
48 | from pylint import interfaces |
||
49 | from pylint import reporters |
||
50 | from pylint import utils |
||
51 | from pylint import config |
||
52 | from pylint.__pkginfo__ import version |
||
53 | from pylint.reporters.ureports import nodes as report_nodes |
||
54 | |||
55 | |||
56 | MANAGER = astroid.MANAGER |
||
57 | INCLUDE_IDS_HELP = ("Deprecated. It was used to include message\'s "
|
||
58 | "id in output. Use --msg-template instead.")
|
||
59 | SYMBOLS_HELP = ("Deprecated. It was used to include symbolic ids of "
|
||
60 | "messages in output. Use --msg-template instead.")
|
||
61 | |||
62 | def _get_new_args(message): |
||
63 | location = ( |
||
64 | message.abspath, |
||
65 | message.path, |
||
66 | message.module, |
||
67 | message.obj, |
||
68 | message.line, |
||
69 | message.column, |
||
70 | ) |
||
71 | return (
|
||
72 | message.msg_id, |
||
73 | message.symbol, |
||
74 | location, |
||
75 | message.msg, |
||
76 | message.confidence, |
||
77 | ) |
||
78 | |||
79 | def _get_python_path(filepath): |
||
80 | dirname = os.path.realpath(os.path.expanduser(filepath)) |
||
81 | if not os.path.isdir(dirname): |
||
82 | dirname = os.path.dirname(dirname) |
||
83 | while True: |
||
84 | if not os.path.exists(os.path.join(dirname, "__init__.py")): |
||
85 | return dirname
|
||
86 | old_dirname = dirname |
||
87 | dirname = os.path.dirname(dirname) |
||
88 | if old_dirname == dirname:
|
||
89 | return os.getcwd()
|
||
90 | |||
91 | |||
92 | def _merge_stats(stats): |
||
93 | merged = {} |
||
94 | by_msg = collections.Counter() |
||
95 | for stat in stats: |
||
96 | message_stats = stat.pop('by_msg', {})
|
||
97 | by_msg.update(message_stats) |
||
98 | |||
99 | for key, item in six.iteritems(stat): |
||
100 | if key not in merged: |
||
101 | merged[key] = item |
||
102 | else:
|
||
103 | if isinstance(item, dict): |
||
104 | merged[key].update(item) |
||
105 | else:
|
||
106 | merged[key] = merged[key] + item |
||
107 | |||
108 | merged['by_msg'] = by_msg
|
||
109 | return merged
|
||
110 | |||
111 | |||
112 | @contextlib.contextmanager
|
||
113 | def _patch_sysmodules(): |
||
114 | # Context manager that permits running pylint, on Windows, with -m switch
|
||
115 | # and with --jobs, as in 'python -2 -m pylint .. --jobs'.
|
||
116 | # For more details why this is needed,
|
||
117 | # see Python issue http://bugs.python.org/issue10845.
|
||
118 | |||
119 | mock_main = __name__ != '__main__' # -m switch |
||
120 | if mock_main:
|
||
121 | sys.modules['__main__'] = sys.modules[__name__]
|
||
122 | |||
123 | try:
|
||
124 | yield
|
||
125 | finally:
|
||
126 | if mock_main:
|
||
127 | sys.modules.pop('__main__')
|
||
128 | |||
129 | |||
130 | # Python Linter class #########################################################
|
||
131 | |||
132 | MSGS = { |
||
133 | 'F0001': ('%s', |
||
134 | 'fatal',
|
||
135 | 'Used when an error occurred preventing the analysis of a \
|
||
136 | module (unable to find it for instance).'),
|
||
137 | 'F0002': ('%s: %s', |
||
138 | 'astroid-error',
|
||
139 | 'Used when an unexpected error occurred while building the '
|
||
140 | 'Astroid representation. This is usually accompanied by a '
|
||
141 | 'traceback. Please report such errors !'),
|
||
142 | 'F0010': ('error while code parsing: %s', |
||
143 | 'parse-error',
|
||
144 | 'Used when an exception occured while building the Astroid '
|
||
145 | 'representation which could be handled by astroid.'),
|
||
146 | |||
147 | 'I0001': ('Unable to run raw checkers on built-in module %s', |
||
148 | 'raw-checker-failed',
|
||
149 | 'Used to inform that a built-in module has not been checked '
|
||
150 | 'using the raw checkers.'),
|
||
151 | |||
152 | 'I0010': ('Unable to consider inline option %r', |
||
153 | 'bad-inline-option',
|
||
154 | 'Used when an inline option is either badly formatted or can\'t '
|
||
155 | 'be used inside modules.'),
|
||
156 | |||
157 | 'I0011': ('Locally disabling %s (%s)', |
||
158 | 'locally-disabled',
|
||
159 | 'Used when an inline option disables a message or a messages '
|
||
160 | 'category.'),
|
||
161 | 'I0012': ('Locally enabling %s (%s)', |
||
162 | 'locally-enabled',
|
||
163 | 'Used when an inline option enables a message or a messages '
|
||
164 | 'category.'),
|
||
165 | 'I0013': ('Ignoring entire file', |
||
166 | 'file-ignored',
|
||
167 | 'Used to inform that the file will not be checked'),
|
||
168 | 'I0020': ('Suppressed %s (from line %d)', |
||
169 | 'suppressed-message',
|
||
170 | 'A message was triggered on a line, but suppressed explicitly '
|
||
171 | 'by a disable= comment in the file. This message is not '
|
||
172 | 'generated for messages that are ignored due to configuration '
|
||
173 | 'settings.'),
|
||
174 | 'I0021': ('Useless suppression of %s', |
||
175 | 'useless-suppression',
|
||
176 | 'Reported when a message is explicitly disabled for a line or '
|
||
177 | 'a block of code, but never triggered.'),
|
||
178 | 'I0022': ('Pragma "%s" is deprecated, use "%s" instead', |
||
179 | 'deprecated-pragma',
|
||
180 | 'Some inline pylint options have been renamed or reworked, '
|
||
181 | 'only the most recent form should be used. '
|
||
182 | 'NOTE:skip-all is only available with pylint >= 0.26',
|
||
183 | {'old_names': [('I0014', 'deprecated-disable-all')]}), |
||
184 | |||
185 | 'E0001': ('%s', |
||
186 | 'syntax-error',
|
||
187 | 'Used when a syntax error is raised for a module.'),
|
||
188 | |||
189 | 'E0011': ('Unrecognized file option %r', |
||
190 | 'unrecognized-inline-option',
|
||
191 | 'Used when an unknown inline option is encountered.'),
|
||
192 | 'E0012': ('Bad option value %r', |
||
193 | 'bad-option-value',
|
||
194 | 'Used when a bad value for an inline option is encountered.'),
|
||
195 | } |
||
196 | |||
197 | |||
198 | if multiprocessing is not None: |
||
199 | class ChildLinter(multiprocessing.Process): |
||
200 | def run(self): |
||
201 | # pylint: disable=no-member, unbalanced-tuple-unpacking
|
||
202 | tasks_queue, results_queue, self._config = self._args |
||
203 | |||
204 | self._config["jobs"] = 1 # Child does not parallelize any further. |
||
205 | self._python3_porting_mode = self._config.pop( |
||
206 | 'python3_porting_mode', None) |
||
207 | self._plugins = self._config.pop('plugins', None) |
||
208 | |||
209 | # Run linter for received files/modules.
|
||
210 | for file_or_module in iter(tasks_queue.get, 'STOP'): |
||
211 | result = self._run_linter(file_or_module[0]) |
||
212 | try:
|
||
213 | results_queue.put(result) |
||
214 | except Exception as ex: |
||
215 | print("internal error with sending report for module %s" %
|
||
216 | file_or_module, file=sys.stderr) |
||
217 | print(ex, file=sys.stderr) |
||
218 | results_queue.put({}) |
||
219 | |||
220 | def _run_linter(self, file_or_module): |
||
221 | linter = PyLinter() |
||
222 | |||
223 | # Register standard checkers.
|
||
224 | linter.load_default_plugins() |
||
225 | # Load command line plugins.
|
||
226 | if self._plugins: |
||
227 | linter.load_plugin_modules(self._plugins)
|
||
228 | |||
229 | linter.load_configuration(**self._config)
|
||
230 | linter.set_reporter(reporters.CollectingReporter()) |
||
231 | |||
232 | # Enable the Python 3 checker mode. This option is
|
||
233 | # passed down from the parent linter up to here, since
|
||
234 | # the Python 3 porting flag belongs to the Run class,
|
||
235 | # instead of the Linter class.
|
||
236 | if self._python3_porting_mode: |
||
237 | linter.python3_porting_mode() |
||
238 | |||
239 | # Run the checks.
|
||
240 | linter.check(file_or_module) |
||
241 | |||
242 | msgs = [_get_new_args(m) for m in linter.reporter.messages] |
||
243 | return (file_or_module, linter.file_state.base_name, linter.current_name,
|
||
244 | msgs, linter.stats, linter.msg_status) |
||
245 | |||
246 | |||
247 | class PyLinter(config.OptionsManagerMixIn, |
||
248 | utils.MessagesHandlerMixIn, |
||
249 | utils.ReportsHandlerMixIn, |
||
250 | checkers.BaseTokenChecker): |
||
251 | """lint Python modules using external checkers.
|
||
252 |
|
||
253 | This is the main checker controlling the other ones and the reports
|
||
254 | generation. It is itself both a raw checker and an astroid checker in order
|
||
255 | to:
|
||
256 | * handle message activation / deactivation at the module level
|
||
257 | * handle some basic but necessary stats'data (number of classes, methods...)
|
||
258 |
|
||
259 | IDE plugins developpers: you may have to call
|
||
260 | `astroid.builder.MANAGER.astroid_cache.clear()` accross run if you want
|
||
261 | to ensure the latest code version is actually checked.
|
||
262 | """
|
||
263 | |||
264 | __implements__ = (interfaces.ITokenChecker, ) |
||
265 | |||
266 | name = 'master'
|
||
267 | priority = 0
|
||
268 | level = 0
|
||
269 | msgs = MSGS |
||
270 | |||
271 | @staticmethod
|
||
272 | def make_options(): |
||
273 | return (('ignore', |
||
274 | {'type' : 'csv', 'metavar' : '<file>[,<file>...]', |
||
275 | 'dest' : 'black_list', 'default' : ('CVS',), |
||
276 | 'help' : 'Add files or directories to the blacklist. ' |
||
277 | 'They should be base names, not paths.'}),
|
||
278 | ('persistent',
|
||
279 | {'default': True, 'type' : 'yn', 'metavar' : '<y_or_n>', |
||
280 | 'level': 1, |
||
281 | 'help' : 'Pickle collected data for later comparisons.'}), |
||
282 | |||
283 | ('load-plugins',
|
||
284 | {'type' : 'csv', 'metavar' : '<modules>', 'default' : (), |
||
285 | 'level': 1, |
||
286 | 'help' : 'List of plugins (as comma separated values of ' |
||
287 | 'python modules names) to load, usually to register '
|
||
288 | 'additional checkers.'}),
|
||
289 | |||
290 | ('output-format',
|
||
291 | {'default': 'text', 'type': 'string', 'metavar' : '<format>', |
||
292 | 'short': 'f', |
||
293 | 'group': 'Reports', |
||
294 | 'help' : 'Set the output format. Available formats are text,' |
||
295 | ' parseable, colorized, msvs (visual studio) and html. You '
|
||
296 | 'can also give a reporter class, eg mypackage.mymodule.'
|
||
297 | 'MyReporterClass.'}),
|
||
298 | |||
299 | ('files-output',
|
||
300 | {'default': 0, 'type' : 'yn', 'metavar' : '<y_or_n>', |
||
301 | 'group': 'Reports', 'level': 1, |
||
302 | 'help' : 'Put messages in a separate file for each module / ' |
||
303 | 'package specified on the command line instead of printing '
|
||
304 | 'them on stdout. Reports (if any) will be written in a file '
|
||
305 | 'name "pylint_global.[txt|html]".'}),
|
||
306 | |||
307 | ('reports',
|
||
308 | {'default': 1, 'type' : 'yn', 'metavar' : '<y_or_n>', |
||
309 | 'short': 'r', |
||
310 | 'group': 'Reports', |
||
311 | 'help' : 'Tells whether to display a full report or only the ' |
||
312 | 'messages'}),
|
||
313 | |||
314 | ('evaluation',
|
||
315 | {'type' : 'string', 'metavar' : '<python_expression>', |
||
316 | 'group': 'Reports', 'level': 1, |
||
317 | 'default': '10.0 - ((float(5 * error + warning + refactor + ' |
||
318 | 'convention) / statement) * 10)',
|
||
319 | 'help' : 'Python expression which should return a note less ' |
||
320 | 'than 10 (10 is the highest note). You have access '
|
||
321 | 'to the variables errors warning, statement which '
|
||
322 | 'respectively contain the number of errors / '
|
||
323 | 'warnings messages and the total number of '
|
||
324 | 'statements analyzed. This is used by the global '
|
||
325 | 'evaluation report (RP0004).'}),
|
||
326 | |||
327 | ('comment', utils.deprecated_option(opt_type='yn')), |
||
328 | |||
329 | ('confidence',
|
||
330 | {'type' : 'multiple_choice', 'metavar': '<levels>', |
||
331 | 'default': '', |
||
332 | 'choices': [c.name for c in interfaces.CONFIDENCE_LEVELS], |
||
333 | 'group': 'Messages control', |
||
334 | 'help' : 'Only show warnings with the listed confidence levels.' |
||
335 | ' Leave empty to show all. Valid levels: %s' % (
|
||
336 | ', '.join(c.name for c in interfaces.CONFIDENCE_LEVELS),)}), |
||
337 | |||
338 | ('enable',
|
||
339 | {'type' : 'csv', 'metavar': '<msg ids>', |
||
340 | 'short': 'e', |
||
341 | 'group': 'Messages control', |
||
342 | 'help' : 'Enable the message, report, category or checker with the ' |
||
343 | 'given id(s). You can either give multiple identifier '
|
||
344 | 'separated by comma (,) or put this option multiple time. '
|
||
345 | 'See also the "--disable" option for examples. '}),
|
||
346 | |||
347 | ('disable',
|
||
348 | {'type' : 'csv', 'metavar': '<msg ids>', |
||
349 | 'short': 'd', |
||
350 | 'group': 'Messages control', |
||
351 | 'help' : 'Disable the message, report, category or checker ' |
||
352 | 'with the given id(s). You can either give multiple identifiers'
|
||
353 | ' separated by comma (,) or put this option multiple times '
|
||
354 | '(only on the command line, not in the configuration file '
|
||
355 | 'where it should appear only once).'
|
||
356 | 'You can also use "--disable=all" to disable everything first '
|
||
357 | 'and then reenable specific checks. For example, if you want '
|
||
358 | 'to run only the similarities checker, you can use '
|
||
359 | '"--disable=all --enable=similarities". '
|
||
360 | 'If you want to run only the classes checker, but have no '
|
||
361 | 'Warning level messages displayed, use'
|
||
362 | '"--disable=all --enable=classes --disable=W"'}),
|
||
363 | |||
364 | ('msg-template',
|
||
365 | {'type' : 'string', 'metavar': '<template>', |
||
366 | 'group': 'Reports', |
||
367 | 'help' : ('Template used to display messages. ' |
||
368 | 'This is a python new-style format string '
|
||
369 | 'used to format the message information. '
|
||
370 | 'See doc for all details')
|
||
371 | }), |
||
372 | |||
373 | ('include-ids', utils.deprecated_option('i', 'yn', INCLUDE_IDS_HELP)), |
||
374 | ('symbols', utils.deprecated_option('s', 'yn', SYMBOLS_HELP)), |
||
375 | |||
376 | ('jobs',
|
||
377 | {'type' : 'int', 'metavar': '<n-processes>', |
||
378 | 'short': 'j', |
||
379 | 'default': 1, |
||
380 | 'help' : '''Use multiple processes to speed up Pylint.''', |
||
381 | }), |
||
382 | |||
383 | ('unsafe-load-any-extension',
|
||
384 | {'type': 'yn', 'metavar': '<yn>', 'default': False, 'hide': True, |
||
385 | 'help': ('Allow loading of arbitrary C extensions. Extensions' |
||
386 | ' are imported into the active Python interpreter and'
|
||
387 | ' may run arbitrary code.')}),
|
||
388 | |||
389 | ('extension-pkg-whitelist',
|
||
390 | {'type': 'csv', 'metavar': '<pkg[,pkg]>', 'default': [], |
||
391 | 'help': ('A comma-separated list of package or module names' |
||
392 | ' from where C extensions may be loaded. Extensions are'
|
||
393 | ' loading into the active Python interpreter and may run'
|
||
394 | ' arbitrary code')}
|
||
395 | ), |
||
396 | |||
397 | ('optimize-ast',
|
||
398 | {'type': 'yn', 'metavar': '<yn>', 'default': False, |
||
399 | 'help': ('Allow optimization of some AST trees. This will ' |
||
400 | 'activate a peephole AST optimizer, which will '
|
||
401 | 'apply various small optimizations. For instance, '
|
||
402 | 'it can be used to obtain the result of joining '
|
||
403 | 'multiple strings with the addition operator. '
|
||
404 | 'Joining a lot of strings can lead to a maximum '
|
||
405 | 'recursion error in Pylint and this flag can prevent '
|
||
406 | 'that. It has one side effect, the resulting AST '
|
||
407 | 'will be different than the one from reality.')}
|
||
408 | ), |
||
409 | ) |
||
410 | |||
411 | option_groups = ( |
||
412 | ('Messages control', 'Options controling analysis messages'), |
||
413 | ('Reports', 'Options related to output formating and reporting'), |
||
414 | ) |
||
415 | |||
416 | def __init__(self, options=(), reporter=None, option_groups=(), |
||
417 | pylintrc=None):
|
||
418 | # some stuff has to be done before ancestors initialization...
|
||
419 | #
|
||
420 | # messages store / checkers / reporter / astroid manager
|
||
421 | self.msgs_store = utils.MessagesStore()
|
||
422 | self.reporter = None |
||
423 | self._reporter_name = None |
||
424 | self._reporters = {}
|
||
425 | self._checkers = collections.defaultdict(list) |
||
426 | self._pragma_lineno = {}
|
||
427 | self._ignore_file = False |
||
428 | # visit variables
|
||
429 | self.file_state = utils.FileState()
|
||
430 | self.current_name = None |
||
431 | self.current_file = None |
||
432 | self.stats = None |
||
433 | # init options
|
||
434 | self._external_opts = options
|
||
435 | self.options = options + PyLinter.make_options()
|
||
436 | self.option_groups = option_groups + PyLinter.option_groups
|
||
437 | self._options_methods = {
|
||
438 | 'enable': self.enable, |
||
439 | 'disable': self.disable} |
||
440 | self._bw_options_methods = {'disable-msg': self.disable, |
||
441 | 'enable-msg': self.enable} |
||
442 | full_version = '%%prog %s, \nastroid %s\nPython %s' % (
|
||
443 | version, astroid_version, sys.version) |
||
444 | utils.MessagesHandlerMixIn.__init__(self)
|
||
445 | utils.ReportsHandlerMixIn.__init__(self)
|
||
446 | super(PyLinter, self).__init__( |
||
447 | usage=__doc__, |
||
448 | version=full_version, |
||
449 | config_file=pylintrc or config.PYLINTRC)
|
||
450 | checkers.BaseTokenChecker.__init__(self)
|
||
451 | # provided reports
|
||
452 | self.reports = (('RP0001', 'Messages by category', |
||
453 | report_total_messages_stats), |
||
454 | ('RP0002', '% errors / warnings by module', |
||
455 | report_messages_by_module_stats), |
||
456 | ('RP0003', 'Messages', |
||
457 | report_messages_stats), |
||
458 | ('RP0004', 'Global evaluation', |
||
459 | self.report_evaluation),
|
||
460 | ) |
||
461 | self.register_checker(self) |
||
462 | self._dynamic_plugins = set() |
||
463 | self._python3_porting_mode = False |
||
464 | self._error_mode = False |
||
465 | self.load_provider_defaults()
|
||
466 | if reporter:
|
||
467 | self.set_reporter(reporter)
|
||
468 | |||
469 | def load_default_plugins(self): |
||
470 | checkers.initialize(self)
|
||
471 | reporters.initialize(self)
|
||
472 | # Make sure to load the default reporter, because
|
||
473 | # the option has been set before the plugins had been loaded.
|
||
474 | if not self.reporter: |
||
475 | self._load_reporter()
|
||
476 | |||
477 | def load_plugin_modules(self, modnames): |
||
478 | """take a list of module names which are pylint plugins and load
|
||
479 | and register them
|
||
480 | """
|
||
481 | for modname in modnames: |
||
482 | if modname in self._dynamic_plugins: |
||
483 | continue
|
||
484 | self._dynamic_plugins.add(modname)
|
||
485 | module = modutils.load_module_from_name(modname) |
||
486 | module.register(self)
|
||
487 | |||
488 | def _load_reporter(self): |
||
489 | name = self._reporter_name.lower()
|
||
490 | if name in self._reporters: |
||
491 | self.set_reporter(self._reporters[name]()) |
||
492 | else:
|
||
493 | qname = self._reporter_name
|
||
494 | module = modutils.load_module_from_name( |
||
495 | modutils.get_module_part(qname)) |
||
496 | class_name = qname.split('.')[-1] |
||
497 | reporter_class = getattr(module, class_name)
|
||
498 | self.set_reporter(reporter_class())
|
||
499 | |||
500 | def set_reporter(self, reporter): |
||
501 | """set the reporter used to display messages and reports"""
|
||
502 | self.reporter = reporter
|
||
503 | reporter.linter = self
|
||
504 | |||
505 | def set_option(self, optname, value, action=None, optdict=None): |
||
506 | """overridden from config.OptionsProviderMixin to handle some
|
||
507 | special options
|
||
508 | """
|
||
509 | if optname in self._options_methods or \ |
||
510 | optname in self._bw_options_methods: |
||
511 | if value:
|
||
512 | try:
|
||
513 | meth = self._options_methods[optname]
|
||
514 | except KeyError: |
||
515 | meth = self._bw_options_methods[optname]
|
||
516 | warnings.warn('%s is deprecated, replace it by %s' % (optname,
|
||
517 | optname.split('-')[0]), |
||
518 | DeprecationWarning)
|
||
519 | value = utils._check_csv(value) |
||
520 | if isinstance(value, (list, tuple)): |
||
521 | for _id in value: |
||
522 | meth(_id, ignore_unknown=True)
|
||
523 | else:
|
||
524 | meth(value) |
||
525 | return # no need to call set_option, disable/enable methods do it |
||
526 | elif optname == 'output-format': |
||
527 | self._reporter_name = value
|
||
528 | # If the reporters are already available, load
|
||
529 | # the reporter class.
|
||
530 | if self._reporters: |
||
531 | self._load_reporter()
|
||
532 | |||
533 | try:
|
||
534 | checkers.BaseTokenChecker.set_option(self, optname,
|
||
535 | value, action, optdict) |
||
536 | except config.UnsupportedAction:
|
||
537 | print('option %s can\'t be read from config file' % \
|
||
538 | optname, file=sys.stderr) |
||
539 | |||
540 | def register_reporter(self, reporter_class): |
||
541 | self._reporters[reporter_class.name] = reporter_class
|
||
542 | |||
543 | def report_order(self): |
||
544 | reports = sorted(self._reports, key=lambda x: getattr(x, 'name', '')) |
||
545 | try:
|
||
546 | # Remove the current reporter and add it
|
||
547 | # at the end of the list.
|
||
548 | reports.pop(reports.index(self))
|
||
549 | except ValueError: |
||
550 | pass
|
||
551 | else:
|
||
552 | reports.append(self)
|
||
553 | return reports
|
||
554 | |||
555 | # checkers manipulation methods ############################################
|
||
556 | |||
557 | def register_checker(self, checker): |
||
558 | """register a new checker
|
||
559 |
|
||
560 | checker is an object implementing IRawChecker or / and IAstroidChecker
|
||
561 | """
|
||
562 | assert checker.priority <= 0, 'checker priority can\'t be >= 0' |
||
563 | self._checkers[checker.name].append(checker)
|
||
564 | for r_id, r_title, r_cb in checker.reports: |
||
565 | self.register_report(r_id, r_title, r_cb, checker)
|
||
566 | self.register_options_provider(checker)
|
||
567 | if hasattr(checker, 'msgs'): |
||
568 | self.msgs_store.register_messages(checker)
|
||
569 | checker.load_defaults() |
||
570 | |||
571 | # Register the checker, but disable all of its messages.
|
||
572 | # TODO(cpopa): we should have a better API for this.
|
||
573 | if not getattr(checker, 'enabled', True): |
||
574 | self.disable(checker.name)
|
||
575 | |||
576 | def disable_noerror_messages(self): |
||
577 | for msgcat, msgids in six.iteritems(self.msgs_store._msgs_by_category): |
||
578 | if msgcat == 'E': |
||
579 | for msgid in msgids: |
||
580 | self.enable(msgid)
|
||
581 | else:
|
||
582 | for msgid in msgids: |
||
583 | self.disable(msgid)
|
||
584 | |||
585 | def disable_reporters(self): |
||
586 | """disable all reporters"""
|
||
587 | for _reporters in six.itervalues(self._reports): |
||
588 | for report_id, _, _ in _reporters: |
||
589 | self.disable_report(report_id)
|
||
590 | |||
591 | def error_mode(self): |
||
592 | """error mode: enable only errors; no reports, no persistent"""
|
||
593 | self._error_mode = True |
||
594 | self.disable_noerror_messages()
|
||
595 | self.disable('miscellaneous') |
||
596 | if self._python3_porting_mode: |
||
597 | self.disable('all') |
||
598 | for msg_id in self._checker_messages('python3'): |
||
599 | if msg_id.startswith('E'): |
||
600 | self.enable(msg_id)
|
||
601 | else:
|
||
602 | self.disable('python3') |
||
603 | self.set_option('reports', False) |
||
604 | self.set_option('persistent', False) |
||
605 | |||
606 | def python3_porting_mode(self): |
||
607 | """Disable all other checkers and enable Python 3 warnings."""
|
||
608 | self.disable('all') |
||
609 | self.enable('python3') |
||
610 | if self._error_mode: |
||
611 | # The error mode was activated, using the -E flag.
|
||
612 | # So we'll need to enable only the errors from the
|
||
613 | # Python 3 porting checker.
|
||
614 | for msg_id in self._checker_messages('python3'): |
||
615 | if msg_id.startswith('E'): |
||
616 | self.enable(msg_id)
|
||
617 | else:
|
||
618 | self.disable(msg_id)
|
||
619 | self._python3_porting_mode = True |
||
620 | |||
621 | # block level option handling #############################################
|
||
622 | #
|
||
623 | # see func_block_disable_msg.py test case for expected behaviour
|
||
624 | |||
625 | def process_tokens(self, tokens): |
||
626 | """process tokens from the current module to search for module/block
|
||
627 | level options
|
||
628 | """
|
||
629 | control_pragmas = {'disable', 'enable'} |
||
630 | for (tok_type, content, start, _, _) in tokens: |
||
631 | if tok_type != tokenize.COMMENT:
|
||
632 | continue
|
||
633 | match = utils.OPTION_RGX.search(content) |
||
634 | if match is None: |
||
635 | continue
|
||
636 | if match.group(1).strip() == "disable-all" or \ |
||
637 | match.group(1).strip() == 'skip-file': |
||
638 | if match.group(1).strip() == "disable-all": |
||
639 | self.add_message('deprecated-pragma', line=start[0], |
||
640 | args=('disable-all', 'skip-file')) |
||
641 | self.add_message('file-ignored', line=start[0]) |
||
642 | self._ignore_file = True |
||
643 | return
|
||
644 | try:
|
||
645 | opt, value = match.group(1).split('=', 1) |
||
646 | except ValueError: |
||
647 | self.add_message('bad-inline-option', args=match.group(1).strip(), |
||
648 | line=start[0])
|
||
649 | continue
|
||
650 | opt = opt.strip() |
||
651 | if opt in self._options_methods or opt in self._bw_options_methods: |
||
652 | try:
|
||
653 | meth = self._options_methods[opt]
|
||
654 | except KeyError: |
||
655 | meth = self._bw_options_methods[opt]
|
||
656 | # found a "(dis|en)able-msg" pragma deprecated suppresssion
|
||
657 | self.add_message('deprecated-pragma', line=start[0], |
||
658 | args=(opt, opt.replace('-msg', ''))) |
||
659 | for msgid in utils._splitstrip(value): |
||
660 | # Add the line where a control pragma was encountered.
|
||
661 | if opt in control_pragmas: |
||
662 | self._pragma_lineno[msgid] = start[0] |
||
663 | |||
664 | try:
|
||
665 | if (opt, msgid) == ('disable', 'all'): |
||
666 | self.add_message('deprecated-pragma', line=start[0], |
||
667 | args=('disable=all', 'skip-file')) |
||
668 | self.add_message('file-ignored', line=start[0]) |
||
669 | self._ignore_file = True |
||
670 | return
|
||
671 | meth(msgid, 'module', start[0]) |
||
672 | except utils.UnknownMessage:
|
||
673 | self.add_message('bad-option-value', args=msgid, line=start[0]) |
||
674 | else:
|
||
675 | self.add_message('unrecognized-inline-option', args=opt, line=start[0]) |
||
676 | |||
677 | |||
678 | # code checking methods ###################################################
|
||
679 | |||
680 | def get_checkers(self): |
||
681 | """return all available checkers as a list"""
|
||
682 | return [self] + [c for _checkers in six.itervalues(self._checkers) |
||
683 | for c in _checkers if c is not self] |
||
684 | |||
685 | def prepare_checkers(self): |
||
686 | """return checkers needed for activated messages and reports"""
|
||
687 | if not self.config.reports: |
||
688 | self.disable_reporters()
|
||
689 | # get needed checkers
|
||
690 | neededcheckers = [self]
|
||
691 | for checker in self.get_checkers()[1:]: |
||
692 | # fatal errors should not trigger enable / disabling a checker
|
||
693 | messages = set(msg for msg in checker.msgs |
||
694 | if msg[0] != 'F' and self.is_message_enabled(msg)) |
||
695 | if (messages or |
||
696 | any(self.report_is_enabled(r[0]) for r in checker.reports)): |
||
697 | neededcheckers.append(checker) |
||
698 | # Sort checkers by priority
|
||
699 | neededcheckers = sorted(neededcheckers,
|
||
700 | key=operator.attrgetter('priority'),
|
||
701 | reverse=True)
|
||
702 | return neededcheckers
|
||
703 | |||
704 | def should_analyze_file(self, modname, path): # pylint: disable=unused-argument, no-self-use |
||
705 | """Returns whether or not a module should be checked.
|
||
706 |
|
||
707 | This implementation returns True for all python source file, indicating
|
||
708 | that all files should be linted.
|
||
709 |
|
||
710 | Subclasses may override this method to indicate that modules satisfying
|
||
711 | certain conditions should not be linted.
|
||
712 |
|
||
713 | :param str modname: The name of the module to be checked.
|
||
714 | :param str path: The full path to the source code of the module.
|
||
715 | :returns: True if the module should be checked.
|
||
716 | :rtype: bool
|
||
717 | """
|
||
718 | return path.endswith('.py') |
||
719 | |||
720 | def check(self, files_or_modules): |
||
721 | """main checking entry: check a list of files or modules from their
|
||
722 | name.
|
||
723 | """
|
||
724 | # initialize msgs_state now that all messages have been registered into
|
||
725 | # the store
|
||
726 | for msg in self.msgs_store.messages: |
||
727 | if not msg.may_be_emitted(): |
||
728 | self._msgs_state[msg.msgid] = False |
||
729 | |||
730 | if not isinstance(files_or_modules, (list, tuple)): |
||
731 | files_or_modules = (files_or_modules,) |
||
732 | |||
733 | if self.config.jobs == 1: |
||
734 | self._do_check(files_or_modules)
|
||
735 | else:
|
||
736 | with _patch_sysmodules():
|
||
737 | self._parallel_check(files_or_modules)
|
||
738 | |||
739 | def _get_jobs_config(self): |
||
740 | child_config = {} |
||
741 | filter_options = {'symbols', 'include-ids', 'long-help'} |
||
742 | filter_options.update((opt_name for opt_name, _ in self._external_opts)) |
||
743 | for opt_providers in six.itervalues(self._all_options): |
||
744 | for optname, optdict, val in opt_providers.options_and_values(): |
||
745 | if optdict.get('deprecated'): |
||
746 | continue
|
||
747 | |||
748 | if optname not in filter_options: |
||
749 | child_config[optname] = utils._format_option_value( |
||
750 | optdict, val) |
||
751 | child_config['python3_porting_mode'] = self._python3_porting_mode |
||
752 | child_config['plugins'] = self._dynamic_plugins |
||
753 | return child_config
|
||
754 | |||
755 | def _parallel_task(self, files_or_modules): |
||
756 | # Prepare configuration for child linters.
|
||
757 | child_config = self._get_jobs_config()
|
||
758 | |||
759 | children = [] |
||
760 | manager = multiprocessing.Manager() |
||
761 | tasks_queue = manager.Queue() |
||
762 | results_queue = manager.Queue() |
||
763 | |||
764 | for _ in range(self.config.jobs): |
||
765 | child_linter = ChildLinter(args=(tasks_queue, results_queue, |
||
766 | child_config)) |
||
767 | child_linter.start() |
||
768 | children.append(child_linter) |
||
769 | |||
770 | # Send files to child linters.
|
||
771 | expanded_files = self.expand_files(files_or_modules)
|
||
772 | for files_or_module in expanded_files: |
||
773 | path = files_or_module['path']
|
||
774 | tasks_queue.put([path]) |
||
775 | |||
776 | # collect results from child linters
|
||
777 | failed = False
|
||
778 | for _ in expanded_files: |
||
779 | try:
|
||
780 | result = results_queue.get() |
||
781 | except Exception as ex: |
||
782 | print("internal error while receiving results from child linter",
|
||
783 | file=sys.stderr) |
||
784 | print(ex, file=sys.stderr) |
||
785 | failed = True
|
||
786 | break
|
||
787 | yield result
|
||
788 | |||
789 | # Stop child linters and wait for their completion.
|
||
790 | for _ in range(self.config.jobs): |
||
791 | tasks_queue.put('STOP')
|
||
792 | for child in children: |
||
793 | child.join() |
||
794 | |||
795 | if failed:
|
||
796 | print("Error occured, stopping the linter.", file=sys.stderr)
|
||
797 | sys.exit(32)
|
||
798 | |||
799 | def _parallel_check(self, files_or_modules): |
||
800 | # Reset stats.
|
||
801 | self.open()
|
||
802 | |||
803 | all_stats = [] |
||
804 | module = None
|
||
805 | for result in self._parallel_task(files_or_modules): |
||
806 | ( |
||
807 | _, |
||
808 | self.file_state.base_name,
|
||
809 | module, |
||
810 | messages, |
||
811 | stats, |
||
812 | msg_status |
||
813 | ) = result |
||
814 | |||
815 | for msg in messages: |
||
816 | msg = utils.Message(*msg) |
||
817 | self.set_current_module(module)
|
||
818 | self.reporter.handle_message(msg)
|
||
819 | |||
820 | all_stats.append(stats) |
||
821 | self.msg_status |= msg_status
|
||
822 | |||
823 | self.stats = _merge_stats(all_stats)
|
||
824 | self.current_name = module
|
||
825 | |||
826 | # Insert stats data to local checkers.
|
||
827 | for checker in self.get_checkers(): |
||
828 | if checker is not self: |
||
829 | checker.stats = self.stats
|
||
830 | |||
831 | def _do_check(self, files_or_modules): |
||
832 | walker = utils.PyLintASTWalker(self)
|
||
833 | _checkers = self.prepare_checkers()
|
||
834 | tokencheckers = [c for c in _checkers |
||
835 | if interfaces.implements(c, interfaces.ITokenChecker)
|
||
836 | and c is not self] |
||
837 | rawcheckers = [c for c in _checkers |
||
838 | if interfaces.implements(c, interfaces.IRawChecker)]
|
||
839 | # notify global begin
|
||
840 | for checker in _checkers: |
||
841 | checker.open() |
||
842 | if interfaces.implements(checker, interfaces.IAstroidChecker):
|
||
843 | walker.add_checker(checker) |
||
844 | # build ast and check modules or packages
|
||
845 | for descr in self.expand_files(files_or_modules): |
||
846 | modname, filepath = descr['name'], descr['path'] |
||
847 | if not descr['isarg'] and not self.should_analyze_file(modname, filepath): |
||
848 | continue
|
||
849 | if self.config.files_output: |
||
850 | reportfile = 'pylint_%s.%s' % (modname, self.reporter.extension) |
||
851 | self.reporter.set_output(open(reportfile, 'w')) |
||
852 | self.set_current_module(modname, filepath)
|
||
853 | # get the module representation
|
||
854 | ast_node = self.get_ast(filepath, modname)
|
||
855 | if ast_node is None: |
||
856 | continue
|
||
857 | # XXX to be correct we need to keep module_msgs_state for every
|
||
858 | # analyzed module (the problem stands with localized messages which
|
||
859 | # are only detected in the .close step)
|
||
860 | self.file_state = utils.FileState(descr['basename']) |
||
861 | self._ignore_file = False |
||
862 | # fix the current file (if the source file was not available or
|
||
863 | # if it's actually a c extension)
|
||
864 | self.current_file = ast_node.file # pylint: disable=maybe-no-member |
||
865 | self.check_astroid_module(ast_node, walker, rawcheckers, tokencheckers)
|
||
866 | # warn about spurious inline messages handling
|
||
867 | spurious_messages = self.file_state.iter_spurious_suppression_messages(self.msgs_store) |
||
868 | for msgid, line, args in spurious_messages: |
||
869 | self.add_message(msgid, line, None, args) |
||
870 | # notify global end
|
||
871 | self.stats['statement'] = walker.nbstatements |
||
872 | for checker in reversed(_checkers): |
||
873 | checker.close() |
||
874 | |||
875 | def expand_files(self, modules): |
||
876 | """get modules and errors from a list of modules and handle errors
|
||
877 | """
|
||
878 | result, errors = utils.expand_modules(modules, self.config.black_list)
|
||
879 | for error in errors: |
||
880 | message = modname = error["mod"]
|
||
881 | key = error["key"]
|
||
882 | self.set_current_module(modname)
|
||
883 | if key == "fatal": |
||
884 | message = str(error["ex"]).replace(os.getcwd() + os.sep, '') |
||
885 | self.add_message(key, args=message)
|
||
886 | return result
|
||
887 | |||
888 | def set_current_module(self, modname, filepath=None): |
||
889 | """set the name of the currently analyzed module and
|
||
890 | init statistics for it
|
||
891 | """
|
||
892 | if not modname and filepath is None: |
||
893 | return
|
||
894 | self.reporter.on_set_current_module(modname, filepath)
|
||
895 | self.current_name = modname
|
||
896 | self.current_file = filepath or modname |
||
897 | self.stats['by_module'][modname] = {} |
||
898 | self.stats['by_module'][modname]['statement'] = 0 |
||
899 | for msg_cat in six.itervalues(utils.MSG_TYPES): |
||
900 | self.stats['by_module'][modname][msg_cat] = 0 |
||
901 | |||
902 | def get_ast(self, filepath, modname): |
||
903 | """return a ast(roid) representation for a module"""
|
||
904 | try:
|
||
905 | return MANAGER.ast_from_file(filepath, modname, source=True) |
||
906 | except astroid.AstroidBuildingException as ex: |
||
907 | if isinstance(ex.args[0], SyntaxError): |
||
908 | ex = ex.args[0]
|
||
909 | self.add_message('syntax-error', |
||
910 | line=ex.lineno or 0, |
||
911 | args=ex.msg) |
||
912 | else:
|
||
913 | self.add_message('parse-error', args=ex) |
||
914 | except Exception as ex: # pylint: disable=broad-except |
||
915 | import traceback |
||
916 | traceback.print_exc() |
||
917 | self.add_message('astroid-error', args=(ex.__class__, ex)) |
||
918 | |||
919 | def check_astroid_module(self, ast_node, walker, |
||
920 | rawcheckers, tokencheckers): |
||
921 | """Check a module from its astroid representation."""
|
||
922 | try:
|
||
923 | tokens = utils.tokenize_module(ast_node) |
||
924 | except tokenize.TokenError as ex: |
||
925 | self.add_message('syntax-error', line=ex.args[1][0], args=ex.args[0]) |
||
926 | return
|
||
927 | |||
928 | if not ast_node.pure_python: |
||
929 | self.add_message('raw-checker-failed', args=ast_node.name) |
||
930 | else:
|
||
931 | #assert astroid.file.endswith('.py')
|
||
932 | # invoke ITokenChecker interface on self to fetch module/block
|
||
933 | # level options
|
||
934 | self.process_tokens(tokens)
|
||
935 | if self._ignore_file: |
||
936 | return False |
||
937 | # walk ast to collect line numbers
|
||
938 | self.file_state.collect_block_lines(self.msgs_store, ast_node) |
||
939 | # run raw and tokens checkers
|
||
940 | for checker in rawcheckers: |
||
941 | checker.process_module(ast_node) |
||
942 | for checker in tokencheckers: |
||
943 | checker.process_tokens(tokens) |
||
944 | # generate events to astroid checkers
|
||
945 | walker.walk(ast_node) |
||
946 | return True |
||
947 | |||
948 | # IAstroidChecker interface #################################################
|
||
949 | |||
950 | def open(self): |
||
951 | """initialize counters"""
|
||
952 | self.stats = {'by_module' : {}, |
||
953 | 'by_msg' : {},
|
||
954 | } |
||
955 | MANAGER.optimize_ast = self.config.optimize_ast
|
||
956 | MANAGER.always_load_extensions = self.config.unsafe_load_any_extension
|
||
957 | MANAGER.extension_package_whitelist.update( |
||
958 | self.config.extension_pkg_whitelist)
|
||
959 | for msg_cat in six.itervalues(utils.MSG_TYPES): |
||
960 | self.stats[msg_cat] = 0 |
||
961 | |||
962 | def generate_reports(self): |
||
963 | """close the whole package /module, it's time to make reports !
|
||
964 |
|
||
965 | if persistent run, pickle results for later comparison
|
||
966 | """
|
||
967 | # Display whatever messages are left on the reporter.
|
||
968 | self.reporter.display_messages(report_nodes.Section())
|
||
969 | |||
970 | if self.file_state.base_name is not None: |
||
971 | # load previous results if any
|
||
972 | previous_stats = config.load_results(self.file_state.base_name)
|
||
973 | # XXX code below needs refactoring to be more reporter agnostic
|
||
974 | self.reporter.on_close(self.stats, previous_stats) |
||
975 | if self.config.reports: |
||
976 | sect = self.make_reports(self.stats, previous_stats) |
||
977 | if self.config.files_output: |
||
978 | filename = 'pylint_global.' + self.reporter.extension |
||
979 | self.reporter.set_output(open(filename, 'w')) |
||
980 | else:
|
||
981 | sect = report_nodes.Section() |
||
982 | if self.config.reports: |
||
983 | self.reporter.display_reports(sect)
|
||
984 | # save results if persistent run
|
||
985 | if self.config.persistent: |
||
986 | config.save_results(self.stats, self.file_state.base_name) |
||
987 | else:
|
||
988 | self.reporter.on_close(self.stats, {}) |
||
989 | |||
990 | # specific reports ########################################################
|
||
991 | |||
992 | def report_evaluation(self, sect, stats, previous_stats): |
||
993 | """make the global evaluation report"""
|
||
994 | # check with at least check 1 statements (usually 0 when there is a
|
||
995 | # syntax error preventing pylint from further processing)
|
||
996 | if stats['statement'] == 0: |
||
997 | raise utils.EmptyReport()
|
||
998 | # get a global note for the code
|
||
999 | evaluation = self.config.evaluation
|
||
1000 | try:
|
||
1001 | note = eval(evaluation, {}, self.stats) # pylint: disable=eval-used |
||
1002 | except Exception as ex: # pylint: disable=broad-except |
||
1003 | msg = 'An exception occurred while rating: %s' % ex
|
||
1004 | else:
|
||
1005 | stats['global_note'] = note
|
||
1006 | msg = 'Your code has been rated at %.2f/10' % note
|
||
1007 | pnote = previous_stats.get('global_note')
|
||
1008 | if pnote is not None: |
||
1009 | msg += ' (previous run: %.2f/10, %+.2f)' % (pnote, note - pnote)
|
||
1010 | sect.append(report_nodes.Text(msg)) |
||
1011 | |||
1012 | # some reporting functions ####################################################
|
||
1013 | |||
1014 | def report_total_messages_stats(sect, stats, previous_stats): |
||
1015 | """make total errors / warnings report"""
|
||
1016 | lines = ['type', 'number', 'previous', 'difference'] |
||
1017 | lines += checkers.table_lines_from_stats(stats, previous_stats, |
||
1018 | ('convention', 'refactor', |
||
1019 | 'warning', 'error')) |
||
1020 | sect.append(report_nodes.Table(children=lines, cols=4, rheaders=1)) |
||
1021 | |||
1022 | def report_messages_stats(sect, stats, _): |
||
1023 | """make messages type report"""
|
||
1024 | if not stats['by_msg']: |
||
1025 | # don't print this report when we didn't detected any errors
|
||
1026 | raise utils.EmptyReport()
|
||
1027 | in_order = sorted([(value, msg_id)
|
||
1028 | for msg_id, value in six.iteritems(stats['by_msg']) |
||
1029 | if not msg_id.startswith('I')]) |
||
1030 | in_order.reverse() |
||
1031 | lines = ('message id', 'occurrences') |
||
1032 | for value, msg_id in in_order: |
||
1033 | lines += (msg_id, str(value))
|
||
1034 | sect.append(report_nodes.Table(children=lines, cols=2, rheaders=1)) |
||
1035 | |||
1036 | def report_messages_by_module_stats(sect, stats, _): |
||
1037 | """make errors / warnings by modules report"""
|
||
1038 | if len(stats['by_module']) == 1: |
||
1039 | # don't print this report when we are analysing a single module
|
||
1040 | raise utils.EmptyReport()
|
||
1041 | by_mod = collections.defaultdict(dict)
|
||
1042 | for m_type in ('fatal', 'error', 'warning', 'refactor', 'convention'): |
||
1043 | total = stats[m_type] |
||
1044 | for module in six.iterkeys(stats['by_module']): |
||
1045 | mod_total = stats['by_module'][module][m_type]
|
||
1046 | if total == 0: |
||
1047 | percent = 0
|
||
1048 | else:
|
||
1049 | percent = float((mod_total)*100) / total |
||
1050 | by_mod[module][m_type] = percent |
||
1051 | sorted_result = [] |
||
1052 | for module, mod_info in six.iteritems(by_mod): |
||
1053 | sorted_result.append((mod_info['error'],
|
||
1054 | mod_info['warning'],
|
||
1055 | mod_info['refactor'],
|
||
1056 | mod_info['convention'],
|
||
1057 | module)) |
||
1058 | sorted_result.sort() |
||
1059 | sorted_result.reverse() |
||
1060 | lines = ['module', 'error', 'warning', 'refactor', 'convention'] |
||
1061 | for line in sorted_result: |
||
1062 | # Don't report clean modules.
|
||
1063 | if all(entry == 0 for entry in line[:-1]): |
||
1064 | continue
|
||
1065 | lines.append(line[-1])
|
||
1066 | for val in line[:-1]: |
||
1067 | lines.append('%.2f' % val)
|
||
1068 | if len(lines) == 5: |
||
1069 | raise utils.EmptyReport()
|
||
1070 | sect.append(report_nodes.Table(children=lines, cols=5, rheaders=1)) |
||
1071 | |||
1072 | |||
1073 | # utilities ###################################################################
|
||
1074 | |||
1075 | |||
1076 | class ArgumentPreprocessingError(Exception): |
||
1077 | """Raised if an error occurs during argument preprocessing."""
|
||
1078 | |||
1079 | |||
1080 | def preprocess_options(args, search_for): |
||
1081 | """look for some options (keys of <search_for>) which have to be processed
|
||
1082 | before others
|
||
1083 |
|
||
1084 | values of <search_for> are callback functions to call when the option is
|
||
1085 | found
|
||
1086 | """
|
||
1087 | i = 0
|
||
1088 | while i < len(args): |
||
1089 | arg = args[i] |
||
1090 | if arg.startswith('--'): |
||
1091 | try:
|
||
1092 | option, val = arg[2:].split('=', 1) |
||
1093 | except ValueError: |
||
1094 | option, val = arg[2:], None |
||
1095 | try:
|
||
1096 | cb, takearg = search_for[option] |
||
1097 | except KeyError: |
||
1098 | i += 1
|
||
1099 | else:
|
||
1100 | del args[i]
|
||
1101 | if takearg and val is None: |
||
1102 | if i >= len(args) or args[i].startswith('-'): |
||
1103 | msg = 'Option %s expects a value' % option
|
||
1104 | raise ArgumentPreprocessingError(msg)
|
||
1105 | val = args[i] |
||
1106 | del args[i]
|
||
1107 | elif not takearg and val is not None: |
||
1108 | msg = "Option %s doesn't expects a value" % option
|
||
1109 | raise ArgumentPreprocessingError(msg)
|
||
1110 | cb(option, val) |
||
1111 | else:
|
||
1112 | i += 1
|
||
1113 | |||
1114 | |||
1115 | @contextlib.contextmanager
|
||
1116 | def fix_import_path(args): |
||
1117 | """Prepare sys.path for running the linter checks.
|
||
1118 |
|
||
1119 | Within this context, each of the given arguments is importable.
|
||
1120 | Paths are added to sys.path in corresponding order to the arguments.
|
||
1121 | We avoid adding duplicate directories to sys.path.
|
||
1122 | `sys.path` is reset to its original value upon exitign this context.
|
||
1123 | """
|
||
1124 | orig = list(sys.path)
|
||
1125 | changes = [] |
||
1126 | for arg in args: |
||
1127 | path = _get_python_path(arg) |
||
1128 | if path in changes: |
||
1129 | continue
|
||
1130 | else:
|
||
1131 | changes.append(path) |
||
1132 | sys.path[:] = changes + sys.path |
||
1133 | try:
|
||
1134 | yield
|
||
1135 | finally:
|
||
1136 | sys.path[:] = orig |
||
1137 | |||
1138 | |||
1139 | class Run(object): |
||
1140 | """helper class to use as main for pylint :
|
||
1141 |
|
||
1142 | run(*sys.argv[1:])
|
||
1143 | """
|
||
1144 | LinterClass = PyLinter |
||
1145 | option_groups = ( |
||
1146 | ('Commands', 'Options which are actually commands. Options in this \ |
||
1147 | group are mutually exclusive.'),
|
||
1148 | ) |
||
1149 | |||
1150 | def __init__(self, args, reporter=None, exit=True): |
||
1151 | self._rcfile = None |
||
1152 | self._plugins = []
|
||
1153 | try:
|
||
1154 | preprocess_options(args, { |
||
1155 | # option: (callback, takearg)
|
||
1156 | 'init-hook': (cb_init_hook, True), |
||
1157 | 'rcfile': (self.cb_set_rcfile, True), |
||
1158 | 'load-plugins': (self.cb_add_plugins, True), |
||
1159 | }) |
||
1160 | except ArgumentPreprocessingError as ex: |
||
1161 | print(ex, file=sys.stderr) |
||
1162 | sys.exit(32)
|
||
1163 | |||
1164 | self.linter = linter = self.LinterClass(( |
||
1165 | ('rcfile',
|
||
1166 | {'action' : 'callback', 'callback' : lambda *args: 1, |
||
1167 | 'type': 'string', 'metavar': '<file>', |
||
1168 | 'help' : 'Specify a configuration file.'}), |
||
1169 | |||
1170 | ('init-hook',
|
||
1171 | {'action' : 'callback', 'callback' : lambda *args: 1, |
||
1172 | 'type' : 'string', 'metavar': '<code>', |
||
1173 | 'level': 1, |
||
1174 | 'help' : 'Python code to execute, usually for sys.path ' |
||
1175 | 'manipulation such as pygtk.require().'}),
|
||
1176 | |||
1177 | ('help-msg',
|
||
1178 | {'action' : 'callback', 'type' : 'string', 'metavar': '<msg-id>', |
||
1179 | 'callback' : self.cb_help_message, |
||
1180 | 'group': 'Commands', |
||
1181 | 'help' : 'Display a help message for the given message id and ' |
||
1182 | 'exit. The value may be a comma separated list of message ids.'}),
|
||
1183 | |||
1184 | ('list-msgs',
|
||
1185 | {'action' : 'callback', 'metavar': '<msg-id>', |
||
1186 | 'callback' : self.cb_list_messages, |
||
1187 | 'group': 'Commands', 'level': 1, |
||
1188 | 'help' : "Generate pylint's messages."}), |
||
1189 | |||
1190 | ('list-conf-levels',
|
||
1191 | {'action' : 'callback', |
||
1192 | 'callback' : cb_list_confidence_levels,
|
||
1193 | 'group': 'Commands', 'level': 1, |
||
1194 | 'help' : "Generate pylint's messages."}), |
||
1195 | |||
1196 | ('full-documentation',
|
||
1197 | {'action' : 'callback', 'metavar': '<msg-id>', |
||
1198 | 'callback' : self.cb_full_documentation, |
||
1199 | 'group': 'Commands', 'level': 1, |
||
1200 | 'help' : "Generate pylint's full documentation."}), |
||
1201 | |||
1202 | ('generate-rcfile',
|
||
1203 | {'action' : 'callback', 'callback' : self.cb_generate_config, |
||
1204 | 'group': 'Commands', |
||
1205 | 'help' : 'Generate a sample configuration file according to ' |
||
1206 | 'the current configuration. You can put other options '
|
||
1207 | 'before this one to get them in the generated '
|
||
1208 | 'configuration.'}),
|
||
1209 | |||
1210 | ('generate-man',
|
||
1211 | {'action' : 'callback', 'callback' : self.cb_generate_manpage, |
||
1212 | 'group': 'Commands', |
||
1213 | 'help' : "Generate pylint's man page.", 'hide': True}), |
||
1214 | |||
1215 | ('errors-only',
|
||
1216 | {'action' : 'callback', 'callback' : self.cb_error_mode, |
||
1217 | 'short': 'E', |
||
1218 | 'help' : 'In error mode, checkers without error messages are ' |
||
1219 | 'disabled and for others, only the ERROR messages are '
|
||
1220 | 'displayed, and no reports are done by default'''}), |
||
1221 | |||
1222 | ('py3k',
|
||
1223 | {'action' : 'callback', 'callback' : self.cb_python3_porting_mode, |
||
1224 | 'help' : 'In Python 3 porting mode, all checkers will be ' |
||
1225 | 'disabled and only messages emitted by the porting '
|
||
1226 | 'checker will be displayed'}),
|
||
1227 | |||
1228 | ('profile', utils.deprecated_option(opt_type='yn')), |
||
1229 | |||
1230 | ), option_groups=self.option_groups, pylintrc=self._rcfile) |
||
1231 | # register standard checkers
|
||
1232 | linter.load_default_plugins() |
||
1233 | # load command line plugins
|
||
1234 | linter.load_plugin_modules(self._plugins)
|
||
1235 | # add some help section
|
||
1236 | linter.add_help_section('Environment variables', config.ENV_HELP, level=1) |
||
1237 | # pylint: disable=bad-continuation
|
||
1238 | linter.add_help_section('Output',
|
||
1239 | 'Using the default text output, the message format is : \n'
|
||
1240 | ' \n'
|
||
1241 | ' MESSAGE_TYPE: LINE_NUM:[OBJECT:] MESSAGE \n'
|
||
1242 | ' \n'
|
||
1243 | 'There are 5 kind of message types : \n'
|
||
1244 | ' * (C) convention, for programming standard violation \n'
|
||
1245 | ' * (R) refactor, for bad code smell \n'
|
||
1246 | ' * (W) warning, for python specific problems \n'
|
||
1247 | ' * (E) error, for probable bugs in the code \n'
|
||
1248 | ' * (F) fatal, if an error occurred which prevented pylint from doing further\n'
|
||
1249 | 'processing.\n'
|
||
1250 | , level=1)
|
||
1251 | linter.add_help_section('Output status code',
|
||
1252 | 'Pylint should leave with following status code: \n'
|
||
1253 | ' * 0 if everything went fine \n'
|
||
1254 | ' * 1 if a fatal message was issued \n'
|
||
1255 | ' * 2 if an error message was issued \n'
|
||
1256 | ' * 4 if a warning message was issued \n'
|
||
1257 | ' * 8 if a refactor message was issued \n'
|
||
1258 | ' * 16 if a convention message was issued \n'
|
||
1259 | ' * 32 on usage error \n'
|
||
1260 | ' \n'
|
||
1261 | 'status 1 to 16 will be bit-ORed so you can know which different categories has\n'
|
||
1262 | 'been issued by analysing pylint output status code\n',
|
||
1263 | level=1)
|
||
1264 | # read configuration
|
||
1265 | linter.disable('suppressed-message')
|
||
1266 | linter.disable('useless-suppression')
|
||
1267 | linter.read_config_file() |
||
1268 | config_parser = linter.cfgfile_parser |
||
1269 | # run init hook, if present, before loading plugins
|
||
1270 | if config_parser.has_option('MASTER', 'init-hook'): |
||
1271 | cb_init_hook('init-hook',
|
||
1272 | utils._unquote(config_parser.get('MASTER',
|
||
1273 | 'init-hook')))
|
||
1274 | # is there some additional plugins in the file configuration, in
|
||
1275 | if config_parser.has_option('MASTER', 'load-plugins'): |
||
1276 | plugins = utils._splitstrip( |
||
1277 | config_parser.get('MASTER', 'load-plugins')) |
||
1278 | linter.load_plugin_modules(plugins) |
||
1279 | # now we can load file config and command line, plugins (which can
|
||
1280 | # provide options) have been registered
|
||
1281 | linter.load_config_file() |
||
1282 | if reporter:
|
||
1283 | # if a custom reporter is provided as argument, it may be overridden
|
||
1284 | # by file parameters, so re-set it here, but before command line
|
||
1285 | # parsing so it's still overrideable by command line option
|
||
1286 | linter.set_reporter(reporter) |
||
1287 | try:
|
||
1288 | args = linter.load_command_line_configuration(args) |
||
1289 | except SystemExit as exc: |
||
1290 | if exc.code == 2: # bad options |
||
1291 | exc.code = 32
|
||
1292 | raise
|
||
1293 | if not args: |
||
1294 | print(linter.help()) |
||
1295 | sys.exit(32)
|
||
1296 | |||
1297 | if linter.config.jobs < 0: |
||
1298 | print("Jobs number (%d) should be greater than 0"
|
||
1299 | % linter.config.jobs, file=sys.stderr) |
||
1300 | sys.exit(32)
|
||
1301 | if linter.config.jobs > 1 or linter.config.jobs == 0: |
||
1302 | if multiprocessing is None: |
||
1303 | print("Multiprocessing library is missing, "
|
||
1304 | "fallback to single process", file=sys.stderr)
|
||
1305 | linter.set_option("jobs", 1) |
||
1306 | else:
|
||
1307 | if linter.config.jobs == 0: |
||
1308 | linter.config.jobs = multiprocessing.cpu_count() |
||
1309 | |||
1310 | # insert current working directory to the python path to have a correct
|
||
1311 | # behaviour
|
||
1312 | with fix_import_path(args):
|
||
1313 | linter.check(args) |
||
1314 | linter.generate_reports() |
||
1315 | if exit: |
||
1316 | sys.exit(self.linter.msg_status)
|
||
1317 | |||
1318 | def cb_set_rcfile(self, name, value): |
||
1319 | """callback for option preprocessing (i.e. before option parsing)"""
|
||
1320 | self._rcfile = value
|
||
1321 | |||
1322 | def cb_add_plugins(self, name, value): |
||
1323 | """callback for option preprocessing (i.e. before option parsing)"""
|
||
1324 | self._plugins.extend(utils._splitstrip(value))
|
||
1325 | |||
1326 | def cb_error_mode(self, *args, **kwargs): |
||
1327 | """error mode:
|
||
1328 | * disable all but error messages
|
||
1329 | * disable the 'miscellaneous' checker which can be safely deactivated in
|
||
1330 | debug
|
||
1331 | * disable reports
|
||
1332 | * do not save execution information
|
||
1333 | """
|
||
1334 | self.linter.error_mode()
|
||
1335 | |||
1336 | def cb_generate_config(self, *args, **kwargs): |
||
1337 | """optik callback for sample config file generation"""
|
||
1338 | self.linter.generate_config(skipsections=('COMMANDS',)) |
||
1339 | sys.exit(0)
|
||
1340 | |||
1341 | def cb_generate_manpage(self, *args, **kwargs): |
||
1342 | """optik callback for sample config file generation"""
|
||
1343 | from pylint import __pkginfo__ |
||
1344 | self.linter.generate_manpage(__pkginfo__)
|
||
1345 | sys.exit(0)
|
||
1346 | |||
1347 | def cb_help_message(self, option, optname, value, parser): |
||
1348 | """optik callback for printing some help about a particular message"""
|
||
1349 | self.linter.msgs_store.help_message(utils._splitstrip(value))
|
||
1350 | sys.exit(0)
|
||
1351 | |||
1352 | def cb_full_documentation(self, option, optname, value, parser): |
||
1353 | """optik callback for printing full documentation"""
|
||
1354 | self.linter.print_full_documentation()
|
||
1355 | sys.exit(0)
|
||
1356 | |||
1357 | def cb_list_messages(self, option, optname, value, parser): # FIXME |
||
1358 | """optik callback for printing available messages"""
|
||
1359 | self.linter.msgs_store.list_messages()
|
||
1360 | sys.exit(0)
|
||
1361 | |||
1362 | def cb_python3_porting_mode(self, *args, **kwargs): |
||
1363 | """Activate only the python3 porting checker."""
|
||
1364 | self.linter.python3_porting_mode()
|
||
1365 | |||
1366 | |||
1367 | def cb_list_confidence_levels(option, optname, value, parser): |
||
1368 | for level in interfaces.CONFIDENCE_LEVELS: |
||
1369 | print('%-18s: %s' % level)
|
||
1370 | sys.exit(0)
|
||
1371 | |||
1372 | def cb_init_hook(optname, value): |
||
1373 | """exec arbitrary code to set sys.path for instance"""
|
||
1374 | exec(value) # pylint: disable=exec-used
|
||
1375 | |||
1376 | |||
1377 | if __name__ == '__main__': |
||
1378 | Run(sys.argv[1:]) |