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