PyLint/PyLintExecDialog.py

Sat, 24 Apr 2021 17:01:22 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 24 Apr 2021 17:01:22 +0200
changeset 95
50eba81e4a9f
parent 94
45d226917534
permissions
-rw-r--r--

- implemented some code simplifications

# -*- coding: utf-8 -*-

# Copyright (c) 2005 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog to show the results of the PyLint run.
"""

import os

from PyQt5.QtCore import pyqtSlot, Qt, QTimer, QProcess
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import (
    QWidget, QHeaderView, QApplication, QDialogButtonBox, QTreeWidgetItem
)

from E5Gui import E5MessageBox, E5FileDialog
from E5Gui.E5Application import e5App
try:
    from E5Gui.E5OverrideCursor import E5OverrideCursorProcess
except ImportError:
    # workaround for eric6 < 20.11
    E5OverrideCursorProcess = QProcess

from .Ui_PyLintExecDialog import Ui_PyLintExecDialog

import Preferences
import Utilities


class PyLintExecDialog(QWidget, Ui_PyLintExecDialog):
    """
    Class implementing a dialog to show the results of the PyLint run.
    
    This class starts a QProcess and displays a dialog that
    shows the results of the PyLint command process.
    """
    filenameRole = Qt.UserRole + 1
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent parent widget of this dialog (QWidget)
        """
        QWidget.__init__(self, parent)
        self.setupUi(self)
        
        self.saveButton = self.buttonBox.addButton(
            self.tr("Save Report..."), QDialogButtonBox.ActionRole)
        self.saveButton.setToolTip(
            self.tr("Press to save the report to a file"))
        self.saveButton.setEnabled(False)
        
        self.refreshButton = self.buttonBox.addButton(
            self.tr("Refresh"), QDialogButtonBox.ActionRole)
        self.refreshButton.setToolTip(self.tr(
            "Press to refresh the result display"))
        self.refreshButton.setEnabled(False)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        
        self.messageList.header().setSortIndicator(0, Qt.AscendingOrder)
        
        self.process = None
        self.noResults = True
        self.htmlOutput = False
        self.parsedOutput = False
        self.__scrollPosition = -1  # illegal value
        
        self.typeDict = {
            'C': self.tr('Convention'),
            'R': self.tr('Refactor'),
            'W': self.tr('Warning'),
            'E': self.tr('Error'),
            'F': self.tr('Fatal'),
        }
    
    def start(self, args, fn, reportFile, ppath):
        """
        Public slot to start PyLint.
        
        @param args commandline arguments for documentation programPyLint
            (list of strings)
        @param fn filename or dirname to be processed by PyLint (string)
        @param reportFile filename of file to write the report to (string)
        @param ppath project path (string)
        @return flag indicating the successful start of the process (boolean)
        """
        self.errorGroup.hide()
        
        self.args = args[:]
        self.fn = fn
        self.reportFile = reportFile
        self.ppath = ppath
        
        self.pathname = os.path.dirname(fn)
        self.filename = os.path.basename(fn)
        
        self.contents.clear()
        self.errors.clear()
        self.messageList.clear()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
        self.saveButton.setEnabled(False)
        self.refreshButton.setEnabled(False)
        
        program = args[0]
        del args[0]
        args.append(self.filename)
        
        self.process = E5OverrideCursorProcess()
        self.process.setWorkingDirectory(self.pathname)
        
        self.process.readyReadStandardError.connect(self.__readStderr)
        self.process.finished.connect(self.__finish)
            
        self.__ioEncoding = Preferences.getSystem("IOEncoding")
        if "--output-format=parseable" in args:
            self.reportFile = None
            self.contents.hide()
            self.process.readyReadStandardOutput.connect(
                self.__readParseStdout)
            self.parsedOutput = True
        else:
            self.process.readyReadStandardOutput.connect(self.__readStdout)
            self.messageList.hide()
            if "--output-format=html" in args:
                self.contents.setAcceptRichText(True)
                self.contents.setHtml('<b>Processing your request...</b>')
                self.htmlOutput = True
            else:
                self.contents.setAcceptRichText(False)
                self.contents.setCurrentFont(
                    Preferences.getEditorOtherFonts("MonospacedFont"))
                self.htmlOutput = False
            self.parsedOutput = False
        self.noResults = True
        
        self.buf = ""
        self.__lastFileItem = None
        
        self.process.start(program, args)
        procStarted = self.process.waitForStarted()
        if not procStarted:
            E5MessageBox.critical(
                self,
                self.tr('Process Generation Error'),
                self.tr(
                    'The process {0} could not be started. '
                    'Ensure, that it is in the search path.'
                ).format(program))
        return procStarted
    
    def on_buttonBox_clicked(self, button):
        """
        Private slot called by a button of the button box clicked.
        
        @param button button that was clicked (QAbstractButton)
        """
        if button == self.buttonBox.button(QDialogButtonBox.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
            self.__finish()
        elif button == self.saveButton:
            self.on_saveButton_clicked()
        elif button == self.refreshButton:
            self.on_refreshButton_clicked()
    
    def __finish(self):
        """
        Private slot called when the process finished.
        
        It is called when the process finished or
        the user pressed the button.
        """
        if self.htmlOutput:
            self.contents.setHtml(self.buf)
        else:
            cursor = self.contents.textCursor()
            cursor.movePosition(QTextCursor.Start)
            self.contents.setTextCursor(cursor)
        
        if (
            self.process is not None and
            self.process.state() != QProcess.NotRunning
        ):
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        self.refreshButton.setEnabled(True)
        if self.parsedOutput:
            QApplication.processEvents()
            self.messageList.sortItems(
                self.messageList.sortColumn(),
                self.messageList.header().sortIndicatorOrder())
            self.messageList.header().resizeSections(
                QHeaderView.ResizeToContents)
            self.messageList.header().setStretchLastSection(True)
        else:
            if self.__scrollPosition != -1:
                self.contents.verticalScrollBar().setValue(
                    self.__scrollPosition)
        
        self.process = None
        
        if self.reportFile:
            self.__writeReport()
        elif not self.parsedOutput:
            self.saveButton.setEnabled(True)
        
        if self.noResults:
            self.__createItem(
                self.tr('No PyLint errors found.'), "", "", "")
    
    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the status display.
        """
        self.__scrollPosition = self.contents.verticalScrollBar().value()
        self.start(self.args, self.fn, self.reportFile, self.ppath)
    
    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.
        
        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.__ioEncoding, 'replace')
            self.buf += s + os.linesep
            if not self.htmlOutput:
                self.contents.insertPlainText(s)
                self.contents.ensureCursorVisible()
    
    def __createItem(self, file, line, type_, message):
        """
        Private method to create an entry in the message list.
        
        @param file filename of file (string)
        @param line linenumber of message (integer or string)
        @param type_ type of message (string)
        @param message message text (string)
        """
        if self.__lastFileItem is None or self.__lastFileItem.text(0) != file:
            matchFlags = Qt.MatchFixedString
            if not Utilities.isWindowsPlatform():
                matchFlags |= Qt.MatchCaseSensitive
            
            itmList = self.messageList.findItems(file, matchFlags)
            if itmList:
                self.__lastFileItem = itmList[0]
            else:
                # It's a new file
                self.__lastFileItem = QTreeWidgetItem(self.messageList, [file])
                self.__lastFileItem.setFirstColumnSpanned(True)
                self.__lastFileItem.setExpanded(True)
                self.__lastFileItem.setData(0, self.filenameRole, file)
        
        itm = QTreeWidgetItem(self.__lastFileItem, [str(line), type_, message])
        itm.setTextAlignment(0, Qt.AlignRight)
        itm.setTextAlignment(1, Qt.AlignHCenter)
        itm.setData(0, self.filenameRole, file)
    
    def __readParseStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal for
        parseable output.
        
        It reads the output of the process, formats it and inserts it into
        the message list pane.
        """
        self.process.setReadChannel(QProcess.StandardOutput)
        
        while self.process.canReadLine():
            s = str(self.process.readLine(), self.__ioEncoding, 'replace')
            if s:
                try:
                    if Utilities.isWindowsPlatform():
                        drive, s = os.path.splitdrive(s)
                        fname, lineno, fullmessage = s.split(':')
                        fname = drive + fname
                    else:
                        fname, lineno, fullmessage = s.split(':')
                    type_, message = fullmessage.strip().split(']', 1)
                    type_ = type_.strip()[1:].split(',', 1)[0]
                    message = message.strip()
                    if type_ and type_[0] in self.typeDict:
                        if len(type_) == 1:
                            self.__createItem(
                                fname, lineno, self.typeDict[type_], message)
                        else:
                            self.__createItem(
                                fname, lineno, "{0} {1}".format(
                                    self.typeDict[type_[0]], type_[1:]),
                                message)
                        self.noResults = False
                except ValueError:
                    continue
    
    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.
        
        It reads the error output of the process and inserts it into the
        error pane.
        """
        self.process.setReadChannel(QProcess.StandardError)
        
        while self.process.canReadLine():
            self.errorGroup.show()
            s = str(self.process.readLine(), self.__ioEncoding, 'replace')
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
        
    def on_messageList_itemActivated(self, itm, column):
        """
        Private slot to handle the itemActivated signal of the message list.
        
        @param itm The message item that was activated (QTreeWidgetItem)
        @param column column the item was activated in (integer)
        """
        if self.noResults:
            return
        
        if itm.parent():
            fn = os.path.join(self.pathname, itm.data(0, self.filenameRole))
            lineno = int(itm.text(0))
            
            vm = e5App().getObject("ViewManager")
            vm.openSourceFile(fn, lineno)
            editor = vm.getOpenEditor(fn)
            editor.toggleWarning(
                lineno, 0, True,
                "{0} | {1}".format(itm.text(1), itm.text(2)))
        else:
            fn = os.path.join(self.pathname, itm.data(0, self.filenameRole))
            vm = e5App().getObject("ViewManager")
            vm.openSourceFile(fn)
            editor = vm.getOpenEditor(fn)
            for index in range(itm.childCount()):
                citm = itm.child(index)
                lineno = int(citm.text(0))
                editor.toggleWarning(
                    lineno, 0, True,
                    "{0} | {1}".format(citm.text(1), citm.text(2)))
        
    def __writeReport(self):
        """
        Private slot to write the report to a report file.
        """
        self.reportFile = self.reportFile
        if os.path.exists(self.reportFile):
            res = E5MessageBox.warning(
                self,
                self.tr("PyLint Report"),
                self.tr(
                    """<p>The PyLint report file <b>{0}</b> already"""
                    """ exists.</p>""")
                .format(self.reportFile),
                E5MessageBox.StandardButtons(
                    E5MessageBox.Cancel |
                    E5MessageBox.Ignore),
                E5MessageBox.Cancel)
            if res == E5MessageBox.Cancel:
                return
        
        try:
            import codecs
            with open(self.reportFile, 'wb') as f:
                f.write(codecs.BOM_UTF8)
                f.write(self.buf.encode('utf-8'))
        except OSError as why:
            E5MessageBox.critical(
                self, self.tr('PyLint Report'),
                self.tr('<p>The PyLint report file <b>{0}</b> could not'
                        ' be written.<br>Reason: {1}</p>')
                .format(self.reportFile, str(why)))
    
    @pyqtSlot()
    def on_saveButton_clicked(self):
        """
        Private slot to save the report to a file.
        """
        fileFilter = (
            self.tr("HTML Files (*.html);;All Files (*)")
            if self.htmlOutput else
            self.tr("Text Files (*.txt);;All Files (*)")
        )
        
        self.reportFile = E5FileDialog.getSaveFileName(
            self,
            self.tr("PyLint Report"),
            self.ppath,
            fileFilter,
            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
        if self.reportFile:
            self.__writeReport()

eric ide

mercurial