src/eric7/CodeFormatting/BlackFormattingDialog.py

branch
eric7
changeset 9283
0e9d2c4e379e
parent 9281
76caf27cb8a8
child 9284
3b3a4f659782
--- a/src/eric7/CodeFormatting/BlackFormattingDialog.py	Sun Aug 07 15:47:54 2022 +0200
+++ b/src/eric7/CodeFormatting/BlackFormattingDialog.py	Sun Aug 07 17:31:26 2022 +0200
@@ -9,6 +9,7 @@
 
 import copy
 import datetime
+import multiprocessing
 import pathlib
 
 from dataclasses import dataclass
@@ -33,6 +34,7 @@
 from .BlackFormattingAction import BlackFormattingAction
 
 import Utilities
+import Preferences
 
 
 class BlackFormattingDialog(QDialog, Ui_BlackFormattingDialog):
@@ -80,12 +82,8 @@
 
         self.__statistics = BlackStatistics()
 
-        self.__report = BlackReport(self)
-        self.__report.check = action is BlackFormattingAction.Check
-        self.__report.diff = action is BlackFormattingAction.Diff
-        self.__report.result.connect(self.__handleBlackFormattingResult)
-
         self.__config = copy.deepcopy(configuration)
+        self.__config["__action__"] = action  # needed by the workers
         self.__project = project
         self.__action = action
 
@@ -101,8 +99,11 @@
         self.show()
         QCoreApplication.processEvents()
 
-        self.__files = self.__filterFiles(filesList)
-        self.__formatFiles()
+        files = self.__filterFiles(filesList)
+        if len(files) > 1:
+            self.__formatManyFiles(files)
+        elif len(files) == 1:
+            self.__formatOneFile(files[0])
 
     def __filterFiles(self, filesList):
         """
@@ -128,7 +129,7 @@
             for filterRegExp in filterRegExps:
                 filterMatch = filterRegExp.search(file)
                 if filterMatch and filterMatch.group(0):
-                    self.__report.path_ignored(file)
+                    self.__handleBlackFormattingResult("ignored", file, "")
                     break
             else:
                 files.append(file)
@@ -190,18 +191,19 @@
         make them visible.
         """
         self.reformattedLabel.setText(
-            self.tr("reformatted")
+            self.tr("Reformatted:")
             if self.__action is BlackFormattingAction.Format
-            else self.tr("would reformat")
+            else self.tr("Would Reformat:")
         )
 
         total = self.progressBar.maximum()
-        processed = total - self.__statistics.ignoreCount
 
         self.totalCountLabel.setText("{0:n}".format(total))
         self.excludedCountLabel.setText("{0:n}".format(self.__statistics.ignoreCount))
         self.failuresCountLabel.setText("{0:n}".format(self.__statistics.failureCount))
-        self.processedCountLabel.setText("{0:n}".format(processed))
+        self.processedCountLabel.setText(
+            "{0:n}".format(self.__statistics.processedCount)
+        )
         self.reformattedCountLabel.setText(
             "{0:n}".format(self.__statistics.changeCount)
         )
@@ -246,13 +248,141 @@
                 self.__diffDialog = BlackDiffWidget()
             self.__diffDialog.showDiff(item.data(0, BlackFormattingDialog.DataRole))
 
