1
# -*- coding: utf-8 -*-
3
# Copyright © 2009-2010 Pierre Raybaut
4
# Licensed under the terms of the MIT License
5
# (see spyderlib/__init__.py for details)
9
# pylint: disable=C0103
10
# pylint: disable=R0903
11
# pylint: disable=R0911
12
# pylint: disable=R0201
14
from __future__ import with_statement, print_function
16
from spyderlib.qt.QtGui import (QHBoxLayout, QWidget, QTreeWidgetItem,
17
QMessageBox, QVBoxLayout, QLabel)
18
from spyderlib.qt.QtCore import SIGNAL, QProcess, QByteArray, QTextCodec
19
locale_codec = QTextCodec.codecForLocale()
20
from spyderlib.qt.compat import getopenfilename
30
from spyderlib import dependencies
31
from spyderlib.utils import programs
32
from spyderlib.utils.encoding import to_unicode_from_fs
33
from spyderlib.utils.qthelpers import get_icon, create_toolbutton
34
from spyderlib.baseconfig import get_conf_path, get_translation
35
from spyderlib.widgets.onecolumntree import OneColumnTree
36
from spyderlib.widgets.texteditor import TextEditor
37
from spyderlib.widgets.comboboxes import (PythonModulesComboBox,
39
from spyderlib.py3compat import to_text_string, getcwd, pickle
40
_ = get_translation("p_pylint", dirname="spyderplugins")
43
PYLINT_PATH = programs.find_program('pylint')
46
def get_pylint_version():
47
"""Return pylint version"""
49
if PYLINT_PATH is None:
51
process = subprocess.Popen(['pylint', '--version'],
52
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
53
cwd=osp.dirname(PYLINT_PATH),
54
shell=True if os.name == 'nt' else False)
55
lines = to_unicode_from_fs(process.stdout.read()).splitlines()
57
match = re.match('(pylint|pylint-script.py) ([0-9\.]*)', lines[0])
59
return match.groups()[1]
62
PYLINT_REQVER = '>=0.25'
63
PYLINT_VER = get_pylint_version()
64
dependencies.add("pylint", _("Static code analysis"),
65
required_version=PYLINT_REQVER, installed_version=PYLINT_VER)
68
#TODO: display results on 3 columns instead of 1: msg_id, lineno, message
69
class ResultsTree(OneColumnTree):
70
def __init__(self, parent):
71
OneColumnTree.__init__(self, parent)
77
def activated(self, item):
78
"""Double-click event"""
79
data = self.data.get(id(item))
82
self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"),
85
def clicked(self, item):
89
def clear_results(self):
93
def set_results(self, filename, results):
94
self.filename = filename
95
self.results = results
99
title = _('Results for ')+self.filename
100
self.set_title(title)
104
results = ((_('Convention'),
105
get_icon('convention.png'), self.results['C:']),
107
get_icon('refactor.png'), self.results['R:']),
109
get_icon('warning.png'), self.results['W:']),
111
get_icon('error.png'), self.results['E:']))
112
for title, icon, messages in results:
113
title += ' (%d message%s)' % (len(messages),
114
's' if len(messages)>1 else '')
115
title_item = QTreeWidgetItem(self, [title], QTreeWidgetItem.Type)
116
title_item.setIcon(0, icon)
118
title_item.setDisabled(True)
120
for module, lineno, message, msg_id in messages:
121
basename = osp.splitext(osp.basename(self.filename))[0]
122
if not module.startswith(basename):
124
i_base = module.find(basename)
125
module = module[i_base:]
126
dirname = osp.dirname(self.filename)
127
if module.startswith('.') or module == basename:
128
modname = osp.join(dirname, module)
130
modname = osp.join(dirname, *module.split('.'))
131
if osp.isdir(modname):
132
modname = osp.join(modname, '__init__')
133
for ext in ('.py', '.pyw'):
134
if osp.isfile(modname+ext):
135
modname = modname + ext
137
if osp.isdir(self.filename):
138
parent = modules.get(modname)
140
item = QTreeWidgetItem(title_item, [module],
141
QTreeWidgetItem.Type)
142
item.setIcon(0, get_icon('py.png'))
143
modules[modname] = item
148
text = "[%s] %d : %s" % (msg_id, lineno, message)
150
text = "%d : %s" % (lineno, message)
151
msg_item = QTreeWidgetItem(parent, [text], QTreeWidgetItem.Type)
152
msg_item.setIcon(0, get_icon('arrow.png'))
153
self.data[id(msg_item)] = (modname, lineno)
156
class PylintWidget(QWidget):
160
DATAPATH = get_conf_path('pylint.results')
163
def __init__(self, parent, max_entries=100):
164
QWidget.__init__(self, parent)
166
self.setWindowTitle("Pylint")
169
self.error_output = None
171
self.max_entries = max_entries
173
if osp.isfile(self.DATAPATH):
175
data = pickle.loads(open(self.DATAPATH, 'rb').read())
176
if data[0] == self.VERSION:
177
self.rdata = data[1:]
178
except (EOFError, ImportError):
181
self.filecombo = PythonModulesComboBox(self)
183
self.remove_obsolete_items()
184
self.filecombo.addItems(self.get_filenames())
186
self.start_button = create_toolbutton(self, icon=get_icon('run.png'),
188
tip=_("Run analysis"),
189
triggered=self.start, text_beside_icon=True)
190
self.stop_button = create_toolbutton(self,
191
icon=get_icon('stop.png'),
193
tip=_("Stop current analysis"),
194
text_beside_icon=True)
195
self.connect(self.filecombo, SIGNAL('valid(bool)'),
196
self.start_button.setEnabled)
197
self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data)
199
browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'),
200
tip=_('Select Python file'),
201
triggered=self.select_file)
203
self.ratelabel = QLabel()
204
self.datelabel = QLabel()
205
self.log_button = create_toolbutton(self, icon=get_icon('log.png'),
207
text_beside_icon=True,
208
tip=_("Complete output"),
209
triggered=self.show_log)
210
self.treewidget = ResultsTree(self)
212
hlayout1 = QHBoxLayout()
213
hlayout1.addWidget(self.filecombo)
214
hlayout1.addWidget(browse_button)
215
hlayout1.addWidget(self.start_button)
216
hlayout1.addWidget(self.stop_button)
218
hlayout2 = QHBoxLayout()
219
hlayout2.addWidget(self.ratelabel)
220
hlayout2.addStretch()
221
hlayout2.addWidget(self.datelabel)
222
hlayout2.addStretch()
223
hlayout2.addWidget(self.log_button)
225
layout = QVBoxLayout()
226
layout.addLayout(hlayout1)
227
layout.addLayout(hlayout2)
228
layout.addWidget(self.treewidget)
229
self.setLayout(layout)
232
self.set_running_state(False)
234
if PYLINT_PATH is None:
235
for widget in (self.treewidget, self.filecombo,
236
self.start_button, self.stop_button):
237
widget.setDisabled(True)
239
and programs.is_module_installed("pylint"):
240
# Pylint is installed but pylint script is not in PATH
241
# (AFAIK, could happen only on Windows)
242
text = _('Pylint script was not found. Please add "%s" to PATH.')
243
text = to_text_string(text) % osp.join(sys.prefix, "Scripts")
245
text = _('Please install <b>pylint</b>:')
246
url = 'http://www.logilab.fr'
247
text += ' <a href=%s>%s</a>' % (url, url)
248
self.ratelabel.setText(text)
252
def analyze(self, filename):
253
if PYLINT_PATH is None:
255
filename = to_text_string(filename) # filename is a QString instance
256
self.kill_if_running()
257
index, _data = self.get_data(filename)
259
self.filecombo.addItem(filename)
260
self.filecombo.setCurrentIndex(self.filecombo.count()-1)
262
self.filecombo.setCurrentIndex(self.filecombo.findText(filename))
263
self.filecombo.selected()
264
if self.filecombo.is_valid():
267
def select_file(self):
268
self.emit(SIGNAL('redirect_stdio(bool)'), False)
269
filename, _selfilter = getopenfilename(self, _("Select Python file"),
270
getcwd(), _("Python files")+" (*.py ; *.pyw)")
271
self.emit(SIGNAL('redirect_stdio(bool)'), False)
273
self.analyze(filename)
275
def remove_obsolete_items(self):
276
"""Removing obsolete items"""
277
self.rdata = [(filename, data) for filename, data in self.rdata
278
if is_module_or_package(filename)]
280
def get_filenames(self):
281
return [filename for filename, _data in self.rdata]
283
def get_data(self, filename):
284
filename = osp.abspath(filename)
285
for index, (fname, data) in enumerate(self.rdata):
286
if fname == filename:
291
def set_data(self, filename, data):
292
filename = osp.abspath(filename)
293
index, _data = self.get_data(filename)
294
if index is not None:
295
self.rdata.pop(index)
296
self.rdata.insert(0, (filename, data))
300
while len(self.rdata) > self.max_entries:
302
pickle.dump([self.VERSION]+self.rdata, open(self.DATAPATH, 'wb'), 2)
306
TextEditor(self.output, title=_("Pylint output"),
307
readonly=True, size=(700, 500)).exec_()
310
filename = to_text_string(self.filecombo.currentText())
312
self.process = QProcess(self)
313
self.process.setProcessChannelMode(QProcess.SeparateChannels)
314
self.process.setWorkingDirectory(osp.dirname(filename))
315
self.connect(self.process, SIGNAL("readyReadStandardOutput()"),
317
self.connect(self.process, SIGNAL("readyReadStandardError()"),
318
lambda: self.read_output(error=True))
319
self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"),
321
self.connect(self.stop_button, SIGNAL("clicked()"),
325
self.error_output = ''
328
if plver is not None:
329
if plver.split('.')[0] == '0':
330
p_args = ['-i', 'yes']
332
# Option '-i' (alias for '--include-ids') was removed in pylint
334
p_args = ["--msg-template='{msg_id}:{line:3d},"\
335
"{column}: {obj}: {msg}"]
336
p_args += [osp.basename(filename)]
338
p_args = [osp.basename(filename)]
339
self.process.start(PYLINT_PATH, p_args)
341
running = self.process.waitForStarted()
342
self.set_running_state(running)
344
QMessageBox.critical(self, _("Error"),
345
_("Process failed to start"))
347
def set_running_state(self, state=True):
348
self.start_button.setEnabled(not state)
349
self.stop_button.setEnabled(state)
351
def read_output(self, error=False):
353
self.process.setReadChannel(QProcess.StandardError)
355
self.process.setReadChannel(QProcess.StandardOutput)
357
while self.process.bytesAvailable():
359
qba += self.process.readAllStandardError()
361
qba += self.process.readAllStandardOutput()
362
text = to_text_string( locale_codec.toUnicode(qba.data()) )
364
self.error_output += text
369
self.set_running_state(False)
371
if self.error_output:
372
QMessageBox.critical(self, _("Error"), self.error_output)
373
print("pylint error:\n\n" + self.error_output, file=sys.stderr)
376
# Convention, Refactor, Warning, Error
377
results = {'C:': [], 'R:': [], 'W:': [], 'E:': []}
378
txt_module = '************* Module '
380
module = '' # Should not be needed - just in case something goes wrong
381
for line in self.output.splitlines():
382
if line.startswith(txt_module):
384
module = line[len(txt_module):]
386
# Supporting option include-ids: ('R3873:' instead of 'R:')
387
if not re.match('^[CRWE]+([0-9]{4})?:', line):
393
i2 = line.find(':', i1+1)
396
line_nb = line[i1+1:i2].strip()
399
line_nb = int(line_nb.split(',')[0])
400
message = line[i2+1:]
401
item = (module, line_nb, message, msg_id)
402
results[line[0]+':'].append(item)
406
txt_rate = 'Your code has been rated at '
407
i_rate = self.output.find(txt_rate)
409
i_rate_end = self.output.find('/10', i_rate)
411
rate = self.output[i_rate+len(txt_rate):i_rate_end]
416
txt_prun = 'previous run: '
417
i_prun = self.output.find(txt_prun, i_rate_end)
419
i_prun_end = self.output.find('/10', i_prun)
420
previous = self.output[i_prun+len(txt_prun):i_prun_end]
423
filename = to_text_string(self.filecombo.currentText())
424
self.set_data(filename, (time.localtime(), rate, previous, results))
425
self.output = self.error_output + self.output
426
self.show_data(justanalyzed=True)
428
def kill_if_running(self):
429
if self.process is not None:
430
if self.process.state() == QProcess.Running:
432
self.process.waitForFinished()
434
def show_data(self, justanalyzed=False):
437
self.log_button.setEnabled(self.output is not None \
438
and len(self.output) > 0)
439
self.kill_if_running()
440
filename = to_text_string(self.filecombo.currentText())
444
_index, data = self.get_data(filename)
446
text = _('Source code has not been rated yet.')
447
self.treewidget.clear_results()
450
datetime, rate, previous_rate, results = data
452
text = _('Analysis did not succeed '
453
'(see output for more details).')
454
self.treewidget.clear_results()
457
text_style = "<span style=\'color: #444444\'><b>%s </b></span>"
458
rate_style = "<span style=\'color: %s\'><b>%s</b></span>"
459
prevrate_style = "<span style=\'color: #666666\'>%s</span>"
463
elif float(rate) > 3.:
465
text = _('Global evaluation:')
466
text = (text_style % text)+(rate_style % (color,
469
text_prun = _('previous run:')
470
text_prun = ' (%s %s/10)' % (text_prun, previous_rate)
471
text += prevrate_style % text_prun
472
self.treewidget.set_results(filename, results)
473
date = to_text_string(time.strftime("%d %b %Y %H:%M", datetime),
475
date_text = text_style % date
477
self.ratelabel.setText(text)
478
self.datelabel.setText(date_text)
482
"""Run pylint widget test"""
483
from spyderlib.utils.qthelpers import qapplication
485
widget = PylintWidget(None)
487
widget.analyze(__file__)
488
sys.exit(app.exec_())
490
if __name__ == '__main__':