PyLintInterface/PyLintExecDialog.py

Sat, 23 Dec 2023 15:48:44 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 23 Dec 2023 15:48:44 +0100
branch
eric7
changeset 115
4a96d169c373
parent 114
524f52c0ac34
child 116
71d3a2e48265
permissions
-rw-r--r--

Updated copyright for 2024.

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

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

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

import os

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

from eric7 import Preferences
from eric7.EricGui.EricOverrideCursor import EricOverrideCursorProcess
from eric7.EricWidgets import EricFileDialog, EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp

try:
    from eric7.SystemUtilities.OSUtilities import isWindowsPlatform
except ImportError:
    # imports for eric < 23.1
    from eric7.Utilities import isWindowsPlatform

from .Ui_PyLintExecDialog import Ui_PyLintExecDialog


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.ItemDataRole.UserRole + 1

    def __init__(self, parent=None):
        """
        Constructor

        @param parent parent widget of this dialog
        @type QWidget
        """
        QWidget.__init__(self, parent)
        self.setupUi(self)

        self.saveButton = self.buttonBox.addButton(
            self.tr("Save Report..."), QDialogButtonBox.ButtonRole.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.ButtonRole.ActionRole
        )
        self.refreshButton.setToolTip(self.tr("Press to refresh the result display"))
        self.refreshButton.setEnabled(False)

        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True)

        self.messageList.header().setSortIndicator(0, Qt.SortOrder.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
        @type list of str
        @param fn filename or dirname to be processed by PyLint
        @type str
        @param reportFile filename of file to write the report to
        @type str
        @param ppath project path
        @type str
        @return flag indicating the successful start of the process
        @rtype bool
        """
        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.StandardButton.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True)
        self.saveButton.setEnabled(False)
        self.refreshButton.setEnabled(False)

        program = args[0]
        del args[0]
        args.append(self.filename)

        self.process = EricOverrideCursorProcess()
        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:
            EricMessageBox.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
        @type QAbstractButton
        """
        if button == self.buttonBox.button(QDialogButtonBox.StandardButton.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.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.MoveOperation.Start)
            self.contents.setTextCursor(cursor)

        if (
            self.process is not None
            and self.process.state() != QProcess.ProcessState.NotRunning
        ):
            self.process.terminate()
            QTimer.singleShot(2000, self.process.kill)
            self.process.waitForFinished(3000)

        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.StandardButton.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.ResizeMode.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.ProcessChannel.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
        @type str
        @param line linenumber of message
        @type int or str
        @param type_ type of message
        @type str
        @param message message text
        @type str
        """
        if self.__lastFileItem is None or self.__lastFileItem.text(0) != file:
            matchFlags = Qt.MatchFlag.MatchFixedString
            if not isWindowsPlatform():
                matchFlags |= Qt.MatchFlag.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.AlignmentFlag.AlignRight)
        itm.setTextAlignment(1, Qt.AlignmentFlag.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.ProcessChannel.StandardOutput)

        while self.process.canReadLine():
            s = str(self.process.readLine(), self.__ioEncoding, "replace")
            if s:
                try:
                    if 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.ProcessChannel.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
        @type QTreeWidgetItem
        @param column column the item was activated in
        @type int
        """
        if self.noResults:
            return

        if itm.parent():
            fn = os.path.join(self.pathname, itm.data(0, self.filenameRole))
            lineno = int(itm.text(0))

            vm = ericApp().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 = ericApp().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 = EricMessageBox.warning(
                self,
                self.tr("PyLint Report"),
                self.tr(
                    """<p>The PyLint report file <b>{0}</b> already"""
                    """ exists.</p>"""
                ).format(self.reportFile),
                EricMessageBox.Cancel | EricMessageBox.Ignore,
                EricMessageBox.Cancel,
            )
            if res == EricMessageBox.Cancel:
                return

        try:
            with open(self.reportFile, "w") as f:
                f.write(self.buf)
        except OSError as why:
            EricMessageBox.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("Report Files (*.rpt);;Text Files (*.txt);;All Files (*)")
        )

        self.reportFile = EricFileDialog.getSaveFileName(
            self,
            self.tr("PyLint Report"),
            self.ppath,
            fileFilter,
            EricFileDialog.DontConfirmOverwrite,
        )
        if self.reportFile:
            self.__writeReport()

eric ide

mercurial