VultureChecker/VultureCheckerDialog.py

Tue, 25 May 2021 19:36:03 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 25 May 2021 19:36:03 +0200
branch
eric7
changeset 79
47e46cd3bb23
parent 69
3c2922b45a9f
child 80
f0eb9553e04f
permissions
-rw-r--r--

Ported the plug-in to PyQt6 for eric7.

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

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

"""
Module implementing a dialog to show the vulture check results.
"""

import os
import fnmatch
import contextlib

from PyQt6.QtCore import pyqtSlot, Qt, QTimer
from PyQt6.QtWidgets import (
    QDialog, QDialogButtonBox, QAbstractButton, QHeaderView, QTreeWidgetItem,
    QApplication, QMenu
)

from .Ui_VultureCheckerDialog import Ui_VultureCheckerDialog

from EricWidgets.EricApplication import ericApp

import Preferences
import Utilities


class VultureItem:
    """
    Class to hold the name, type, confidence and location of defined code.
    """
    def __init__(self, name, typ, filename, firstLineno, lastLineno,
                 confidence):
        """
        Constructor
        
        @param name item name
        @type str
        @param typ item type
        @type str
        @param filename name of the file containing item
        @type str
        @param firstLineno first line number
        @type int
        @param lastLineno last line number
        @type int
        @param confidence confidence level
        @type int
        """
        self.name = name
        self.typ = typ
        self.filename = filename
        self.first_lineno = firstLineno
        self.last_lineno = lastLineno
        self.confidence = confidence


