PluginPyLint.py

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

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 10 Dec 2024 15:48:50 +0100
branch
eric7
changeset 119
ebb5306aeb60
parent 117
f8955e5dba87
permissions
-rw-r--r--

Updated copyright for 2025.

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

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

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

import contextlib
import copy
import os
import platform
import re

from PyQt6.QtCore import QCoreApplication, QObject, QProcess, QTranslator
from PyQt6.QtWidgets import QDialog

from eric7 import Preferences
from eric7.EricGui.EricAction import EricAction
from eric7.EricWidgets import EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp
from eric7.Project.ProjectBrowserModel import ProjectBrowserFileItem

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

# Start-of-Header
__header__ = {
    "name": "PyLint Plugin",
    "author": "Detlev Offenbach <detlev@die-offenbachs.de>",
    "autoactivate": True,
    "deactivateable": True,
    "version": "10.3.0",
    "className": "PyLintPlugin",
    "packageName": "PyLintInterface",
    "shortDescription": "Show the PyLint dialogs.",
    "longDescription": (
        "This plug-in implements the PyLint dialogs. PyLint is used to check"
        " Python source files according to various rules."
    ),
    "needsRestart": False,
    "hasCompiledForms": True,
    "pyqtApi": 2,
}
# End-of-Header

error = ""

exePy3 = []


def exeDisplayDataList():
    """
    Public method to support the display of some executable info.

    @return list of dictionaries containing the data to query the presence of
        the executable
    @rtype list of dict
    """
    dataList = []
    data = {
        "programEntry": True,
        "header": QCoreApplication.translate("PyLintPlugin", "Checkers - Pylint"),
        "exe": "dummypylint",
        "versionCommand": "--version",
        "versionStartsWith": "dummypylint",
        "versionPosition": -1,
        "version": "",
        "versionCleanup": None,
    }
    if _checkProgram():
        for exePath in (exePy3[0],):
            if exePath:
                data["exe"] = exePath
                data["versionStartsWith"] = "pylint"
                dataList.append(data.copy())
    else:
        dataList.append(data)
    return dataList


def __getProgramVersion(exe):
    """
    Private method to generate a program entry.

    @param exe name of the executable program
    @type str
    @return version string of detected version
    @rtype str
    """
    proc = QProcess()
    proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
    proc.start(exe, ["--version"])
    finished = proc.waitForFinished(10000)
    if finished:
        output = str(
            proc.readAllStandardOutput(), Preferences.getSystem("IOEncoding"), "replace"
        )
        versionRe = re.compile("^pylint", re.UNICODE)
        for line in output.splitlines():
            if versionRe.search(line):
                return line.split()[-1]

    return "0.0.0"


