Fri, 31 Mar 2017 18:07:33 +0200
Fixed an issue causing the executable search to fail on macOSX, if the Python 3 interpreter is installed as 'python'.
# -*- coding: utf-8 -*- # Copyright (c) 2007 - 2017 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the PyLint plug-in. """ from __future__ import unicode_literals try: str = unicode except NameError: pass 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 = "6.1.6" 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 python2Compatible = True # End-of-Header error = "" 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]): 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) """ version = '0.0.0' 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 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): 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 for minorVersion in minorVersions: versionStr = '{0}.{1}'.format(majorVersion, minorVersion) 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) exePath = getExePath( winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_64KEY | winreg.KEY_READ, versionStr) if exePaths: for exePath in exePaths: 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 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 if Utilities.isMacPlatform(): checkStrings = ["Python.framework/Versions/3".lower(), "python3", "python"] 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.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 = {"Python": exePy2, "Python2": exePy2, "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.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) # # eflag: noqa = M801