~ubuntu-branches/debian/experimental/spyder/experimental

« back to all changes in this revision

Viewing changes to spyderplugins/widgets/pylintgui.py

  • Committer: Package Import Robot
  • Author(s): Picca Frédéric-Emmanuel
  • Date: 2013-02-27 09:51:28 UTC
  • mfrom: (1.1.18)
  • Revision ID: package-import@ubuntu.com-20130227095128-wtx1irpvf4vl79lj
Tags: 2.2.0~beta3+dfsg-1
* Imported Upstream version 2.2.0~beta3+dfsg
* debian /patches
  - 0002-feature-forwarded-add-icon-to-desktop-file.patch (deleted)
    this patch was integrated by the upstream.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
#
3
 
# Copyright © 2009-2010 Pierre Raybaut
4
 
# Licensed under the terms of the MIT License
5
 
# (see spyderlib/__init__.py for details)
6
 
 
7
 
"""Pylint widget"""
8
 
 
9
 
# pylint: disable=C0103
10
 
# pylint: disable=R0903
11
 
# pylint: disable=R0911
12
 
# pylint: disable=R0201
13
 
 
14
 
from __future__ import with_statement
15
 
 
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
21
 
 
22
 
import sys
23
 
import os
24
 
import os.path as osp
25
 
import time
26
 
import cPickle
27
 
import re
28
 
 
29
 
# Local imports
30
 
from spyderlib.utils import programs
31
 
from spyderlib.utils.qthelpers import create_toolbutton
32
 
from spyderlib.baseconfig import get_conf_path, get_translation
33
 
from spyderlib.guiconfig import get_icon
34
 
from spyderlib.widgets.onecolumntree import OneColumnTree
35
 
from spyderlib.widgets.texteditor import TextEditor
36
 
from spyderlib.widgets.comboboxes import (PythonModulesComboBox,
37
 
                                          is_module_or_package)
38
 
_ = get_translation("p_pylint", dirname="spyderplugins")
39
 
 
40
 
 
41
 
PYLINT_PATH = programs.find_program('pylint')
42
 
 
43
 
 
44
 
#TODO: display results on 3 columns instead of 1: msg_id, lineno, message
45
 
class ResultsTree(OneColumnTree):
46
 
    def __init__(self, parent):
47
 
        OneColumnTree.__init__(self, parent)
48
 
        self.filename = None
49
 
        self.results = None
50
 
        self.data = None
51
 
        self.set_title('')
52
 
        
53
 
    def activated(self, item):
54
 
        """Double-click event"""
55
 
        data = self.data.get(item)
56
 
        if data is not None:
57
 
            fname, lineno = data
58
 
            self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"),
59
 
                               fname, lineno, '')
60
 
 
61
 
    def clicked(self, item):
62
 
        """Click event"""
63
 
        self.activated(item)
64
 
        
65
 
    def clear_results(self):
66
 
        self.clear()
67
 
        self.set_title('')
68
 
        
69
 
    def set_results(self, filename, results):
70
 
        self.filename = filename
71
 
        self.results = results
72
 
        self.refresh()
73
 
        
74
 
    def refresh(self):
75
 
        title = _('Results for ')+self.filename
76
 
        self.set_title(title)
77
 
        self.clear()
78
 
        self.data = {}
79
 
        # Populating tree
80
 
        results = ((_('Convention'),
81
 
                    get_icon('convention.png'), self.results['C:']),
82
 
                   (_('Refactor'),
83
 
                    get_icon('refactor.png'), self.results['R:']),
84
 
                   (_('Warning'),
85
 
                    get_icon('warning.png'), self.results['W:']),
86
 
                   (_('Error'),
87
 
                    get_icon('error.png'), self.results['E:']))
88
 
        for title, icon, messages in results:
89
 
            title += ' (%d message%s)' % (len(messages),
90
 
                                          's' if len(messages)>1 else '')
91
 
            title_item = QTreeWidgetItem(self, [title], QTreeWidgetItem.Type)
92
 
            title_item.setIcon(0, icon)
93
 
            if not messages:
94
 
                title_item.setDisabled(True)
95
 
            modules = {}
96
 
            for module, lineno, message, msg_id in messages:
97
 
                basename = osp.splitext(osp.basename(self.filename))[0]
98
 
                if not module.startswith(basename):
99
 
                    # Pylint bug
100
 
                    i_base = module.find(basename)
101
 
                    module = module[i_base:]
102
 
                dirname = osp.dirname(self.filename)
103
 
                if module.startswith('.') or module == basename:
104
 
                    modname = osp.join(dirname, module)
105
 
                else:
106
 
                    modname = osp.join(dirname, *module.split('.'))
107
 
                if osp.isdir(modname):
108
 
                    modname = osp.join(modname, '__init__')
109
 
                for ext in ('.py', '.pyw'):
110
 
                    if osp.isfile(modname+ext):
