Tue, 10 Dec 2024 15:48:50 +0100
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