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 |
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) |
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 ) |