111
 
                        modname = modname + ext
112
 
                        break
113
 
                if osp.isdir(self.filename):
114
 
                    parent = modules.get(modname)
115
 
                    if parent is None:
116
 
                        item = QTreeWidgetItem(title_item, [module],
117
 
                                               QTreeWidgetItem.Type)
118
 
                        item.setIcon(0, get_icon('py.png'))
119
 
                        modules[modname] = item
120
 
                        parent = item
121
 
                else:
122
 
                    parent = title_item
123
 
                if len(msg_id) > 1:
124
 
                    text = "[%s] %d : %s" % (msg_id, lineno, message)
125
 
                else:
126
 
                    text = "%d : %s" % (lineno, message)
127
 
                msg_item = QTreeWidgetItem(parent, [text], QTreeWidgetItem.Type)
128
 
                msg_item.setIcon(0, get_icon('arrow.png'))
129
 
                self.data[msg_item] = (modname, lineno)
130
 
 
131
 
 
132
 
class PylintWidget(QWidget):
133
 
    """
134
 
    Pylint widget
135
 
    """
136
 
    DATAPATH = get_conf_path('.pylint.results')
137
 
    VERSION = '1.1.0'
138
 
    
139
 
    def __init__(self, parent, max_entries=100):
140
 
        QWidget.__init__(self, parent)
141
 
        
142
 
        self.setWindowTitle("Pylint")
143
 
        
144
 
        self.output = None
145
 
        self.error_output = None
146
 
        
147
 
        self.max_entries = max_entries
148
 
        self.rdata = []
149
 
        if osp.isfile(self.DATAPATH):
150
 
            try:
151
 
                data = cPickle.loads(file(self.DATAPATH, 'U').read())
152
 
                if data[0] == self.VERSION:
153
 
                    self.rdata = data[1:]
154
 
            except EOFError:
155
 
                pass
156
 
 
157
 
        self.filecombo = PythonModulesComboBox(self)
158
 
        if self.rdata:
159
 
            self.remove_obsolete_items()
160
 
            self.filecombo.addItems(self.get_filenames())
161
 
        
162
 
        self.start_button = create_toolbutton(self, icon=get_icon('run.png'),
163
 
                                    text=_("Analyze"),
164
 
                                    tip=_("Run analysis"),
165
 
                                    triggered=self.start, text_beside_icon=True)
166
 
        self.stop_button = create_toolbutton(self,
167
 
                                    icon=get_icon('terminate.png'),
168
 
                                    text=_("Stop"),
169
 
                                    tip=_(
170
 
                                                  "Stop current analysis"),
171
 
                                    text_beside_icon=True)
172
 
        self.connect(self.filecombo, SIGNAL('valid(bool)'),
173
 
                     self.start_button.setEnabled)
174
 
        self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data)
175
 
 
176
 
        browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'),
177
 
                               tip=_('Select Python script'),
178
 
                               triggered=self.select_file)
179
 
 
180
 
        self.ratelabel = QLabel()
181
 
        self.datelabel = QLabel()
182
 
        self.log_button = create_toolbutton(self, icon=get_icon('log.png'),
183
 
                                    text=_("Output"),
184
 
                                    text_beside_icon=True,
185
 
                                    tip=_("Complete Pylint output"),
186
 
                                    triggered=self.show_log)
187
 
        self.treewidget = ResultsTree(self)
188
 
        
189
 
        hlayout1 = QHBoxLayout()
190
 
        hlayout1.addWidget(self.filecombo)
191
 
        hlayout1.addWidget(browse_button)
192
 
        hlayout1.addWidget(self.start_button)
193
 
        hlayout1.addWidget(self.stop_button)
194
 
 
195
 
        hlayout2 = QHBoxLayout()
196
 
        hlayout2.addWidget(self.ratelabel)
197
 
        hlayout2.addStretch()
198
 
        hlayout2.addWidget(self.datelabel)
199
 
        hlayout2.addStretch()
200
 
        hlayout2.addWidget(self.log_button)
201
 
        
202
 
        layout = QVBoxLayout()
203
 
        layout.addLayout(hlayout1)
204
 
        layout.addLayout(hlayout2)
205
 
        layout.addWidget(self.treewidget)
206
 
        self.setLayout(layout)
207
 
        
208
 
        self.process = None
209
 
        self.set_running_state(False)
210
 
        
211
 
        if PYLINT_PATH is None:
212
 
            for widget in (self.treewidget, self.filecombo,
213
 
                           self.start_button, self.stop_button):
214
 
                widget.setDisabled(True)
215
 
            if os.name == 'nt' \
216
 
               and programs.is_module_installed("pylint"):
217
 
                # Pylint is installed but pylint script is not in PATH
218
 
                # (AFAIK, could happen only on Windows)
219
 
                text = _('Pylint script was not found. Please add "%s" to PATH.')
