1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2011 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing the code style checker plug-in. 8""" 9 10import os 11import textwrap 12import contextlib 13 14from PyQt5.QtCore import QObject, pyqtSignal, QCoreApplication 15 16from E5Gui.E5Application import e5App 17from E5Gui.E5Action import E5Action 18from Project.ProjectBrowserModel import ProjectBrowserFileItem 19from Utilities import determinePythonVersion 20 21import Preferences 22import UI.Info 23 24# Start-Of-Header 25name = "Code Style Checker Plugin" 26author = "Detlev Offenbach <detlev@die-offenbachs.de>" 27autoactivate = True 28deactivateable = True 29version = UI.Info.VersionOnly 30className = "CodeStyleCheckerPlugin" 31packageName = "__core__" 32shortDescription = "Show the Python Code Style Checker dialog." 33longDescription = ( 34 """This plugin implements the Python Code Style""" 35 """ Checker dialog. A PEP-8 checker is used to check Python source""" 36 """ files for compliance to the code style conventions given in PEP-8.""" 37 """ A PEP-257 checker is used to check Python source files for""" 38 """ compliance to docstring conventions given in PEP-257 and an""" 39 """ eric variant is used to check against eric conventions.""" 40) 41pyqtApi = 2 42# End-Of-Header 43 44 45error = "" 46 47 48class CodeStyleCheckerPlugin(QObject): 49 """ 50 Class implementing the code style checker plug-in. 51 52 @signal styleChecked(str, dict, int, list) emitted when the style check was 53 done for a file. 54 @signal batchFinished() emitted when a style check batch is done 55 @signal error(str, str) emitted in case of an error 56 """ 57 styleChecked = pyqtSignal(str, dict, int, list) 58 batchFinished = pyqtSignal() 59 error = pyqtSignal(str, str) 60 61 def __init__(self, ui): 62 """ 63 Constructor 64 65 @param ui reference to the user interface object (UI.UserInterface) 66 """ 67 super().__init__(ui) 68 self.__ui = ui 69 self.__initialize() 70 71 self.backgroundService = e5App().getObject("BackgroundService") 72 73 path = os.path.join( 74 os.path.dirname(__file__), 'CheckerPlugins', 'CodeStyleChecker') 75 self.backgroundService.serviceConnect( 76 'style', 'Python3', path, 'CodeStyleChecker', 77 self.__translateStyleCheck, 78 onErrorCallback=self.serviceErrorPy3, 79 onBatchDone=self.batchJobDone) 80 81 self.queuedBatches = [] 82 self.batchesFinished = True 83 84 self.__wrapper = textwrap.TextWrapper(width=80) 85 86 def __serviceError(self, fn, msg): 87 """ 88 Private slot handling service errors. 89 90 @param fn file name (string) 91 @param msg message text (string) 92 """ 93 self.error.emit(fn, msg) 94 95 def serviceErrorPy3(self, fx, lang, fn, msg): 96 """ 97 Public slot handling service errors for Python 3. 98 99 @param fx service name (string) 100 @param lang language (string) 101 @param fn file name (string) 102 @param msg message text (string) 103 """ 104 if fx in ['style', 'batch_style'] and lang == 'Python3': 105 if fx == 'style': 106 self.__serviceError(fn, msg) 107 else: 108 self.__serviceError(self.tr("Python 3 batch check"), msg) 109 self.batchJobDone(fx, lang) 110 111 def batchJobDone(self, fx, lang): 112 """ 113 Public slot handling the completion of a batch job. 114 115 @param fx service name (string) 116 @param lang language (string) 117 """ 118 if fx in ['style', 'batch_style']: 119 if lang in self.queuedBatches: 120 self.queuedBatches.remove(lang) 121 # prevent sending the signal multiple times 122 if len(self.queuedBatches) == 0 and not self.batchesFinished: 123 self.batchFinished.emit() 124 self.batchesFinished = True 125 126 def __initialize(self): 127 """ 128 Private slot to (re)initialize the plugin. 129 """ 130 self.__projectAct = None 131 self.__projectCodeStyleCheckerDialog = None 132 133 self.__projectBrowserAct = None 134 self.__projectBrowserMenu = None 135 self.__projectBrowserCodeStyleCheckerDialog = None 136 137 self.__editors = [] 138 self.__editorAct = None 139 self.__editorCodeStyleCheckerDialog = None 140 141 def styleCheck(self, lang, filename, source, args): 142 """ 143 Public method to prepare a style check on one Python source file. 144 145 @param lang language of the file or None to determine by internal 146 algorithm 147 @type str or None 148 @param filename source filename 149 @type str 150 @param source string containing the code to check 151 @type str 152 @param args arguments used by the codeStyleCheck function (list of 153 excludeMessages, includeMessages, repeatMessages, fixCodes, 154 noFixCodes, fixIssues, maxLineLength, blankLines, hangClosing, 155 docType, codeComplexityArgs, miscellaneousArgs, errors, eol, 156 encoding, backup) 157 @type list of (str, str, bool, str, str, bool, int, list of (int, int), 158 bool, str, dict, dict, list of str, str, str, bool) 159 """ 160 if lang is None: 161 lang = 'Python{0}'.format(determinePythonVersion(filename, source)) 162 if lang != 'Python3': 163 return 164 165 data = [source, args] 166 self.backgroundService.enqueueRequest('style', lang, filename, data) 167 168 def styleBatchCheck(self, argumentsList): 169 """ 170 Public method to prepare a style check on multiple Python source files. 171 172 @param argumentsList list of arguments tuples with each tuple 173 containing filename, source and args as given in styleCheck() 174 method 175 @type list of tuple of (str, str, list) 176 """ 177 data = { 178 "Python3": [], 179 } 180 for filename, source, args in argumentsList: 181 lang = 'Python{0}'.format(determinePythonVersion(filename, source)) 182 if lang != 'Python3': 183 continue 184 else: 185 data[lang].append((filename, source, args)) 186 187 self.queuedBatches = [] 188 if data['Python3']: 189 self.queuedBatches.append('Python3') 190 self.backgroundService.enqueueRequest('batch_style', 'Python3', "", 191 data['Python3']) 192 self.batchesFinished = False 193 194 def cancelStyleBatchCheck(self): 195 """ 196 Public method to cancel all batch jobs. 197 """ 198 self.backgroundService.requestCancel('batch_style', 'Python3') 199 200 def __translateStyleCheck(self, fn, codeStyleCheckerStats, results): 201 """ 202 Private slot called after perfoming a style check on one file. 203 204 @param fn filename of the just checked file 205 @type str 206 @param codeStyleCheckerStats stats of style and name check 207 @type dict 208 @param results dictionary containing the check result data 209 (see CodesStyleChecker.__checkCodeStyle for details) 210 @type dict 211 """ 212 from CheckerPlugins.CodeStyleChecker.translations import ( 213 getTranslatedMessage 214 ) 215 216 fixes = 0 217 for result in results: 218 msg = getTranslatedMessage(result["code"], result["args"]) 219 220 if result["fixcode"]: 221 fixes += 1 222 trFixedMsg = getTranslatedMessage(result["fixcode"], 223 result["fixargs"]) 224 225 msg += "\n" + QCoreApplication.translate( 226 'CodeStyleCheckerDialog', "Fix: {0}").format(trFixedMsg) 227 228 result["display"] = "\n".join(self.__wrapper.wrap(msg)) 229 self.styleChecked.emit(fn, codeStyleCheckerStats, fixes, results) 230 231 def activate(self): 232 """ 233 Public method to activate this plugin. 234 235 @return tuple of None and activation status (boolean) 236 """ 237 menu = e5App().getObject("Project").getMenu("Checks") 238 if menu: 239 self.__projectAct = E5Action( 240 self.tr('Check Code Style'), 241 self.tr('&Code Style...'), 0, 0, 242 self, 'project_check_pep8') 243 self.__projectAct.setStatusTip( 244 self.tr('Check code style.')) 245 self.__projectAct.setWhatsThis(self.tr( 246 """<b>Check Code Style...</b>""" 247 """<p>This checks Python files for compliance to the""" 248 """ code style conventions given in various PEPs.</p>""" 249 )) 250 self.__projectAct.triggered.connect( 251 self.__projectCodeStyleCheck) 252 e5App().getObject("Project").addE5Actions([self.__projectAct]) 253 menu.addAction(self.__projectAct) 254 255 self.__editorAct = E5Action( 256 self.tr('Check Code Style'), 257 self.tr('&Code Style...'), 0, 0, 258 self, "") 259 self.__editorAct.setWhatsThis(self.tr( 260 """<b>Check Code Style...</b>""" 261 """<p>This checks Python files for compliance to the""" 262 """ code style conventions given in various PEPs.</p>""" 263 )) 264 self.__editorAct.triggered.connect(self.__editorCodeStyleCheck) 265 266 e5App().getObject("Project").showMenu.connect(self.__projectShowMenu) 267 e5App().getObject("ProjectBrowser").getProjectBrowser( 268 "sources").showMenu.connect(self.__projectBrowserShowMenu) 269 e5App().getObject("ViewManager").editorOpenedEd.connect( 270 self.__editorOpened) 271 e5App().getObject("ViewManager").editorClosedEd.connect( 272 self.__editorClosed) 273 274 for editor in e5App().getObject("ViewManager").getOpenEditors(): 275 self.__editorOpened(editor) 276 277 return None, True 278 279 def deactivate(self): 280 """ 281 Public method to deactivate this plugin. 282 """ 283 e5App().getObject("Project").showMenu.disconnect( 284 self.__projectShowMenu) 285 e5App().getObject("ProjectBrowser").getProjectBrowser( 286 "sources").showMenu.disconnect(self.__projectBrowserShowMenu) 287 e5App().getObject("ViewManager").editorOpenedEd.disconnect( 288 self.__editorOpened) 289 e5App().getObject("ViewManager").editorClosedEd.disconnect( 290 self.__editorClosed) 291 292 menu = e5App().getObject("Project").getMenu("Checks") 293 if menu: 294 menu.removeAction(self.__projectAct) 295 296 if self.__projectBrowserMenu and self.__projectBrowserAct: 297 self.__projectBrowserMenu.removeAction( 298 self.__projectBrowserAct) 299 300 for editor in self.__editors: 301 editor.showMenu.disconnect(self.__editorShowMenu) 302 menu = editor.getMenu("Checks") 303 if menu is not None: 304 menu.removeAction(self.__editorAct) 305 306 self.__initialize() 307 308 def __projectShowMenu(self, menuName, menu): 309 """ 310 Private slot called, when the the project menu or a submenu is 311 about to be shown. 312 313 @param menuName name of the menu to be shown (string) 314 @param menu reference to the menu (QMenu) 315 """ 316 if menuName == "Checks" and self.__projectAct is not None: 317 self.__projectAct.setEnabled( 318 e5App().getObject("Project").getProjectLanguage() in 319 ["Python3", "MicroPython"]) 320 321 def __projectBrowserShowMenu(self, menuName, menu): 322 """ 323 Private slot called, when the the project browser menu or a submenu is 324 about to be shown. 325 326 @param menuName name of the menu to be shown (string) 327 @param menu reference to the menu (QMenu) 328 """ 329 if ( 330 menuName == "Checks" and 331 e5App().getObject("Project").getProjectLanguage() in 332 ["Python3", "MicroPython"] 333 ): 334 self.__projectBrowserMenu = menu 335 if self.__projectBrowserAct is None: 336 self.__projectBrowserAct = E5Action( 337 self.tr('Check Code Style'), 338 self.tr('&Code Style...'), 0, 0, 339 self, "") 340 self.__projectBrowserAct.setWhatsThis(self.tr( 341 """<b>Check Code Style...</b>""" 342 """<p>This checks Python files for compliance to the""" 343 """ code style conventions given in various PEPs.</p>""" 344 )) 345 self.__projectBrowserAct.triggered.connect( 346 self.__projectBrowserCodeStyleCheck) 347 if self.__projectBrowserAct not in menu.actions(): 348 menu.addAction(self.__projectBrowserAct) 349 350 def __projectCodeStyleCheck(self): 351 """ 352 Private slot used to check the project files for code style. 353 """ 354 project = e5App().getObject("Project") 355 project.saveAllScripts() 356 ppath = project.getProjectPath() 357 files = [os.path.join(ppath, file) 358 for file in project.pdata["SOURCES"] 359 if file.endswith( 360 tuple(Preferences.getPython("Python3Extensions")))] 361 362 from CheckerPlugins.CodeStyleChecker import CodeStyleCheckerDialog 363 self.__projectCodeStyleCheckerDialog = ( 364 CodeStyleCheckerDialog.CodeStyleCheckerDialog(self) 365 ) 366 self.__projectCodeStyleCheckerDialog.show() 367 self.__projectCodeStyleCheckerDialog.prepare(files, project) 368 369 def __projectBrowserCodeStyleCheck(self): 370 """ 371 Private method to handle the code style check context menu action of 372 the project sources browser. 373 """ 374 browser = ( 375 e5App().getObject("ProjectBrowser").getProjectBrowser("sources") 376 ) 377 if browser.getSelectedItemsCount([ProjectBrowserFileItem]) > 1: 378 fn = [] 379 for itm in browser.getSelectedItems([ProjectBrowserFileItem]): 380 fn.append(itm.fileName()) 381 isDir = False 382 else: 383 itm = browser.model().item(browser.currentIndex()) 384 try: 385 fn = itm.fileName() 386 isDir = False 387 except AttributeError: 388 fn = itm.dirName() 389 isDir = True 390 391 from CheckerPlugins.CodeStyleChecker import CodeStyleCheckerDialog 392 self.__projectBrowserCodeStyleCheckerDialog = ( 393 CodeStyleCheckerDialog.CodeStyleCheckerDialog(self) 394 ) 395 self.__projectBrowserCodeStyleCheckerDialog.show() 396 if isDir: 397 self.__projectBrowserCodeStyleCheckerDialog.start( 398 fn, save=True) 399 else: 400 self.__projectBrowserCodeStyleCheckerDialog.start( 401 fn, save=True, repeat=True) 402 403 def __editorOpened(self, editor): 404 """ 405 Private slot called, when a new editor was opened. 406 407 @param editor reference to the new editor (QScintilla.Editor) 408 """ 409 menu = editor.getMenu("Checks") 410 if menu is not None: 411 menu.addAction(self.__editorAct) 412 editor.showMenu.connect(self.__editorShowMenu) 413 self.__editors.append(editor) 414 415 def __editorClosed(self, editor): 416 """ 417 Private slot called, when an editor was closed. 418 419 @param editor reference to the editor (QScintilla.Editor) 420 """ 421 with contextlib.suppress(ValueError): 422 self.__editors.remove(editor) 423 424 def __editorShowMenu(self, menuName, menu, editor): 425 """ 426 Private slot called, when the the editor context menu or a submenu is 427 about to be shown. 428 429 @param menuName name of the menu to be shown (string) 430 @param menu reference to the menu (QMenu) 431 @param editor reference to the editor 432 """ 433 if menuName == "Checks": 434 if self.__editorAct not in menu.actions(): 435 menu.addAction(self.__editorAct) 436 self.__editorAct.setEnabled(editor.isPyFile()) 437 438 def __editorCodeStyleCheck(self): 439 """ 440 Private slot to handle the code style check context menu action 441 of the editors. 442 """ 443 editor = e5App().getObject("ViewManager").activeWindow() 444 if ( 445 editor is not None and 446 editor.checkDirty() and 447 editor.getFileName() is not None 448 ): 449 from CheckerPlugins.CodeStyleChecker import ( 450 CodeStyleCheckerDialog 451 ) 452 self.__editorCodeStyleCheckerDialog = ( 453 CodeStyleCheckerDialog.CodeStyleCheckerDialog(self) 454 ) 455 self.__editorCodeStyleCheckerDialog.show() 456 self.__editorCodeStyleCheckerDialog.start( 457 editor.getFileName(), 458 save=True, 459 repeat=True) 460