~ubuntu-branches/ubuntu/trusty/spyder/trusty-backports

« back to all changes in this revision

Viewing changes to spyderplugins/widgets/pylintgui.py

  • Committer: Bazaar Package Importer
  • Author(s): Picca Frédéric-Emmanuel
  • Date: 2011-03-05 18:03:43 UTC
  • mfrom: (1.1.3 upstream)
  • Revision ID: james.westby@ubuntu.com-20110305180343-its88tucbyvtevjf
Tags: 2.0.8-1
* Imported Upstream version 2.0.8 (Closes: #609789)
* add a watch file
* build for all python2 versions (it can be use as module by other packages)
* change the documentation section to Programming/Python
* use the Recommendes found in the documentation.

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-msg=C0103
 
10
# pylint: disable-msg=R0903
 
11
# pylint: disable-msg=R0911
 
12
# pylint: disable-msg=R0201
 
13
 
 
14
from __future__ import with_statement
 
15
 
 
16
try:
 
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
 
22
except ImportError:
 
23
    pass
 
24
 
 
25
from PyQt4.QtGui import (QHBoxLayout, QWidget, QTreeWidgetItem, QMessageBox,
 
26
                         QVBoxLayout, QLabel, QFileDialog)
 
27
from PyQt4.QtCore import SIGNAL, QProcess, QByteArray, QString
 
28
 
 
29
import sys, os, time, cPickle, os.path as osp, re
 
30
 
 
31
# For debugging purpose:
 
32
STDOUT = sys.stdout
 
33
 
 
34
# Local imports
 
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,
 
41
                                          is_module_or_package)
 
42
 
 
43
 
 
44
PYLINT_PATH = programs.get_nt_program_name('pylint')
 
45
 
 
46
def is_pylint_installed():
 
47
    return programs.is_program_installed(PYLINT_PATH)
 
48
 
 
49
class ResultsTree(OneColumnTree):
 
50
    def __init__(self, parent):
 
51
        OneColumnTree.__init__(self, parent)
 
52
        self.filename = None
 
53
        self.results = None
 
54
        self.data = None
 
55
        self.set_title('')
 
56
        
 
57
    def activated(self):
 
58
        data = self.data.get(self.currentItem())
 
59
        if data is not None:
 
60
            fname, lineno = data
 
61
            self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"),
 
62
                               fname, lineno, '')
 
63
        
 
64
    def set_results(self, filename, results):
 
65
        self.filename = filename
 
66
        self.results = results
 
67
        self.refresh()
 
68
        
 
69
    def refresh(self):
 
70
        title = translate('Pylint', 'Results for ')+self.filename
 
71
        self.set_title(title)
 
72
        self.clear()
 
73
        self.data = {}
 
74
        # Populating tree
 
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)
 
88
            if not messages:
 
89
                title_item.setDisabled(True)
 
90
            modules = {}
 
91
            for module, lineno, message in messages:
 
92
                basename = osp.splitext(osp.basename(self.filename))[0]
 
93
                if not module.startswith(basename):
 
94
                    # Pylint bug
 
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)
 
100
                else:
 
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
 
107
                        break
 
108
                if osp.isdir(self.filename):
 
109
                    parent = modules.get(modname)
 
110
                    if parent is None:
 
111
                        item = QTreeWidgetItem(title_item, [module],
 
112
                                               QTreeWidgetItem.Type)
 
113
                        item.setIcon(0, get_icon('py.png'))
 
114
                        modules[modname] = item
 
115
                        parent = item
 
116
                else:
 
117
                    parent = title_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)
 
123
 
 
124
 
 
125
class PylintWidget(QWidget):
 
126
    """
 
127
    Pylint widget
 
128
    """
 
129
    DATAPATH = get_conf_path('.pylint.results')
 
130
    VERSION = '1.0.2'
 
131
    
 
132
    def __init__(self, parent, max_entries=100):
 
133
        QWidget.__init__(self, parent)
 
134
        
 
135
        self.setWindowTitle("Pylint")
 
136
        
 
137
        self.output = None
 
138
        self.error_output = None
 
139
        
 
140
        self.max_entries = max_entries
 
141
        self.rdata = []
 
142
        if osp.isfile(self.DATAPATH):
 
143
            try:
 
144
                data = cPickle.loads(file(self.DATAPATH, 'U').read())
 