220
 
                text = unicode(text) % osp.join(sys.prefix, "Scripts")
221
 
            else:
222
 
                text = _('Please install <b>pylint</b>:')
223
 
                url = 'http://www.logilab.fr'
224
 
                text += ' <a href=%s>%s</a>' % (url, url)
225
 
            self.ratelabel.setText(text)
226
 
        else:
227
 
            self.show_data()
228
 
        
229
 
    def analyze(self, filename):
230
 
        if PYLINT_PATH is None:
231
 
            return
232
 
        filename = unicode(filename) # filename is a QString instance
233
 
        self.kill_if_running()
234
 
        index, _data = self.get_data(filename)
235
 
        if index is None:
236
 
            self.filecombo.addItem(filename)
237
 
            self.filecombo.setCurrentIndex(self.filecombo.count()-1)
238
 
        else:
239
 
            self.filecombo.setCurrentIndex(self.filecombo.findText(filename))
240
 
        self.filecombo.selected()
241
 
        if self.filecombo.is_valid():
242
 
            self.start()
243
 
            
244
 
    def select_file(self):
245
 
        self.emit(SIGNAL('redirect_stdio(bool)'), False)
246
 
        filename, _selfilter = getopenfilename(self, _("Select Python script"),
247
 
                           os.getcwdu(), _("Python scripts")+" (*.py ; *.pyw)")
248
 
        self.emit(SIGNAL('redirect_stdio(bool)'), False)
249
 
        if filename:
250
 
            self.analyze(filename)
251
 
            
252
 
    def remove_obsolete_items(self):
253
 
        """Removing obsolete items"""
254
 
        self.rdata = [(filename, data) for filename, data in self.rdata
255
 
                      if is_module_or_package(filename)]
256
 
        
257
 
    def get_filenames(self):
258
 
        return [filename for filename, _data in self.rdata]
259
 
    
260
 
    def get_data(self, filename):
261
 
        filename = osp.abspath(filename)
262
 
        for index, (fname, data) in enumerate(self.rdata):
263
 
            if fname == filename:
264
 
                return index, data
265
 
        else:
266
 
            return None, None
267
 
            
268
 
    def set_data(self, filename, data):
269
 
        filename = osp.abspath(filename)
270
 
        index, _data = self.get_data(filename)
271
 
        if index is not None:
272
 
            self.rdata.pop(index)
273
 
        self.rdata.append( (filename, data) )
274
 
        self.save()
275
 
        
276
 
    def save(self):
277
 
        while len(self.rdata) > self.max_entries:
278
 
            self.rdata.pop(0)
279
 
        cPickle.dump([self.VERSION]+self.rdata, file(self.DATAPATH, 'w'))
280
 
        
281
 
    def show_log(self):
282
 
        if self.output:
283
 
            TextEditor(self.output, title=_("Pylint output"),
284
 
                       readonly=True, size=(700, 500)).exec_()
285
 
        
286
 
    def start(self):
287
 
        filename = unicode(self.filecombo.currentText())
288
 
        
289
 
        self.process = QProcess(self)
290
 
        self.process.setProcessChannelMode(QProcess.SeparateChannels)
291
 
        self.process.setWorkingDirectory(osp.dirname(filename))
292
 
        self.connect(self.process, SIGNAL("readyReadStandardOutput()"),
293
 
                     self.read_output)
294
 
        self.connect(self.process, SIGNAL("readyReadStandardError()"),
295
 
                     lambda: self.read_output(error=True))
296
 
        self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"),
297
 
                     self.finished)
298
 
        self.connect(self.stop_button, SIGNAL("clicked()"),
299
 
                     self.process.kill)
300
 
        
301
 
        self.output = ''
302
 
        self.error_output = ''
303
 
        p_args = ['-i', 'yes', osp.basename(filename)]
304
 
        self.process.start(PYLINT_PATH, p_args)
305
 
        
306
 
        running = self.process.waitForStarted()
307
 
        self.set_running_state(running)
308
 
        if not running:
309
 
            QMessageBox.critical(self, _("Error"),
310
 
                                 _("Process failed to start"))
311
 
    
312
 
    def set_running_state(self, state=True):
313
 
        self.start_button.setEnabled(not state)
314
 
        self.stop_button.setEnabled(state)
315
 
        
316
 
    def read_output(self, error=False):
317
 
        if error:
318
 
            self.process.setReadChannel(QProcess.StandardError)
319
 
        else:
320
 
            self.process.setReadChannel(QProcess.StandardOutput)
321
 
        bytes = QByteArray()
322
 
        while self.process.bytesAvailable():
323
 
            if error:
324
 
                bytes += self.process.readAllStandardError()
325
 
            else:
326
 
                bytes += self.process.readAllStandardOutput()
327
 
        text = unicode( locale_codec.toUnicode(bytes.data()) )
