src/eric7/CodeFormatting/BlackFormattingDialog.py

branch
eric7
changeset 9283
0e9d2c4e379e
parent 9281
76caf27cb8a8
child 9284
3b3a4f659782
equal deleted inserted replaced
9282:c44b7aa4cf71 9283:0e9d2c4e379e
7 Module implementing a dialog showing the code formatting progress and the result. 7 Module implementing a dialog showing the code formatting progress and the result.
8 """ 8 """
9 9
10 import copy 10 import copy
11 import datetime 11 import datetime
12 import multiprocessing
12 import pathlib 13 import pathlib
13 14
14 from dataclasses import dataclass 15 from dataclasses import dataclass
15 16
16 import black 17 import black
31 from . import BlackUtilities 32 from . import BlackUtilities
32 from .BlackDiffWidget import BlackDiffWidget 33 from .BlackDiffWidget import BlackDiffWidget
33 from .BlackFormattingAction import BlackFormattingAction 34 from .BlackFormattingAction import BlackFormattingAction
34 35
35 import Utilities 36 import Utilities
37 import Preferences
36 38
37 39
38 class BlackFormattingDialog(QDialog, Ui_BlackFormattingDialog): 40 class BlackFormattingDialog(QDialog, Ui_BlackFormattingDialog):
39 """ 41 """
40 Class implementing a dialog showing the code formatting progress and the result. 42 Class implementing a dialog showing the code formatting progress and the result.
78 80
79 self.statisticsGroup.setVisible(False) 81 self.statisticsGroup.setVisible(False)
80 82
81 self.__statistics = BlackStatistics() 83 self.__statistics = BlackStatistics()
82 84
83 self.__report = BlackReport(self)
84 self.__report.check = action is BlackFormattingAction.Check
85 self.__report.diff = action is BlackFormattingAction.Diff
86 self.__report.result.connect(self.__handleBlackFormattingResult)
87
88 self.__config = copy.deepcopy(configuration) 85 self.__config = copy.deepcopy(configuration)
86 self.__config["__action__"] = action # needed by the workers
89 self.__project = project 87 self.__project = project
90 self.__action = action 88 self.__action = action
91 89
92 self.__cancelled = False 90 self.__cancelled = False
93 self.__diffDialog = None 91 self.__diffDialog = None
99 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True) 97 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True)
100 98
101 self.show() 99 self.show()
102 QCoreApplication.processEvents() 100 QCoreApplication.processEvents()
103 101
104 self.__files = self.__filterFiles(filesList) 102 files = self.__filterFiles(filesList)
105 self.__formatFiles() 103 if len(files) > 1:
104 self.__formatManyFiles(files)
105 elif len(files) == 1:
106 self.__formatOneFile(files[0])
106 107
107 def __filterFiles(self, filesList): 108 def __filterFiles(self, filesList):
108 """ 109 """
109 Private method to filter the given list of files according the 110 Private method to filter the given list of files according the
110 configuration parameters. 111 configuration parameters.
126 for file in filesList: 127 for file in filesList:
127 file = Utilities.fromNativeSeparators(file) 128 file = Utilities.fromNativeSeparators(file)
128 for filterRegExp in filterRegExps: 129 for filterRegExp in filterRegExps:
129 filterMatch = filterRegExp.search(file) 130 filterMatch = filterRegExp.search(file)
130 if filterMatch and filterMatch.group(0): 131 if filterMatch and filterMatch.group(0):
131 self.__report.path_ignored(file) 132 self.__handleBlackFormattingResult("ignored", file, "")
132 break 133 break
133 else: 134 else:
134 files.append(file) 135 files.append(file)
135 136
136 return files 137 return files
188 """ 189 """
189 Private method to update the statistics about the recent formatting run and 190 Private method to update the statistics about the recent formatting run and
190 make them visible. 191 make them visible.
191 """ 192 """
192 self.reformattedLabel.setText( 193 self.reformattedLabel.setText(
193 self.tr("reformatted") 194 self.tr("Reformatted:")
194 if self.__action is BlackFormattingAction.Format 195 if self.__action is BlackFormattingAction.Format
195 else self.tr("would reformat") 196 else self.tr("Would Reformat:")
196 ) 197 )
197 198
198 total = self.progressBar.maximum() 199 total = self.progressBar.maximum()
199 processed = total - self.__statistics.ignoreCount
200 200
201 self.totalCountLabel.setText("{0:n}".format(total)) 201 self.totalCountLabel.setText("{0:n}".format(total))
202 self.excludedCountLabel.setText("{0:n}".format(self.__statistics.ignoreCount)) 202 self.excludedCountLabel.setText("{0:n}".format(self.__statistics.ignoreCount))
203 self.failuresCountLabel.setText("{0:n}".format(self.__statistics.failureCount)) 203 self.failuresCountLabel.setText("{0:n}".format(self.__statistics.failureCount))
204 self.processedCountLabel.setText("{0:n}".format(processed)) 204 self.processedCountLabel.setText(
205 "{0:n}".format(self.__statistics.processedCount)
206 )
205 self.reformattedCountLabel.setText( 207 self.reformattedCountLabel.setText(
206 "{0:n}".format(self.__statistics.changeCount) 208 "{0:n}".format(self.__statistics.changeCount)
207 ) 209 )
208 self.unchangedCountLabel.setText("{0:n}".format(self.__statistics.sameCount)) 210 self.unchangedCountLabel.setText("{0:n}".format(self.__statistics.sameCount))
209 211
244 elif dataType == "diff": 246 elif dataType == "diff":
245 if self.__diffDialog is None: 247 if self.__diffDialog is None:
246 self.__diffDialog = BlackDiffWidget() 248 self.__diffDialog = BlackDiffWidget()
247 self.__diffDialog.showDiff(item.data(0, BlackFormattingDialog.DataRole)) 249 self.__diffDialog.showDiff(item.data(0, BlackFormattingDialog.DataRole))
248 250
249 def __formatFiles(self): 251 def __formatManyFiles(self, files):
252 """
253 Private method to format the list of files according the configuration using
254 multiple processes in parallel.
255
256 @param files list of files to be processed
257 @type list of str
258 """
259 maxProcesses = Preferences.getUI("BackgroundServiceProcesses")
260 if maxProcesses == 0:
261 # determine based on CPU count
262 try:
263 NumberOfProcesses = multiprocessing.cpu_count()
264 if NumberOfProcesses >= 1:
265 NumberOfProcesses -= 1
266 except NotImplementedError:
267 NumberOfProcesses = 1
268 else:
269 NumberOfProcesses = maxProcesses
270
271 # Create queues
272 taskQueue = multiprocessing.Queue()
273 doneQueue = multiprocessing.Queue()
274
275 # Submit tasks
276 for file in files:
277 relSrc = self.__project.getRelativePath(str(file)) if self.__project else ""
278 taskQueue.put((file, relSrc))
279
280 # Start worker processes
281 workers = [
282 multiprocessing.Process(
283 target=self.formattingWorkerTask,
284 args=(taskQueue, doneQueue, self.__config),
285 )
286 for _ in range(NumberOfProcesses)
287 ]
288 for worker in workers:
289 worker.start()
290
291 # Get the results from the worker tasks
292 for _ in range(len(files)):
293 result = doneQueue.get()
294 self.__handleBlackFormattingResult(
295 result.status, result.filename, result.data
296 )
297
298 if self.__cancelled:
299 break
300
301 if self.__cancelled:
302 for worker in workers:
303 worker.terminate()
304 else:
305 # Tell child processes to stop
306 for _ in range(NumberOfProcesses):
307 taskQueue.put("STOP")
308
309 for worker in workers:
310 worker.join()
311 worker.close()
312
313 taskQueue.close()
314 doneQueue.close()
315
316 self.__finish()
317
318 @staticmethod
319 def formattingWorkerTask(inputQueue, outputQueue, config):
320 """
321 Static method acting as the parallel worker for the formatting task.
322
323 @param inputQueue input queue
324 @type multiprocessing.Queue
325 @param outputQueue output queue
326 @type multiprocessing.Queue
327 @param config dictionary containing the configuration parameters
328 @type dict
329 """
330 report = BlackMultiprocessingReport(outputQueue)
331 report.check = config["__action__"] is BlackFormattingAction.Check
332 report.diff = config["__action__"] is BlackFormattingAction.Diff
333
334 versions = (
335 {black.TargetVersion[target.upper()] for target in config["target-version"]}
336 if config["target-version"]
337 else set()
338 )
339
340 mode = black.Mode(
341 target_versions=versions,
342 line_length=int(config["line-length"]),
343 string_normalization=not config["skip-string-normalization"],
344 magic_trailing_comma=not config["skip-magic-trailing-comma"],
345 )
346
347 if config["__action__"] is BlackFormattingAction.Diff:
348 for file, relSrc in iter(inputQueue.get, "STOP"):
349 BlackFormattingDialog.__diffFormatFile(
350 pathlib.Path(file),
351 fast=False,
352 mode=mode,
353 report=report,
354 relSrc=relSrc,
355 )
356 else:
357 writeBack = black.WriteBack.from_configuration(
358 check=config["__action__"] is BlackFormattingAction.Check,
359 diff=config["__action__"] is BlackFormattingAction.Diff,
360 )
361
362 for file, _relSrc in iter(inputQueue.get, "STOP"):
363 black.reformat_one(
364 pathlib.Path(file),
365 fast=False,
366 write_back=writeBack,
367 mode=mode,
368 report=report,
369 )
370
371 def __formatOneFile(self, file):
250 """ 372 """
251 Private method to format the list of files according the configuration. 373 Private method to format the list of files according the configuration.
252 """ 374
375 @param file name of the file to be processed
376 @type str
377 """
378 report = BlackReport(self)
379 report.check = self.__config["__action__"] is BlackFormattingAction.Check
380 report.diff = self.__config["__action__"] is BlackFormattingAction.Diff
381 report.result.connect(self.__handleBlackFormattingResult)
382
253 writeBack = black.WriteBack.from_configuration( 383 writeBack = black.WriteBack.from_configuration(
254 check=self.__action is BlackFormattingAction.Check, 384 check=self.__config["__action__"] is BlackFormattingAction.Check,
255 diff=self.__action is BlackFormattingAction.Diff, 385 diff=self.__config["__action__"] is BlackFormattingAction.Diff,
256 ) 386 )
257 387
258 versions = ( 388 versions = (
259 { 389 {
260 black.TargetVersion[target.upper()] 390 black.TargetVersion[target.upper()]
269 line_length=int(self.__config["line-length"]), 399 line_length=int(self.__config["line-length"]),
270 string_normalization=not self.__config["skip-string-normalization"], 400 string_normalization=not self.__config["skip-string-normalization"],
271 magic_trailing_comma=not self.__config["skip-magic-trailing-comma"], 401 magic_trailing_comma=not self.__config["skip-magic-trailing-comma"],
272 ) 402 )
273 403
274 for file in self.__files: 404 if self.__action is BlackFormattingAction.Diff:
275 if self.__action is BlackFormattingAction.Diff: 405 relSrc = self.__project.getRelativePath(str(file)) if self.__project else ""
276 self.__diffFormatFile( 406 self.__diffFormatFile(
277 pathlib.Path(file), fast=False, mode=mode, report=self.__report 407 pathlib.Path(file), fast=False, mode=mode, report=report, relSrc=relSrc
278 ) 408 )
279 else: 409 else:
280 black.reformat_one( 410 black.reformat_one(
281 pathlib.Path(file), 411 pathlib.Path(file),
282 fast=False, 412 fast=False,
283 write_back=writeBack, 413 write_back=writeBack,
284 mode=mode, 414 mode=mode,
285 report=self.__report, 415 report=report,
286 ) 416 )
287
288 if self.__cancelled:
289 break
290 417
291 self.__finish() 418 self.__finish()
292 419
293 def __diffFormatFile(self, src, fast, mode, report): 420 @staticmethod
294 """ 421 def __diffFormatFile(src, fast, mode, report, relSrc=""):
295 Private method to check, if the given files need to be reformatted, and generate 422 """
423 Static method to check, if the given files need to be reformatted, and generate
296 a unified diff. 424 a unified diff.
297 425
298 @param src path of file to be checked 426 @param src path of file to be checked
299 @type pathlib.Path 427 @type pathlib.Path
300 @param fast flag indicating fast operation 428 @param fast flag indicating fast operation
301 @type bool 429 @type bool
302 @param mode code formatting options 430 @param mode code formatting options
303 @type black.Mode 431 @type black.Mode
304 @param report reference to the report object 432 @param report reference to the report object
305 @type BlackReport 433 @type BlackReport
434 @param relSrc name of the file relative to the project (defaults to "")
435 @type str (optional)
306 """ 436 """
307 then = datetime.datetime.utcfromtimestamp(src.stat().st_mtime) 437 then = datetime.datetime.utcfromtimestamp(src.stat().st_mtime)
308 with open(src, "rb") as buf: 438 with open(src, "rb") as buf:
309 srcContents, _, _ = black.decode_bytes(buf.read()) 439 srcContents, _, _ = black.decode_bytes(buf.read())
310 try: 440 try:
311 dstContents = black.format_file_contents(srcContents, fast=fast, mode=mode) 441 dstContents = black.format_file_contents(srcContents, fast=fast, mode=mode)
312 except black.NothingChanged: 442 except black.NothingChanged:
313 report.done(src, black.Changed.NO) 443 report.done(src, black.Changed.NO)
314 return 444 return
315 445
316 fileName = str(src) 446 fileName = relSrc if bool(relSrc) else str(src)
317 if self.__project:
318 fileName = self.__project.getRelativePath(fileName)
319 447
320 now = datetime.datetime.utcnow() 448 now = datetime.datetime.utcnow()
321 srcName = f"{fileName}\t{then} +0000" 449 srcName = f"{fileName}\t{then} +0000"
322 dstName = f"{fileName}\t{now} +0000" 450 dstName = f"{fileName}\t{now} +0000"
323 diffContents = black.diff(srcContents, dstContents, srcName, dstName) 451 diffContents = black.diff(srcContents, dstContents, srcName, dstName)
393 else: 521 else:
394 statusMsg = self.tr("invalid status ({0})").format(status) 522 statusMsg = self.tr("invalid status ({0})").format(status)
395 self.__statistics.failureCount += 1 523 self.__statistics.failureCount += 1
396 isError = True 524 isError = True
397 525
526 if status != "ignored":
527 self.__statistics.processedCount += 1
528
398 if self.__project: 529 if self.__project:
399 filename = self.__project.getRelativePath(filename) 530 filename = self.__project.getRelativePath(filename)
400 531
401 itm = QTreeWidgetItem(self.resultsList, [statusMsg, filename]) 532 itm = QTreeWidgetItem(self.resultsList, [statusMsg, filename])
402 if data: 533 if data:
418 549
419 ignoreCount: int = 0 550 ignoreCount: int = 0
420 changeCount: int = 0 551 changeCount: int = 0
421 sameCount: int = 0 552 sameCount: int = 0
422 failureCount: int = 0 553 failureCount: int = 0
554 processedCount: int = 0
423 555
424 556
425 class BlackReport(QObject, black.Report): 557 class BlackReport(QObject, black.Report):
426 """ 558 """
427 Class extending the black Report to work with our dialog. 559 Class extending the black Report to work with our dialog.
431 'failed' or 'ignored'), the file name and data related to the result 563 'failed' or 'ignored'), the file name and data related to the result
432 """ 564 """
433 565
434 result = pyqtSignal(str, str, str) 566 result = pyqtSignal(str, str, str)
435 567
436 def __init__(self, dialog): 568 def __init__(self, parent=None):
437 """ 569 """
438 Constructor 570 Constructor
439 571
440 @param dialog reference to the result dialog 572 @param parent reference to the parent object (defaults to None
441 @type QDialog 573 @type QObject (optional)
442 """ 574 """
443 QObject.__init__(self, dialog) 575 QObject.__init__(self, parent)
444 black.Report.__init__(self) 576 black.Report.__init__(self)
445
446 self.ignored_count = 0
447
448 self.__dialog = dialog
449 577
450 def done(self, src, changed, diff=""): 578 def done(self, src, changed, diff=""):
451 """ 579 """
452 Public method to handle the end of a reformat. 580 Public method to handle the end of a reformat.
453 581
488 @type pathlib.Path or str 616 @type pathlib.Path or str
489 @param message ignore message (default to "") 617 @param message ignore message (default to "")
490 @type str (optional) 618 @type str (optional)
491 """ 619 """
492 self.result.emit("ignored", str(src), "") 620 self.result.emit("ignored", str(src), "")
621
622
623 @dataclass
624 class BlackMultiprocessingResult:
625 """
626 Class containing the reformatting result data.
627
628 This class is used when reformatting multiple files in parallel using processes.
629 """
630
631 status: str = ""
632 filename: str = ""
633 data: str = ""
634
635
636 class BlackMultiprocessingReport(black.Report):
637 """
638 Class extending the black Report to work with multiprocessing.
639 """
640
641 def __init__(self, resultQueue):
642 """
643 Constructor
644
645 @param resultQueue reference to the queue to put the results into
646 @type multiprocessing.Queue
647 """
648 super().__init__()
649
650 self.__queue = resultQueue
651
652 def done(self, src, changed, diff=""):
653 """
654 Public method to handle the end of a reformat.
655
656 @param src name of the processed file
657 @type pathlib.Path
658 @param changed change status
659 @type black.Changed
660 @param diff unified diff of potential changes (defaults to "")
661 @type str
662 """
663 if changed is black.Changed.YES:
664 status = "changed"
665
666 elif changed is black.Changed.NO:
667 status = "unchanged"
668
669 elif changed is black.Changed.CACHED:
670 status = "unmodified"
671
672 self.__queue.put(
673 BlackMultiprocessingResult(status=status, filename=str(src), data=diff)
674 )
675
676 def failed(self, src, message):
677 """
678 Public method to handle a reformat failure.
679
680 @param src name of the processed file
681 @type pathlib.Path
682 @param message error message
683 @type str
684 """
685 self.__queue.put(
686 BlackMultiprocessingResult(status="failed", filename=str(src), data=message)
687 )
688
689 def path_ignored(self, src, message=""):
690 """
691 Public method handling an ignored path.
692
693 @param src name of the processed file
694 @type pathlib.Path or str
695 @param message ignore message (default to "")
696 @type str (optional)
697 """
698 self.__queue.put(
699 BlackMultiprocessingResult(status="ignored", filename=str(src), data="")
700 )

eric ide

mercurial