--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Plugins/PluginCodeStyleChecker.py Sat May 15 18:45:04 2021 +0200 @@ -0,0 +1,459 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the code style checker plug-in. +""" + +import os +import textwrap +import contextlib + +from PyQt5.QtCore import QObject, pyqtSignal, QCoreApplication + +from E5Gui.E5Application import e5App +from E5Gui.E5Action import E5Action +from Project.ProjectBrowserModel import ProjectBrowserFileItem +from Utilities import determinePythonVersion + +import Preferences +import UI.Info + +# Start-Of-Header +name = "Code Style Checker Plugin" +author = "Detlev Offenbach <detlev@die-offenbachs.de>" +autoactivate = True +deactivateable = True +version = UI.Info.VersionOnly +className = "CodeStyleCheckerPlugin" +packageName = "__core__" +shortDescription = "Show the Python Code Style Checker dialog." +longDescription = ( + """This plugin implements the Python Code Style""" + """ Checker dialog. A PEP-8 checker is used to check Python source""" + """ files for compliance to the code style conventions given in PEP-8.""" + """ A PEP-257 checker is used to check Python source files for""" + """ compliance to docstring conventions given in PEP-257 and an""" + """ eric variant is used to check against eric conventions.""" +) +pyqtApi = 2 +# End-Of-Header + + +error = "" + + +class CodeStyleCheckerPlugin(QObject): + """ + Class implementing the code style checker plug-in. + + @signal styleChecked(str, dict, int, list) emitted when the style check was + done for a file. + @signal batchFinished() emitted when a style check batch is done + @signal error(str, str) emitted in case of an error + """ + styleChecked = pyqtSignal(str, dict, int, list) + batchFinished = pyqtSignal() + error = pyqtSignal(str, str) + + def __init__(self, ui): + """ + Constructor + + @param ui reference to the user interface object (UI.UserInterface) + """ + super().__init__(ui) + self.__ui = ui + self.__initialize() + + self.backgroundService = e5App().getObject("BackgroundService") + + path = os.path.join( + os.path.dirname(__file__), 'CheckerPlugins', 'CodeStyleChecker') + self.backgroundService.serviceConnect( + 'style', 'Python3', path, 'CodeStyleChecker', + self.__translateStyleCheck, + onErrorCallback=self.serviceErrorPy3, + onBatchDone=self.batchJobDone) + + self.queuedBatches = [] + self.batchesFinished = True + + self.__wrapper = textwrap.TextWrapper(width=80) + + def __serviceError(self, fn, msg): + """ + Private slot handling service errors. + + @param fn file name (string) + @param msg message text (string) + """ + self.error.emit(fn, msg) + + def serviceErrorPy3(self, fx, lang, fn, msg): + """ + Public slot handling service errors for Python 3. + + @param fx service name (string) + @param lang language (string) + @param fn file name (string) + @param msg message text (string) + """ + if fx in ['style', 'batch_style'] and lang == 'Python3': + if fx == 'style': + self.__serviceError(fn, msg) + else: + self.__serviceError(self.tr("Python 3 batch check"), msg) + self.batchJobDone(fx, lang) + + def batchJobDone(self, fx, lang): + """ + Public slot handling the completion of a batch job. + + @param fx service name (string) + @param lang language (string) + """ + if fx in ['style', 'batch_style']: + if lang in self.queuedBatches: + self.queuedBatches.remove(lang) + # prevent sending the signal multiple times + if len(self.queuedBatches) == 0 and not self.batchesFinished: + self.batchFinished.emit() + self.batchesFinished = True + + def __initialize(self): + """ + Private slot to (re)initialize the plugin. + """ + self.__projectAct = None + self.__projectCodeStyleCheckerDialog = None + + self.__projectBrowserAct = None + self.__projectBrowserMenu = None + self.__projectBrowserCodeStyleCheckerDialog = None + + self.__editors = [] + self.__editorAct = None + self.__editorCodeStyleCheckerDialog = None + + def styleCheck(self, lang, filename, source, args): + """ + Public method to prepare a style check on one Python source file. + + @param lang language of the file or None to determine by internal + algorithm + @type str or None + @param filename source filename + @type str + @param source string containing the code to check + @type str + @param args arguments used by the codeStyleCheck function (list of + excludeMessages, includeMessages, repeatMessages, fixCodes, + noFixCodes, fixIssues, maxLineLength, blankLines, hangClosing, + docType, codeComplexityArgs, miscellaneousArgs, errors, eol, + encoding, backup) + @type list of (str, str, bool, str, str, bool, int, list of (int, int), + bool, str, dict, dict, list of str, str, str, bool) + """ + if lang is None: + lang = 'Python{0}'.format(determinePythonVersion(filename, source)) + if lang != 'Python3': + return + + data = [source, args] + self.backgroundService.enqueueRequest('style', lang, filename, data) + + def styleBatchCheck(self, argumentsList): + """ + Public method to prepare a style check on multiple Python source files. + + @param argumentsList list of arguments tuples with each tuple + containing filename, source and args as given in styleCheck() + method + @type list of tuple of (str, str, list) + """ + data = { + "Python3": [], + } + for filename, source, args in argumentsList: + lang = 'Python{0}'.format(determinePythonVersion(filename, source)) + if lang != 'Python3': + continue + else: + data[lang].append((filename, source, args)) + + self.queuedBatches = [] + if data['Python3']: + self.queuedBatches.append('Python3') + self.backgroundService.enqueueRequest('batch_style', 'Python3', "", + data['Python3']) + self.batchesFinished = False + + def cancelStyleBatchCheck(self): + """ + Public method to cancel all batch jobs. + """ + self.backgroundService.requestCancel('batch_style', 'Python3') + + def __translateStyleCheck(self, fn, codeStyleCheckerStats, results): + """ + Private slot called after perfoming a style check on one file. + + @param fn filename of the just checked file + @type str + @param codeStyleCheckerStats stats of style and name check + @type dict + @param results dictionary containing the check result data + (see CodesStyleChecker.__checkCodeStyle for details) + @type dict + """ + from CheckerPlugins.CodeStyleChecker.translations import ( + getTranslatedMessage + ) + + fixes = 0 + for result in results: + msg = getTranslatedMessage(result["code"], result["args"]) + + if result["fixcode"]: + fixes += 1 + trFixedMsg = getTranslatedMessage(result["fixcode"], + result["fixargs"]) + + msg += "\n" + QCoreApplication.translate( + 'CodeStyleCheckerDialog', "Fix: {0}").format(trFixedMsg) + + result["display"] = "\n".join(self.__wrapper.wrap(msg)) + self.styleChecked.emit(fn, codeStyleCheckerStats, fixes, results) + + def activate(self): + """ + Public method to activate this plugin. + + @return tuple of None and activation status (boolean) + """ + menu = e5App().getObject("Project").getMenu("Checks") + if menu: + self.__projectAct = E5Action( + self.tr('Check Code Style'), + self.tr('&Code Style...'), 0, 0, + self, 'project_check_pep8') + self.__projectAct.setStatusTip( + self.tr('Check code style.')) + self.__projectAct.setWhatsThis(self.tr( + """<b>Check Code Style...</b>""" + """<p>This checks Python files for compliance to the""" + """ code style conventions given in various PEPs.</p>""" + )) + self.__projectAct.triggered.connect( + self.__projectCodeStyleCheck) + e5App().getObject("Project").addE5Actions([self.__projectAct]) + menu.addAction(self.__projectAct) + + self.__editorAct = E5Action( + self.tr('Check Code Style'), + self.tr('&Code Style...'), 0, 0, + self, "") + self.__editorAct.setWhatsThis(self.tr( + """<b>Check Code Style...</b>""" + """<p>This checks Python files for compliance to the""" + """ code style conventions given in various PEPs.</p>""" + )) + self.__editorAct.triggered.connect(self.__editorCodeStyleCheck) + + 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) + + 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: + menu.removeAction(self.__projectAct) + + if self.__projectBrowserMenu and self.__projectBrowserAct: + self.__projectBrowserMenu.removeAction( + self.__projectBrowserAct) + + 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 __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" and self.__projectAct is not None: + self.__projectAct.setEnabled( + e5App().getObject("Project").getProjectLanguage() in + ["Python3", "MicroPython"]) + + 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() in + ["Python3", "MicroPython"] + ): + self.__projectBrowserMenu = menu + if self.__projectBrowserAct is None: + self.__projectBrowserAct = E5Action( + self.tr('Check Code Style'), + self.tr('&Code Style...'), 0, 0, + self, "") + self.__projectBrowserAct.setWhatsThis(self.tr( + """<b>Check Code Style...</b>""" + """<p>This checks Python files for compliance to the""" + """ code style conventions given in various PEPs.</p>""" + )) + self.__projectBrowserAct.triggered.connect( + self.__projectBrowserCodeStyleCheck) + if self.__projectBrowserAct not in menu.actions(): + menu.addAction(self.__projectBrowserAct) + + def __projectCodeStyleCheck(self): + """ + Private slot used to check the project files for code style. + """ + project = e5App().getObject("Project") + project.saveAllScripts() + ppath = project.getProjectPath() + files = [os.path.join(ppath, file) + for file in project.pdata["SOURCES"] + if file.endswith( + tuple(Preferences.getPython("Python3Extensions")))] + + from CheckerPlugins.CodeStyleChecker import CodeStyleCheckerDialog + self.__projectCodeStyleCheckerDialog = ( + CodeStyleCheckerDialog.CodeStyleCheckerDialog(self) + ) + self.__projectCodeStyleCheckerDialog.show() + self.__projectCodeStyleCheckerDialog.prepare(files, project) + + def __projectBrowserCodeStyleCheck(self): + """ + Private method to handle the code style check context menu action of + the project sources browser. + """ + browser = ( + e5App().getObject("ProjectBrowser").getProjectBrowser("sources") + ) + if browser.getSelectedItemsCount([ProjectBrowserFileItem]) > 1: + fn = [] + for itm in browser.getSelectedItems([ProjectBrowserFileItem]): + fn.append(itm.fileName()) + isDir = False + else: + itm = browser.model().item(browser.currentIndex()) + try: + fn = itm.fileName() + isDir = False + except AttributeError: + fn = itm.dirName() + isDir = True + + from CheckerPlugins.CodeStyleChecker import CodeStyleCheckerDialog + self.__projectBrowserCodeStyleCheckerDialog = ( + CodeStyleCheckerDialog.CodeStyleCheckerDialog(self) + ) + self.__projectBrowserCodeStyleCheckerDialog.show() + if isDir: + self.__projectBrowserCodeStyleCheckerDialog.start( + fn, save=True) + else: + self.__projectBrowserCodeStyleCheckerDialog.start( + fn, save=True, repeat=True) + + 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) + """ + 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 (string) + @param menu reference to the menu (QMenu) + @param editor reference to the editor + """ + if menuName == "Checks": + if self.__editorAct not in menu.actions(): + menu.addAction(self.__editorAct) + self.__editorAct.setEnabled(editor.isPyFile()) + + def __editorCodeStyleCheck(self): + """ + Private slot to handle the code style check context menu action + of the editors. + """ + editor = e5App().getObject("ViewManager").activeWindow() + if ( + editor is not None and + editor.checkDirty() and + editor.getFileName() is not None + ): + from CheckerPlugins.CodeStyleChecker import ( + CodeStyleCheckerDialog + ) + self.__editorCodeStyleCheckerDialog = ( + CodeStyleCheckerDialog.CodeStyleCheckerDialog(self) + ) + self.__editorCodeStyleCheckerDialog.show() + self.__editorCodeStyleCheckerDialog.start( + editor.getFileName(), + save=True, + repeat=True)