328
 
        if error:
329
 
            self.error_output += text
330
 
        else:
331
 
            self.output += text
332
 
        
333
 
    def finished(self):
334
 
        self.set_running_state(False)
335
 
        if not self.output:
336
 
            return
337
 
        
338
 
        # Convention, Refactor, Warning, Error
339
 
        results = {'C:': [], 'R:': [], 'W:': [], 'E:': []}
340
 
        txt_module = '************* Module '
341
 
        
342
 
        module = '' # Should not be needed - just in case something goes wrong
343
 
        for line in self.output.splitlines():
344
 
            if line.startswith(txt_module):
345
 
                # New module
346
 
                module = line[len(txt_module):]
347
 
                continue
348
 
            # Supporting option include-ids: ('R3873:' instead of 'R:')
349
 
            if not re.match('^[CRWE]+([0-9]{4})?:', line):
350
 
                continue
351
 
            i1 = line.find(':')
352
 
            if i1 == -1:
353
 
                continue
354
 
            msg_id = line[:i1]
355
 
            i2 = line.find(':', i1+1)
356
 
            if i2 == -1:
357
 
                continue
358
 
            line_nb = line[i1+1:i2].strip()
359
 
            if not line_nb:
360
 
                continue
361
 
            line_nb = int(line_nb.split(',')[0])
362
 
            message = line[i2+1:]
363
 
            item = (module, line_nb, message, msg_id)
364
 
            results[line[0]+':'].append(item)
365
 
            
366
 
        # Rate
367
 
        rate = None
368
 
        txt_rate = 'Your code has been rated at '
369
 
        i_rate = self.output.find(txt_rate)
370
 
        if i_rate > 0:
371
 
            i_rate_end = self.output.find('/10', i_rate)
372
 
            if i_rate_end > 0:
373
 
                rate = self.output[i_rate+len(txt_rate):i_rate_end]
374
 
        
375
 
        # Previous run
376
 
        previous = ''
377
 
        if rate is not None:
378
 
            txt_prun = 'previous run: '
379
 
            i_prun = self.output.find(txt_prun, i_rate_end)
380
 
            if i_prun > 0:
381
 
                i_prun_end = self.output.find('/10', i_prun)
382
 
                previous = self.output[i_prun+len(txt_prun):i_prun_end]
383
 
            
384
 
        
385
 
        filename = unicode(self.filecombo.currentText())
386
 
        self.set_data(filename, (time.localtime(), rate, previous, results))
387
 
        self.output = self.error_output + self.output
388
 
        self.show_data(justanalyzed=True)
389
 
        
390
 
    def kill_if_running(self):
391
 
        if self.process is not None:
392
 
            if self.process.state() == QProcess.Running:
393
 
                self.process.kill()
394
 
                self.process.waitForFinished()
395
 
        
396
 
    def show_data(self, justanalyzed=False):
397
 
        if not justanalyzed:
398
 
            self.output = None
399
 
        self.log_button.setEnabled(self.output is not None \
400
 
                                   and len(self.output) > 0)
401
 
        self.kill_if_running()
402
 
        filename = unicode(self.filecombo.currentText())
403
 
        if not filename:
404
 
            return
405
 
        
406
 
        _index, data = self.get_data(filename)
407
 
        if data is None:
408
 
            text = _('Source code has not been rated yet.')
409
 
            self.treewidget.clear_results()
410
 
            date_text = ''
411
 
        else:
412
 
            datetime, rate, previous_rate, results = data
413
 
            if rate is None:
414
 
                text = _('Analysis did not succeed '
415
 
                         '(see output for more details).')
416
 
                self.treewidget.clear_results()
417
 
                date_text = ''
418
 
            else:
419
 
                text_style = "<span style=\'color: #444444\'><b>%s </b></span>"
420
 
                rate_style = "<span style=\'color: %s\'><b>%s</b></span>"
421
 
                prevrate_style = "<span style=\'color: #666666\'>%s</span>"
422
 
                color = "#FF0000"
423
 
                if float(rate) > 5.:
424
 
                    color = "#22AA22"
425
 
                elif float(rate) > 3.:
426
 
                    color = "#EE5500"
427
 
                text = _('Global evaluation:')
428
 
                text = (text_style % text)+(rate_style % (color,
429
 
                                                          ('%s/10' % rate)))
430
 
                if previous_rate:
431
 
                    text_prun = _('previous run:')
432
 
                    text_prun = ' (%s %s/10)' % (text_prun, previous_rate)
433
 
                    text += prevrate_style % text_prun
434
 
                self.treewidget.set_results(filename, results)
435
 
                date_text = text_style % time.strftime("%d %b %Y %H:%M",
436
 
                                                       datetime)
437
 
            
438
 
        self.ratelabel.setText(text)
439
 
        self.datelabel.setText(date_text)
440
 
 
441
 
 
442
 
