PluginPyLint.py

Sat, 28 Sep 2013 13:16:58 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 28 Sep 2013 13:16:58 +0200
changeset 24
b01348dd84d5
parent 22
68beeb22dade
child 28
1dae294006e8
permissions
-rw-r--r--

Prepared new plug-in release.

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

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

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

from __future__ import unicode_literals     # __IGNORE_WARNING__
try:
    str = unicode                           # __IGNORE_WARNING__
except (NameError):
    pass

import re
import os
import copy
import platform

from PyQt4.QtCore import QObject, QTranslator, QCoreApplication, QProcess
from PyQt4.QtGui import QDialog

try:
    from E5Gui.E5Application import e5App
    from E5Gui.E5Action import E5Action
    from E5Gui import E5MessageBox
    error = ""
except ImportError:
    error = QCoreApplication.translate("PyLintPlugin",
        """Your version of Eric5 is not supported."""
        """ At least version 5.1.0 of Eric5 is needed.""")

import Preferences
import Utilities

# Start-of-Header
name = "PyLint Plugin"
author = "Detlev Offenbach <detlev@die-offenbachs.de>"
autoactivate = True
deactivateable = True
version = "5.3.0"
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

exePy2 = []
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": (0, -1),
    }
    if _checkProgram():
        for exePath in (exePy2[0], exePy3[0]):
            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):
                version = line.split()[-1]
                version = version[:-1]
                break
        else:
            version = '0.0.0'
    return version

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(5)
    elif majorVersion == 2:
        minorVersions = range(5, 9)
    else:
        return []
    
    executables = set()
    if Utilities.isWindowsPlatform():
        #
        # Windows
        #
        try:
            import winreg
        except ImportError:
            import _winreg as winreg    # __IGNORE_WARNING__
        
        def getExePath(branch, access, versionStr):
            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')
                exe = os.path.join(installpath, 'Scripts', 'pylint.bat')
                if os.access(exe, os.X_OK):
                    return exe
            except WindowsError:        # __IGNORE_WARNING__
                return None
            return None
        
        for minorVersion in minorVersions:
            versionStr = '{0}.{1}'.format(majorVersion, minorVersion)
            exePath = getExePath(winreg.HKEY_CURRENT_USER,
                winreg.KEY_WOW64_32KEY | winreg.KEY_READ, versionStr)
                
            if exePath is not None:
                executables.add(exePath)
            exePath = getExePath(winreg.HKEY_LOCAL_MACHINE,
                winreg.KEY_WOW64_32KEY | winreg.KEY_READ, versionStr)
            
            # Even on Intel 64-bit machines it's 'AMD64'
            if platform.machine() == 'AMD64':
                if exePath is not None:
                    executables.add(exePath)
                exePath = getExePath(winreg.HKEY_CURRENT_USER,
                    winreg.KEY_WOW64_64KEY | winreg.KEY_READ, versionStr)
                
                if exePath is not None:
                    executables.add(exePath)
                exePath = getExePath(winreg.HKEY_LOCAL_MACHINE,
                    winreg.KEY_WOW64_64KEY | winreg.KEY_READ, versionStr)
                
                if exePath is not None:
                    executables.add(exePath)
    else:
        #
        # Linux, Unix ...
        pylintScript = 'pylint'
        scriptSuffixes = ["",
                    "-python{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 dir in dirs:
            for suffix in scriptSuffixes:
                exe = os.path.join(dir, pylintScript + suffix)
                if os.access(exe, os.X_OK):
                    exes.append(exe)
        
        # step 2: determine the Python variant
        if Utilities.isMacPlatform():
            checkStrings = ["Python.framework/Versions/3".lower(),
                            "python3"]
        else:
            checkStrings = ["python3"]
        
        _exePy2 = set()
        _exePy3 = set()
        for exe in exes:
            try:
                f = open(exe, "r")
                line0 = f.readline()
                for checkStr in checkStrings:
                    if checkStr in line0.lower():
                        _exePy3.add(exe)
                        break
                else:
                    _exePy2.add(exe)
            finally:
                f.close()
        
        executables = _exePy3 if majorVersion == 3 else _exePy2
    
    # 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, exePy2, exePy3
    
    exePy2 = _findExecutable(2)
    exePy3 = _findExecutable(3)
    if exePy2[0] == '' and exePy3[0] == '':
        error = QCoreApplication.translate("PyLintPlugin",
            "The pylint executable could not be found.")
        return False
    elif exePy2[1] < '0.23.0' and 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.trUtf8('Run PyLint'),
                    self.trUtf8('Run &PyLint...'), 0, 0,
                    self, 'project_check_pylint')
            self.__projectAct.setStatusTip(
                self.trUtf8('Check project, packages or modules with pylint.'))
            self.__projectAct.setWhatsThis(self.trUtf8(
                """<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.trUtf8('Show PyLint Dialog'),
                    self.trUtf8('Show Py&Lint Dialog...'), 0, 0,
                    self, 'project_check_pylintshow')
            self.__projectShowAct.setStatusTip(
                self.trUtf8('Show the PyLint dialog with the results of the last run.'))
            self.__projectShowAct.setWhatsThis(self.trUtf8(
                """<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.trUtf8('Run PyLint'),
                self.trUtf8('Run &PyLint...'), 0, 0,
                self, "")
        self.__editorAct.setWhatsThis(self.trUtf8(
                """<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.trUtf8('Run PyLint'),
                        self.trUtf8('Run &PyLint...'), 0, 0,
                        self, '')
                self.__projectBrowserAct.setWhatsThis(self.trUtf8(
                    """<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.trUtf8('Show PyLint Dialog'),
                        self.trUtf8('Show Py&Lint Dialog...'), 0, 0,
                        self, '')
                self.__projectBrowserShowAct.setWhatsThis(self.trUtf8(
                    """<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 not self.__projectBrowserAct in menu.actions():
                menu.addAction(self.__projectBrowserAct)
            if not self.__projectBrowserShowAct in menu.actions():
                menu.addAction(self.__projectBrowserShowAct)
            self.__projectBrowserShowAct.setEnabled(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)
        """
        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 = {"Python": exePy2, "Python2": exePy2,
                              "Python3": exePy3}.get(majorVersionStr)
        if exe == '':
            E5MessageBox.critical(None,
                self.trUtf8("pylint"),
                self.trUtf8("""The pylint executable could not be found."""))
            return
        elif version < '0.23.0':
            E5MessageBox.critical(None,
                self.trUtf8("pylint"),
                self.trUtf8("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):
        """
        Public slot used to check the project files with Pylint.
        """
        project = e5App().getObject("Project")
        project.saveAllScripts()
        self.__pyLint(project, project.getProjectPath(), True)
    
    def __projectPylintShow(self):
        """
        Public 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):
        """
        Public 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 not self.__editorAct in menu.actions():
                menu.addAction(self.__editorAct)
            self.__editorAct.setEnabled(editor.isPy3File() or editor.isPy2File())
    
    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)

eric ide

mercurial