src/eric7/CodeFormatting/BlackFormattingDialog.py

branch
eric7
changeset 9214
bd28e56047d7
child 9220
e9e7eca7efee
equal deleted inserted replaced
9213:2bf743848d2f 9214:bd28e56047d7
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog showing the code formatting progress and the result.
8 """
9
10 import copy
11 import datetime
12 import pathlib
13
14 import black
15
16 from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication
17 from PyQt6.QtWidgets import (
18 QAbstractButton,
19 QDialog,
20 QDialogButtonBox,
21 QHeaderView,
22 QTreeWidgetItem
23 )
24
25 from EricWidgets import EricMessageBox
26
27 from .Ui_BlackFormattingDialog import Ui_BlackFormattingDialog
28
29 from . import BlackUtilities
30 from .BlackDiffWidget import BlackDiffWidget
31 from .BlackFormattingAction import BlackFormattingAction
32
33 import Utilities
34
35
36 class BlackFormattingDialog(QDialog, Ui_BlackFormattingDialog):
37 """
38 Class implementing a dialog showing the code formatting progress and the result.
39 """
40 DataTypeRole = Qt.ItemDataRole.UserRole
41 DataRole = Qt.ItemDataRole.UserRole + 1
42
43 def __init__(self, configuration, filesList, project=None,
44 action=BlackFormattingAction.Format, parent=None):
45 """
46 Constructor
47
48 @param configuration dictionary containing the configuration parameters
49 @type dict
50 @param filesList list of absolute file paths to be processed
51 @type list of str
52 @param project reference to the project object (defaults to None)
53 @type Project (optional)
54 @param action action to be performed (defaults to BlackFormattingAction.Format)
55 @type BlackFormattingAction (optional)
56 @param parent reference to the parent widget (defaults to None)
57 @type QWidget (optional)
58 """
59 super().__init__(parent)
60 self.setupUi(self)
61
62 self.progressBar.setMaximum(len(filesList))
63 self.progressBar.setValue(0)
64
65 self.resultsList.header().setSortIndicator(1, Qt.SortOrder.AscendingOrder)
66
67 self.__report = BlackReport(self)
68 self.__report.check = action is BlackFormattingAction.Check
69 self.__report.diff = action is BlackFormattingAction.Diff
70
71 self.__config = copy.deepcopy(configuration)
72 self.__project = project
73 self.__action = action
74
75 self.__cancelled = False
76 self.__diffDialog = None
77
78 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
79 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False)
80 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True)
81
82 self.show()
83 QCoreApplication.processEvents()
84
85 self.__files = self.__filterFiles(filesList)
86 self.__formatFiles()
87
88 def __filterFiles(self, filesList):
89 """
90 Private method to filter the given list of files according the
91 configuration parameters.
92
93 @param filesList list of files
94 @type list of str
95 @return list of filtered files
96 @rtype list of str
97 """
98 filterRegExps = [
99 BlackUtilities.compileRegExp(self.__config[k])
100 for k in ["force-exclude", "extend-exclude", "exclude"]
101 if k in self.__config and bool(self.__config[k])
102 and BlackUtilities.validateRegExp(self.__config[k])[0]
103 ]
104
105 files = []
106 for file in filesList:
107 file = Utilities.fromNativeSeparators(file)
108 for filterRegExp in filterRegExps:
109 filterMatch = filterRegExp.search(file)
110 if filterMatch and filterMatch.group(0):
111 self.__report.path_ignored(file)
112 break
113 else:
114 files.append(file)
115
116 return files
117
118 def __resort(self):
119 """
120 Private method to resort the result list.
121 """
122 self.resultsList.sortItems(
123 self.resultsList.sortColumn(),
124 self.resultsList.header().sortIndicatorOrder())
125
126 def __resizeColumns(self):
127 """
128 Private method to resize the columns of the result list.
129 """
130 self.resultsList.header().resizeSections(
131 QHeaderView.ResizeMode.ResizeToContents)
132 self.resultsList.header().setStretchLastSection(True)
133
134 def __finish(self):
135 """
136 Private method to perform some actions after the run was performed or canceled.
137 """
138 self.__resort()
139 self.__resizeColumns()
140
141 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
142 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True)
143 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True)
144
145 self.progressBar.setVisible(False)
146
147 self.__updateStatistics()
148
149 def __updateStatistics(self):
150 """
151 Private method to update the statistics about the recent formatting run.
152 """
153 self.reformattedLabel.setText(
154 self.tr("reformatted")
155 if self.__action is BlackFormattingAction.Format else
156 self.tr("would reformat")
157 )
158
159 total = self.progressBar.maximum()
160 processed = total - self.__report.ignored_count
161
162 self.totalCountLabel.setText("{0:n}".format(total))
163 self.excludedCountLabel.setText("{0:n}".format(self.__report.ignored_count))
164 self.failuresCountLabel.setText("{0:n}".format(self.__report.failure_count))
165 self.processedCountLabel.setText("{0:n}".format(processed))
166 self.reformattedCountLabel.setText("{0:n}".format(self.__report.change_count))
167 self.unchangedCountLabel.setText("{0:n}".format(self.__report.same_count))
168
169 @pyqtSlot(QAbstractButton)
170 def on_buttonBox_clicked(self, button):
171 """
172 Private slot to handle button presses of the dialog buttons.
173
174 @param button reference to the pressed button
175 @type QAbstractButton
176 """
177 if button == self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel):
178 self.__cancelled = True
179 elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.Close):
180 self.accept()
181
182 @pyqtSlot(QTreeWidgetItem, int)
183 def on_resultsList_itemDoubleClicked(self, item, column):
184 """
185 Private slot handling a double click of a result item.
186
187 @param item reference to the double clicked item
188 @type QTreeWidgetItem
189 @param column column number that was double clicked
190 @type int
191 """
192 dataType = item.data(0, BlackFormattingDialog.DataTypeRole)
193 if dataType == "error":
194 EricMessageBox.critical(
195 self,
196 self.tr("Formatting Failure"),
197 self.tr(
198 "<p>Formatting failed due to this error.</p><p>{0}</p>"
199 ).format(item.data(0, BlackFormattingDialog.DataRole))
200 )
201 elif dataType == "diff":
202 if self.__diffDialog is None:
203 self.__diffDialog = BlackDiffWidget()
204 self.__diffDialog.showDiff(item.data(0, BlackFormattingDialog.DataRole))
205
206 def addResultEntry(self, status, fileName, isError=False, data=None):
207 """
208 Public method to add an entry to the result list.
209
210 @param status status of the operation
211 @type str
212 @param fileName name of the processed file
213 @type str
214 @param isError flag indicating that data contains an error message (defaults to
215 False)
216 @type bool (optional)
217 @param data associated data (diff or error message) (defaults to None)
218 @type str (optional)
219 """
220 if self.__project:
221 fileName = self.__project.getRelativePath(fileName)
222
223 itm = QTreeWidgetItem(self.resultsList, [status, fileName])
224 if data:
225 itm.setData(
226 0,
227 BlackFormattingDialog.DataTypeRole,
228 "error" if isError else "diff"
229 )
230 itm.setData(0, BlackFormattingDialog.DataRole, data)
231
232 self.progressBar.setValue(self.progressBar.value() + 1)
233
234 QCoreApplication.processEvents()
235
236 def __formatFiles(self):
237 """
238 Private method to format the list of files according the configuration.
239 """
240 writeBack = black.WriteBack.from_configuration(
241 check=self.__action is BlackFormattingAction.Check,
242 diff=self.__action is BlackFormattingAction.Diff
243 )
244
245 versions = (
246 {
247 black.TargetVersion[target.upper()]
248 for target in self.__config["target-version"]
249 }
250 if self.__config["target-version"] else
251 set()
252 )
253
254 mode = black.Mode(
255 target_versions=versions,
256 line_length=int(self.__config["line-length"]),
257 string_normalization=not self.__config["skip-string-normalization"],
258 magic_trailing_comma=not self.__config["skip-magic-trailing-comma"]
259 )
260
261 for file in self.__files:
262 if self.__action is BlackFormattingAction.Diff:
263 self.__diffFormatFile(
264 pathlib.Path(file),
265 fast=False,
266 mode=mode,
267 report=self.__report
268 )
269 else:
270 black.reformat_one(
271 pathlib.Path(file),
272 fast=False,
273 write_back=writeBack,
274 mode=mode,
275 report=self.__report
276 )
277
278 if self.__cancelled:
279 break
280
281 self.__finish()
282
283 def __diffFormatFile(self, src, fast, mode, report):
284 """
285 Private method to check, if the given files need to be reformatted, and generate
286 a unified diff.
287
288 @param src path of file to be checked
289 @type pathlib.Path
290 @param fast flag indicating fast operation
291 @type bool
292 @param mode code formatting options
293 @type black.Mode
294 @param report reference to the report object
295 @type BlackReport
296 """
297 then = datetime.datetime.utcfromtimestamp(src.stat().st_mtime)
298 with open(src, "rb") as buf:
299 srcContents, _, _ = black.decode_bytes(buf.read())
300 try:
301 dstContents = black.format_file_contents(srcContents, fast=fast, mode=mode)
302 except black.NothingChanged:
303 report.done(src, black.Changed.NO)
304 return
305
306 fileName = str(src)
307 if self.__project:
308 fileName = self.__project.getRelativePath(fileName)
309
310 now = datetime.datetime.utcnow()
311 srcName = f"{fileName}\t{then} +0000"
312 dstName = f"{fileName}\t{now} +0000"
313 diffContents = black.diff(srcContents, dstContents, srcName, dstName)
314 report.done(src, black.Changed.YES, diff=diffContents)
315
316 def closeEvent(self, evt):
317 """
318 Protected slot implementing a close event handler.
319
320 @param evt reference to the close event
321 @type QCloseEvent
322 """
323 if self.__diffDialog is not None:
324 self.__diffDialog.close()
325 evt.accept()
326
327
328 class BlackReport(black.Report):
329 """
330 Class extending the black Report to work with our dialog.
331 """
332 def __init__(self, dialog):
333 """
334 Constructor
335
336 @param dialog reference to the result dialog
337 @type QDialog
338 """
339 super().__init__()
340
341 self.ignored_count = 0
342
343 self.__dialog = dialog
344
345 def done(self, src, changed, diff=""):
346 """
347 Public method to handle the end of a reformat.
348
349 @param src name of the processed file
350 @type pathlib.Path
351 @param changed change status
352 @type black.Changed
353 @param diff unified diff of potential changes (defaults to "")
354 @type str
355 """
356 if changed is black.Changed.YES:
357 status = (
358 QCoreApplication.translate("BlackFormattingDialog", "would reformat")
359 if self.check or self.diff else
360 QCoreApplication.translate("BlackFormattingDialog", "reformatted")
361 )
362 self.change_count += 1
363
364 elif changed is black.Changed.NO:
365 status = QCoreApplication.translate("BlackFormattingDialog", "unchanged")
366 self.same_count += 1
367
368 elif changed is black.Changed.CACHED:
369 status = QCoreApplication.translate("BlackFormattingDialog", "unmodified")
370 self.same_count += 1
371
372 if self.diff:
373 self.__dialog.addResultEntry(status, str(src), data=diff)
374 else:
375 self.__dialog.addResultEntry(status, str(src))
376
377 def failed(self, src, message):
378 """
379 Public method to handle a reformat failure.
380
381 @param src name of the processed file
382 @type pathlib.Path
383 @param message error message
384 @type str
385 """
386 status = QCoreApplication.translate("BlackFormattingDialog", "failed")
387 self.failure_count += 1
388
389 self.__dialog.addResultEntry(status, str(src), isError=True, data=message)
390
391 def path_ignored(self, src, message=""):
392 """
393 Public method handling an ignored path.
394
395 @param src name of the processed file
396 @type pathlib.Path or str
397 @param message ignore message (default to "")
398 @type str (optional)
399 """
400 status = QCoreApplication.translate("BlackFormattingDialog", "ignored")
401 self.ignored_count += 1
402
403 self.__dialog.addResultEntry(status, str(src))

eric ide

mercurial