~ubuntu-branches/ubuntu/wily/spyder/wily

« back to all changes in this revision

Viewing changes to .pc/backport-pylint3-support.patch/spyderplugins/widgets/pylintgui.py

  • Committer: Package Import Robot
  • Author(s): Benjamin Drung
  • Date: 2015-01-15 12:20:11 UTC
  • mfrom: (18.1.7 experimental)
  • Revision ID: package-import@ubuntu.com-20150115122011-cc7j5dhy2h9uo13m
Tags: 2.3.2+dfsg-1ubuntu1
Backport patch to support pylint3.

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, print_function
 
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 re
 
27
import subprocess
 
28
 
 
29
# Local imports
 
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,
 
38
                                          is_module_or_package)
 
39
from spyderlib.py3compat import to_text_string, getcwd, pickle
 
40
_ = get_translation("p_pylint", dirname="spyderplugins")
 
41
 
 
42
 
 
43
PYLINT_PATH = programs.find_program('pylint')
 
44
 
 
45
 
 
46
def get_pylint_version():
 
47
    """Return pylint version"""
 
48
    global PYLINT_PATH
 
49
    if PYLINT_PATH is None:
 
50
        return
 
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()
 
56
    if lines:
 
57
        match = re.match('(pylint|pylint-script.py) ([0-9\.]*)', lines[0])
 
58
        if match is not None:
 
59
            return match.groups()[1]
 
60
 
 
61
 
 
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)
 
66
 
 
67
 
 
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)
 
72
        self.filename = None
 
73
        self.results = None
 
74
        self.data = None
 
75
        self.set_title('')
 
76
        
 
77
    def activated(self, item):
 
78
        """Double-click event"""
 
79
        data = self.data.get(id(item))
 
80
        if data is not None:
 
81
            fname, lineno = data
 
82
            self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"),
 
83
                               fname, lineno, '')
 
84
 
 
85
    def clicked(self, item):
 
86
        """Click event"""
 
87
        self.activated(item)
 
88
        
 
89
    def clear_results(self):
 
90
        self.clear()
 
91
        self.set_title('')
 
92
        
 
93
    def set_results(self, filename, results):
 
94
        self.filename = filename
 
95
        self.results = results
 
96
        self.refresh()
 
97
        
 
98
    def refresh(self):
 
99
        title = _('Results for ')+self.filename
 
100
        self.set_title(title)
 
101
        self.clear()
 
102
        self.data = {}
 
103
        # Populating tree
 
104
        results = ((_('Convention'),
 
105
                    get_icon('convention.png'), self.results['C:']),
 
106
                   (_('Refactor'),
 
107
                    get_icon('refactor.png'), self.results['R:']),
 
108
                   (_('Warning'),
 
109
                    get_icon('warning.png'), self.results['W:']),
 
110
                   (_('Error'),
 
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)
 
117
            if not messages:
 
118
                title_item.setDisabled(True)
 
119
            modules = {}
 
120
            for module, lineno, message, msg_id in messages:
 
121
                basename = osp.splitext(osp.basename(self.filename))[0]
 
122
                if not module.startswith(basename):
 
123
                    # Pylint bug
 
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)
 
129
                else:
 
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
 
136
                        break
 
137
                if osp.isdir(self.filename):
 
138
                    parent = modules.get(modname)
 
139
                    if parent is None:
 
140
                        item = QTreeWidgetItem(title_item, [module],
 
141
                                               QTreeWidgetItem.Type)
 
142
                        item.setIcon(0, get_icon('py.png'))
 
143
                        modules[modname] = item
 
144
                        parent = item
 
145
                else:
 
146
                    parent = title_item
 
147
                if len(msg_id) > 1:
 
148
                    text = "[%s] %d : %s" % (msg_id, lineno, message)
 
149
                else:
 
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)
 
154
 
 
155
 
 
156
class PylintWidget(QWidget):
 
157
    """
 
158
    Pylint widget
 
159
    """
 
160
    DATAPATH = get_conf_path('pylint.results')
 
161
    VERSION = '1.1.0'
 
162
    
 
163
    def __init__(self, parent, max_entries=100):
 
164
        QWidget.__init__(self, parent)
 
165
        
 
166
        self.setWindowTitle("Pylint")
 
167
        
 
168
        self.output = None
 
169
        self.error_output = None
 
170
        
 
171
        self.max_entries = max_entries
 
172
        self.rdata = []
 
173
        if osp.isfile(self.DATAPATH):
 
174
            try:
 
175
                data = pickle.loads(open(self.DATAPATH, 'rb').read())
 
176
                if data[0] == self.VERSION:
 
177
                    self.rdata = data[1:]
 
178
            except (EOFError, ImportError):
 