class VultureCheckerDialog(QDialog, Ui_VultureCheckerDialog):
    """
    Class implementing a dialog to show the vulture check results.
    """
    FilePathRole = Qt.ItemDataRole.UserRole + 1
    TypeRole = Qt.ItemDataRole.UserRole + 2
    
    def __init__(self, vultureService, parent=None):
        """
        Constructor
        
        @param vultureService reference to the service
        @type VulturePlugin
        @param parent reference to the parent widget
        @type QWidget
        """
        super().__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.WindowType.Window)
        
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Close).setEnabled(False)
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Cancel).setDefault(True)
        
        self.resultList.headerItem().setText(self.resultList.columnCount(), "")
        
        self.__menu = QMenu(self)
        self.__whiteListAct = self.__menu.addAction(
            self.tr("Add to Whitelist"), self.__whiteList)
        self.__menu.addSeparator()
        self.__menu.addAction(
            self.tr("Edit Whitelist"), self.__editWhiteList)
        self.__menu.addSeparator()
        self.__collapseAct = self.__menu.addAction(
            self.tr("Collapse all"), self.__resultCollapse)
        self.__expandAct = self.__menu.addAction(
            self.tr("Expand all"), self.__resultExpand)
        self.resultList.customContextMenuRequested.connect(
            self.__showContextMenu)
        
        self.vultureService = vultureService
        self.vultureService.analysisDone.connect(self.__processResult)
        self.vultureService.error.connect(self.__processError)
        self.vultureService.batchFinished.connect(self.__batchFinished)
        
        self.cancelled = False
        
        self.__project = ericApp().getObject("Project")
        self.__finished = True
        self.__errorItem = None
        self.__data = None
        self.__slotsAreUsed = True
        
        self.__fileList = []
        self.filterFrame.setVisible(False)
        
        self.__translatedTypes = {
            "property": self.tr("Property"),
            "function": self.tr("Function"),
            "method": self.tr("Methode"),
            "attribute": self.tr("Attribute"),
            "variable": self.tr("Variable"),
            "class": self.tr("Class"),
            "import": self.tr("Import"),
        }
    
    def __createErrorItem(self, filename, message):
        """
        Private slot to create a new error item in the result list.
        
        @param filename name of the file
        @type str
        @param message error message
        @type str
        """
        if self.__errorItem is None:
            self.__errorItem = QTreeWidgetItem(self.resultList, [
                self.tr("Errors")])
            self.__errorItem.setExpanded(True)
            self.__errorItem.setForeground(0, Qt.GlobalColor.red)
        
        msg = "{0} ({1})".format(self.__project.getRelativePath(filename),
                                 message)
        if not self.resultList.findItems(msg, Qt.MatchFlag.MatchExactly):
            itm = QTreeWidgetItem(self.__errorItem, [msg])
            itm.setForeground(0, Qt.GlobalColor.red)
            itm.setFirstColumnSpanned(True)
    
    def prepare(self, fileList, project):
        """
        Public method to prepare the dialog with a list of filenames.
        
        @param fileList list of filenames
        @type list of str
        @param project reference to the project object
        @type Project
        """
        self.__fileList = fileList[:]
        self.__project = project
        
        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.filterFrame.setVisible(True)
        
        self.__data = self.__project.getData(
            "CHECKERSPARMS", "Vulture")
        if self.__data is None:
            self.__data = {}
        if "ExcludeFiles" not in self.__data:
            self.__data["ExcludeFiles"] = ""
        if "WhiteLists" not in self.__data:
            self.__data["WhiteLists"] = {
                "property": [],
                "function": [],
                "attribute": [],
                "variable": [],
                "class": [],
                "import": [],
                "__patterns__": [
                    "on_*",
                    "visit_*",
                ],
            }
        if "method" not in self.__data["WhiteLists"]:
            self.__data["WhiteLists"]["method"] = []
        if "import" not in self.__data["WhiteLists"]:
            self.__data["WhiteLists"]["import"] = []
        
        self.excludeFilesEdit.setText(self.__data["ExcludeFiles"])
    
    def start(self, fn):
        """
        Public slot to start the code metrics determination.
        
        @param fn file or list of files or directory to show
            the code metrics for
        @type str or list of str
        """
        self.cancelled = False
        self.__errorItem = None
        self.resultList.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)
        QApplication.processEvents()
        
        self.__prepareResultLists()
        
        if isinstance(fn, list):
            self.files = fn
        elif os.path.isdir(fn):
            self.files = []
            extensions = set(Preferences.getPython("Python3Extensions"))
            for ext in extensions:
                self.files.extend(
                    Utilities.direntries(fn, True, '*{0}'.format(ext), 0))
        else:
            self.files = [fn]
        self.files.sort()
        # check for missing files
        for f in self.files[:]:
            if not os.path.exists(f):
                self.files.remove(f)
        
        if len(self.files) > 0:
            # disable updates of the list for speed
            self.resultList.setUpdatesEnabled(False)
            self.resultList.setSortingEnabled(False)
            
            self.checkProgress.setMaximum(len(self.files))
            self.checkProgress.setVisible(len(self.files) > 1)
            self.checkProgressLabel.setVisible(len(self.files) > 1)
            QApplication.processEvents()
            
            # now go through all the files
            self.progress = 0
            if len(self.files) == 1:
                self.__batch = False
                self.vultureCheck()
            else:
                self.__batch = True
                self.vultureCheckBatch()
    
    def vultureCheck(self):
        """
        Public method to start a vulture check for one Python file.
        
        The results are reported to the __processResult slot.
        """
        if not self.files:
            self.checkProgressLabel.setPath("")
            self.checkProgress.setMaximum(1)
            self.checkProgress.setValue(1)
            self.__finish()
            return
        
        self.filename = self.files.pop(0)
        self.checkProgress.setValue(self.progress)
        self.checkProgressLabel.setPath(self.filename)
        QApplication.processEvents()
        
        if self.cancelled:
            return
        
        try:
            self.source = Utilities.readEncodedFile(self.filename)[0]
            self.source = Utilities.normalizeCode(self.source)
        except (UnicodeError, OSError) as msg:
            self.__createErrorItem(self.filename, str(msg).rstrip())
            self.progress += 1
            # Continue with next file
            self.vultureCheck()
            return

        self.__finished = False
        self.vultureService.vultureCheck(
            None, self.filename, self.source)

    def vultureCheckBatch(self):
        """
        Public method to start a vulture check batch job.
        
        The results are reported to the __processResult slot.
        """
        self.checkProgressLabel.setPath(self.tr("Preparing files..."))
        
        argumentsList = []
        for progress, filename in enumerate(self.files, start=1):
            self.checkProgress.setValue(progress)
            QApplication.processEvents()
            
            try:
                source = Utilities.readEncodedFile(filename)[0]
                source = Utilities.normalizeCode(source)
            except (UnicodeError, OSError) as msg:
                self.__createErrorItem(filename, str(msg).rstrip())
                continue
            
            argumentsList.append((filename, source))
        
        # reset the progress bar to the checked files
        self.checkProgress.setValue(self.progress)
        self.checkProgressLabel.setPath(self.tr("Transferring data..."))
        QApplication.processEvents()
        
        self.__finished = False
        self.vultureService.vultureCheckBatch(argumentsList)
    
    def __batchFinished(self):
        """
        Private slot handling the completion of a batch job.
        """
        self.checkProgressLabel.setPath("")
        self.checkProgress.setMaximum(1)
        self.checkProgress.setValue(1)
        self.__finish()
    
    def __processError(self, fn, msg):
        """
        Private slot to process an error indication from the service.
        
        @param fn filename of the file
        @type str
        @param msg error message
        @type str
        """
        self.__createErrorItem(fn, msg)
    
    def __processResult(self, fn, result):
        """
        Private slot called after performing a vulture analysis on one file.
        
        @param fn filename of the file
        @type str
        @param result result dict
        @type dict
        """
        if self.__finished:
            return
        
        # Check if it's the requested file, otherwise ignore signal if not
        # in batch mode
        if not self.__batch and fn != self.filename:
            return
        
        self.checkProgressLabel.setPath(self.__project.getRelativePath(fn))
        QApplication.processEvents()
        
        if "error" in result:
            self.__createErrorItem(fn, result["error"])
        else:
            self.__storeResult(result)
        
        self.progress += 1
        
        self.checkProgress.setValue(self.progress)
        QApplication.processEvents()
        
        if not self.__batch:
            self.vultureCheck()
    
    def __finish(self):
        """
        Private slot called when the action finished or the user pressed the
        cancel button.
        """
        if not self.__finished:
            self.__finished = True
            
            if not self.cancelled:
                self.__createResultItems()
            
            # reenable updates of the list
            self.resultList.setSortingEnabled(True)
            self.resultList.sortItems(0, Qt.SortOrder.AscendingOrder)
            self.resultList.setUpdatesEnabled(True)
            
            self.cancelled = True
            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.resultList.header().resizeSections(
                QHeaderView.ResizeToContents)
            self.resultList.header().setStretchLastSection(True)
            self.resultList.header().setSectionResizeMode(
                QHeaderView.ResizeMode.Interactive)
            
            self.checkProgress.setVisible(False)
            self.checkProgressLabel.setVisible(False)
            
            if self.resultList.topLevelItemCount() == 0:
                itm = QTreeWidgetItem(self.resultList,
                                      [self.tr("No unused code found.")])
                itm.setFirstColumnSpanned(True)
    
    @pyqtSlot(QAbstractButton)
    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.cancelled = True
            if self.__batch:
                self.vultureService.cancelVultureCheckBatch()
                QTimer.singleShot(1000, self.__finish)
            else:
                self.__finish()
    
    @pyqtSlot()
    def on_startButton_clicked(self):
        """
        Private slot to start a code metrics run.
        """
        fileList = self.__fileList[:]
        
        filterString = self.excludeFilesEdit.text()
        if filterString != self.__data["ExcludeFiles"]:
            self.__data["ExcludeFiles"] = filterString
            self.__project.setData(
                "CHECKERSPARMS", "Vulture", self.__data)
        filterList = [f.strip() for f in filterString.split(",")
                      if f.strip()]
        if filterList:
            for fileFilter in filterList:
                fileList = [f for f in fileList
                            if not fnmatch.fnmatch(f, fileFilter)]
        
        self.start(fileList)
    
    def clear(self):
        """
        Public method to clear all results.
        """
        self.resultList.clear()
    
    @pyqtSlot(QTreeWidgetItem, int)
    def on_resultList_itemActivated(self, item, column):
        """
        Private slot to handle the activation of a result item.
        
        @param item reference to the activated item
        @type QTreeWidgetItem
        @param column column the item was activated in
        @type int
        """
        if item.parent() is not None:
            filename = item.data(0, self.FilePathRole)
            try:
                lineno = int(item.text(0))
            except ValueError:
                lineno = 1
            if filename:
                vm = ericApp().getObject("ViewManager")
                vm.openSourceFile(filename, lineno)
    
    def __prepareResultLists(self):
        """
        Private method to prepare the result lists.
        """
        self.__unusedAttrs = []
        self.__unusedClasses = []
        self.__unusedFuncs = []
        self.__unusedMethods = []
        self.__unusedImports = []
        self.__unusedProps = []
        self.__unusedVars = []
    
    def __storeResult(self, result):
        """
        Private method to store the result of an analysis.
        
        @param result result dictionary
        @type dict
        """
        self.__unusedAttrs.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["UnusedAttributes"]]))
        self.__unusedClasses.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["UnusedClasses"]]))
        self.__unusedFuncs.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["UnusedFunctions"]]))
        self.__unusedMethods.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["UnusedMethods"]]))
        self.__unusedImports.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["UnusedImports"]]))
        self.__unusedProps.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["UnusedProperties"]]))
        self.__unusedVars.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["UnusedVariables"]]))
    
    def __dict2Item(self, d):
        """
        Private method to convert an item dictionary to a vulture item.
        
        @param d item dictionary
        @type dict
        @return vulture item
        @rtype VultureItem
        """
        return VultureItem(d["name"], d["type"], d["file"], d["first_line"],
                           d["last_line"], confidence=d["confidence"])
    
    def __filteredList(self, itemList):
        """
        Private method to filter a list against the whitelist patterns
        returning items not matching the whitelist.
        
        @param itemList list of items to be filtered
        @type list of VultureItem
        @return list of filtered items
        @rtype list of VultureItem
        """
        filteredList = itemList
        for pattern in self.__data["WhiteLists"]["__patterns__"]:
            filteredList = [item for item in filteredList
                            if not fnmatch.fnmatchcase(item.name, pattern)]
        return filteredList     # __IGNORE_WARNING_M834__
    
    def __filterUnusedItems(self, unused, whitelistName):
        """
        Private method to get a list of unused items.
        
        @param unused list of unused items
        @type list of VultureItem
        @param whitelistName name of the whitelist to use as a filter
        @type str
        @return list of unused items
        @rtype list of VultureItem
        """
        return [
            item for item in set(unused)
            if item.name not in self.__data["WhiteLists"][whitelistName]
        ]

    def __filterUnusedFunctions(self):
        """
        Private method to get the list of unused functions.
        
        @return list of unused functions
        @rtype list of VultureItem
        """
        return self.__filterUnusedItems(self.__unusedFuncs, "function")

    def __filterUnusedMethods(self):
        """
        Private method to get the list of unused methods.
        
        @return list of unused methods
        @rtype list of VultureItem
        """
        return self.__filterUnusedItems(self.__unusedMethods, "method")

    def __filterUnusedClasses(self):
        """
        Private method to get the list of unused classes.
        
        @return list of unused classes
        @rtype list of VultureItem
        """
        return self.__filterUnusedItems(self.__unusedClasses, "class")

    def __filterUnusedImports(self):
        """
        Private method to get a list of unused imports.
        
        @return list of unused imports
        @rtype list of VultureItem
        """
        return self.__filterUnusedItems(self.__unusedImports, "import")
    
    def __filterUnusedProperties(self):
        """
        Private method to get the list of unused properties.
        
        @return list of unused properties
        @rtype list of VultureItem
        """
        return self.__filterUnusedItems(self.__unusedProps, "property")

    def __filterUnusedVariables(self):
        """
        Private method to get the list of unused variables.
        
        @return list of unused variables
        @rtype list of VultureItem
        """
        return self.__filterUnusedItems(self.__unusedVars, "variable")

    def __filterUnusedAttributes(self):
        """
        Private method to get the list of unused attributes.
        
        @return list of unused attributes
        @rtype list of VultureItem
        """
        return self.__filterUnusedItems(self.__unusedAttrs, "attribute")
    
    def __createResultItems(self):
        """
        Private method to populate the list with the analysis result.
        """
        lastFileItem = None
        lastFileName = ""
        items = (self.__filterUnusedFunctions() +
                 self.__filterUnusedMethods() +
                 self.__filterUnusedClasses() +
                 self.__filterUnusedImports() +
                 self.__filterUnusedProperties() +
                 self.__filterUnusedVariables() +
                 self.__filterUnusedAttributes())
        for item in sorted(items, key=lambda item: item.filename):
            if lastFileItem is None or lastFileName != item.filename:
                lastFileItem = self.__createFileItem(item.filename)
                lastFileName = item.filename
            
            self.__createResultItem(lastFileItem, item)
    
    def __createResultItem(self, parent, item):
        """
        Private method to create a result item.
        
        @param parent reference to the parent item
        @type QTreeWidgetItem
        @param item reference to the item
        @type VultureItem
        """
        try:
            translatedType = self.__translatedTypes[item.typ]
        except KeyError:
            translatedType = item.typ
        itm = QTreeWidgetItem(parent, [
            "{0:6d}".format(item.first_lineno), item.name,
            "{0:3d}%".format(item.confidence), translatedType])
        itm.setData(0, self.FilePathRole, item.filename)
        itm.setData(0, self.TypeRole, item.typ)
        itm.setTextAlignment(0, Qt.AlignmentFlag.AlignRight)    # line no
        itm.setTextAlignment(2, Qt.AlignmentFlag.AlignRight)    # confidence
    
    def __createFileItem(self, filename):
        """
        Private method to create a file item.
        
        @param filename file name for the item
        @type str
        @return reference to the created item
        @rtype QTreeWidgetItem
        """
        itm = QTreeWidgetItem(self.resultList, [
            self.__project.getRelativePath(filename)])
        itm.setData(0, self.FilePathRole, filename)
        itm.setExpanded(True)
        itm.setFirstColumnSpanned(True)
        
        return itm
    
    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu of the listview.
        
        @param coord the position of the mouse pointer
        @type QPoint
        """
        topLevelPresent = self.resultList.topLevelItemCount() > 0
        self.__collapseAct.setEnabled(topLevelPresent)
        self.__expandAct.setEnabled(topLevelPresent)
        
        self.__whiteListAct.setEnabled(
            len(self.__getSelectedNonFileItems()) != 0)
        
        self.__menu.popup(self.resultList.mapToGlobal(coord))
        
    def __resultCollapse(self):
        """
        Private slot to collapse all entries of the resultlist.
        """
        for index in range(self.resultList.topLevelItemCount()):
            self.resultList.topLevelItem(index).setExpanded(False)
        
    def __resultExpand(self):
        """
        Private slot to expand all entries of the resultlist.
        """
        for index in range(self.resultList.topLevelItemCount()):
            self.resultList.topLevelItem(index).setExpanded(True)
    
    def __getSelectedNonFileItems(self):
        """
        Private method to get a list of selected non file items.
        
        @return list of selected non file items
        @rtype list of QTreeWidgetItem
        """
        return [i for i in self.resultList.selectedItems()
                if i.parent() is not None]
    
    def __editWhiteList(self):
        """
        Private slot to edit the whitelist.
        """
        from .EditWhiteListDialog import EditWhiteListDialog
        dlg = EditWhiteListDialog(self.__data["WhiteLists"])
        if dlg.exec() == QDialog.DialogCode.Accepted:
            whitelists = dlg.getWhiteLists()
            self.__storeWhiteLists(whitelists)
    
    def __whiteList(self):
        """
        Private slot to add entries to the whitelist.
        """
        whitelists = {}
        for key in self.__data["WhiteLists"]:
            whitelists[key] = self.__data["WhiteLists"][key][:]
        for itm in self.__getSelectedNonFileItems():
            with contextlib.suppress(KeyError):
                whitelists[itm.data(0, self.TypeRole)].append(itm.text(1))
            # remove the item from the result list
            pitm = itm.parent()
            pitm.removeChild(itm)
            del itm
            if pitm.childCount() == 0:
                self.resultList.takeTopLevelItem(
                    self.resultList.indexOfTopLevelItem(pitm))
                del pitm
        self.__storeWhiteLists(whitelists)
    
    def __storeWhiteLists(self, whitelists):
        """
        Private method to store the new whitelists, if they have changed.
        
        @param whitelists dictionary of lists of whitelisted names
        @type dict of list of str
        """
        changed = False
        for key in whitelists:
            whitelist = list(set(whitelists[key]))
            with contextlib.suppress(KeyError):
                if sorted(whitelist) != sorted(self.__data["WhiteLists"][key]):
                    self.__data["WhiteLists"][key] = whitelist[:]
                    changed = True
        
        if changed:
            self.__project.setData("CHECKERSPARMS", "Vulture", self.__data)

eric ide

mercurial