# -*- coding: utf-8 -*- # # Copyright © 2009-2010 Pierre Raybaut # Licensed under the terms of the MIT License # (see spyderlib/__init__.py for details) """Pylint widget""" # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 from __future__ import with_statement, print_function from spyderlib.qt.QtGui import (QHBoxLayout, QWidget, QTreeWidgetItem, QMessageBox, QVBoxLayout, QLabel) from spyderlib.qt.QtCore import SIGNAL, QProcess, QByteArray, QTextCodec locale_codec = QTextCodec.codecForLocale() from spyderlib.qt.compat import getopenfilename import sys import os import os.path as osp import time import re import subprocess # Local imports from spyderlib import dependencies from spyderlib.utils import programs from spyderlib.utils.encoding import to_unicode_from_fs from spyderlib.utils.qthelpers import get_icon, create_toolbutton from spyderlib.baseconfig import get_conf_path, get_translation from spyderlib.widgets.onecolumntree import OneColumnTree from spyderlib.widgets.texteditor import TextEditor from spyderlib.widgets.comboboxes import (PythonModulesComboBox, is_module_or_package) from spyderlib.py3compat import PY3, to_text_string, getcwd, pickle _ = get_translation("p_pylint", dirname="spyderplugins") PYLINT = 'pylint' if PY3: if programs.find_program('pylint3'): PYLINT = 'pylint3' elif programs.find_program('python3-pylint'): PYLINT = 'python3-pylint' PYLINT_PATH = programs.find_program(PYLINT) def get_pylint_version(): """Return pylint version""" global PYLINT_PATH if PYLINT_PATH is None: return process = subprocess.Popen([PYLINT, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=osp.dirname(PYLINT_PATH), shell=True if os.name == 'nt' else False) lines = to_unicode_from_fs(process.stdout.read()).splitlines() if lines: regex = '({0}*|pylint-script.py) ([0-9\.]*)'.format(PYLINT) match = re.match(regex, lines[0]) if match is not None: return match.groups()[1] PYLINT_REQVER = '>=0.25' PYLINT_VER = get_pylint_version() dependencies.add("pylint", _("Static code analysis"), required_version=PYLINT_REQVER, installed_version=PYLINT_VER) #TODO: display results on 3 columns instead of 1: msg_id, lineno, message class ResultsTree(OneColumnTree): def __init__(self, parent): OneColumnTree.__init__(self, parent) self.filename = None self.results = None self.data = None self.set_title('') def activated(self, item): """Double-click event""" data = self.data.get(id(item)) if data is not None: fname, lineno = data self.parent().emit(SIGNAL("edit_goto(QString,int,QString)"), fname, lineno, '') def clicked(self, item): """Click event""" self.activated(item) def clear_results(self): self.clear() self.set_title('') def set_results(self, filename, results): self.filename = filename self.results = results self.refresh() def refresh(self): title = _('Results for ')+self.filename self.set_title(title) self.clear() self.data = {} # Populating tree results = ((_('Convention'), get_icon('convention.png'), self.results['C:']), (_('Refactor'), get_icon('refactor.png'), self.results['R:']), (_('Warning'), get_icon('warning.png'), self.results['W:']), (_('Error'), get_icon('error.png'), self.results['E:'])) for title, icon, messages in results: title += ' (%d message%s)' % (len(messages), 's' if len(messages)>1 else '') title_item = QTreeWidgetItem(self, [title], QTreeWidgetItem.Type) title_item.setIcon(0, icon) if not messages: title_item.setDisabled(True) modules = {} for module, lineno, message, msg_id in messages: basename = osp.splitext(osp.basename(self.filename))[0] if not module.startswith(basename): # Pylint bug i_base = module.find(basename) module = module[i_base:] dirname = osp.dirname(self.filename) if module.startswith('.') or module == basename: modname = osp.join(dirname, module) else: modname = osp.join(dirname, *module.split('.')) if osp.isdir(modname): modname = osp.join(modname, '__init__') for ext in ('.py', '.pyw'): if osp.isfile(modname+ext): modname = modname + ext break if osp.isdir(self.filename): parent = modules.get(modname) if parent is None: item = QTreeWidgetItem(title_item, [module], QTreeWidgetItem.Type) item.setIcon(0, get_icon('py.png')) modules[modname] = item parent = item else: parent = title_item if len(msg_id) > 1: text = "[%s] %d : %s" % (msg_id, lineno, message) else: text = "%d : %s" % (lineno, message) msg_item = QTreeWidgetItem(parent, [text], QTreeWidgetItem.Type) msg_item.setIcon(0, get_icon('arrow.png')) self.data[id(msg_item)] = (modname, lineno) class PylintWidget(QWidget): """ Pylint widget """ DATAPATH = get_conf_path('pylint.results') VERSION = '1.1.0' def __init__(self, parent, max_entries=100): QWidget.__init__(self, parent) self.setWindowTitle("Pylint") self.output = None self.error_output = None self.max_entries = max_entries self.rdata = [] if osp.isfile(self.DATAPATH): try: data = pickle.loads(open(self.DATAPATH, 'rb').read()) if data[0] == self.VERSION: self.rdata = data[1:] except (EOFError, ImportError): pass self.filecombo = PythonModulesComboBox(self) if self.rdata: self.remove_obsolete_items() self.filecombo.addItems(self.get_filenames()) self.start_button = create_toolbutton(self, icon=get_icon('run.png'), text=_("Analyze"), tip=_("Run analysis"), triggered=self.start, text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=get_icon('stop.png'), text=_("Stop"), tip=_("Stop current analysis"), text_beside_icon=True) self.connect(self.filecombo, SIGNAL('valid(bool)'), self.start_button.setEnabled) self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'), tip=_('Select Python file'), triggered=self.select_file) self.ratelabel = QLabel() self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=get_icon('log.png'), text=_("Output"), text_beside_icon=True, tip=_("Complete output"), triggered=self.show_log) self.treewidget = ResultsTree(self) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.ratelabel) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.treewidget) self.setLayout(layout) self.process = None self.set_running_state(False) if PYLINT_PATH is None: for widget in (self.treewidget, self.filecombo, self.start_button, self.stop_button): widget.setDisabled(True) if os.name == 'nt' \ and programs.is_module_installed("pylint"): # Pylint is installed but pylint script is not in PATH # (AFAIK, could happen only on Windows) text = _('Pylint script was not found. Please add "%s" to PATH.') text = to_text_string(text) % osp.join(sys.prefix, "Scripts") else: text = _('Please install pylint:') url = 'http://www.logilab.fr' text += ' %s' % (url, url) self.ratelabel.setText(text) else: self.show_data() def analyze(self, filename): if PYLINT_PATH is None: return filename = to_text_string(filename) # filename is a QString instance self.kill_if_running() index, _data = self.get_data(filename) if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count()-1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): self.start() def select_file(self): self.emit(SIGNAL('redirect_stdio(bool)'), False) filename, _selfilter = getopenfilename(self, _("Select Python file"), getcwd(), _("Python files")+" (*.py ; *.pyw)") self.emit(SIGNAL('redirect_stdio(bool)'), False) if filename: self.analyze(filename) def remove_obsolete_items(self): """Removing obsolete items""" self.rdata = [(filename, data) for filename, data in self.rdata if is_module_or_package(filename)] def get_filenames(self): return [filename for filename, _data in self.rdata] def get_data(self, filename): filename = osp.abspath(filename) for index, (fname, data) in enumerate(self.rdata): if fname == filename: return index, data else: return None, None def set_data(self, filename, data): filename = osp.abspath(filename) index, _data = self.get_data(filename) if index is not None: self.rdata.pop(index) self.rdata.insert(0, (filename, data)) self.save() def save(self): while len(self.rdata) > self.max_entries: self.rdata.pop(-1) pickle.dump([self.VERSION]+self.rdata, open(self.DATAPATH, 'wb'), 2) def show_log(self): if self.output: TextEditor(self.output, title=_("Pylint output"), readonly=True, size=(700, 500)).exec_() def start(self): filename = to_text_string(self.filecombo.currentText()) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(osp.dirname(filename)) self.connect(self.process, SIGNAL("readyReadStandardOutput()"), self.read_output) self.connect(self.process, SIGNAL("readyReadStandardError()"), lambda: self.read_output(error=True)) self.connect(self.process, SIGNAL("finished(int,QProcess::ExitStatus)"), self.finished) self.connect(self.stop_button, SIGNAL("clicked()"), self.process.kill) self.output = '' self.error_output = '' plver = PYLINT_VER if plver is not None: if plver.split('.')[0] == '0': p_args = ['-i', 'yes'] else: # Option '-i' (alias for '--include-ids') was removed in pylint # 1.0 p_args = ["--msg-template='{msg_id}:{line:3d},"\ "{column}: {obj}: {msg}"] p_args += [osp.basename(filename)] else: p_args = [osp.basename(filename)] self.process.start(PYLINT_PATH, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string( locale_codec.toUnicode(qba.data()) ) if error: self.error_output += text else: self.output += text def finished(self): self.set_running_state(False) if not self.output: if self.error_output: QMessageBox.critical(self, _("Error"), self.error_output) print("pylint error:\n\n" + self.error_output, file=sys.stderr) return # Convention, Refactor, Warning, Error results = {'C:': [], 'R:': [], 'W:': [], 'E:': []} txt_module = '************* Module ' module = '' # Should not be needed - just in case something goes wrong for line in self.output.splitlines(): if line.startswith(txt_module): # New module module = line[len(txt_module):] continue # Supporting option include-ids: ('R3873:' instead of 'R:') if not re.match('^[CRWE]+([0-9]{4})?:', line): continue i1 = line.find(':') if i1 == -1: continue msg_id = line[:i1] i2 = line.find(':', i1+1) if i2 == -1: continue line_nb = line[i1+1:i2].strip() if not line_nb: continue line_nb = int(line_nb.split(',')[0]) message = line[i2+1:] item = (module, line_nb, message, msg_id) results[line[0]+':'].append(item) # Rate rate = None txt_rate = 'Your code has been rated at ' i_rate = self.output.find(txt_rate) if i_rate > 0: i_rate_end = self.output.find('/10', i_rate) if i_rate_end > 0: rate = self.output[i_rate+len(txt_rate):i_rate_end] # Previous run previous = '' if rate is not None: txt_prun = 'previous run: ' i_prun = self.output.find(txt_prun, i_rate_end) if i_prun > 0: i_prun_end = self.output.find('/10', i_prun) previous = self.output[i_prun+len(txt_prun):i_prun_end] filename = to_text_string(self.filecombo.currentText()) self.set_data(filename, (time.localtime(), rate, previous, results)) self.output = self.error_output + self.output self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None \ and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return _index, data = self.get_data(filename) if data is None: text = _('Source code has not been rated yet.') self.treewidget.clear_results() date_text = '' else: datetime, rate, previous_rate, results = data if rate is None: text = _('Analysis did not succeed ' '(see output for more details).') self.treewidget.clear_results() date_text = '' else: text_style = "%s " rate_style = "%s" prevrate_style = "%s" color = "#FF0000" if float(rate) > 5.: color = "#22AA22" elif float(rate) > 3.: color = "#EE5500" text = _('Global evaluation:') text = (text_style % text)+(rate_style % (color, ('%s/10' % rate))) if previous_rate: text_prun = _('previous run:') text_prun = ' (%s %s/10)' % (text_prun, previous_rate) text += prevrate_style % text_prun self.treewidget.set_results(filename, results) date = to_text_string(time.strftime("%d %b %Y %H:%M", datetime), encoding='utf8') date_text = text_style % date self.ratelabel.setText(text) self.datelabel.setText(date_text) def test(): """Run pylint widget test""" from spyderlib.utils.qthelpers import qapplication app = qapplication() widget = PylintWidget(None) widget.show() widget.analyze(__file__) sys.exit(app.exec_()) if __name__ == '__main__': test()