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