def test():
443
 
    """Run pylint widget test"""
444
 
    from spyderlib.utils.qthelpers import qapplication
445
 
    app = qapplication()
446
 
    widget = PylintWidget(None)
447
 
    widget.show()
448
 
    widget.analyze(__file__)
449
 
    sys.exit(app.exec_())
450
 
    
451
 
if __name__ == '__main__':
452
 
    test()
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Copyright © 2009-2010 Pierre Raybaut
 
4
# Licensed under the terms of the MIT License
 
5
# (see spyderlib/__init__.py for details)
 
6
 
 
7
"""Pylint widget"""
 
8
 
 
9
# pylint: disable=C0103
 
10
# pylint: disable=R0903
 
11
# pylint: disable=R0911
 
12
# pylint: disable=R0201
 
13
 
 
14
from __future__ import with_statement
 
15
 
 
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
 
21
 
 
22
import sys
 
23
import os
 
24
import os.path as osp
 
25
import time
 
26
import cPickle
 
27
import re
 
28
 
 
29
# Local imports
 
30
from spyderlib.utils import programs
 
31
from spyderlib.utils.qthelpers import get_icon, create_toolbutton
 
32
from spyderlib.baseconfig import get_conf_path, get_translation
 
33
from spyderlib.widgets.onecolumntree import OneColumnTree
 
34
from spyderlib.widgets.texteditor import TextEditor
 
35
from spyderlib.widgets.comboboxes import (PythonModulesComboBox,
 
36
                                          is_module_or_package)
 
37
_ = get_translation("p_pylint", dirname="spyderplugins")
 
38
 
 
39
 
 
40
PYLINT_PATH = programs.find_program('pylint')
 
41
 
 
42
 
 
43
#TODO: display results on 3 columns instead of 1: msg_id, lineno, message
 
44
class ResultsTree(OneColumnTree):
 
45
    def __init__(self, parent):
 
46
        OneColumnTree.__init__(self, parent)
 
47
        self.filename = None
 
48
        self.results = None
 
49
        self.data = None
 
50
        self.set_title('')
 
51
        
 
52
    def activated(self, item):
 
53
        """Double-click event"""
 
54
        data = self.data.get(item)
 
55
        if data is not None:
 
56
            fname, lineno = data
 
57
            self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"),
 
58
                               fname, lineno, '')
 
59
 
 
60
    def clicked(self, item):
 
61
        """Click event"""
 
62
        self.activated(item)
 
63
        
 
64
    def clear_results(self):
 
65
        self.clear()
 
66
        self.set_title('')
 
67
        
 
68
    def set_results(self, filename, results):
 
69
        self.filename = filename
 
70
        self.results = results
 
71
        self.refresh()
 
72
        
 
73
    def refresh(self):
 
74
        title = _('Results for ')+self.filename
 
75
        self.set_title(title)
 
76
        self.clear()
 
77
        self.data = {}
 
78
        # Populating tree
 
79
        results = ((_('Convention'),
 
80
                    get_icon('convention.png'), self.results['C:']),
 
81
                   (_('Refactor'),
 
82
                    get_icon('refactor.png'), self.results['R:']),
 
83
                   (_('Warning'),
 
84
                    get_icon('warning.png'), self.results['W:']),
 
85
                   (_('Error'),
 
86
                    get_icon('error.png'), self.results['E:']))
 
87
        for title, icon, messages in results:
 
88
            title += ' (%d message%s)' % (len(messages),
 
89
                                          's' if len(messages)>1 else '')
 
90
            title_item = QTreeWidgetItem(self, [title], QTreeWidgetItem.Type)
 
91
            title_item.setIcon(0, icon)
 
92
            if not messages:
 
93
                title_item.setDisabled(True)
 
94
            modules = {}
 
95
            for module, lineno, message, msg_id in messages:
 
96
                basename = osp.splitext(osp.basename(self.filename))[0]
 
97
                if not module.startswith(basename):
 
98
                    # Pylint bug
 
99
                    i_base = module.find(basename)
 
100
                    module = module[i_base:]
 
101
                dirname = osp.dirname(self.filename)
 
102
                if module.startswith('.') or module == basename:
 
103
                    modname = osp.join(dirname, module)
 
104
                else:
 
105
                    modname = osp.join(dirname, *module.split('.'))
 
106
                if osp.isdir(modname):
 
107
                    modname = osp.join(modname, '__init__')
 
108
                for ext in ('.py', '.pyw'):
 
109
                    if osp.isfile(modname+ext):
 
110
                        modname = modname + ext
 
111
                        break
 
112
                if osp.isdir(self.filename):
 
113
                    parent = modules.get(modname)
 
114
                    if parent is None:
 
115
                        item = QTreeWidgetItem(title_item, [module],
 
116
                                               QTreeWidgetItem.Type)
 
117
                        item.setIcon(0, get_icon('py.png'))
 