145
                if data[0] == self.VERSION:
 
146
                    self.rdata = data[1:]
 
147
            except EOFError:
 
148
                pass
 
149
 
 
150
        self.filecombo = PythonModulesComboBox(self)
 
151
        if self.rdata:
 
152
            self.remove_obsolete_items()
 
153
            self.filecombo.addItems(self.get_filenames())
 
154
        
 
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)
 
168
 
 
169
        browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'),
 
170
                               tip=translate('Pylint', 'Select Python script'),
 
171
                               triggered=self.select_file)
 
172
 
 
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)
 
182
        
 
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)
 
188
 
 
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)
 
195
        
 
196
        layout = QVBoxLayout()
 
197
        layout.addLayout(hlayout1)
 
198
        layout.addLayout(hlayout2)
 
199
        layout.addWidget(self.treewidget)
 
200
        self.setLayout(layout)
 
201
        
 
202
        self.process = None
 
203
        self.set_running_state(False)
 
204
        
 
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)
 
209
            if os.name == 'nt' \
 
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")
 
216
            else:
 
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)
 
221
        else:
 
222
            self.show_data()
 
223
        
 
224
    def analyze(self, filename):
 
225
        if not is_pylint_installed():
 
226
            return
 
227
        filename = unicode(filename) # filename is a QString instance
 
228
        self.kill_if_running()
 
229
        index, _data = self.get_data(filename)
 
230
        if index is None:
 
231
            self.filecombo.addItem(filename)
 
232
            self.filecombo.setCurrentIndex(self.filecombo.count()-1)
 
233
        else:
 
234
            self.filecombo.setCurrentIndex(self.filecombo.findText(filename))
 
235
        self.filecombo.selected()
 
236
        if self.filecombo.is_valid():
 
237
            self.start()
 
238
            
 
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)
 
245
        if filename:
 
246
            self.analyze(filename)
 
247
            
 
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)]
 
252
        
 
253
    def get_filenames(self):
 
254
        return [filename for filename, _data in self.rdata]
 
255
    
 
256
    def get_data(self, filename):
 
257
        filename = osp.abspath(filename)
 
258
        for index, (fname, data) in enumerate(self.rdata):
 
259
            if fname == filename:
 
260
                return index, data
 
261
        else:
 
262
            return None, None
 
263
            
 
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) )
 
270
        self.save()
 
271
        
 
272
    def save(self):
 
273
        while len(self.rdata) > self.max_entries:
 
274
            self.rdata.pop(0)
 
275
        cPickle.dump([self.VERSION]+self.rdata, file(self.DATAPATH, 'w'))
 
276
        
 
277
    def show_log(self):
 
278
        if self.output:
 
279
            TextEditor(self.output, title=translate('Pylint', "Pylint output"),
 
280
                       readonly=True, size=(700, 500)).exec_()
 
281
        
 
282
    def start(self):
 
283
        filename = unicode(self.filecombo.currentText())
 
284
        
 
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()"),
 
289
                     self.read_output)
 
290
        self.connect(self.process, SIGNAL("readyReadStandardError()"),
 
291
                     lambda: self.read_output(error=True))
 
292
        self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"),
 
293
                     self.finished)
 
294
        self.connect(self.stop_button, SIGNAL("clicked()"),
 
295
                     self.process.kill)
 
296
        
 
297
        self.output = ''
 
298
        self.error_output = ''
 
299
        p_args = [osp.basename(filename)]
 
300
        self.process.start(PYLINT_PATH, p_args)
 
301
        
 
302
        running = self.process.waitForStarted()
 
303
        self.set_running_state(running)
 
304
        if not running:
 
305
            QMessageBox.critical(self, translate('Pylint', "Error"),
 
306
                                 translate('Pylint', "Process failed to start"))
 
307
    
 
308
    def set_running_state(self, state=True):
 
309
        self.start_button.setEnabled(not state)
 
310
        self.stop_button.setEnabled(state)
 
311
        
 
312
    def read_output(self, error=False):
 
313
        if error:
 
314
            self.process.setReadChannel(QProcess.StandardError)
 
315
        else:
 
316
            self.process.setReadChannel(QProcess.StandardOutput)
 
317
        bytes = QByteArray()
 
318
        while self.process.bytesAvailable():
 
319
            if error:
 