def _findExecutable(majorVersion):
    """
    Restricted function to determine the name and path of the executable.

    @param majorVersion major python version of the executables
    @type int
    @return path name of the executable
    @rtype str
    """
    # Determine Python Version
    if majorVersion == 3:
        minorVersions = range(10)
    else:
        return []

    executables = set()
    if isWindowsPlatform():
        #
        # Windows
        #
        try:
            import winreg  # noqa: I101, I103
        except ImportError:
            import _winreg as winreg  # __IGNORE_WARNING__

        def getExePath(branch, access, versionStr):
            exes = []
            with contextlib.suppress(WindowsError, OSError):
                software = winreg.OpenKey(branch, "Software", 0, access)
                python = winreg.OpenKey(software, "Python", 0, access)
                pcore = winreg.OpenKey(python, "PythonCore", 0, access)
                version = winreg.OpenKey(pcore, versionStr, 0, access)
                installpath = winreg.QueryValue(version, "InstallPath")
                # Look for the batch script variant
                exe = os.path.join(installpath, "Scripts", "pylint.bat")
                if os.access(exe, os.X_OK):
                    exes.append(exe)
                # Look for the executable variant
                exe = os.path.join(installpath, "Scripts", "pylint.exe")
                if os.access(exe, os.X_OK):
                    exes.append(exe)
            return exes

        versionSuffixes = ["", "-32", "-64"]
        for minorVersion in minorVersions:
            for versionSuffix in versionSuffixes:
                versionStr = "{0}.{1}{2}".format(
                    majorVersion, minorVersion, versionSuffix
                )
                exePaths = getExePath(
                    winreg.HKEY_CURRENT_USER,
                    winreg.KEY_WOW64_32KEY | winreg.KEY_READ,
                    versionStr,
                )
                if exePaths:
                    for exePath in exePaths:
                        executables.add(exePath)

                exePaths = getExePath(
                    winreg.HKEY_LOCAL_MACHINE,
                    winreg.KEY_WOW64_32KEY | winreg.KEY_READ,
                    versionStr,
                )
                if exePaths:
                    for exePath in exePaths:
                        executables.add(exePath)

                # Even on Intel 64-bit machines it's 'AMD64'
                if platform.machine() == "AMD64":
                    exePaths = getExePath(
                        winreg.HKEY_CURRENT_USER,
                        winreg.KEY_WOW64_64KEY | winreg.KEY_READ,
                        versionStr,
                    )
                    if exePaths:
                        for exePath in exePaths:
                            executables.add(exePath)

                    exePaths = getExePath(
                        winreg.HKEY_LOCAL_MACHINE,
                        winreg.KEY_WOW64_64KEY | winreg.KEY_READ,
                        versionStr,
                    )
                    if exePaths:
                        for exePath in exePaths:
                            executables.add(exePath)

        if not executables and majorVersion >= 3:
            # check the PATH environment variable if nothing was found
            # Python 3 only
            path = getEnvironmentEntry("PATH")
            if path:
                dirs = path.split(os.pathsep)
                for directory in dirs:
                    for suffix in (".bat", ".exe"):
                        exe = os.path.join(directory, "pylint" + suffix)
                        if os.access(exe, os.X_OK):
                            executables.add(exe)
    else:
        #
        # Linux, Unix ...
        pylintScript = "pylint"
        scriptSuffixes = [
            "",
            "-python{0}".format(majorVersion),
            "{0}".format(majorVersion),
        ]
        for minorVersion in minorVersions:
            scriptSuffixes.append("-python{0}.{1}".format(majorVersion, minorVersion))
        # There could be multiple pylint executables in the path
        # e.g. for different python variants
        path = getEnvironmentEntry("PATH")
        # environment variable not defined
        if path is None:
            return []

        # step 1: determine possible candidates
        exes = []
        dirs = path.split(os.pathsep)
        for directory in dirs:
            for suffix in scriptSuffixes:
                exe = os.path.join(directory, pylintScript + suffix)
                if os.access(exe, os.X_OK):
                    exes.append(exe)

        # step 2: determine the Python variant
        _exePy3 = set()
        versionArgs = ["-c", "import sys; print(sys.version_info[0])"]
        for exe in exes:
            with open(exe, "r") as f:
                line0 = f.readline()
            program = line0.replace("#!", "").strip()
            process = QProcess()
            process.start(program, versionArgs)
            process.waitForFinished(5000)
            # get a QByteArray of the output
            versionBytes = process.readAllStandardOutput()
            versionStr = str(versionBytes, encoding="utf-8").strip()
            if versionStr == "3":
                _exePy3.add(exe)

        executables = _exePy3

    # Find the executable with the highest version number
    maxVersion = "0.0.0"
    maxExe = ""
    for executable in list(executables):
        version = __getProgramVersion(executable)
        if version > maxVersion:
            maxVersion = version
            maxExe = executable

    return maxExe, maxVersion


def _checkProgram():
    """
    Restricted function to check the availability of pylint.

    @return flag indicating availability
    @rtype bool
    """
    global error, exePy3

    exePy3 = _findExecutable(3)
    if exePy3[0] == "":
        error = QCoreApplication.translate(
            "PyLintPlugin", "The pylint executable could not be found."
        )
        return False
    else:
        return True