118
                        modules[modname] = item
 
119
                        parent = item
 
120
                else:
 
121
                    parent = title_item
 
122
                if len(msg_id) > 1:
 
123
                    text = "[%s] %d : %s" % (msg_id, lineno, message)
 
124
                else:
 
125
                    text = "%d : %s" % (lineno, message)
 
126
                msg_item = QTreeWidgetItem(parent, [text], QTreeWidgetItem.Type)
 
127
                msg_item.setIcon(0, get_icon('arrow.png'))
 
128
                self.data[msg_item] = (modname, lineno)
 
129
 
 
130
 
 
131
class PylintWidget(QWidget):
 
132
    """
 
133
    Pylint widget
 
134
    """
 
135
    DATAPATH = get_conf_path('.pylint.results')
 
136
    VERSION = '1.1.0'
 
137
    
 
138
    def __init__(self, parent, max_entries=100):
 
139
        QWidget.__init__(self, parent)
 
140
        
 
141
        self.setWindowTitle("Pylint")
 
142
        
 
143
        self.output = None
 
144
        self.error_output = None
 
145
        
 
146
        self.max_entries = max_entries
 
147
        self.rdata = []
 
148
        if osp.isfile(self.DATAPATH):
 
149
            try:
 
150
                data = cPickle.loads(file(self.DATAPATH, 'U').read())
 
151
                if data[0] == self.VERSION:
 
152
                    self.rdata = data[1:]
 
153
            except EOFError:
 
154
                pass
 
155
 
 
156
        self.filecombo = PythonModulesComboBox(self)
 
157
        if self.rdata:
 
158
            self.remove_obsolete_items()
 
159
            self.filecombo.addItems(self.get_filenames())
 
160
        
 
161
        self.start_button = create_toolbutton(self, icon=get_icon('run.png'),
 
162
                                    text=_("Analyze"),
 
163
                                    tip=_("Run analysis"),
 
164
                                    triggered=self.start, text_beside_icon=True)
 
165
        self.stop_button = create_toolbutton(self,
 
166
                                    icon=get_icon('terminate.png'),
 
167
                                    text=_("Stop"),
 
168
                                    tip=_(
 
169
                                                  "Stop current analysis"),
 
170
                                    text_beside_icon=True)
 
171
        self.connect(self.filecombo, SIGNAL('valid(bool)'),
 
172
                     self.start_button.setEnabled)
 
173
        self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data)
 
174
 
 
175
        browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'),
 
176
                               tip=_('Select Python script'),
 
177
                               triggered=self.select_file)
 
178
 
 
179
        self.ratelabel = QLabel()
 
180
        self.datelabel = QLabel()
 
181
        self.log_button = create_toolbutton(self, icon=get_icon('log.png'),
 
182
                                    text=_("Output"),
 
183
                                    text_beside_icon=True,
 
184
                                    tip=_("Complete Pylint output"),
 
185
                                    triggered=self.show_log)
 
186
        self.treewidget = ResultsTree(self)
 
187
        
 
188
        hlayout1 = QHBoxLayout()
 
189
        hlayout1.addWidget(self.filecombo)
 
190
        hlayout1.addWidget(browse_button)
 
191
        hlayout1.addWidget(self.start_button)
 
192
        hlayout1.addWidget(self.stop_button)
 
193
 
 
194
        hlayout2 = QHBoxLayout()
 
195
        hlayout2.addWidget(self.ratelabel)
 
196
        hlayout2.addStretch()
 
197
        hlayout2.addWidget(self.datelabel)
 
198
        hlayout2.addStretch()
 
199
        hlayout2.addWidget(self.log_button)
 
200
        
 
201
        layout = QVBoxLayout()
 
202
        layout.addLayout(hlayout1)
 
203
        layout.addLayout(hlayout2)
 
204
        layout.addWidget(self.treewidget)
 
205
        self.setLayout(layout)
 
206
        
 
207
        self.process = None
 
208
        self.set_running_state(False)
 
209
        
 
210
        if PYLINT_PATH is None:
 
211
            for widget in (self.treewidget, self.filecombo,
 
212
                           self.start_button, self.stop_button):
 
213
                widget.setDisabled(True)
 
214
            if os.name == 'nt' \
 
215
               and programs.is_module_installed("pylint"):
 
216
                # Pylint is installed but pylint script is not in PATH
 
217
                # (AFAIK, could happen only on Windows)
 
218
                text = _('Pylint script was not found. Please add "%s" to PATH.')
 
219
                text = unicode(text) % osp.join(sys.prefix, "Scripts")
 
220
            else:
 
221
                text = _('Please install <b>pylint</b>:')
 
222
                url = 'http://www.logilab.fr'
 
223
                text += ' <a href=%s>%s</a>' % (url, url)
 
224
            self.ratelabel.setText(text)
 
225
        else:
 
