PluginVulture.py

Tue, 10 Dec 2024 15:48:52 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 10 Dec 2024 15:48:52 +0100
branch
eric7
changeset 123
f515f0152188
parent 121
9b0f92e34a3f
permissions
-rw-r--r--

Updated copyright for 2025.

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

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

"""
Module implementing the vulture plug-in.
"""

import os

from PyQt6.QtCore import QObject, QTranslator, pyqtSignal

from eric7 import Preferences
from eric7.EricGui.EricAction import EricAction
from eric7.EricWidgets.EricApplication import ericApp
from eric7.SystemUtilities import PythonUtilities

# Start-Of-Header
__header__ = {
    "name": "Unused Code Checker Plug-in",
    "author": "Detlev Offenbach <detlev@die-offenbachs.de>",
    "autoactivate": True,
    "deactivateable": True,
    "version": "10.3.4",
    "className": "VulturePlugin",
    "packageName": "VultureChecker",
    "shortDescription": "Plug-in to detect unused code using the 'vulture' library",
    "longDescription": "Plug-in to detect unused code using the 'vulture' library.",
    "needsRestart": False,
    "hasCompiledForms": True,
    "pyqtApi": 2,
}
# End-Of-Header

error = ""


class VulturePlugin(QObject):
    """
    Class documentation goes here.

    @signal analysisDone(str, dict) emitted when the code analysis has
        been completed for a file
    @signal error(str, str) emitted in case of an error
    @signal batchFinished() emitted when a style check batch is done
    """

    analysisDone = pyqtSignal(str, dict)
    batchFinished = pyqtSignal()
    error = pyqtSignal(str, str)

    def __init__(self, ui):
        """
        Constructor

        @param ui reference to the user interface object
        @type UserInterface
        """
        super().__init__(ui)
        self.__ui = ui
        self.__initialize()

        self.backgroundService = ericApp().getObject("BackgroundService")

        path = os.path.join(os.path.dirname(__file__), __header__["packageName"])
        self.backgroundService.serviceConnect(
            "vulture",
            "Python3",
            path,
            "VultureCheckerService",
            self.vultureCheckDone,
            onErrorCallback=self.serviceErrorPy3,
            onBatchDone=self.batchJobDone,
        )

        self.queuedBatches = []
        self.batchesFinished = True

        self.__translator = None
        self.__loadTranslator()

    def __serviceError(self, fn, msg):
        """
        Private slot handling service errors.

        @param fn file name
        @type str
        @param msg message text
        @type str
        """
        self.error.emit(fn, msg)

    def serviceErrorPy3(self, fx, lang, fn, msg):
        """
        Public slot handling service errors for Python 3.

        @param fx service name
        @type str
        @param lang language
        @type str
        @param fn file name
        @type str
        @param msg message text
        @type str
        """
        if fx in ["vulture", "batch_vulture"] and lang == "Python3":
            if fx == "vulture":
                self.__serviceError(fn, msg)
            else:
                self.__serviceError(self.tr("Python 3 batch job"), msg)
                self.batchJobDone(fx, lang)

    def batchJobDone(self, fx, lang):
        """
        Public slot handling the completion of a batch job.

        @param fx service name
        @type str
        @param lang language
        @type str
        """
        if fx in ["vulture", "batch_vulture"]:
            if lang in self.queuedBatches:
                self.queuedBatches.remove(lang)
            # prevent sending the signal multiple times
            if len(self.queuedBatches) == 0 and not self.batchesFinished:
                self.batchFinished.emit()
                self.batchesFinished = True

    def vultureCheckDone(self, filename, result):
        """
        Public slot to dispatch the result.

        @param filename name of the file the results belong to
        @type str
        @param result result dictionary
        @type dict
        """
        self.analysisDone.emit(filename, result)

    def __initialize(self):
        """
        Private slot to (re)initialize the plug-in.
        """
        self.__projectAct = None
        self.__projectVultureCheckerDialog = None

    def vultureCheck(self, lang, filename, source):
        """
        Public method to prepare a vulture check for a Python project.

        @param lang language of the files or None to determine by internal
            algorithm
        @type str or None
        @param filename name of the file to analyze
        @type str
        @param source string containing the code
        @type str
        """
        if lang is None:
            try:
                if PythonUtilities.isPythonSource(filename, source):
                    lang = "Python3"
            except AttributeError:
                # backward compatibility for eric-ide < 24.8
                if PythonUtilities.determinePythonVersion(filename, source) == 3:
                    lang = "Python3"
        if lang == "Python3":
            self.backgroundService.enqueueRequest("vulture", lang, filename, [source])

    def vultureCheckBatch(self, argumentsList):
        """
        Public method to prepare a vulture check for a Python project using
        the batch mode.

        @param argumentsList list of arguments tuples with each tuple
            containing filename and source
        @type (str, str)
        """
        data = {
            "Python3": [],
        }
        for filename, source in argumentsList:
            try:
                if PythonUtilities.isPythonSource(filename, source):
                    data["Python3"].append((filename, source))
            except AttributeError:
                # backward compatibility for eric-ide < 24.8
                if PythonUtilities.determinePythonVersion(filename, source) == 3:
                    data["Python3"].append((filename, source))

        self.queuedBatches = []
        if data["Python3"]:
            self.queuedBatches.append("Python3")
            self.backgroundService.enqueueRequest(
                "batch_vulture", "Python3", "", data["Python3"]
            )
            self.batchesFinished = False

    def cancelVultureCheckBatch(self):
        """
        Public method to cancel all batch jobs.
        """
        self.backgroundService.requestCancel("batch_vulture", "Python3")

    def activate(self):
        """
        Public method to activate this plug-in.

        @return tuple of None and activation status
        @rtype tuple of (None, bool)
        """
        global error
        error = ""  # clear previous error

        menu = ericApp().getObject("Project").getMenu("Checks")
        if menu:
            self.__projectAct = EricAction(
                self.tr("Check Unused Code"),
                self.tr("&Unused Code..."),
                0,
                0,
                self,
                "project_check_vulture",
            )
            self.__projectAct.setStatusTip(self.tr("Check for unused code"))
            self.__projectAct.setWhatsThis(
                self.tr(
                    """<b>Check Unused Code...</b>"""
                    """<p>This checks a Python project for unused code.</p>"""
                )
            )
            self.__projectAct.triggered.connect(self.__projectVultureCheck)
            ericApp().getObject("Project").addEricActions([self.__projectAct])
            menu.addAction(self.__projectAct)

        ericApp().getObject("Project").showMenu.connect(self.__projectShowMenu)
        ericApp().getObject("Project").projectClosed.connect(self.__projectClosed)

        return None, True

    def deactivate(self):
        """
        Public method to deactivate this plug-in.
        """
        ericApp().getObject("Project").showMenu.disconnect(self.__projectShowMenu)
        ericApp().getObject("Project").projectClosed.disconnect(self.__projectClosed)

        menu = ericApp().getObject("Project").getMenu("Checks")
        if menu is not None and self.__projectAct is not None:
            menu.removeAction(self.__projectAct)
            ericApp().getObject("Project").removeEricActions([self.__projectAct])

        self.__initialize()

    def __loadTranslator(self):
        """
        Private method to load the translation file.
        """
        if self.__ui is not None:
            loc = self.__ui.getLocale()
            if loc and loc != "C":
                locale_dir = os.path.join(
                    os.path.dirname(__file__), "VultureChecker", "i18n"
                )
                translation = "vulture_{0}".format(loc)
                translator = QTranslator(None)
                loaded = translator.load(translation, locale_dir)
                if loaded:
                    self.__translator = translator
                    ericApp().installTranslator(self.__translator)
                else:
                    print(
                        "Warning: translation file '{0}' could not be"
                        " loaded.".format(translation)
                    )
                    print("Using default.")

    def __projectShowMenu(
        self,
        menuName,
        menu,  # noqa: U100
    ):
        """
        Private slot called, when the the project menu or a submenu is
        about to be shown.

        @param menuName name of the menu to be shown
        @type str
        @param menu reference to the menu
        @type QMenu
        """
        if menuName == "Check" and self.__projectAct is not None:
            self.__projectAct.setEnabled(
                ericApp().getObject("Project").getProjectLanguage() == "Python3"
            )

    def __projectVultureCheck(self):
        """
        Private slot used to check the project for unused code.
        """
        project = ericApp().getObject("Project")
        project.saveAllScripts()
        ppath = project.getProjectPath()
        files = [
            os.path.join(ppath, file_)
            for file_ in project.getSources()
            if file_.endswith(tuple(Preferences.getPython("Python3Extensions")))
        ]

        if self.__projectVultureCheckerDialog is None:
            from VultureChecker.VultureCheckerDialog import (  # noqa: I101
                VultureCheckerDialog,
            )

            self.__projectVultureCheckerDialog = VultureCheckerDialog(self)
        self.__projectVultureCheckerDialog.show()
        self.__projectVultureCheckerDialog.prepare(files, project)

    def __projectClosed(self):
        """
        Private slot to handle closing a project.
        """
        if self.__projectVultureCheckerDialog:
            self.__projectVultureCheckerDialog.clear()


def installDependencies(pipInstall):
    """
    Function to install dependencies of this plug-in.

    @param pipInstall function to be called with a list of package names.
    @type function
    """
    try:
        import vulture  # __IGNORE_WARNING__
    except ImportError:
        pipInstall(["vulture>=2.3"])


#
# eflag: noqa = M801

eric ide

mercurial