PyLint/PyLintExecDialog.py

branch
eric7
changeset 98
ab4aabca55ec
parent 97
2226347d86e4
child 99
f34bc41cd4b4
equal deleted inserted replaced
97:2226347d86e4 98:ab4aabca55ec
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()

eric ide

mercurial