226
            self.show_data()
 
227
        
 
228
    def analyze(self, filename):
 
229
        if PYLINT_PATH is None:
 
230
            return
 
231
        filename = unicode(filename) # filename is a QString instance
 
232
        self.kill_if_running()
 
233
        index, _data = self.get_data(filename)
 
234
        if index is None:
 
235
            self.filecombo.addItem(filename)
 
236
            self.filecombo.setCurrentIndex(self.filecombo.count()-1)
 
237
        else:
 
238
            self.filecombo.setCurrentIndex(self.filecombo.findText(filename))
 
239
        self.filecombo.selected()
 
240
        if self.filecombo.is_valid():
 
241
            self.start()
 
242
            
 
243
    def select_file(self):
 
244
        self.emit(SIGNAL('redirect_stdio(bool)'), False)
 
245
        filename, _selfilter = getopenfilename(self, _("Select Python script"),
 
246
                           os.getcwdu(), _("Python scripts")+" (*.py ; *.pyw)")
 
247
        self.emit(SIGNAL('redirect_stdio(bool)'), False)
 
248
        if filename:
 
249
            self.analyze(filename)
 
250
            
 
251
    def remove_obsolete_items(self):
 
252
        """Removing obsolete items"""
 
253
        self.rdata = [(filename, data) for filename, data in self.rdata
 
254
                      if is_module_or_package(filename)]
 
255
        
 
256
    def get_filenames(self):
 
257
        return [filename for filename, _data in self.rdata]
 
258
    
 
259
    def get_data(self, filename):
 
260
        filename = osp.abspath(filename)
 
261
        for index, (fname, data) in enumerate(self.rdata):
 
262
            if fname == filename:
 
263
                return index, data
 
264
        else:
 
265
            return None, None
 
266
            
 
267
    def set_data(self, filename, data):
 
268
        filename = osp.abspath(filename)
 
269
        index, _data = self.get_data(filename)
 
270
        if index is not None:
 
271
            self.rdata.pop(index)
 
272
        self.rdata.append( (filename, data) )
 
273
        self.save()
 
274
        
 
275
    def save(self):
 
276
        while len(self.rdata) > self.max_entries:
 
277
            self.rdata.pop(0)
 
278
        cPickle.dump([self.VERSION]+self.rdata, file(self.DATAPATH, 'w'))
 
279
        
 
280
    def show_log(self):
 
281
        if self.output:
 
282
            TextEditor(self.output, title=_("Pylint output"),
 
283
                       readonly=True, size=(700, 500)).exec_()
 
284
        
 
285
    def start(self):
 
286
        filename = unicode(self.filecombo.currentText())
 
287
        
 
288
        self.process = QProcess(self)
 
289
        self.process.setProcessChannelMode(QProcess.SeparateChannels)
 
290
        self.process.setWorkingDirectory(osp.dirname(filename))
 
291
        self.connect(self.process, SIGNAL("readyReadStandardOutput()"),
 
292
                     self.read_output)
 
293
        self.connect(self.process, SIGNAL("readyReadStandardError()"),
 
294
                     lambda: self.read_output(error=True))
 
295
        self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"),
 
296
                     self.finished)
 
297
        self.connect(self.stop_button, SIGNAL("clicked()"),
 
298
                     self.process.kill)
 
299
        
 
300
        self.output = ''
 
301
        self.error_output = ''
 
302
        p_args = ['-i', 'yes', osp.basename(filename)]
 
303
        self.process.start(PYLINT_PATH, p_args)
 
304
        
 
305
        running = self.process.waitForStarted()
 
306
        self.set_running_state(running)
 
307
        if not running:
 
308
            QMessageBox.critical(self, _("Error"),
 
309
                                 _("Process failed to start"))
 
310
    
 
311
    def set_running_state(self, state=True):
 
312
        self.start_button.setEnabled(not state)
 
313
        self.stop_button.setEnabled(state)
 
314
        
 
315
    def read_output(self, error=False):
 
316
        if error:
 
317
            self.process.setReadChannel(QProcess.StandardError)
 
318
        else:
 
319
            self.process.setReadChannel(QProcess.StandardOutput)
 
320
        bytes = QByteArray()
 
321
        while self.process.bytesAvailable():
 
322
            if error:
 
323
                bytes += self.process.readAllStandardError()
 
324
            else:
 
325
                bytes += self.process.readAllStandardOutput()
 
326
        text = unicode( locale_codec.toUnicode(bytes.data()) )
 
327
        if error:
 
328
            self.error_output += text
 
329
        else:
 
330
            self.output += text
 
331
        
 
332
    def finished(self):
 
333
        self.set_running_state(False)
 
334
        if not self.output:
 
335
            return
 
336
        
 
337
        # Convention, Refactor, Warning, Error
 
338
        results = {'C:': [], 'R:': [], 'W:': [], 'E:': []}
 