320
                bytes += self.process.readAllStandardError()
 
321
            else:
 
322
                bytes += self.process.readAllStandardOutput()
 
323
        text = unicode( QString.fromLocal8Bit(bytes.data()) )
 
324
        if error:
 
325
            self.error_output += text
 
326
        else:
 
327
            self.output += text
 
328
        
 
329
    def finished(self):
 
330
        self.set_running_state(False)
 
331
        if not self.output:
 
332
            return
 
333
        
 
334
        # Convention, Refactor, Warning, Error
 
335
        results = {'C:': [], 'R:': [], 'W:': [], 'E:': []}
 
336
        txt_module = '************* Module '
 
337
        
 
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):
 
341
                # New module
 
342
                module = line[len(txt_module):]
 
343
                continue
 
344
            # Supporting option include-ids: ('R3873:' instead of 'R:')
 
345
            if not re.match('^[CRWE]+([0-9]{4})?:', line):
 
346
                continue
 
347
            i1 = line.find(':')
 
348
            if i1 == -1:
 
349
                continue
 
350
            i2 = line.find(':', i1+1)
 
351
            if i2 == -1:
 
352
                continue
 
353
            line_nb = line[i1+1:i2].strip()
 
354
            if not line_nb:
 
355
                continue
 
356
            line_nb = int(line_nb)
 
357
            message = line[i2+1:]
 
358
            item = (module, line_nb, message)
 
359
            results[line[0]+':'].append(item)                
 
360
            
 
361
        # Rate
 
362
        rate = None
 
363
        txt_rate = 'Your code has been rated at '
 
364
        i_rate = self.output.find(txt_rate)
 
365
        if i_rate > 0:
 
366
            i_rate_end = self.output.find('/10', i_rate)
 
367
            if i_rate_end > 0:
 
368
                rate = self.output[i_rate+len(txt_rate):i_rate_end]
 
369
        
 
370
        # Previous run
 
371
        previous = ''
 
372
        if rate is not None:
 
373
            txt_prun = 'previous run: '
 
374
            i_prun = self.output.find(txt_prun, i_rate_end)
 
375
            if i_prun > 0:
 
376
                i_prun_end = self.output.find('/10', i_prun)
 
377
                previous = self.output[i_prun+len(txt_prun):i_prun_end]
 
378
            
 
379
        
 
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)
 
384
        
 
385
    def kill_if_running(self):
 
386
        if self.process is not None:
 
387
            if self.process.state() == QProcess.Running:
 
388
                self.process.kill()
 
389
                self.process.waitForFinished()
 
390
        
 
391
    def show_data(self, justanalyzed=False):
 
392
        if not justanalyzed:
 
393
            self.output = None
 
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())
 
398
        if not filename:
 
399
            return
 
400
        
 
401
        _index, data = self.get_data(filename)
 
402
        if data is None:
 
403
            text = translate('Pylint', 'Source code has not been rated yet.')
 
404
            self.treewidget.clear()
 
405
            date_text = ''
 
406
        else:
 
407
            datetime, rate, previous_rate, results = data
 
408
            if rate is None:
 
409
                text = translate('Pylint', 'Analysis did not succeed '
 
410
                                           '(see output for more details).')
 
411
                self.treewidget.clear()
 
412
                date_text = ''
 
413
            else:
 
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>"
 
417
                color = "#FF0000"
 
418
                if float(rate) > 5.:
 
419
                    color = "#22AA22"
 
420
                elif float(rate) > 3.:
 
421
                    color = "#EE5500"
 
422
                text = translate('Pylint', 'Global evaluation:')
 
423
                text = (text_style % text)+(rate_style % (color,
 
424
                                                          ('%s/10' % rate)))
 
425
                if previous_rate:
 
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",
 
431
                                                       datetime)
 
432
            
 
433
        self.ratelabel.setText(text)
 
434
        self.datelabel.setText(date_text)
 
435
 
 
436
 
 
437
def test():
 
438
    """Run pylint widget test"""
 
439
    from spyderlib.utils.qthelpers import qapplication
 
440
    app = qapplication()
 
441
    widget = PylintWidget(None)
 
442
    widget.show()
 
443
    widget.analyze(__file__)
 
444
    sys.exit(app.exec_())
 
445
    
 
446
if __name__ == '__main__':
 
447
    test()