VultureChecker/VultureCheckerDialog.py

changeset 4
2a12ec365dd1
parent 3
0e548b994980
child 6
76c0f3ed7ac5
--- a/VultureChecker/VultureCheckerDialog.py	Sun Oct 04 18:40:19 2015 +0200
+++ b/VultureChecker/VultureCheckerDialog.py	Mon Oct 05 19:40:51 2015 +0200
@@ -17,10 +17,10 @@
 import os
 import fnmatch
 
-from PyQt5.QtCore import pyqtSlot, qVersion, Qt, QTimer, QLocale
+from PyQt5.QtCore import pyqtSlot, qVersion, Qt, QTimer, QRegExp
 from PyQt5.QtWidgets import (
     QDialog, QDialogButtonBox, QAbstractButton, QHeaderView, QTreeWidgetItem,
-    QApplication
+    QApplication, QMenu
 )
 
 from .Ui_VultureCheckerDialog import Ui_VultureCheckerDialog
@@ -33,12 +33,12 @@
 from .vulture import Item
 
 
-# TODO: add a whitelist (incl. context menu to add an entry to the list)
 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):
         """
@@ -58,6 +58,20 @@
         
         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)
@@ -66,19 +80,20 @@
         self.cancelled = False
         
         self.__project = e5App().getObject("Project")
-        self.__locale = QLocale()
         self.__finished = True
         self.__errorItem = None
+        self.__data = None
         
         self.__fileList = []
         self.filterFrame.setVisible(False)
-    
-    def __resizeResultColumns(self):
-        """
-        Private method to resize the list columns.
-        """
-        self.resultList.header().resizeSections(QHeaderView.ResizeToContents)
-        self.resultList.header().setStretchLastSection(True)
+        
+        self.__translatedTypes = {
+            "property": self.tr("Property"),
+            "function": self.tr("Function"),
+            "attribute": self.tr("Attribute"),
+            "variable": self.tr("Variable"),
+            "class": self.tr("Class"),
+        }
     
     def __createErrorItem(self, filename, message):
         """
@@ -122,8 +137,22 @@
         
         self.__data = self.__project.getData(
             "CHECKERSPARMS", "Vulture")
-        if self.__data is None or "ExcludeFiles" not in self.__data:
-            self.__data = {"ExcludeFiles": ""}
+        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": [],
+                "__patterns__": [
+                    "on_*",
+                    "visit_*",
+                ],
+            }
         self.excludeFilesEdit.setText(self.__data["ExcludeFiles"])
     
     def start(self, fn):
@@ -226,8 +255,6 @@
         
         The results are reported to the __processResult slot.
         """
-        self.__lastFileItem = None
-        
         self.checkProgressLabel.setPath(self.tr("Preparing files..."))
         progress = 0
         
@@ -364,8 +391,7 @@
         fileList = self.__fileList[:]
         
         filterString = self.excludeFilesEdit.text()
-        if "ExcludeFiles" not in self.__data or \
-           filterString != self.__data["ExcludeFiles"]:
+        if filterString != self.__data["ExcludeFiles"]:
             self.__data["ExcludeFiles"] = filterString
             self.__project.setData(
                 "CHECKERSPARMS", "Vulture", self.__data)
@@ -424,14 +450,14 @@
         @param result result dictionary
         @type dict
         """
-        self.__definedAttrs.extend(
-            [self.__dict2Item(d) for d in result["DefinedAttributes"]])
-        self.__definedFuncs.extend(
-            [self.__dict2Item(d) for d in result["DefinedFunctions"]])
-        self.__definedProps.extend(
-            [self.__dict2Item(d) for d in result["DefinedProperties"]])
-        self.__definedVars.extend(
-            [self.__dict2Item(d) for d in result["DefinedVariables"]])
+        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.__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(
@@ -452,6 +478,23 @@
         """
         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.
@@ -474,7 +517,10 @@
         """
         return self.__getUnusedItems(
             self.__definedFuncs,
-            self.__usedAttrs + self.__usedVars + self.__namesImportedAsAliases)
+            self.__usedAttrs + self.__usedVars + 
+            self.__namesImportedAsAliases +
+            self.__data["WhiteLists"]["function"] +
+            self.__data["WhiteLists"]["class"])
 
     def __unusedProperties(self):
         """
@@ -483,7 +529,9 @@
         @return list of unused properties
         @rtype list of vulture.Item
         """
-        return self.__getUnusedItems(self.__definedProps, self.__usedAttrs)
+        return self.__getUnusedItems(
+            self.__definedProps,
+            self.__usedAttrs + self.__data["WhiteLists"]["property"])
 
     def __unusedVariables(self):
         """
@@ -495,7 +543,8 @@
         return self.__getUnusedItems(
             self.__definedVars,
             self.__usedAttrs + self.__usedVars + self.__tupleAssignVars +
-            self.__namesImportedAsAliases)
+            self.__namesImportedAsAliases +
+            self.__data["WhiteLists"]["variable"])
 
     def __unusedAttributes(self):
         """
@@ -504,30 +553,153 @@
         @return list of unused attributes
         @rtype list of vulture.Item
         """
-        return self.__getUnusedItems(self.__definedAttrs, self.__usedAttrs)
+        return self.__getUnusedItems(
+            self.__definedAttrs,
+            self.__usedAttrs + self.__data["WhiteLists"]["attribute"])
     
     def __createResultItems(self):
         """
         Private method to populate the list with the analysis result.
-        """
+        """         # __IGNORE_WARNING__
         def filename(item):
             return item.file
         
         lastFileItem = None
         lastFileName = ""
-        for item in sorted(self.__unusedFunctions() + 
+        for item in sorted(self.__unusedFunctions() +
                            self.__unusedProperties() +
-                           self.__unusedVariables() + 
+                           self.__unusedVariables() +
                            self.__unusedAttributes(),
                            key=filename):
             if lastFileItem is None or lastFileName != item.file:
-                lastFileItem = QTreeWidgetItem(self.resultList, [
-                    self.__project.getRelativePath(item.file)])
-                lastFileItem.setData(0, self.FilePathRole, item.file)
-                lastFileItem.setExpanded(True)
-                lastFileItem.setFirstColumnSpanned(True)
+                lastFileItem = self.__createFileItem(item.file)
                 lastFileName = item.file
             
-            itm = QTreeWidgetItem(lastFileItem, [
-                "{0:6d}".format(item.lineno), str(item), item.typ])
-            itm.setData(0, self.FilePathRole, item.file)
+            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.file)
+        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
+        """
+        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.
+        """
+        # TODO: add a whitelist edit dialog with "add" and "delete"
+##        whitelist = dlg.getWhiteList()
+##        self.__storeWhiteList(whitelist)
+    
+    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.__storeWhiteList(whitelists)
+    
+    def __storeWhiteList(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