VultureChecker/VultureCheckerDialog.py

Sun, 31 Dec 2017 16:59:08 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 31 Dec 2017 16:59:08 +0100
changeset 53
4eb2ec8fff7c
parent 45
232ed15782bb
child 55
7925ae5c9f17
permissions
-rw-r--r--

Updated copyright for 2018.

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

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

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

from __future__ import unicode_literals

try:
    str = unicode       # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
except NameError:
    pass

import os
import fnmatch

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

from .Ui_VultureCheckerDialog import Ui_VultureCheckerDialog

from E5Gui.E5Application import e5App

import Preferences
import Utilities

from .vulture import Item


class VultureCheckerDialog(QDialog, Ui_VultureCheckerDialog):
    """
    Class implementing a dialog to show the vulture check results.
    """
    FilePathRole = Qt.UserRole + 1
    TypeRole = Qt.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(VultureCheckerDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.Window)
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.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 = e5App().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"),
            "slot": self.tr("Slot"),
            "attribute": self.tr("Attribute"),
            "variable": self.tr("Variable"),
            "class": self.tr("Class"),
        }
    
    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.red)
        
        msg = "{0} ({1})".format(self.__project.getRelativePath(filename),
                                 message)
        if not self.resultList.findItems(msg, Qt.MatchExactly):
            itm = QTreeWidgetItem(self.__errorItem, [msg])
            itm.setForeground(0, Qt.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.Close).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.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": [],
                "slot": [],
                "attribute": [],
                "variable": [],
                "class": [],
                "__patterns__": [
                    "on_*",
                    "visit_*",
                ],
            }
        if "SlotsAreUsed" not in self.__data:
            self.__data["SlotsAreUsed"] = True
        
        self.excludeFilesEdit.setText(self.__data["ExcludeFiles"])
        self.slotsCheckBox.setChecked(self.__data["SlotsAreUsed"])
    
    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.__errorItem = None
        self.resultList.clear()
        self.cancelled = False
        QApplication.processEvents()
        
        self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
        self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
        self.buttonBox.button(QDialogButtonBox.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("PythonExtensions") +
                             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 or not self.vultureService.hasBatch:
                self.__batch = False
                self.vultureCheck()
            else:
                self.__batch = True
                self.vultureCheckBatch()
    
    def vultureCheck(self, codestring=''):
        """
        Public method to start a vulture check for one Python file.
        
        The results are reported to the __processResult slot.
        
        @keyparam codestring optional sourcestring
        @type str
        """
        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, IOError) 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..."))
        progress = 0
        
        argumentsList = []
        for filename in self.files:
            progress += 1
            self.checkProgress.setValue(progress)
            QApplication.processEvents()
            
            try:
                source = Utilities.readEncodedFile(filename)[0]
                source = Utilities.normalizeCode(source)
            except (UnicodeError, IOError) 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 perfoming 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 or the user pressed the 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.AscendingOrder)
            self.resultList.setUpdatesEnabled(True)
            
            self.cancelled = True
            self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
            self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
            self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
            
            self.resultList.header().resizeSections(
                QHeaderView.ResizeToContents)
            self.resultList.header().setStretchLastSection(True)
            if qVersion() >= "5.0.0":
                self.resultList.header().setSectionResizeMode(
                    QHeaderView.Interactive)
            else:
                self.resultList.header().setResizeMode(QHeaderView.Interactive)
            
            self.checkProgress.setVisible(False)
            self.checkProgressLabel.setVisible(False)
    
    @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.Close):
            self.close()
        elif button == self.buttonBox.button(QDialogButtonBox.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.__slotsAreUsed = self.slotsCheckBox.isChecked()
        if self.__slotsAreUsed != self.__data["SlotsAreUsed"]:
            self.__data["SlotsAreUsed"] = self.__slotsAreUsed
            self.__project.setData(
                "CHECKERSPARMS", "Vulture", self.__data)
        
        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 = e5App().getObject("ViewManager")
                vm.openSourceFile(filename, lineno)
    
    def __prepareResultLists(self):
        """
        Private method to prepare the result lists.
        """
        self.__definedAttrs = []
        self.__definedFuncs = []
        self.__definedSlots = []
        self.__definedProps = []
        self.__definedVars = []
        self.__usedAttrs = []
        self.__usedVars = []
        self.__tupleAssignVars = []
        self.__namesImportedAsAliases = []
    
    def __storeResult(self, result):
        """
        Private method to store the result of an analysis.
        
        @param result result dictionary
        @type dict
        """
        self.__definedAttrs.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["DefinedAttributes"]]))
        self.__definedFuncs.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["DefinedFunctions"]]))
        self.__definedSlots.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["DefinedSlots"]]))
        self.__definedProps.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["DefinedProperties"]]))
        self.__definedVars.extend(self.__filteredList(
            [self.__dict2Item(d) for d in result["DefinedVariables"]]))
        self.__usedAttrs.extend(
            [self.__dict2Item(d) for d in result["UsedAttributes"]])
        self.__usedVars.extend(
            [self.__dict2Item(d) for d in result["UsedVariables"]])
        self.__tupleAssignVars.extend(
            [self.__dict2Item(d) for d in result["TupleVariables"]])
        self.__namesImportedAsAliases.extend(
            [self.__dict2Item(d) for d in result["Aliases"]])
    
    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 vulture.Item
        """
        return Item(d["name"], d["type"], d["file"], d["line"])
    
    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 vulture.Item
        @return list of filtered items
        @rtype list of vulture.Item
        """
        filteredList = itemList
        for pattern in self.__data["WhiteLists"]["__patterns__"]:
            regExp = QRegExp(pattern, Qt.CaseSensitive, QRegExp.Wildcard)
            filteredList = [name for name in filteredList
                            if not regExp.exactMatch(name)]
        return filteredList
    
    def __getUnusedItems(self, defined, used):
        """
        Private method to get a list of unused items.
        
        @param defined list of defined items
        @type list of vulture.Item
        @param used list of used items
        @type list of vulture.Item
        @return list of unused items
        @rtype list of vulture.Item
        """
        return list(set(defined) - set(used))

    def __unusedFunctions(self):
        """
        Private method to get the list of unused functions.
        
        @return list of unused functions
        @rtype list of vulture.Item
        """
        return self.__getUnusedItems(
            self.__definedFuncs,
            self.__usedAttrs + self.__usedVars +
            self.__namesImportedAsAliases +
            self.__data["WhiteLists"]["function"] +
            self.__data["WhiteLists"]["class"])

    def __unusedSlots(self):
        """
        Private method to get the list of unused PyQt slots.
        
        @return list of unused PyQt slots
        @rtype list of vulture.Item
        """
        return self.__getUnusedItems(
            self.__definedSlots,
            self.__usedAttrs + self.__usedVars +
            self.__namesImportedAsAliases +
            self.__data["WhiteLists"]["slot"])

    def __unusedProperties(self):
        """
        Private method to get the list of unused properties.
        
        @return list of unused properties
        @rtype list of vulture.Item
        """
        return self.__getUnusedItems(
            self.__definedProps,
            self.__usedAttrs + self.__data["WhiteLists"]["property"])

    def __unusedVariables(self):
        """
        Private method to get the list of unused variables.
        
        @return list of unused variables
        @rtype list of vulture.Item
        """
        return self.__getUnusedItems(
            self.__definedVars,
            self.__usedAttrs + self.__usedVars + self.__tupleAssignVars +
            self.__namesImportedAsAliases +
            self.__data["WhiteLists"]["variable"])

    def __unusedAttributes(self):
        """
        Private method to get the list of unused attributes.
        
        @return list of unused attributes
        @rtype list of vulture.Item
        """
        return self.__getUnusedItems(
            self.__definedAttrs,
            self.__usedAttrs + self.__usedVars +
            self.__data["WhiteLists"]["attribute"])
    
    def __createResultItems(self):
        """
        Private method to populate the list with the analysis result.
        """         # __IGNORE_WARNING__
        def filename(item):
            return item.filename
        
        lastFileItem = None
        lastFileName = ""
        items = (self.__unusedFunctions() +
                 self.__unusedProperties() +
                 self.__unusedVariables() +
                 self.__unusedAttributes())
        if not self.__slotsAreUsed:
            items += self.__unusedSlots()
        for item in sorted(items, key=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 vulture.Item
        """
        try:
            translatedType = self.__translatedTypes[item.typ]
        except KeyError:
            translatedType = item.typ
        itm = QTreeWidgetItem(parent, [
            "{0:6d}".format(item.lineno), str(item), translatedType])
        itm.setData(0, self.FilePathRole, item.filename)
        itm.setData(0, self.TypeRole, item.typ)
        itm.setTextAlignment(0, Qt.Alignment(Qt.AlignRight))
    
    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
        """
        itmList = [i for i in self.resultList.selectedItems()
                   if i.parent() is not None]
        return itmList
    
    def __editWhiteList(self):
        """
        Private slot to edit the whitelist.
        """
        from .EditWhiteListDialog import EditWhiteListDialog
        dlg = EditWhiteListDialog(self.__data["WhiteLists"])
        if dlg.exec_() == QDialog.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():
            try:
                whitelists[itm.data(0, self.TypeRole)].append(itm.text(1))
            except KeyError:
                # ignore non-existing types
                pass
            # 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]))
            try:
                if sorted(whitelist) != sorted(self.__data["WhiteLists"][key]):
                    self.__data["WhiteLists"][key] = whitelist[:]
                    changed = True
            except KeyError:
                # ignore non-existing types
                pass
        
        if changed:
            self.__project.setData("CHECKERSPARMS", "Vulture", self.__data)

eric ide

mercurial