339
        txt_module = '************* Module '
 
340
        
 
341
        module = '' # Should not be needed - just in case something goes wrong
 
342
        for line in self.output.splitlines():
 
343
            if line.startswith(txt_module):
 
344
                # New module
 
345
                module = line[len(txt_module):]
 
346
                continue
 
347
            # Supporting option include-ids: ('R3873:' instead of 'R:')
 
348
            if not re.match('^[CRWE]+([0-9]{4})?:', line):
 
349
                continue
 
350
            i1 = line.find(':')
 
351
            if i1 == -1:
 
352
                continue
 
353
            msg_id = line[:i1]
 
354
            i2 = line.find(':', i1+1)
 
355
            if i2 == -1:
 
356
                continue
 
357
            line_nb = line[i1+1:i2].strip()
 
358
            if not line_nb:
 
359
                continue
 
360
            line_nb = int(line_nb.split(',')[0])
 
361
            message = line[i2+1:]
 
362
            item = (module, line_nb, message, msg_id)
 
363
            results[line[0]+':'].append(item)
 
364
            
 
365
        # Rate
 
366
        rate = None
 
367
        txt_rate = 'Your code has been rated at '
 
368
        i_rate = self.output.find(txt_rate)
 
369
        if i_rate > 0:
 
370
            i_rate_end = self.output.find('/10', i_rate)
 
371
            if i_rate_end > 0:
 
372
                rate = self.output[i_rate+len(txt_rate):i_rate_end]
 
373
        
 
374
        # Previous run
 
375
        previous = ''
 
376
        if rate is not None:
 
377
            txt_prun = 'previous run: '
 
378
            i_prun = self.output.find(txt_prun, i_rate_end)
 
379
            if i_prun > 0:
 
380
                i_prun_end = self.output.find('/10', i_prun)
 
381
                previous = self.output[i_prun+len(txt_prun):i_prun_end]
 
382
            
 
383
        
 
384
        filename = unicode(self.filecombo.currentText())
 
385
        self.set_data(filename, (time.localtime(), rate, previous, results))
 
386
        self.output = self.error_output + self.output
 
387
        self.show_data(justanalyzed=True)
 
388
        
 
389
    def kill_if_running(self):
 
390
        if self.process is not None:
 
391
            if self.process.state() == QProcess.Running:
 
392
                self.process.kill()
 
393
                self.process.waitForFinished()
 
394
        
 
395
    def show_data(self, justanalyzed=False):
 
396
        if not justanalyzed:
 
397
            self.output = None
 
398
        self.log_button.setEnabled(self.output is not None \
 
399
                                   and len(self.output) > 0)
 
400
        self.kill_if_running()
 
401
        filename = unicode(self.filecombo.currentText())
 
402
        if not filename:
 
403
            return
 
404
        
 
405
        _index, data = self.get_data(filename)
 
406
        if data is None:
 
407
            text = _('Source code has not been rated yet.')
 
408
            self.treewidget.clear_results()
 
409
            date_text = ''
 
410
        else:
 
411
            datetime, rate, previous_rate, results = data
 
412
            if rate is None:
 
413
                text = _('Analysis did not succeed '
 
414
                         '(see output for more details).')
 
415
                self.treewidget.clear_results()
 
416
                date_text = ''
 
417
            else:
 
418
                text_style = "<span style=\'color: #444444\'><b>%s </b></span>"
 
419
                rate_style = "<span style=\'color: %s\'><b>%s</b></span>"
 
420
                prevrate_style = "<span style=\'color: #666666\'>%s</span>"
 
421
                color = "#FF0000"
 
422
                if float(rate) > 5.:
 
423
                    color = "#22AA22"
 
424
                elif float(rate) > 3.:
 
425
                    color = "#EE5500"
 
426
                text = _('Global evaluation:')
 
427
                text = (text_style % text)+(rate_style % (color,
 
428
                                                          ('%s/10' % rate)))
 
429
                if previous_rate:
 
430
                    text_prun = _('previous run:')
 
431
                    text_prun = ' (%s %s/10)' % (text_prun, previous_rate)
 
432
                    text += prevrate_style % text_prun
 
433
                self.treewidget.set_results(filename, results)
 
434
                date_text = text_style % time.strftime("%d %b %Y %H:%M",
 
435
                                                       datetime)
 
436
            
 
437
        self.ratelabel.setText(text)
 
438
        self.datelabel.setText(date_text)
 
439
 
 
440
 
 
441
def test():
 
442
    """Run pylint widget test"""
 
443
    from spyderlib.utils.qthelpers import qapplication
 
444
    app = qapplication()
 
445
    widget = PylintWidget(None)
 
446
    widget.show()
 
447
    widget.analyze(__file__)
 
448
    sys.exit(app.exec_())
 
449
    
 
450
if __name__ == '__main__':
 
451
    test()