-    def __formatFiles(self):
+    def __formatManyFiles(self, files):
+        """
+        Private method to format the list of files according the configuration using
+        multiple processes in parallel.
+
+        @param files list of files to be processed
+        @type list of str
+        """
+        maxProcesses = Preferences.getUI("BackgroundServiceProcesses")
+        if maxProcesses == 0:
+            # determine based on CPU count
+            try:
+                NumberOfProcesses = multiprocessing.cpu_count()
+                if NumberOfProcesses >= 1:
+                    NumberOfProcesses -= 1
+            except NotImplementedError:
+                NumberOfProcesses = 1
+        else:
+            NumberOfProcesses = maxProcesses
+
+        # Create queues
+        taskQueue = multiprocessing.Queue()
+        doneQueue = multiprocessing.Queue()
+
+        # Submit tasks
+        for file in files:
+            relSrc = self.__project.getRelativePath(str(file)) if self.__project else ""
+            taskQueue.put((file, relSrc))
+
+        # Start worker processes
+        workers = [
+            multiprocessing.Process(
+                target=self.formattingWorkerTask,
+                args=(taskQueue, doneQueue, self.__config),
+            )
+            for _ in range(NumberOfProcesses)
+        ]
+        for worker in workers:
+            worker.start()
+
+        # Get the results from the worker tasks
+        for _ in range(len(files)):
+            result = doneQueue.get()
+            self.__handleBlackFormattingResult(
+                result.status, result.filename, result.data
+            )
+
+            if self.__cancelled:
+                break
+
+        if self.__cancelled:
+            for worker in workers:
+                worker.terminate()
+        else:
+            # Tell child processes to stop
+            for _ in range(NumberOfProcesses):
+                taskQueue.put("STOP")
+
+        for worker in workers:
+            worker.join()
+            worker.close()
+
+        taskQueue.close()
+        doneQueue.close()
+
+        self.__finish()
+
+    @staticmethod
+    def formattingWorkerTask(inputQueue, outputQueue, config):
+        """
+        Static method acting as the parallel worker for the formatting task.
+
+        @param inputQueue input queue
+        @type multiprocessing.Queue
+        @param outputQueue output queue
+        @type multiprocessing.Queue
+        @param config dictionary containing the configuration parameters
+        @type dict
+        """
+        report = BlackMultiprocessingReport(outputQueue)
+        report.check = config["__action__"] is BlackFormattingAction.Check
+        report.diff = config["__action__"] is BlackFormattingAction.Diff
+
+        versions = (
+            {black.TargetVersion[target.upper()] for target in config["target-version"]}
+            if config["target-version"]
+            else set()
+        )
+
+        mode = black.Mode(
+            target_versions=versions,
+            line_length=int(config["line-length"]),
+            string_normalization=not config["skip-string-normalization"],
+            magic_trailing_comma=not config["skip-magic-trailing-comma"],
+        )
+
+        if config["__action__"] is BlackFormattingAction.Diff:
+            for file, relSrc in iter(inputQueue.get, "STOP"):
+                BlackFormattingDialog.__diffFormatFile(
+                    pathlib.Path(file),
+                    fast=False,
+                    mode=mode,
+                    report=report,
+                    relSrc=relSrc,
+                )
+        else:
+            writeBack = black.WriteBack.from_configuration(
+                check=config["__action__"] is BlackFormattingAction.Check,
+                diff=config["__action__"] is BlackFormattingAction.Diff,
+            )
+
+            for file, _relSrc in iter(inputQueue.get, "STOP"):
+                black.reformat_one(
+                    pathlib.Path(file),
+                    fast=False,
+                    write_back=writeBack,
+                    mode=mode,
+                    report=report,
+                )
+
+    def __formatOneFile(self, file):
         """
         Private method to format the list of files according the configuration.
+        
+        @param file name of the file to be processed
+        @type str
         """
+        report = BlackReport(self)
+        report.check = self.__config["__action__"] is BlackFormattingAction.Check
+        report.diff = self.__config["__action__"] is BlackFormattingAction.Diff
+        report.result.connect(self.__handleBlackFormattingResult)
+
         writeBack = black.WriteBack.from_configuration(
-            check=self.__action is BlackFormattingAction.Check,
-            diff=self.__action is BlackFormattingAction.Diff,
+            check=self.__config["__action__"] is BlackFormattingAction.Check,
+            diff=self.__config["__action__"] is BlackFormattingAction.Diff,
         )
 
         versions = (
@@ -271,28 +401,26 @@
             magic_trailing_comma=not self.__config["skip-magic-trailing-comma"],
         )
 
-        for file in self.__files:
-            if self.__action is BlackFormattingAction.Diff:
-                self.__diffFormatFile(
-                    pathlib.Path(file), fast=False, mode=mode, report=self.__report
-                )
-            else:
-                black.reformat_one(
-                    pathlib.Path(file),
-                    fast=False,
-                    write_back=writeBack,
-                    mode=mode,
-                    report=self.__report,
-                )
-
-            if self.__cancelled:
-                break
+        if self.__action is BlackFormattingAction.Diff:
+            relSrc = self.__project.getRelativePath(str(file)) if self.__project else ""
+            self.__diffFormatFile(
+                pathlib.Path(file), fast=False, mode=mode, report=report, relSrc=relSrc
+            )
+        else:
+            black.reformat_one(
+                pathlib.Path(file),
+                fast=False,
+                write_back=writeBack,
+                mode=mode,
+                report=report,
+            )
 
         self.__finish()
 
