PyLintInterface/PyLintExecDialog.py

branch
eric7
changeset 98
ab4aabca55ec
parent 95
50eba81e4a9f
child 101
98784d037491
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 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()

eric ide

mercurial