PluginPyLint.py

Wed, 30 Dec 2020 11:02:01 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 30 Dec 2020 11:02:01 +0100
changeset 94
45d226917534
parent 91
9dc14465123e
child 95
50eba81e4a9f
permissions
-rw-r--r--

Updated copyright for 2021.

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

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

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

import re
import os
import copy
import platform

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

from E5Gui.E5Application import e5App
from E5Gui.E5Action import E5Action
from E5Gui import E5MessageBox

from Project.ProjectBrowserModel import ProjectBrowserFileItem

import Preferences
import Utilities

# Start-of-Header
name = "PyLint Plugin"
author = "Detlev Offenbach <detlev@die-offenbachs.de>"
autoactivate = True
deactivateable = True
version = "7.1.2"
className = "PyLintPlugin"
packageName = "PyLint"
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
pyqtApi = 2
# End-of-Header

error = ""

exePy3 = []


def exeDisplayDataList():
    """
    Public method to support the display of some executable info.
    
    @return dictionary containing the data to query the presence of
        the executable
    """
    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 (string)
    @return version string of detected version (string)
    """
    proc = QProcess()
    proc.setProcessChannelMode(QProcess.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 (int)
    @return path name of the executable (string)
    """
    # Determine Python Version
    if majorVersion == 3:
        minorVersions = range(10)
    else:
        return []
    
    executables = set()
    if Utilities.isWindowsPlatform():
        #
        # Windows
        #
        try:
            import winreg
        except ImportError:
            import _winreg as winreg    # __IGNORE_WARNING__
        
        def getExePath(branch, access, versionStr):
            exes = []
            try:
                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)
            except (WindowsError, OSError):   # __IGNORE_WARNING__
                pass
            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 = Utilities.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 = Utilities.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 (boolean)
    """
    global error, exePy3
    
    exePy3 = _findExecutable(3)
    if exePy3[0] == '':
        error = QCoreApplication.translate(
            "PyLintPlugin", "The pylint executable could not be found.")
        return False
    elif exePy3[1] < '0.23.0':
        error = QCoreApplication.translate(
            "PyLintPlugin", "PyLint version < 0.23.0.")
        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 (UI.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 (boolean)
        """
        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 = e5App().getObject("Project").getMenu("Checks")
        if menu:
            self.__projectAct = E5Action(
                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)
            e5App().getObject("Project").addE5Actions([self.__projectAct])
            menu.addAction(self.__projectAct)
            
            self.__projectShowAct = E5Action(
                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)
            e5App().getObject("Project").addE5Actions([self.__projectShowAct])
            menu.addAction(self.__projectShowAct)
        
        self.__editorAct = E5Action(
            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)
        
        e5App().getObject("Project").showMenu.connect(self.__projectShowMenu)
        e5App().getObject("ProjectBrowser").getProjectBrowser(
            "sources").showMenu.connect(self.__projectBrowserShowMenu)
        e5App().getObject("ViewManager").editorOpenedEd.connect(
            self.__editorOpened)
        e5App().getObject("ViewManager").editorClosedEd.connect(
            self.__editorClosed)
        
        for editor in e5App().getObject("ViewManager").getOpenEditors():
            self.__editorOpened(editor)
        
        error = ""
        return None, True

    def deactivate(self):
        """
        Public method to deactivate this plugin.
        """
        e5App().getObject("Project").showMenu.disconnect(
            self.__projectShowMenu)
        e5App().getObject("ProjectBrowser").getProjectBrowser(
            "sources").showMenu.disconnect(self.__projectBrowserShowMenu)
        e5App().getObject("ViewManager").editorOpenedEd.disconnect(
            self.__editorOpened)
        e5App().getObject("ViewManager").editorClosedEd.disconnect(
            self.__editorClosed)
        
        menu = e5App().getObject("Project").getMenu("Checks")
        if menu:
            if self.__projectAct:
                menu.removeAction(self.__projectAct)
                e5App().getObject("Project").removeE5Actions(
                    [self.__projectAct])
            if self.__projectShowAct:
                menu.removeAction(self.__projectShowAct)
                e5App().getObject("Project").removeE5Actions(
                    [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__),
                                          "PyLint", "i18n")
                translation = "pylint_{0}".format(loc)
                translator = QTranslator(None)
                loaded = translator.load(translation, locale_dir)
                if loaded:
                    self.__translator = translator
                    e5App().installTranslator(self.__translator)
                else:
                    print("Warning: translation file '{0}' could not be"
                          " loaded.".format(translation))
                    print("Using default.")
    
    def __projectShowMenu(self, menuName, menu):
        """
        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 (string)
        @param menu reference to the menu (QMenu)
        """
        if menuName == "Checks":
            lang = e5App().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 (string)
        @param menu reference to the menu (QMenu)
        """
        if (
            menuName == "Checks" and
            e5App().getObject("Project").getProjectLanguage()
            .startswith("Python")
        ):
            self.__projectBrowserMenu = menu
            if self.__projectBrowserAct is None:
                self.__projectBrowserAct = E5Action(
                    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 = E5Action(
                    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 = (
                e5App().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
        @param mpName name of module or package to be checked (string)
        @param forProject flag indicating a run for the project (boolean)
        @param forEditor flag indicating a run for an editor (boolean)
        """
        if forEditor:
            parms = copy.deepcopy(self.__editorParms)
            editor = e5App().getObject("ViewManager").getOpenEditor(mpName)
            majorVersionStr = editor.getLanguage()
        else:
            parms = project.getData('CHECKERSPARMS', "PYLINT")
            majorVersionStr = project.getProjectLanguage()
        exe, version = {"Python3": exePy3}.get(majorVersionStr)
        if exe == '':
            E5MessageBox.critical(
                None,
                self.tr("pylint"),
                self.tr("""The pylint executable could not be found."""))
            return
        elif version < '0.23.0':
            E5MessageBox.critical(
                None,
                self.tr("pylint"),
                self.tr("PyLint version < 0.23.0."))
            return
        
        from PyLint.PyLintConfigDialog import PyLintConfigDialog
        dlg = PyLintConfigDialog(project.getProjectPath(), exe, parms, version)
        if dlg.exec() == QDialog.Accepted:
            args, parms = dlg.generateParameters()
            self.__editorParms = copy.deepcopy(parms)
            if not forEditor:
                project.setData('CHECKERSPARMS', "PYLINT", parms)
            
            # now do the call
            from PyLint.PyLintExecDialog import PyLintExecDialog
            dlg2 = PyLintExecDialog()
            reportFile = parms.get('reportFile', None)
            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 = e5App().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 = e5App().getObject("Project")
        browser = (
            e5App().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 (QScintilla.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 (QScintilla.Editor)
        """
        try:
            self.__editors.remove(editor)
        except ValueError:
            pass
    
    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 (string)
        @param menu reference to the menu (QMenu)
        @param editor reference to the editor (QScintilla.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 = e5App().getObject("ViewManager").activeWindow()
        if editor is not None:
            if not editor.checkDirty():
                return
        
        fn = editor.getFileName()
        project = e5App().getObject("Project")
        self.__pyLint(project, fn, False, True)

#
# eflag: noqa = M801

eric ide

mercurial