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-msg=C0103
10
# pylint: disable-msg=R0903
11
# pylint: disable-msg=R0911
12
# pylint: disable-msg=R0201
14
from __future__ import with_statement
17
# PyQt4 4.3.3 on Windows (static DLLs) with py2exe installed:
18
# -> pythoncom must be imported first, otherwise py2exe's boot_com_servers
19
# will raise an exception ("ImportError: DLL load failed [...]") when
20
# calling any of the QFileDialog static methods (getOpenFileName, ...)
21
import pythoncom #@UnusedImport
25
from PyQt4.QtGui import (QHBoxLayout, QWidget, QTreeWidgetItem, QMessageBox,
26
QVBoxLayout, QLabel, QFileDialog)
27
from PyQt4.QtCore import SIGNAL, QProcess, QByteArray, QString
29
import sys, os, time, cPickle, os.path as osp, re
31
# For debugging purpose:
35
from spyderlib.utils import programs
36
from spyderlib.utils.qthelpers import create_toolbutton, translate
37
from spyderlib.config import get_icon, get_conf_path
38
from spyderlib.widgets.onecolumntree import OneColumnTree
39
from spyderlib.widgets.texteditor import TextEditor
40
from spyderlib.widgets.comboboxes import (PythonModulesComboBox,
44
PYLINT_PATH = programs.get_nt_program_name('pylint')
46
def is_pylint_installed():
47
return programs.is_program_installed(PYLINT_PATH)
49
class ResultsTree(OneColumnTree):
50
def __init__(self, parent):
51
OneColumnTree.__init__(self, parent)
58
data = self.data.get(self.currentItem())
61
self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"),
64
def set_results(self, filename, results):
65
self.filename = filename
66
self.results = results
70
title = translate('Pylint', 'Results for ')+self.filename
75
results = ((translate('Pylint', 'Convention'),
76
get_icon('convention.png'), self.results['C:']),
77
(translate('Pylint', 'Refactor'),
78
get_icon('refactor.png'), self.results['R:']),
79
(translate('Pylint', 'Warning'),
80
get_icon('warning.png'), self.results['W:']),
81
(translate('Pylint', 'Error'),
82
get_icon('error.png'), self.results['E:']))
83
for title, icon, messages in results:
84
title += ' (%d message%s)' % (len(messages),
85
's' if len(messages)>1 else '')
86
title_item = QTreeWidgetItem(self, [title], QTreeWidgetItem.Type)
87
title_item.setIcon(0, icon)
89
title_item.setDisabled(True)
91
for module, lineno, message in messages:
92
basename = osp.splitext(osp.basename(self.filename))[0]
93
if not module.startswith(basename):
95
i_base = module.find(basename)
96
module = module[i_base:]
97
dirname = osp.dirname(self.filename)
98
if module.startswith('.'):
99
modname = osp.join(dirname, module)
101
modname = osp.join(dirname, *module.split('.'))
102
if osp.isdir(modname):
103
modname = osp.join(modname, '__init__')
104
for ext in ('.py', '.pyw'):
105
if osp.isfile(modname+ext):
106
modname = modname + ext
108
if osp.isdir(self.filename):
109
parent = modules.get(modname)
111
item = QTreeWidgetItem(title_item, [module],
112
QTreeWidgetItem.Type)
113
item.setIcon(0, get_icon('py.png'))
114
modules[modname] = item
118
msg_item = QTreeWidgetItem(parent,
119
["%d : %s" % (lineno, message)],
120
QTreeWidgetItem.Type)
121
msg_item.setIcon(0, get_icon('arrow.png'))
122
self.data[msg_item] = (modname, lineno)
125
class PylintWidget(QWidget):
129
DATAPATH = get_conf_path('.pylint.results')
132
def __init__(self, parent, max_entries=100):
133
QWidget.__init__(self, parent)
135
self.setWindowTitle("Pylint")
138
self.error_output = None
140
self.max_entries = max_entries
142
if osp.isfile(self.DATAPATH):
144
data = cPickle.loads(file(self.DATAPATH, 'U').read())
145
if data[0] == self.VERSION:
146
self.rdata = data[1:]
150
self.filecombo = PythonModulesComboBox(self)
152
self.remove_obsolete_items()
153
self.filecombo.addItems(self.get_filenames())
155
self.start_button = create_toolbutton(self, icon=get_icon('run.png'),
156
text=translate('Pylint', "Analyze"),
157
tip=translate('Pylint', "Run analysis"),
158
triggered=self.start, text_beside_icon=True)
159
self.stop_button = create_toolbutton(self,
160
icon=get_icon('terminate.png'),
161
text=translate('Pylint', "Stop"),
162
tip=translate('Pylint',
163
"Stop current analysis"),
164
text_beside_icon=True)
165
self.connect(self.filecombo, SIGNAL('valid(bool)'),
166
self.start_button.setEnabled)
167
self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data)
169
browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'),
170
tip=translate('Pylint', 'Select Python script'),
171
triggered=self.select_file)
173
self.ratelabel = QLabel()
174
self.datelabel = QLabel()
175
self.log_button = create_toolbutton(self, icon=get_icon('log.png'),
176
text=translate('Pylint', "Output"),
177
text_beside_icon=True,
178
tip=translate('Pylint',
179
"Complete Pylint output"),
180
triggered=self.show_log)
181
self.treewidget = ResultsTree(self)
183
hlayout1 = QHBoxLayout()
184
hlayout1.addWidget(self.filecombo)
185
hlayout1.addWidget(browse_button)
186
hlayout1.addWidget(self.start_button)
187
hlayout1.addWidget(self.stop_button)
189
hlayout2 = QHBoxLayout()
190
hlayout2.addWidget(self.ratelabel)
191
hlayout2.addStretch()
192
hlayout2.addWidget(self.datelabel)
193
hlayout2.addStretch()
194
hlayout2.addWidget(self.log_button)
196
layout = QVBoxLayout()
197
layout.addLayout(hlayout1)
198
layout.addLayout(hlayout2)
199
layout.addWidget(self.treewidget)
200
self.setLayout(layout)
203
self.set_running_state(False)
205
if not is_pylint_installed():
206
for widget in (self.treewidget, self.filecombo,
207
self.start_button, self.stop_button):
208
widget.setDisabled(True)
210
and programs.is_module_installed("pylint"):
211
# Pylint is installed but pylint script is not in PATH
212
# (AFAIK, could happen only on Windows)
213
text = translate('Pylint',
214
'Pylint script was not found. Please add "%s" to PATH.')
215
text = unicode(text) % osp.join(sys.prefix, "Scripts")
217
text = translate('Pylint', 'Please install <b>pylint</b>:')
218
url = 'http://www.logilab.fr'
219
text += ' <a href=%s>%s</a>' % (url, url)
220
self.ratelabel.setText(text)
224
def analyze(self, filename):
225
if not is_pylint_installed():
227
filename = unicode(filename) # filename is a QString instance
228
self.kill_if_running()
229
index, _data = self.get_data(filename)
231
self.filecombo.addItem(filename)
232
self.filecombo.setCurrentIndex(self.filecombo.count()-1)
234
self.filecombo.setCurrentIndex(self.filecombo.findText(filename))
235
self.filecombo.selected()
236
if self.filecombo.is_valid():
239
def select_file(self):
240
self.emit(SIGNAL('redirect_stdio(bool)'), False)
241
filename = QFileDialog.getOpenFileName(self,
242
translate('Pylint', "Select Python script"), os.getcwdu(),
243
translate('Pylint', "Python scripts")+" (*.py ; *.pyw)")
244
self.emit(SIGNAL('redirect_stdio(bool)'), False)
246
self.analyze(filename)
248
def remove_obsolete_items(self):
249
"""Removing obsolete items"""
250
self.rdata = [(filename, data) for filename, data in self.rdata
251
if is_module_or_package(filename)]
253
def get_filenames(self):
254
return [filename for filename, _data in self.rdata]
256
def get_data(self, filename):
257
filename = osp.abspath(filename)
258
for index, (fname, data) in enumerate(self.rdata):
259
if fname == filename:
264
def set_data(self, filename, data):
265
filename = osp.abspath(filename)
266
index, _data = self.get_data(filename)
267
if index is not None:
268
self.rdata.pop(index)
269
self.rdata.append( (filename, data) )
273
while len(self.rdata) > self.max_entries:
275
cPickle.dump([self.VERSION]+self.rdata, file(self.DATAPATH, 'w'))
279
TextEditor(self.output, title=translate('Pylint', "Pylint output"),
280
readonly=True, size=(700, 500)).exec_()
283
filename = unicode(self.filecombo.currentText())
285
self.process = QProcess(self)
286
self.process.setProcessChannelMode(QProcess.SeparateChannels)
287
self.process.setWorkingDirectory(osp.dirname(filename))
288
self.connect(self.process, SIGNAL("readyReadStandardOutput()"),
290
self.connect(self.process, SIGNAL("readyReadStandardError()"),
291
lambda: self.read_output(error=True))
292
self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"),
294
self.connect(self.stop_button, SIGNAL("clicked()"),
298
self.error_output = ''
299
p_args = [osp.basename(filename)]
300
self.process.start(PYLINT_PATH, p_args)
302
running = self.process.waitForStarted()
303
self.set_running_state(running)
305
QMessageBox.critical(self, translate('Pylint', "Error"),
306
translate('Pylint', "Process failed to start"))
308
def set_running_state(self, state=True):
309
self.start_button.setEnabled(not state)
310
self.stop_button.setEnabled(state)
312
def read_output(self, error=False):
314
self.process.setReadChannel(QProcess.StandardError)
316
self.process.setReadChannel(QProcess.StandardOutput)
318
while self.process.bytesAvailable():
320
bytes += self.process.readAllStandardError()
322
bytes += self.process.readAllStandardOutput()
323
text = unicode( QString.fromLocal8Bit(bytes.data()) )
325
self.error_output += text
330
self.set_running_state(False)
334
# Convention, Refactor, Warning, Error
335
results = {'C:': [], 'R:': [], 'W:': [], 'E:': []}
336
txt_module = '************* Module '
338
module = '' # Should not be needed - just in case something goes wrong
339
for line in self.output.splitlines():
340
if line.startswith(txt_module):
342
module = line[len(txt_module):]
344
# Supporting option include-ids: ('R3873:' instead of 'R:')
345
if not re.match('^[CRWE]+([0-9]{4})?:', line):
350
i2 = line.find(':', i1+1)
353
line_nb = line[i1+1:i2].strip()
356
line_nb = int(line_nb)
357
message = line[i2+1:]
358
item = (module, line_nb, message)
359
results[line[0]+':'].append(item)
363
txt_rate = 'Your code has been rated at '
364
i_rate = self.output.find(txt_rate)
366
i_rate_end = self.output.find('/10', i_rate)
368
rate = self.output[i_rate+len(txt_rate):i_rate_end]
373
txt_prun = 'previous run: '
374
i_prun = self.output.find(txt_prun, i_rate_end)
376
i_prun_end = self.output.find('/10', i_prun)
377
previous = self.output[i_prun+len(txt_prun):i_prun_end]
380
filename = unicode(self.filecombo.currentText())
381
self.set_data(filename, (time.localtime(), rate, previous, results))
382
self.output = self.error_output + self.output
383
self.show_data(justanalyzed=True)
385
def kill_if_running(self):
386
if self.process is not None:
387
if self.process.state() == QProcess.Running:
389
self.process.waitForFinished()
391
def show_data(self, justanalyzed=False):
394
self.log_button.setEnabled(self.output is not None \
395
and len(self.output) > 0)
396
self.kill_if_running()
397
filename = unicode(self.filecombo.currentText())
401
_index, data = self.get_data(filename)
403
text = translate('Pylint', 'Source code has not been rated yet.')
404
self.treewidget.clear()
407
datetime, rate, previous_rate, results = data
409
text = translate('Pylint', 'Analysis did not succeed '
410
'(see output for more details).')
411
self.treewidget.clear()
414
text_style = "<span style=\'color: #444444\'><b>%s </b></span>"
415
rate_style = "<span style=\'color: %s\'><b>%s</b></span>"
416
prevrate_style = "<span style=\'color: #666666\'>%s</span>"
420
elif float(rate) > 3.:
422
text = translate('Pylint', 'Global evaluation:')
423
text = (text_style % text)+(rate_style % (color,
426
text_prun = translate('Pylint', 'previous run:')
427
text_prun = ' (%s %s/10)' % (text_prun, previous_rate)
428
text += prevrate_style % text_prun
429
self.treewidget.set_results(filename, results)
430
date_text = text_style % time.strftime("%d %b %Y %H:%M",
433
self.ratelabel.setText(text)
434
self.datelabel.setText(date_text)
438
"""Run pylint widget test"""
439
from spyderlib.utils.qthelpers import qapplication
441
widget = PylintWidget(None)
443
widget.analyze(__file__)
444
sys.exit(app.exec_())
446
if __name__ == '__main__':