179
                pass
 
180
 
 
181
        self.filecombo = PythonModulesComboBox(self)
 
182
        if self.rdata:
 
183
            self.remove_obsolete_items()
 
184
            self.filecombo.addItems(self.get_filenames())
 
185
        
 
186
        self.start_button = create_toolbutton(self, icon=get_icon('run.png'),
 
187
                                    text=_("Analyze"),
 
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'),
 
192
                                             text=_("Stop"),
 
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)
 
198
 
 
199
        browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'),
 
200
                               tip=_('Select Python file'),
 
201
                               triggered=self.select_file)
 
202
 
 
203
        self.ratelabel = QLabel()
 
204
        self.datelabel = QLabel()
 
205
        self.log_button = create_toolbutton(self, icon=get_icon('log.png'),
 
206
                                    text=_("Output"),
 
207
                                    text_beside_icon=True,
 
208
                                    tip=_("Complete output"),
 
209
                                    triggered=self.show_log)
 
210
        self.treewidget = ResultsTree(self)
 
211
        
 
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)
 
217
 
 
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)
 
224
        
 
225
        layout = QVBoxLayout()
 
226
        layout.addLayout(hlayout1)
 
227
        layout.addLayout(hlayout2)
 
228
        layout.addWidget(self.treewidget)
 
229
        self.setLayout(layout)
 
230
        
 
231
        self.process = None
 
232
        self.set_running_state(False)
 
233
        
 
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)
 
238
            if os.name == 'nt' \
 
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")
 
244
            else:
 
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)
 
249
        else:
 
250
            self.show_data()
 
251
        
 
252
    def analyze(self, filename):
 
253
        if PYLINT_PATH is None:
 
254
            return
 
255
        filename = to_text_string(filename) # filename is a QString instance
 
256
        self.kill_if_running()
 
257
        index, _data = self.get_data(filename)
 
258
        if index is None:
 
259
            self.filecombo.addItem(filename)
 
260
            self.filecombo.setCurrentIndex(self.filecombo.count()-1)
 
261
        else:
 
262
            self.filecombo.setCurrentIndex(self.filecombo.findText(filename))
 
263
        self.filecombo.selected()
 
264
        if self.filecombo.is_valid():
 
265
            self.start()
 
266
            
 
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)
 
272
        if filename:
 
273
            self.analyze(filename)
 
274
            
 
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)]
 
279
        
 
280
    def get_filenames(self):
 
281
        return [filename for filename, _data in self.rdata]
 
282
    
 
283
    def get_data(self, filename):
 
284
        filename = osp.abspath(filename)
 
285
        for index, (fname, data) in enumerate(self.rdata):
 
286
            if fname == filename:
 
287
                return index, data
 
288
        else:
 
289
            return None, None
 
290
            
 
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))
 
297
        self.save()
 
298
        
 
299
    def save(self):
 
300
        while len(self.rdata) > self.max_entries:
 
301
            self.rdata.pop(-1)
 
302
        pickle.dump([self.VERSION]+self.rdata, open(self.DATAPATH, 'wb'), 2)
 
303
        
 
304
    def show_log(self):
 
305
        if self.output:
 
306
            TextEditor(self.output, title=_("Pylint output"),
 
307
                       readonly=True, size=(700, 500)).exec_()
 
308
        
 
309
    def start(self):
 
310
        filename = to_text_string(self.filecombo.currentText())
 
311
        
 
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()"),
 
316
                     self.read_output)
 
317
        self.connect(self.process, SIGNAL("readyReadStandardError()"),
 
318
                     lambda: self.read_output(error=True))
 
319
        self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"),
 
320
                     self.finished)
 
321
        self.connect(self.stop_button, SIGNAL("clicked()"),
 
322
                     self.process.kill)
 
323
        
 
324
        self.output = ''
 
325
        self.error_output = ''
 
326
        
 
327
        plver = PYLINT_VER
 
328
        if plver is not None:
 
329
            if plver.split('.')[0] == '0':
 
330
                p_args = ['-i', 'yes']
 
331
            else:
 
332
                # Option '-i' (alias for '--include-ids') was removed in pylint
 
333
                # 1.0
 
334
                p_args = ["--msg-template='{msg_id}:{line:3d},"\
 
335
                          "{column}: {obj}: {msg}"]
 
336
            p_args += [osp.basename(filename)]
 
337
        else:
 
338
            p_args = [osp.basename(filename)]
 
339
        self.process.start(PYLINT_PATH, p_args)
 
340
        
 
341
        running = self.process.waitForStarted()
 
342
        self.set_running_state(running)
 
343
        if not running:
 
344
            QMessageBox.critical(self, _("Error"),
 
345
                                 _("Process failed to start"))
 