class PyLintPlugin(QObject):
    """
    Class implementing the PyLint plug-in.
    """

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

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

        self.__translator = None
        self.__loadTranslator()

    def __initialize(self):
        """
        Private slot to (re)initialize the plugin.
        """
        self.__projectAct = None
        self.__projectShowAct = None
        self.__pylintPDialog = None

        self.__projectBrowserAct = None
        self.__projectBrowserShowAct = None
        self.__projectBrowserMenu = None
        self.__pylintPsbDialog = None

        self.__editors = []
        self.__editorAct = None
        self.__editorPylintDialog = None
        self.__editorParms = None

    def activate(self):
        """
        Public method to activate this plugin.

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

        # There is already an error, don't activate
        if error:
            return None, False
        # pylint is only activated if it is available
        if not _checkProgram():
            return None, False

        menu = ericApp().getObject("Project").getMenu("Checks")
        if menu:
            self.__projectAct = EricAction(
                self.tr("Run PyLint"),
                self.tr("Run &PyLint..."),
                0,
                0,
                self,
                "project_check_pylint",
            )
            self.__projectAct.setStatusTip(
                self.tr("Check project, packages or modules with pylint.")
            )
            self.__projectAct.setWhatsThis(
                self.tr(
                    """<b>Run PyLint...</b>"""
                    """<p>This checks the project, packages or modules using"""
                    """ pylint.</p>"""
                )
            )
            self.__projectAct.triggered.connect(self.__projectPylint)
            ericApp().getObject("Project").addEricActions([self.__projectAct])
            menu.addAction(self.__projectAct)

            self.__projectShowAct = EricAction(
                self.tr("Show PyLint Dialog"),
                self.tr("Show Py&Lint Dialog..."),
                0,
                0,
                self,
                "project_check_pylintshow",
            )
            self.__projectShowAct.setStatusTip(
                self.tr("Show the PyLint dialog with the results of the last run.")
            )
            self.__projectShowAct.setWhatsThis(
                self.tr(
                    """<b>Show PyLint Dialog...</b>"""
                    """<p>This shows the PyLint dialog with the results"""
                    """ of the last run.</p>"""
                )
            )
            self.__projectShowAct.triggered.connect(self.__projectPylintShow)
            ericApp().getObject("Project").addEricActions([self.__projectShowAct])
            menu.addAction(self.__projectShowAct)

        self.__editorAct = EricAction(
            self.tr("Run PyLint"), self.tr("Run &PyLint..."), 0, 0, self, ""
        )
        self.__editorAct.setWhatsThis(
            self.tr(
                """<b>Run PyLint...</b>"""
                """<p>This checks the loaded module using pylint.</p>"""
            )
        )
        self.__editorAct.triggered.connect(self.__editorPylint)

        ericApp().getObject("Project").showMenu.connect(self.__projectShowMenu)
        ericApp().getObject("ProjectBrowser").getProjectBrowser(
            "sources"
        ).showMenu.connect(self.__projectBrowserShowMenu)
        ericApp().getObject("ViewManager").editorOpenedEd.connect(self.__editorOpened)
        ericApp().getObject("ViewManager").editorClosedEd.connect(self.__editorClosed)

        for editor in ericApp().getObject("ViewManager").getOpenEditors():
            self.__editorOpened(editor)

        error = ""
        return None, True

    def deactivate(self):
        """
        Public method to deactivate this plugin.
        """
        ericApp().getObject("Project").showMenu.disconnect(self.__projectShowMenu)
        ericApp().getObject("ProjectBrowser").getProjectBrowser(
            "sources"
        ).showMenu.disconnect(self.__projectBrowserShowMenu)
        ericApp().getObject("ViewManager").editorOpenedEd.disconnect(
            self.__editorOpened
        )
        ericApp().getObject("ViewManager").editorClosedEd.disconnect(
            self.__editorClosed
        )

        menu = ericApp().getObject("Project").getMenu("Checks")
        if menu:
            if self.__projectAct:
                menu.removeAction(self.__projectAct)
                ericApp().getObject("Project").removeEricActions([self.__projectAct])
            if self.__projectShowAct:
                menu.removeAction(self.__projectShowAct)
                ericApp().getObject("Project").removeEricActions(
                    [self.__projectShowAct]
                )

        if self.__projectBrowserMenu:
            if self.__projectBrowserAct:
                self.__projectBrowserMenu.removeAction(self.__projectBrowserAct)
            if self.__projectBrowserShowAct:
                self.__projectBrowserMenu.removeAction(self.__projectBrowserShowAct)

        for editor in self.__editors:
            editor.showMenu.disconnect(self.__editorShowMenu)
            menu = editor.getMenu("Checks")
            if menu is not None:
                menu.removeAction(self.__editorAct)

        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__), "PyLintInterface", "i18n"
                )
                translation = "pylint_{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 == "Checks":
            lang = ericApp().getObject("Project").getProjectLanguage()
            if self.__projectAct is not None:
                self.__projectAct.setEnabled(lang.startswith("Python"))
            if self.__projectShowAct is not None:
                self.__projectShowAct.setEnabled(lang.startswith("Python"))
            self.__projectShowAct.setEnabled(self.__pylintPDialog is not None)

    def __projectBrowserShowMenu(self, menuName, menu):
        """
        Private slot called, when the the project browser 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 == "Checks" and ericApp().getObject(
            "Project"
        ).getProjectLanguage().startswith("Python"):
            self.__projectBrowserMenu = menu
            if self.__projectBrowserAct is None:
                self.__projectBrowserAct = EricAction(
                    self.tr("Run PyLint"), self.tr("Run &PyLint..."), 0, 0, self, ""
                )
                self.__projectBrowserAct.setWhatsThis(
                    self.tr(
                        """<b>Run PyLint...</b>"""
                        """<p>This checks the project, packages or modules"""
                        """ using pylint.</p>"""
                    )
                )
                self.__projectBrowserAct.triggered.connect(self.__projectBrowserPylint)

            if self.__projectBrowserShowAct is None:
                self.__projectBrowserShowAct = EricAction(
                    self.tr("Show PyLint Dialog"),
                    self.tr("Show Py&Lint Dialog..."),
                    0,
                    0,
                    self,
                    "",
                )
                self.__projectBrowserShowAct.setWhatsThis(
                    self.tr(
                        """<b>Show PyLint Dialog...</b>"""
                        """<p>This shows the PyLint dialog with the results"""
                        """ of the last run.</p>"""
                    )
                )
                self.__projectBrowserShowAct.triggered.connect(
                    self.__projectBrowserPylintShow
                )

            if self.__projectBrowserAct not in menu.actions():
                menu.addAction(self.__projectBrowserAct)
            if self.__projectBrowserShowAct not in menu.actions():
                menu.addAction(self.__projectBrowserShowAct)

            enable = (
                ericApp()
                .getObject("ProjectBrowser")
                .getProjectBrowser("sources")
                .getSelectedItemsCount([ProjectBrowserFileItem])
                == 1
            )
            self.__projectBrowserAct.setEnabled(enable)
            self.__projectBrowserShowAct.setEnabled(
                enable and self.__pylintPsbDialog is not None
            )

    def __pyLint(self, project, mpName, forProject, forEditor=False):
        """
        Private method used to perform a PyLint run.

        @param project reference to the Project object
        @type Project
        @param mpName name of module or package to be checked
        @type str
        @param forProject flag indicating a run for the project
        @type bool
        @param forEditor flag indicating a run for an editor
        @type bool
        """
        from PyLintInterface.PyLintConfigDialog import PyLintConfigDialog
        from PyLintInterface.PyLintExecDialog import PyLintExecDialog

        if forEditor:
            parms = copy.deepcopy(self.__editorParms)
            editor = ericApp().getObject("ViewManager").getOpenEditor(mpName)
            majorVersionStr = editor.getLanguage()
        else:
            parms = project.getData("CHECKERSPARMS", "PYLINT")
            majorVersionStr = project.getProjectLanguage()
        exe, version = {"Python3": exePy3}.get(majorVersionStr)
        if exe == "":
            EricMessageBox.critical(
                None,
                self.tr("pylint"),
                self.tr("""The pylint executable could not be found."""),
            )
            return

        dlg = PyLintConfigDialog(
            project.getProjectPath(), exe, parms, version, parent=self.__ui
        )
        if dlg.exec() == QDialog.DialogCode.Accepted:
            args, parms = dlg.generateParameters()
            self.__editorParms = copy.deepcopy(parms)
            if not forEditor:
                project.setData("CHECKERSPARMS", "PYLINT", parms)

            # now do the call
            dlg2 = PyLintExecDialog()
            reportFile = parms.get("reportFile")
            res = dlg2.start(args, mpName, reportFile, project.getProjectPath())
            if res:
                dlg2.show()
            if forProject:
                self.__pylintPDialog = dlg2
            elif forEditor:
                self.__editorPylintDialog = dlg2
            else:
                self.__pylintPsbDialog = dlg2

    def __projectPylint(self):
        """
        Private slot used to check the project files with Pylint.
        """
        project = ericApp().getObject("Project")
        project.saveAllScripts()
        self.__pyLint(project, project.getProjectPath(), True)

    def __projectPylintShow(self):
        """
        Private slot to show the PyLint dialog with the results of the last
        run.
        """
        if self.__pylintPDialog is not None:
            self.__pylintPDialog.show()

    def __projectBrowserPylint(self):
        """
        Private method to handle the Pylint context menu action of the project
        sources browser.
        """
        project = ericApp().getObject("Project")
        browser = ericApp().getObject("ProjectBrowser").getProjectBrowser("sources")
        itm = browser.model().item(browser.currentIndex())
        try:
            fn = itm.fileName()
        except AttributeError:
            fn = itm.dirName()
        self.__pyLint(project, fn, False)

    def __projectBrowserPylintShow(self):
        """
        Private slot to show the PyLint dialog with the results of the last
        run.
        """
        if self.__pylintPsbDialog is not None:
            self.__pylintPsbDialog.show()

    def __editorOpened(self, editor):
        """
        Private slot called, when a new editor was opened.

        @param editor reference to the new editor
        @type Editor
        """
        menu = editor.getMenu("Checks")
        if menu is not None:
            menu.addAction(self.__editorAct)
            editor.showMenu.connect(self.__editorShowMenu)
            self.__editors.append(editor)

    def __editorClosed(self, editor):
        """
        Private slot called, when an editor was closed.

        @param editor reference to the editor
        @type Editor
        """
        with contextlib.suppress(ValueError):
            self.__editors.remove(editor)

    def __editorShowMenu(self, menuName, menu, editor):
        """
        Private slot called, when the the editor context 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
        @param editor reference to the editor
        @type Editor
        """
        if menuName == "Checks":
            if self.__editorAct not in menu.actions():
                menu.addAction(self.__editorAct)
            self.__editorAct.setEnabled(editor.isPyFile())

    def __editorPylint(self):
        """
        Private slot to handle the Pylint context menu action of the editors.
        """
        editor = ericApp().getObject("ViewManager").activeWindow()
        if editor is not None and not editor.checkDirty():
            return

        fn = editor.getFileName()
        project = ericApp().getObject("Project")
        self.__pyLint(project, fn, False, True)


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 pylint  # __IGNORE_WARNING__
    except ImportError:
        pipInstall(["pylint"])


#
# eflag: noqa = M801, U200

eric ide

mercurial