-    def __diffFormatFile(self, src, fast, mode, report):
+    @staticmethod
+    def __diffFormatFile(src, fast, mode, report, relSrc=""):
         """
-        Private method to check, if the given files need to be reformatted, and generate
+        Static method to check, if the given files need to be reformatted, and generate
         a unified diff.
 
         @param src path of file to be checked
@@ -303,6 +431,8 @@
         @type black.Mode
         @param report reference to the report object
         @type BlackReport
+        @param relSrc name of the file relative to the project (defaults to "")
+        @type str (optional)
         """
         then = datetime.datetime.utcfromtimestamp(src.stat().st_mtime)
         with open(src, "rb") as buf:
@@ -313,9 +443,7 @@
             report.done(src, black.Changed.NO)
             return
 
-        fileName = str(src)
-        if self.__project:
-            fileName = self.__project.getRelativePath(fileName)
+        fileName = relSrc if bool(relSrc) else str(src)
 
         now = datetime.datetime.utcnow()
         srcName = f"{fileName}\t{then} +0000"
@@ -395,6 +523,9 @@
             self.__statistics.failureCount += 1
             isError = True
 
+        if status != "ignored":
+            self.__statistics.processedCount += 1
+
         if self.__project:
             filename = self.__project.getRelativePath(filename)
 
@@ -420,6 +551,7 @@
     changeCount: int = 0
     sameCount: int = 0
     failureCount: int = 0
+    processedCount: int = 0
 
 
 class BlackReport(QObject, black.Report):
@@ -433,20 +565,16 @@
 
     result = pyqtSignal(str, str, str)
 
-    def __init__(self, dialog):
+    def __init__(self, parent=None):
         """
         Constructor
 
-        @param dialog reference to the result dialog
-        @type QDialog
+        @param parent reference to the parent object (defaults to None
+        @type QObject (optional)
         """
-        QObject.__init__(self, dialog)
+        QObject.__init__(self, parent)
         black.Report.__init__(self)
 
-        self.ignored_count = 0
-
-        self.__dialog = dialog
-
     def done(self, src, changed, diff=""):
         """
         Public method to handle the end of a reformat.
@@ -490,3 +618,83 @@
         @type str (optional)
         """
         self.result.emit("ignored", str(src), "")
+
+
+@dataclass
+class BlackMultiprocessingResult:
+    """
+    Class containing the reformatting result data.
+
+    This class is used when reformatting multiple files in parallel using processes.
+    """
+
+    status: str = ""
+    filename: str = ""
+    data: str = ""
+
+
+class BlackMultiprocessingReport(black.Report):
+    """
+    Class extending the black Report to work with multiprocessing.
+    """
+
+    def __init__(self, resultQueue):
+        """
+        Constructor
+
+        @param resultQueue reference to the queue to put the results into
+        @type multiprocessing.Queue
+        """
+        super().__init__()
+
+        self.__queue = resultQueue
+
+    def done(self, src, changed, diff=""):
+        """
+        Public method to handle the end of a reformat.
+
+        @param src name of the processed file
+        @type pathlib.Path
+        @param changed change status
+        @type black.Changed
+        @param diff unified diff of potential changes (defaults to "")
+        @type str
+        """
+        if changed is black.Changed.YES:
+            status = "changed"
+
+        elif changed is black.Changed.NO:
+            status = "unchanged"
+
+        elif changed is black.Changed.CACHED:
+            status = "unmodified"
+
+        self.__queue.put(
+            BlackMultiprocessingResult(status=status, filename=str(src), data=diff)
+        )
+
+    def failed(self, src, message):
+        """
+        Public method to handle a reformat failure.
+
+        @param src name of the processed file
+        @type pathlib.Path
+        @param message error message
+        @type str
+        """
+        self.__queue.put(
+            BlackMultiprocessingResult(status="failed", filename=str(src), data=message)
+        )
+
+    def path_ignored(self, src, message=""):
+        """
+        Public method handling an ignored path.
+
+        @param src name of the processed file
+        @type pathlib.Path or str
+        @param message ignore message (default to "")
+        @type str (optional)
+        """
+        self.__queue.put(
+            BlackMultiprocessingResult(status="ignored", filename=str(src), data="")
+        )

eric ide

mercurial