346
    
 
347
    def set_running_state(self, state=True):
 
348
        self.start_button.setEnabled(not state)
 
349
        self.stop_button.setEnabled(state)
 
350
        
 
351
    def read_output(self, error=False):
 
352
        if error:
 
353
            self.process.setReadChannel(QProcess.StandardError)
 
354
        else:
 
355
            self.process.setReadChannel(QProcess.StandardOutput)
 
356
        qba = QByteArray()
 
357
        while self.process.bytesAvailable():
 
358
            if error:
 
359
                qba += self.process.readAllStandardError()
 
360
            else:
 
361
                qba += self.process.readAllStandardOutput()
 
362
        text = to_text_string( locale_codec.toUnicode(qba.data()) )
 
363
        if error:
 
364
            self.error_output += text
 
365
        else:
 
366
            self.output += text
 
367
        
 
368
    def finished(self):
 
369
        self.set_running_state(False)
 
370
        if not self.output:
 
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)
 
374
            return
 
375
        
 
376
        # Convention, Refactor, Warning, Error
 
377
        results = {'C:': [], 'R:': [], 'W:': [], 'E:': []}
 
378
        txt_module = '************* Module '
 
379
        
 
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):
 
383
                # New module
 
384
                module = line[len(txt_module):]
 
385
                continue
 
386
            # Supporting option include-ids: ('R3873:' instead of 'R:')
 
387
            if not re.match('^[CRWE]+([0-9]{4})?:', line):
 
388
                continue
 
389
            i1 = line.find(':')
 
390
            if i1 == -1:
 
391
                continue
 
392
            msg_id = line[:i1]
 
393
            i2 = line.find(':', i1+1)
 
394
            if i2 == -1:
 
395
                continue
 
396
            line_nb = line[i1+1:i2].strip()
 
397
            if not line_nb:
 
398
                continue
 
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)
 
403
            
 
404
        # Rate
 
405
        rate = None
 
406
        txt_rate = 'Your code has been rated at '
 
407
        i_rate = self.output.find(txt_rate)
 
408
        if i_rate > 0:
 
409
            i_rate_end = self.output.find('/10', i_rate)
 
410
            if i_rate_end > 0:
 
411
                rate = self.output[i_rate+len(txt_rate):i_rate_end]
 
412
        
 
413
        # Previous run
 
414
        previous = ''
 
415
        if rate is not None:
 
416
            txt_prun = 'previous run: '
 
417
            i_prun = self.output.find(txt_prun, i_rate_end)
 
418
            if i_prun > 0:
 
419
                i_prun_end = self.output.find('/10', i_prun)
 
420
                previous = self.output[i_prun+len(txt_prun):i_prun_end]
 
421
            
 
422
        
 
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)
 
427
        
 
428
    def kill_if_running(self):
 
429
        if self.process is not None:
 
430
            if self.process.state() == QProcess.Running:
 
431
                self.process.kill()
 
432
                self.process.waitForFinished()
 
433
        
 
434
    def show_data(self, justanalyzed=False):
 
435
        if not justanalyzed:
 
436
            self.output = None
 
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())
 
441
        if not filename:
 
442
            return
 
443
        
 
444
        _index, data = self.get_data(filename)
 
445
        if data is None:
 
446
            text = _('Source code has not been rated yet.')
 
447
            self.treewidget.clear_results()
 
448
            date_text = ''
 
449
        else:
 
450
            datetime, rate, previous_rate, results = data
 
451
            if rate is None:
 
452
                text = _('Analysis did not succeed '
 
453
                         '(see output for more details).')
 
454
                self.treewidget.clear_results()
 
455
                date_text = ''
 
456
            else:
 
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>"
 
460
                color = "#FF0000"
 
461
                if float(rate) > 5.:
 
462
                    color = "#22AA22"
 
463
                elif float(rate) > 3.:
 
464
                    color = "#EE5500"
 
465
                text = _('Global evaluation:')
 
466
                text = (text_style % text)+(rate_style % (color,
 
467
                                                          ('%s/10' % rate)))
 
468
                if previous_rate:
 
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),
 
474
                                      encoding='utf8')
 
475
                date_text = text_style % date
 
476
            
 
477
        self.ratelabel.setText(text)
 
478
        self.datelabel.setText(date_text)
 
479
 
 
480
 
 
481
def test():
 
482
    """Run pylint widget test"""
 
483
    from spyderlib.utils.qthelpers import qapplication
 
484
    app = qapplication()
 
485
    widget = PylintWidget(None)
 
486
    widget.show()
 
487
    widget.analyze(__file__)
 
488
    sys.exit(app.exec_())
 
489
    
 
490
if __name__ == '__main__':
 
491
    test()