1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2005 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to show the results of the PyLint run. |
|
8 """ |
|
9 |
|
10 import os |
|
11 |
|
12 from PyQt5.QtCore import pyqtSlot, Qt, QTimer, QProcess |
|
13 from PyQt5.QtGui import QTextCursor |
|
14 from PyQt5.QtWidgets import ( |
|
15 QWidget, QHeaderView, QApplication, QDialogButtonBox, QTreeWidgetItem |
|
16 ) |
|
17 |
|
18 from E5Gui import E5MessageBox, E5FileDialog |
|
19 from E5Gui.E5Application import e5App |
|
20 try: |
|
21 from E5Gui.E5OverrideCursor import E5OverrideCursorProcess |
|
22 except ImportError: |
|
23 # workaround for eric6 < 20.11 |
|
24 E5OverrideCursorProcess = QProcess |
|
25 |
|
26 from .Ui_PyLintExecDialog import Ui_PyLintExecDialog |
|
27 |
|
28 import Preferences |
|
29 import Utilities |
|
30 |
|
31 |
|
32 class PyLintExecDialog(QWidget, Ui_PyLintExecDialog): |
|
33 """ |
|
34 Class implementing a dialog to show the results of the PyLint run. |
|
35 |
|
36 This class starts a QProcess and displays a dialog that |
|
37 shows the results of the PyLint command process. |
|
38 """ |
|
39 filenameRole = Qt.UserRole + 1 |
|
40 |
|
41 def __init__(self, parent=None): |
|
42 """ |
|
43 Constructor |
|
44 |
|
45 @param parent parent widget of this dialog (QWidget) |
|
46 """ |
|
47 QWidget.__init__(self, parent) |
|
48 self.setupUi(self) |
|
49 |
|
50 self.saveButton = self.buttonBox.addButton( |
|
51 self.tr("Save Report..."), QDialogButtonBox.ActionRole) |
|
52 self.saveButton.setToolTip( |
|
53 self.tr("Press to save the report to a file")) |
|
54 self.saveButton.setEnabled(False) |
|
55 |
|
56 self.refreshButton = self.buttonBox.addButton( |
|
57 self.tr("Refresh"), QDialogButtonBox.ActionRole) |
|
58 self.refreshButton.setToolTip(self.tr( |
|
59 "Press to refresh the result display")) |
|
60 self.refreshButton.setEnabled(False) |
|
61 |
|
62 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) |
|
63 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) |
|
64 |
|
65 self.messageList.header().setSortIndicator(0, Qt.AscendingOrder) |
|
66 |
|
67 self.process = None |
|
68 self.noResults = True |
|
69 self.htmlOutput = False |
|
70 self.parsedOutput = False |
|
71 self.__scrollPosition = -1 # illegal value |
|
72 |
|
73 self.typeDict = { |
|
74 'C': self.tr('Convention'), |
|
75 'R': self.tr('Refactor'), |
|
76 'W': self.tr('Warning'), |
|
77 'E': self.tr('Error'), |
|
78 'F': self.tr('Fatal'), |
|
79 } |
|
80 |
|
81 def start(self, args, fn, reportFile, ppath): |
|
82 """ |
|
83 Public slot to start PyLint. |
|
84 |
|
85 @param args commandline arguments for documentation programPyLint |
|
86 (list of strings) |
|
87 @param fn filename or dirname to be processed by PyLint (string) |
|
88 @param reportFile filename of file to write the report to (string) |
|
89 @param ppath project path (string) |
|
90 @return flag indicating the successful start of the process (boolean) |
|
91 """ |
|
92 self.errorGroup.hide() |
|
93 |
|
94 self.args = args[:] |
|
95 self.fn = fn |
|
96 self.reportFile = reportFile |
|
97 self.ppath = ppath |
|
98 |
|
99 self.pathname = os.path.dirname(fn) |
|
100 self.filename = os.path.basename(fn) |
|
101 |
|
102 self.contents.clear() |
|
103 self.errors.clear() |
|
104 self.messageList.clear() |
|
105 |
|
106 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) |
|
107 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) |
|
108 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) |
|
109 self.saveButton.setEnabled(False) |
|
110 self.refreshButton.setEnabled(False) |
|
111 |
|
112 program = args[0] |
|
113 del args[0] |
|
114 args.append(self.filename) |
|
115 |
|
116 self.process = E5OverrideCursorProcess() |
|
117 self.process.setWorkingDirectory(self.pathname) |
|
118 |
|
119 self.process.readyReadStandardError.connect(self.__readStderr) |
|
120 self.process.finished.connect(self.__finish) |
|
121 |
|
122 self.__ioEncoding = Preferences.getSystem("IOEncoding") |
|
123 if "--output-format=parseable" in args: |
|
124 self.reportFile = None |
|
125 self.contents.hide() |
|
126 self.process.readyReadStandardOutput.connect( |
|
127 self.__readParseStdout) |
|
128 self.parsedOutput = True |
|
129 else: |
|
130 self.process.readyReadStandardOutput.connect(self.__readStdout) |
|
131 self.messageList.hide() |
|
132 if "--output-format=html" in args: |
|
133 self.contents.setAcceptRichText(True) |
|
134 self.contents.setHtml('<b>Processing your request...</b>') |
|
135 self.htmlOutput = True |
|
136 else: |
|
137 self.contents.setAcceptRichText(False) |
|
138 self.contents.setCurrentFont( |
|
139 Preferences.getEditorOtherFonts("MonospacedFont")) |
|
140 self.htmlOutput = False |
|
141 self.parsedOutput = False |
|
142 self.noResults = True |
|
143 |
|
144 self.buf = "" |
|
145 self.__lastFileItem = None |
|
146 |
|
147 self.process.start(program, args) |
|
148 procStarted = self.process.waitForStarted() |
|
149 if not procStarted: |
|
150 E5MessageBox.critical( |
|
151 self, |
|
152 self.tr('Process Generation Error'), |
|
153 self.tr( |
|
154 'The process {0} could not be started. ' |
|
155 'Ensure, that it is in the search path.' |
|
156 ).format(program)) |
|
157 return procStarted |
|
158 |
|
159 def on_buttonBox_clicked(self, button): |
|
160 """ |
|
161 Private slot called by a button of the button box clicked. |
|
162 |
|
163 @param button button that was clicked (QAbstractButton) |
|
164 """ |
|
165 if button == self.buttonBox.button(QDialogButtonBox.Close): |
|
166 self.close() |
|
167 elif button == self.buttonBox.button(QDialogButtonBox.Cancel): |
|
168 self.__finish() |
|
169 elif button == self.saveButton: |
|
170 self.on_saveButton_clicked() |
|
171 elif button == self.refreshButton: |
|
172 self.on_refreshButton_clicked() |
|
173 |
|
174 def __finish(self): |
|
175 """ |
|
176 Private slot called when the process finished. |
|
177 |
|
178 It is called when the process finished or |
|
179 the user pressed the button. |
|
180 """ |
|
181 if self.htmlOutput: |
|
182 self.contents.setHtml(self.buf) |
|
183 else: |
|
184 cursor = self.contents.textCursor() |
|
185 cursor.movePosition(QTextCursor.Start) |
|
186 self.contents.setTextCursor(cursor) |
|
187 |
|
188 if ( |
|
189 self.process is not None and |
|
190 self.process.state() != QProcess.NotRunning |
|
191 ): |
|
192 self.process.terminate() |
|
193 QTimer.singleShot(2000, self.process.kill) |
|
194 self.process.waitForFinished(3000) |
|
195 |
|
196 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) |
|
197 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) |
|
198 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) |
|
199 self.refreshButton.setEnabled(True) |
|
200 if self.parsedOutput: |
|
201 QApplication.processEvents() |
|
202 self.messageList.sortItems( |
|
203 self.messageList.sortColumn(), |
|
204 self.messageList.header().sortIndicatorOrder()) |
|
205 self.messageList.header().resizeSections( |
|
206 QHeaderView.ResizeToContents) |
|
207 self.messageList.header().setStretchLastSection(True) |
|
208 else: |
|
209 if self.__scrollPosition != -1: |
|
210 self.contents.verticalScrollBar().setValue( |
|
211 self.__scrollPosition) |
|
212 |
|
213 self.process = None |
|
214 |
|
215 if self.reportFile: |
|
216 self.__writeReport() |
|
217 elif not self.parsedOutput: |
|
218 self.saveButton.setEnabled(True) |
|
219 |
|
220 if self.noResults: |
|
221 self.__createItem( |
|
222 self.tr('No PyLint errors found.'), "", "", "") |
|
223 |
|
224 @pyqtSlot() |
|
225 def on_refreshButton_clicked(self): |
|
226 """ |
|
227 Private slot to refresh the status display. |
|
228 """ |
|
229 self.__scrollPosition = self.contents.verticalScrollBar().value() |
|
230 self.start(self.args, self.fn, self.reportFile, self.ppath) |
|
231 |
|
232 def __readStdout(self): |
|
233 """ |
|
234 Private slot to handle the readyReadStandardOutput signal. |
|
235 |
|
236 It reads the output of the process, formats it and inserts it into |
|
237 the contents pane. |
|
238 """ |
|
239 self.process.setReadChannel(QProcess.StandardOutput) |
|
240 |
|
241 while self.process.canReadLine(): |
|
242 s = str(self.process.readLine(), self.__ioEncoding, 'replace') |
|
243 self.buf += s + os.linesep |
|
244 if not self.htmlOutput: |
|
245 self.contents.insertPlainText(s) |
|
246 self.contents.ensureCursorVisible() |
|
247 |
|
248 def __createItem(self, file, line, type_, message): |
|
249 """ |
|
250 Private method to create an entry in the message list. |
|
251 |
|
252 @param file filename of file (string) |
|
253 @param line linenumber of message (integer or string) |
|
254 @param type_ type of message (string) |
|
255 @param message message text (string) |
|
256 """ |
|
257 if self.__lastFileItem is None or self.__lastFileItem.text(0) != file: |
|
258 matchFlags = Qt.MatchFixedString |
|
259 if not Utilities.isWindowsPlatform(): |
|
260 matchFlags |= Qt.MatchCaseSensitive |
|
261 |
|
262 itmList = self.messageList.findItems(file, matchFlags) |
|
263 if itmList: |
|
264 self.__lastFileItem = itmList[0] |
|
265 else: |
|
266 # It's a new file |
|
267 self.__lastFileItem = QTreeWidgetItem(self.messageList, [file]) |
|
268 self.__lastFileItem.setFirstColumnSpanned(True) |
|
269 self.__lastFileItem.setExpanded(True) |
|
270 self.__lastFileItem.setData(0, self.filenameRole, file) |
|
271 |
|
272 itm = QTreeWidgetItem(self.__lastFileItem, [str(line), type_, message]) |
|
273 itm.setTextAlignment(0, Qt.AlignRight) |
|
274 itm.setTextAlignment(1, Qt.AlignHCenter) |
|
275 itm.setData(0, self.filenameRole, file) |
|
276 |
|
277 def __readParseStdout(self): |
|
278 """ |
|
279 Private slot to handle the readyReadStandardOutput signal for |
|
280 parseable output. |
|
281 |
|
282 It reads the output of the process, formats it and inserts it into |
|
283 the message list pane. |
|
284 """ |
|
285 self.process.setReadChannel(QProcess.StandardOutput) |
|
286 |
|
287 while self.process.canReadLine(): |
|
288 s = str(self.process.readLine(), self.__ioEncoding, 'replace') |
|
289 if s: |
|
290 try: |
|
291 if Utilities.isWindowsPlatform(): |
|
292 drive, s = os.path.splitdrive(s) |
|
293 fname, lineno, fullmessage = s.split(':') |
|
294 fname = drive + fname |
|
295 else: |
|
296 fname, lineno, fullmessage = s.split(':') |
|
297 type_, message = fullmessage.strip().split(']', 1) |
|
298 type_ = type_.strip()[1:].split(',', 1)[0] |
|
299 message = message.strip() |
|
300 if type_ and type_[0] in self.typeDict: |
|
301 if len(type_) == 1: |
|
302 self.__createItem( |
|
303 fname, lineno, self.typeDict[type_], message) |
|
304 else: |
|
305 self.__createItem( |
|
306 fname, lineno, "{0} {1}".format( |
|
307 self.typeDict[type_[0]], type_[1:]), |
|
308 message) |
|
309 self.noResults = False |
|
310 except ValueError: |
|
311 continue |
|
312 |
|
313 def __readStderr(self): |
|
314 """ |
|
315 Private slot to handle the readyReadStandardError signal. |
|
316 |
|
317 It reads the error output of the process and inserts it into the |
|
318 error pane. |
|
319 """ |
|
320 self.process.setReadChannel(QProcess.StandardError) |
|
321 |
|
322 while self.process.canReadLine(): |
|
323 self.errorGroup.show() |
|
324 s = str(self.process.readLine(), self.__ioEncoding, 'replace') |
|
325 self.errors.insertPlainText(s) |
|
326 self.errors.ensureCursorVisible() |
|
327 |
|
328 def on_messageList_itemActivated(self, itm, column): |
|
329 """ |
|
330 Private slot to handle the itemActivated signal of the message list. |
|
331 |
|
332 @param itm The message item that was activated (QTreeWidgetItem) |
|
333 @param column column the item was activated in (integer) |
|
334 """ |
|
335 if self.noResults: |
|
336 return |
|
337 |
|
338 if itm.parent(): |
|
339 fn = os.path.join(self.pathname, itm.data(0, self.filenameRole)) |
|
340 lineno = int(itm.text(0)) |
|
341 |
|
342 vm = e5App().getObject("ViewManager") |
|
343 vm.openSourceFile(fn, lineno) |
|
344 editor = vm.getOpenEditor(fn) |
|
345 editor.toggleWarning( |
|
346 lineno, 0, True, |
|
347 "{0} | {1}".format(itm.text(1), itm.text(2))) |
|
348 else: |
|
349 fn = os.path.join(self.pathname, itm.data(0, self.filenameRole)) |
|
350 vm = e5App().getObject("ViewManager") |
|
351 vm.openSourceFile(fn) |
|
352 editor = vm.getOpenEditor(fn) |
|
353 for index in range(itm.childCount()): |
|
354 citm = itm.child(index) |
|
355 lineno = int(citm.text(0)) |
|
356 editor.toggleWarning( |
|
357 lineno, 0, True, |
|
358 "{0} | {1}".format(citm.text(1), citm.text(2))) |
|
359 |
|
360 def __writeReport(self): |
|
361 """ |
|
362 Private slot to write the report to a report file. |
|
363 """ |
|
364 self.reportFile = self.reportFile |
|
365 if os.path.exists(self.reportFile): |
|
366 res = E5MessageBox.warning( |
|
367 self, |
|
368 self.tr("PyLint Report"), |
|
369 self.tr( |
|
370 """<p>The PyLint report file <b>{0}</b> already""" |
|
371 """ exists.</p>""") |
|
372 .format(self.reportFile), |
|
373 E5MessageBox.StandardButtons( |
|
374 E5MessageBox.Cancel | |
|
375 E5MessageBox.Ignore), |
|
376 E5MessageBox.Cancel) |
|
377 if res == E5MessageBox.Cancel: |
|
378 return |
|
379 |
|
380 try: |
|
381 import codecs |
|
382 with open(self.reportFile, 'wb') as f: |
|
383 f.write(codecs.BOM_UTF8) |
|
384 f.write(self.buf.encode('utf-8')) |
|
385 except OSError as why: |
|
386 E5MessageBox.critical( |
|
387 self, self.tr('PyLint Report'), |
|
388 self.tr('<p>The PyLint report file <b>{0}</b> could not' |
|
389 ' be written.<br>Reason: {1}</p>') |
|
390 .format(self.reportFile, str(why))) |
|
391 |
|
392 @pyqtSlot() |
|
393 def on_saveButton_clicked(self): |
|
394 """ |
|
395 Private slot to save the report to a file. |
|
396 """ |
|
397 fileFilter = ( |
|
398 self.tr("HTML Files (*.html);;All Files (*)") |
|
399 if self.htmlOutput else |
|
400 self.tr("Text Files (*.txt);;All Files (*)") |
|
401 ) |
|
402 |
|
403 self.reportFile = E5FileDialog.getSaveFileName( |
|
404 self, |
|
405 self.tr("PyLint Report"), |
|
406 self.ppath, |
|
407 fileFilter, |
|
408 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) |
|
409 if self.reportFile: |
|
410 self.__writeReport() |
|