Code Formatting eric7

Mon, 11 Jul 2022 16:42:50 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 11 Jul 2022 16:42:50 +0200
branch
eric7
changeset 9214
bd28e56047d7
parent 9213
2bf743848d2f
child 9215
30d7f7fd2b4a

Code Formatting
- added an interface to reformat Python source code with the 'Black' utility

docs/changelog file | annotate | diff | comparison | revisions
eric7.epj file | annotate | diff | comparison | revisions
pyproject.toml file | annotate | diff | comparison | revisions
scripts/install.py file | annotate | diff | comparison | revisions
src/__init__.py file | annotate | diff | comparison | revisions
src/eric7/CodeFormatting/BlackConfigurationDialog.py file | annotate | diff | comparison | revisions
src/eric7/CodeFormatting/BlackConfigurationDialog.ui file | annotate | diff | comparison | revisions
src/eric7/CodeFormatting/BlackDiffWidget.py file | annotate | diff | comparison | revisions
src/eric7/CodeFormatting/BlackDiffWidget.ui file | annotate | diff | comparison | revisions
src/eric7/CodeFormatting/BlackFormattingAction.py file | annotate | diff | comparison | revisions
src/eric7/CodeFormatting/BlackFormattingDialog.py file | annotate | diff | comparison | revisions
src/eric7/CodeFormatting/BlackFormattingDialog.ui file | annotate | diff | comparison | revisions
src/eric7/CodeFormatting/BlackUtilities.py file | annotate | diff | comparison | revisions
src/eric7/CodeFormatting/__init__.py file | annotate | diff | comparison | revisions
src/eric7/JediInterface/AssistantJedi.py file | annotate | diff | comparison | revisions
src/eric7/Project/Project.py file | annotate | diff | comparison | revisions
src/eric7/Project/ProjectSourcesBrowser.py file | annotate | diff | comparison | revisions
src/eric7/QScintilla/Editor.py file | annotate | diff | comparison | revisions
src/eric7/Utilities/__init__.py file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/jedi.png file | annotate | diff | comparison | revisions
--- a/docs/changelog	Mon Jul 11 16:09:04 2022 +0200
+++ b/docs/changelog	Mon Jul 11 16:42:50 2022 +0200
@@ -8,6 +8,8 @@
 - Code Documentation Generator
   -- added capability to configure a start directory for the documentation
      generation process
+- Code Formatting
+  -- added an interface to reformat Python source code with the 'Black' utility
 - setup Wizard
   -- added support for 'project_urls'
   -- added support for 'entry_points'
--- a/eric7.epj	Mon Jul 11 16:09:04 2022 +0200
+++ b/eric7.epj	Mon Jul 11 16:42:50 2022 +0200
@@ -61,7 +61,7 @@
         "CopyrightMinFileSize": 0,
         "DocstringType": "eric",
         "EnabledCheckerCategories": "C, D, E, M, N, Y, W",
-        "ExcludeFiles": "*/ThirdParty/*, */coverage/*, */Ui_*.py, */Examples/*, */*_rc.py,*/pycodestyle.py,*/pyflakes/checker.py,*/mccabe.py,*/eradicate.py,*/ast_unparse.py,*/piplicenses.py",
+        "ExcludeFiles": "*/ThirdParty/*, */coverage/*, */Ui_*.py, */Examples/*, */pycodestyle.py,*/pyflakes/checker.py,*/mccabe.py,*/eradicate.py,*/ast_unparse.py,*/piplicenses.py",
         "ExcludeMessages": "C101,E203,E265,E266,E305,E402,M201,M301,M302,M303,M304,M305,M306,M307,M308,M311,M312,M313,M314,M315,M321,M701,M702,M811,M834,N802,N803,N807,N808,N821,W293,W503,Y119,Y401,Y402",
         "FixCodes": "",
         "FixIssues": false,
@@ -270,6 +270,9 @@
       "Ui_*.py": "__IGNORE__"
     },
     "FORMS": [
+      "src/eric7/CodeFormatting/BlackConfigurationDialog.ui",
+      "src/eric7/CodeFormatting/BlackDiffWidget.ui",
+      "src/eric7/CodeFormatting/BlackFormattingDialog.ui",
       "src/eric7/CondaInterface/CondaExecDialog.ui",
       "src/eric7/CondaInterface/CondaExportDialog.ui",
       "src/eric7/CondaInterface/CondaInfoDialog.ui",
@@ -992,6 +995,13 @@
       "scripts/uninstall-debugclients.py",
       "scripts/uninstall.py",
       "setup.py",
+      "src/__init__.py",
+      "src/eric7/CodeFormatting/BlackConfigurationDialog.py",
+      "src/eric7/CodeFormatting/BlackDiffWidget.py",
+      "src/eric7/CodeFormatting/BlackFormattingAction.py",
+      "src/eric7/CodeFormatting/BlackFormattingDialog.py",
+      "src/eric7/CodeFormatting/BlackUtilities.py",
+      "src/eric7/CodeFormatting/__init__.py",
       "src/eric7/CondaInterface/Conda.py",
       "src/eric7/CondaInterface/CondaExecDialog.py",
       "src/eric7/CondaInterface/CondaExportDialog.py",
@@ -2389,7 +2399,7 @@
     "TRANSLATIONSBINPATH": "",
     "UICPARAMS": {
       "Package": "",
-      "PackagesRoot": "eric7",
+      "PackagesRoot": "src/eric7",
       "RcSuffix": ""
     },
     "VCS": "Mercurial",
--- a/pyproject.toml	Mon Jul 11 16:09:04 2022 +0200
+++ b/pyproject.toml	Mon Jul 11 16:42:50 2022 +0200
@@ -69,6 +69,7 @@
   "cyclonedx-python-lib",
   "cyclonedx-bom",
   "trove-classifiers",
+  "black>=22.6.0",
   "pywin32>=1.0;platform_system=='Windows'",
 ]
 dynamic = ["version"]
--- a/scripts/install.py	Mon Jul 11 16:09:04 2022 +0200
+++ b/scripts/install.py	Mon Jul 11 16:42:50 2022 +0200
@@ -1632,6 +1632,7 @@
         "cyclonedx-python-lib": ("cyclonedx", ""),
         "cyclonedx-bom": ("cyclonedx_py", ""),
         "trove-classifiers": ("trove_classifiers", ""),
+        "black": ("black", ">=22.6.0"),
     }
     if not ignorePyqt6Tools:
         optionalModulesList["qt6-applications"] = ("qt6_applications", "")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/__init__.py	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Empty module to make this a package.
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CodeFormatting/BlackConfigurationDialog.py	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,269 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to enter the parameters for a Black formatting run.
+"""
+
+import contextlib
+import copy
+import pathlib
+
+import black
+import tomlkit
+
+from PyQt6.QtCore import pyqtSlot, Qt
+from PyQt6.QtGui import QFontMetricsF, QGuiApplication
+from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem
+
+from EricWidgets import EricMessageBox
+from EricWidgets.EricApplication import ericApp
+
+from .Ui_BlackConfigurationDialog import Ui_BlackConfigurationDialog
+
+from . import BlackUtilities
+
+
+class BlackConfigurationDialog(QDialog, Ui_BlackConfigurationDialog):
+    """
+    Class implementing a dialog to enter the parameters for a Black formatting run.
+    """
+    def __init__(self, withProject=True, parent=None):
+        """
+        Constructor
+        
+        @param withProject flag indicating to look for project configurations
+            (defaults to True)
+        @type bool
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.__project = ericApp().getObject("Project") if withProject else None
+        
+        indentTabWidth = (
+            QFontMetricsF(self.excludeEdit.font()).horizontalAdvance(" ") * 2
+        )
+        self.excludeEdit.document().setIndentWidth(indentTabWidth)
+        self.excludeEdit.setTabStopDistance(indentTabWidth)
+        
+        self.__pyprojectData = {}
+        self.__projectData = {}
+        
+        self.__tomlButton = self.buttonBox.addButton(
+            self.tr("Generate TOML"),
+            QDialogButtonBox.ButtonRole.ActionRole
+        )
+        self.__tomlButton.setToolTip(self.tr(
+            "Place a code snippet for 'pyproject.toml' into the clipboard."
+        ))
+        self.__tomlButton.clicked.connect(self.__createTomlSnippet)
+        
+        # setup the source combobox
+        self.sourceComboBox.addItem("", "")
+        if self.__project:
+            pyprojectPath = pathlib.Path(
+                self.__project.getProjectPath()
+            ) / "pyproject.toml"
+            if pyprojectPath.exists():
+                with contextlib.suppress(tomlkit.exceptions.ParseError, OSError):
+                    with pyprojectPath.open("r", encoding="utf-8") as f:
+                        data = tomlkit.load(f)
+                    config = data.get("tool", {}).get("black", {})
+                if config:
+                    self.__pyprojectData = {
+                        k.replace("--", "").replace("-", "_"): v
+                        for k, v in config.items()
+                    }
+                    self.sourceComboBox.addItem("pyproject.toml", "pyproject")
+            if self.__project.getData("OTHERTOOLSPARMS", "Black") is not None:
+                self.__projectData = copy.deepcopy(
+                    self.__project.getData("OTHERTOOLSPARMS", "Black")
+                )
+                self.sourceComboBox.addItem(self.tr("Project File"), "project")
+        self.sourceComboBox.addItem(self.tr("Defaults"), "default")
+        self.sourceComboBox.addItem(self.tr("Configuration Below"), "dialog")
+        
+        self.__populateTargetVersionsList()
+        
+        if self.__projectData:
+            source = self.__projectData.get("source", "")
+            self.sourceComboBox.setCurrentIndex(self.sourceComboBox.findData(source))
+    
+    def __populateTargetVersionsList(self):
+        """
+        Private method to populate the target versions list widget with checkable
+        Python version entries.
+        """
+        targets = [
+            (int(t[2]), int(t[3:]), t)
+            for t in dir(black.TargetVersion)
+            if t.startswith("PY")
+        ]
+        for target in sorted(targets):
+            itm = QListWidgetItem(
+                "Python {0}.{1}".format(target[0], target[1]), self.targetVersionsList
+            )
+            itm.setData(Qt.ItemDataRole.UserRole, target[2])
+            itm.setFlags(itm.flags() | Qt.ItemFlag.ItemIsUserCheckable)
+            itm.setCheckState(Qt.CheckState.Unchecked)
+    
+    def __loadConfiguration(self, configurationDict):
+        """
+        Private method to load the configuration section with data of the given
+        dictionary.
+        
+        @param configurationDict reference to the data to be loaded
+        @type dict
+        """
+        confDict = copy.deepcopy(BlackUtilities.getDefaultConfiguration())
+        confDict.update(configurationDict)
+        
+        self.lineLengthSpinBox.setValue(int(confDict["line-length"]))
+        self.skipStringNormalCheckBox.setChecked(confDict["skip-string-normalization"])
+        self.skipMagicCommaCheckBox.setChecked(confDict["skip-magic-trailing-comma"])
+        self.excludeEdit.setPlainText(confDict["extend-exclude"])
+        for row in range(self.targetVersionsList.count()):
+            itm = self.targetVersionsList.item(row)
+            itm.setCheckState(
+                Qt.CheckState.Checked
+                if itm.data(Qt.ItemDataRole.UserRole).lower()
+                in confDict["target-version"] else
+                Qt.CheckState.Unchecked
+            )
+    
+    @pyqtSlot(str)
+    def on_sourceComboBox_currentTextChanged(self, selection):
+        """
+        Private slot to handle the selection of a configuration source.
+        
+        @param selection text of the currently selected item
+        @type str
+        """
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(
+            bool(selection)
+        )
+        
+        source = self.sourceComboBox.currentData()
+        if source == "pyproject":
+            self.__loadConfiguration(self.__pyprojectData)
+        elif source == "project":
+            self.__loadConfiguration(self.__projectData)
+        elif source == "default":
+            self.__loadConfiguration(BlackUtilities.getDefaultConfiguration())
+        elif source == "dialog":
+            # just leave the current entries
+            pass
+    
+    @pyqtSlot()
+    def on_excludeEdit_textChanged(self):
+        """
+        Private slot to enable the validate button depending on the exclude text.
+        """
+        self.validateButton.setEnabled(bool(self.excludeEdit.toPlainText()))
+    
+    @pyqtSlot()
+    def on_validateButton_clicked(self):
+        """
+        Private slot to validate the entered exclusion regular expression.
+        """
+        regexp = self.excludeEdit.toPlainText()
+        valid, error = BlackUtilities.validateRegExp(regexp)
+        if valid:
+            EricMessageBox.information(
+                self,
+                self.tr("Validation"),
+                self.tr("""The exclusion expression is valid.""")
+            )
+        else:
+            EricMessageBox.critical(
+                self,
+                self.tr("Validation Error"),
+                error
+            )
+    
+    def __getTargetList(self):
+        """
+        Private method to get the list of checked target versions.
+        
+        @return list of target versions
+        @rtype list of str
+        """
+        targets = []
+        for row in range(self.targetVersionsList.count()):
+            itm = self.targetVersionsList.item(row)
+            if itm.checkState() == Qt.CheckState.Checked:
+                targets.append(itm.data(Qt.ItemDataRole.UserRole).lower())
+        
+        return targets
+    
+    @pyqtSlot()
+    def __createTomlSnippet(self):
+        """
+        Private slot to generate a TOML snippet of the current configuration.
+        
+        Note: Only non-default values are included in this snippet.
+        
+        The code snippet is copied to the clipboard and may be placed inside the
+        'pyproject.toml' file.
+        """
+        doc = tomlkit.document()
+        
+        black = tomlkit.table()
+        targetList = self.__getTargetList()
+        if targetList:
+            black["target-version"] = targetList
+        black["line-length"] = self.lineLengthSpinBox.value()
+        if self.skipStringNormalCheckBox.isChecked():
+            black["skip-string-normalization"] = True
+        if self.skipMagicCommaCheckBox.isChecked():
+            black["skip-magic-trailing-comma"] = True
+        
+        excludeRegexp = self.excludeEdit.toPlainText()
+        if excludeRegexp and BlackUtilities.validateRegExp(excludeRegexp)[0]:
+            black["extend-exclude"] = tomlkit.string(
+                "\n{0}\n".format(excludeRegexp.strip()),
+                literal=True,
+                multiline=True
+            )
+        
+        doc["tool"] = tomlkit.table(is_super_table=True)
+        doc["tool"]["black"] = black
+        
+        QGuiApplication.clipboard().setText(tomlkit.dumps(doc))
+        
+        EricMessageBox.information(
+            self,
+            self.tr("Create TOML snipper"),
+            self.tr("""The 'pyproject.toml' snippet was copied to the clipboard"""
+                    """ successfully.""")
+        )
+    
+    def getConfiguration(self):
+        """
+        Public method to get the current configuration parameters.
+        
+        @return dictionary containing the configuration parameters
+        @rtype dict
+        """
+        configuration = BlackUtilities.getDefaultConfiguration()
+        
+        configuration["source"] = self.sourceComboBox.currentData()
+        configuration["target-version"] = self.__getTargetList()
+        configuration["line-length"] = self.lineLengthSpinBox.value()
+        configuration["skip-string-normalization"] = (
+            self.skipStringNormalCheckBox.isChecked()
+        )
+        configuration["skip-magic-trailing-comma"] = (
+            self.skipMagicCommaCheckBox.isChecked()
+        )
+        configuration["extend-exclude"] = self.excludeEdit.toPlainText().strip()
+        
+        if self.__project:
+            self.__project.setData("OTHERTOOLSPARMS", "Black", configuration)
+        
+        return configuration
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CodeFormatting/BlackConfigurationDialog.ui	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BlackConfigurationDialog</class>
+ <widget class="QDialog" name="BlackConfigurationDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>500</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Black Configuration</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_2">
+   <item row="0" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Configuration Source:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="QComboBox" name="sourceComboBox">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="toolTip">
+      <string>Select the configuration source.</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0" colspan="2">
+    <widget class="QGroupBox" name="configurationGroup">
+     <property name="title">
+      <string>Configuration</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <widget class="QLabel" name="label_2">
+        <property name="text">
+         <string>Line Length:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QSpinBox" name="lineLengthSpinBox">
+        <property name="toolTip">
+         <string>Enter the allowed maximum line length.</string>
+        </property>
+        <property name="minimum">
+         <number>70</number>
+        </property>
+        <property name="maximum">
+         <number>120</number>
+        </property>
+        <property name="value">
+         <number>88</number>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="2" colspan="2">
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>387</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label_3">
+        <property name="text">
+         <string>Target Versions:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1" rowspan="2" colspan="2">
+       <widget class="QListWidget" name="targetVersionsList">
+        <property name="toolTip">
+         <string>Select the python versions to be supported (none for auto-detection).</string>
+        </property>
+        <property name="alternatingRowColors">
+         <bool>true</bool>
+        </property>
+        <property name="selectionMode">
+         <enum>QAbstractItemView::NoSelection</enum>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="3">
+       <spacer name="horizontalSpacer_2">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>322</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="3" column="0" colspan="4">
+       <widget class="QCheckBox" name="skipStringNormalCheckBox">
+        <property name="toolTip">
+         <string>Select to not normalize string quotes or prefixes.</string>
+        </property>
+        <property name="text">
+         <string>Skip string normalization</string>
+        </property>
+       </widget>
+      </item>
+      <item row="4" column="0" colspan="4">
+       <widget class="QCheckBox" name="skipMagicCommaCheckBox">
+        <property name="toolTip">
+         <string>Select to not use trailing commas as a reason to split lines.</string>
+        </property>
+        <property name="text">
+         <string>Skip magic trailing comma</string>
+        </property>
+       </widget>
+      </item>
+      <item row="5" column="0">
+       <widget class="QLabel" name="label_4">
+        <property name="text">
+         <string>Exclude:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+        </property>
+       </widget>
+      </item>
+      <item row="5" column="1" rowspan="2" colspan="3">
+       <widget class="QPlainTextEdit" name="excludeEdit">
+        <property name="toolTip">
+         <string>Enter a regular expression that matches files and directories that should be excluded in addition to the default exclusions.</string>
+        </property>
+       </widget>
+      </item>
+      <item row="6" column="0">
+       <widget class="QPushButton" name="validateButton">
+        <property name="toolTip">
+         <string>Press to validate the entered exclusion pattern.</string>
+        </property>
+        <property name="text">
+         <string>Validate</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="2" column="0" colspan="2">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>sourceComboBox</tabstop>
+  <tabstop>lineLengthSpinBox</tabstop>
+  <tabstop>targetVersionsList</tabstop>
+  <tabstop>skipStringNormalCheckBox</tabstop>
+  <tabstop>skipMagicCommaCheckBox</tabstop>
+  <tabstop>excludeEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>BlackConfigurationDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>BlackConfigurationDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CodeFormatting/BlackDiffWidget.py	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a window to show a unified diff..
+"""
+
+from PyQt6.QtWidgets import QWidget
+
+from .Ui_BlackDiffWidget import Ui_BlackDiffWidget
+
+from UI.DiffHighlighter import DiffHighlighter
+
+import Preferences
+
+
+class BlackDiffWidget(QWidget, Ui_BlackDiffWidget):
+    """
+    Class implementing a window to show a unified diff..
+    """
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        font = Preferences.getEditorOtherFonts("MonospacedFont")
+        self.diffEdit.document().setDefaultFont(font)
+        
+        self.__highlighter = DiffHighlighter(self.diffEdit.document())
+        self.__savedGeometry = None
+    
+    def showDiff(self, diff):
+        """
+        Public method to show the given diff.
+        
+        @param diff text containing the unified diff
+        @type str
+        """
+        self.diffEdit.clear()
+        self.__highlighter.regenerateRules()
+        
+        if diff:
+            self.diffEdit.setPlainText(diff)
+        else:
+            self.diffEdit.setPlainText(self.tr('There is no difference.'))
+        
+        if self.__savedGeometry is not None:
+            self.restoreGeometry(self.__savedGeometry)
+        
+        self.show()
+    
+    def closeEvent(self, evt):
+        """
+        Protected slot implementing a close event handler.
+        
+        @param evt reference to the close event
+        @type QCloseEvent
+        """
+        self.__savedGeometry = self.saveGeometry()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CodeFormatting/BlackDiffWidget.ui	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BlackDiffWidget</class>
+ <widget class="QWidget" name="BlackDiffWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>700</width>
+    <height>700</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Reformatting Differences</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QPlainTextEdit" name="diffEdit">
+     <property name="lineWrapMode">
+      <enum>QPlainTextEdit::NoWrap</enum>
+     </property>
+     <property name="readOnly">
+      <bool>true</bool>
+     </property>
+     <property name="tabStopDistance">
+      <double>8.000000000000000</double>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>BlackDiffWidget</receiver>
+   <slot>close()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>447</x>
+     <y>680</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>598</x>
+     <y>639</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CodeFormatting/BlackFormattingAction.py	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing an enum defining the various code formatting actions.
+"""
+
+import enum
+
+
+class BlackFormattingAction(enum.Enum):
+    """
+    Class defining the various code formatting actions.
+    """
+    Format = 0
+    Check = 1
+    Diff = 2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CodeFormatting/BlackFormattingDialog.py	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,403 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog showing the code formatting progress and the result.
+"""
+
+import copy
+import datetime
+import pathlib
+
+import black
+
+from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication
+from PyQt6.QtWidgets import (
+    QAbstractButton,
+    QDialog,
+    QDialogButtonBox,
+    QHeaderView,
+    QTreeWidgetItem
+)
+
+from EricWidgets import EricMessageBox
+
+from .Ui_BlackFormattingDialog import Ui_BlackFormattingDialog
+
+from . import BlackUtilities
+from .BlackDiffWidget import BlackDiffWidget
+from .BlackFormattingAction import BlackFormattingAction
+
+import Utilities
+
+
+class BlackFormattingDialog(QDialog, Ui_BlackFormattingDialog):
+    """
+    Class implementing a dialog showing the code formatting progress and the result.
+    """
+    DataTypeRole = Qt.ItemDataRole.UserRole
+    DataRole = Qt.ItemDataRole.UserRole + 1
+    
+    def __init__(self, configuration, filesList, project=None,
+                 action=BlackFormattingAction.Format, parent=None):
+        """
+        Constructor
+        
+        @param configuration dictionary containing the configuration parameters
+        @type dict
+        @param filesList list of absolute file paths to be processed
+        @type list of str
+        @param project reference to the project object (defaults to None)
+        @type Project (optional)
+        @param action action to be performed (defaults to BlackFormattingAction.Format)
+        @type BlackFormattingAction (optional)
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.progressBar.setMaximum(len(filesList))
+        self.progressBar.setValue(0)
+        
+        self.resultsList.header().setSortIndicator(1, Qt.SortOrder.AscendingOrder)
+        
+        self.__report = BlackReport(self)
+        self.__report.check = action is BlackFormattingAction.Check
+        self.__report.diff = action is BlackFormattingAction.Diff
+        
+        self.__config = copy.deepcopy(configuration)
+        self.__project = project
+        self.__action = action
+        
+        self.__cancelled = False
+        self.__diffDialog = None
+        
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True)
+        
+        self.show()
+        QCoreApplication.processEvents()
+        
+        self.__files = self.__filterFiles(filesList)
+        self.__formatFiles()
+    
+    def __filterFiles(self, filesList):
+        """
+        Private method to filter the given list of files according the
+        configuration parameters.
+        
+        @param filesList list of files
+        @type list of str
+        @return list of filtered files
+        @rtype list of str
+        """
+        filterRegExps = [
+            BlackUtilities.compileRegExp(self.__config[k])
+            for k in ["force-exclude", "extend-exclude", "exclude"]
+            if k in self.__config and bool(self.__config[k])
+            and BlackUtilities.validateRegExp(self.__config[k])[0]
+        ]
+        
+        files = []
+        for file in filesList:
+            file = Utilities.fromNativeSeparators(file)
+            for filterRegExp in filterRegExps:
+                filterMatch = filterRegExp.search(file)
+                if filterMatch and filterMatch.group(0):
+                    self.__report.path_ignored(file)
+                    break
+            else:
+                files.append(file)
+        
+        return files
+    
+    def __resort(self):
+        """
+        Private method to resort the result list.
+        """
+        self.resultsList.sortItems(
+            self.resultsList.sortColumn(),
+            self.resultsList.header().sortIndicatorOrder())
+    
+    def __resizeColumns(self):
+        """
+        Private method to resize the columns of the result list.
+        """
+        self.resultsList.header().resizeSections(
+            QHeaderView.ResizeMode.ResizeToContents)
+        self.resultsList.header().setStretchLastSection(True)
+    
+    def __finish(self):
+        """
+        Private method to perform some actions after the run was performed or canceled.
+        """
+        self.__resort()
+        self.__resizeColumns()
+        
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(True)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setDefault(True)
+        
+        self.progressBar.setVisible(False)
+        
+        self.__updateStatistics()
+    
+    def __updateStatistics(self):
+        """
+        Private method to update the statistics about the recent formatting run.
+        """
+        self.reformattedLabel.setText(
+            self.tr("reformatted")
+            if self.__action is BlackFormattingAction.Format else
+            self.tr("would reformat")
+        )
+        
+        total = self.progressBar.maximum()
+        processed = total - self.__report.ignored_count
+        
+        self.totalCountLabel.setText("{0:n}".format(total))
+        self.excludedCountLabel.setText("{0:n}".format(self.__report.ignored_count))
+        self.failuresCountLabel.setText("{0:n}".format(self.__report.failure_count))
+        self.processedCountLabel.setText("{0:n}".format(processed))
+        self.reformattedCountLabel.setText("{0:n}".format(self.__report.change_count))
+        self.unchangedCountLabel.setText("{0:n}".format(self.__report.same_count))
+    
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot to handle button presses of the dialog buttons.
+        
+        @param button reference to the pressed button
+        @type QAbstractButton
+        """
+        if button == self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel):
+            self.__cancelled = True
+        elif button == self.buttonBox.button(QDialogButtonBox.StandardButton.Close):
+            self.accept()
+    
+    @pyqtSlot(QTreeWidgetItem, int)
+    def on_resultsList_itemDoubleClicked(self, item, column):
+        """
+        Private slot handling a double click of a result item.
+        
+        @param item reference to the double clicked item
+        @type QTreeWidgetItem
+        @param column column number that was double clicked
+        @type int
+        """
+        dataType = item.data(0, BlackFormattingDialog.DataTypeRole)
+        if dataType == "error":
+            EricMessageBox.critical(
+                self,
+                self.tr("Formatting Failure"),
+                self.tr(
+                    "<p>Formatting failed due to this error.</p><p>{0}</p>"
+                ).format(item.data(0, BlackFormattingDialog.DataRole))
+            )
+        elif dataType == "diff":
+            if self.__diffDialog is None:
+                self.__diffDialog = BlackDiffWidget()
+            self.__diffDialog.showDiff(item.data(0, BlackFormattingDialog.DataRole))
+    
+    def addResultEntry(self, status, fileName, isError=False, data=None):
+        """
+        Public method to add an entry to the result list.
+        
+        @param status status of the operation
+        @type str
+        @param fileName name of the processed file
+        @type str
+        @param isError flag indicating that data contains an error message (defaults to
+            False)
+        @type bool (optional)
+        @param data associated data (diff or error message) (defaults to None)
+        @type str (optional)
+        """
+        if self.__project:
+            fileName = self.__project.getRelativePath(fileName)
+        
+        itm = QTreeWidgetItem(self.resultsList, [status, fileName])
+        if data:
+            itm.setData(
+                0,
+                BlackFormattingDialog.DataTypeRole,
+                "error" if isError else "diff"
+            )
+            itm.setData(0, BlackFormattingDialog.DataRole, data)
+        
+        self.progressBar.setValue(self.progressBar.value() + 1)
+        
+        QCoreApplication.processEvents()
+    
+    def __formatFiles(self):
+        """
+        Private method to format the list of files according the configuration.
+        """
+        writeBack = black.WriteBack.from_configuration(
+            check=self.__action is BlackFormattingAction.Check,
+            diff=self.__action is BlackFormattingAction.Diff
+        )
+        
+        versions = (
+            {
+                black.TargetVersion[target.upper()]
+                for target in self.__config["target-version"]
+            }
+            if self.__config["target-version"] else
+            set()
+        )
+        
+        mode = black.Mode(
+            target_versions=versions,
+            line_length=int(self.__config["line-length"]),
+            string_normalization=not self.__config["skip-string-normalization"],
+            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
+        
+        self.__finish()
+    
+    def __diffFormatFile(self, src, fast, mode, report):
+        """
+        Private method to check, if the given files need to be reformatted, and generate
+        a unified diff.
+        
+        @param src path of file to be checked
+        @type pathlib.Path
+        @param fast flag indicating fast operation
+        @type bool
+        @param mode code formatting options
+        @type black.Mode
+        @param report reference to the report object
+        @type BlackReport
+        """
+        then = datetime.datetime.utcfromtimestamp(src.stat().st_mtime)
+        with open(src, "rb") as buf:
+            srcContents, _, _ = black.decode_bytes(buf.read())
+        try:
+            dstContents = black.format_file_contents(srcContents, fast=fast, mode=mode)
+        except black.NothingChanged:
+            report.done(src, black.Changed.NO)
+            return
+        
+        fileName = str(src)
+        if self.__project:
+            fileName = self.__project.getRelativePath(fileName)
+        
+        now = datetime.datetime.utcnow()
+        srcName = f"{fileName}\t{then} +0000"
+        dstName = f"{fileName}\t{now} +0000"
+        diffContents = black.diff(srcContents, dstContents, srcName, dstName)
+        report.done(src, black.Changed.YES, diff=diffContents)
+    
+    def closeEvent(self, evt):
+        """
+        Protected slot implementing a close event handler.
+        
+        @param evt reference to the close event
+        @type QCloseEvent
+        """
+        if self.__diffDialog is not None:
+            self.__diffDialog.close()
+        evt.accept()
+
+
+class BlackReport(black.Report):
+    """
+    Class extending the black Report to work with our dialog.
+    """
+    def __init__(self, dialog):
+        """
+        Constructor
+        
+        @param dialog reference to the result dialog
+        @type QDialog
+        """
+        super().__init__()
+        
+        self.ignored_count = 0
+        
+        self.__dialog = dialog
+    
+    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 = (
+                QCoreApplication.translate("BlackFormattingDialog", "would reformat")
+                if self.check or self.diff else
+                QCoreApplication.translate("BlackFormattingDialog", "reformatted")
+            )
+            self.change_count += 1
+        
+        elif changed is black.Changed.NO:
+            status = QCoreApplication.translate("BlackFormattingDialog", "unchanged")
+            self.same_count += 1
+        
+        elif changed is black.Changed.CACHED:
+            status = QCoreApplication.translate("BlackFormattingDialog", "unmodified")
+            self.same_count += 1
+        
+        if self.diff:
+            self.__dialog.addResultEntry(status, str(src), data=diff)
+        else:
+            self.__dialog.addResultEntry(status, str(src))
+    
+    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
+        """
+        status = QCoreApplication.translate("BlackFormattingDialog", "failed")
+        self.failure_count += 1
+        
+        self.__dialog.addResultEntry(status, str(src), isError=True, 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)
+        """
+        status = QCoreApplication.translate("BlackFormattingDialog", "ignored")
+        self.ignored_count += 1
+        
+        self.__dialog.addResultEntry(status, str(src))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CodeFormatting/BlackFormattingDialog.ui	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,204 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BlackFormattingDialog</class>
+ <widget class="QDialog" name="BlackFormattingDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>700</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Code Formatting with Black</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QTreeWidget" name="resultsList">
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="rootIsDecorated">
+      <bool>false</bool>
+     </property>
+     <property name="itemsExpandable">
+      <bool>false</bool>
+     </property>
+     <property name="sortingEnabled">
+      <bool>true</bool>
+     </property>
+     <column>
+      <property name="text">
+       <string>Status</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>File Name</string>
+      </property>
+     </column>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Statistics</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <widget class="QLabel" name="label">
+        <property name="text">
+         <string>Total Files:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QLabel" name="totalCountLabel">
+        <property name="text">
+         <string notr="true">0</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="2">
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>183</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="0" column="3">
+       <widget class="QLabel" name="label_3">
+        <property name="text">
+         <string>Excluded:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="4">
+       <widget class="QLabel" name="excludedCountLabel">
+        <property name="text">
+         <string notr="true">0</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="5">
+       <spacer name="horizontalSpacer_2">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>182</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="0" column="6">
+       <widget class="QLabel" name="label_5">
+        <property name="text">
+         <string>Failures:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="7">
+       <widget class="QLabel" name="failuresCountLabel">
+        <property name="text">
+         <string notr="true">0</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label_6">
+        <property name="text">
+         <string>Processed:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <widget class="QLabel" name="processedCountLabel">
+        <property name="text">
+         <string notr="true">0</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="3">
+       <widget class="QLabel" name="reformattedLabel">
+        <property name="text">
+         <string>Reformatted:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="4">
+       <widget class="QLabel" name="reformattedCountLabel">
+        <property name="text">
+         <string notr="true">0</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="6">
+       <widget class="QLabel" name="label_4">
+        <property name="text">
+         <string>Unchanged:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="7">
+       <widget class="QLabel" name="unchangedCountLabel">
+        <property name="text">
+         <string notr="true">0</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QProgressBar" name="progressBar">
+     <property name="format">
+      <string>%v/%m Files</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CodeFormatting/BlackUtilities.py	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing some utility functions for the Black based code formatting.
+"""
+
+import re
+
+from PyQt6.QtCore import QCoreApplication
+
+import black
+
+
+def getDefaultConfiguration():
+    """
+    Function to generate a default set of configuration parameters.
+    
+    @return dictionary containing the default parameters
+    @rtype dict
+    """
+    return {
+        "target-version": set(),
+        "line-length": black.DEFAULT_LINE_LENGTH,
+        "skip-string-normalization": False,
+        "skip-magic-trailing-comma": False,
+        "extend-exclude": "",
+        "exclude": black.DEFAULT_EXCLUDES,  # not shown in config dialog
+        "force-exclude": "",  # not shown in config dialog
+    }
+
+
+def compileRegExp(regexp):
+    """
+    Function to compile a given regular expression.
+    
+    @param regexp regular expression to be compiled
+    @type str
+    @return compiled regular expression object
+    @rtype re.Pattern
+    """
+    if "\n" in regexp:
+        # multi line regexp
+        regexp = f"(?x){regexp}"
+    compiled = re.compile(regexp)
+    return compiled
+
+
+def validateRegExp(regexp):
+    """
+    Function to validate a given regular expression.
+    
+    @param regexp regular expression to be validated
+    @type str
+    @return tuple containing a flag indicating validity and an error message
+    @rtype tuple of (bool, str)
+    """
+    if regexp:
+        try:
+            compileRegExp(regexp)
+            return True, ""
+        except re.error as e:
+            return (
+                False,
+                QCoreApplication.translate(
+                    "BlackUtilities",
+                    "Invalid regular expression: {0}"
+                ).format(str(e))
+            )
+        except IndexError:
+            return (
+                False,
+                QCoreApplication.translate(
+                    "BlackUtilities",
+                    "Invalid regular expression: missing group name"
+                )
+            )
+    else:
+        return (
+            False,
+            QCoreApplication.translate(
+                "BlackUtilities",
+                "A regular expression must be given."
+            )
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/CodeFormatting/__init__.py	Mon Jul 11 16:42:50 2022 +0200
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package implementing dialogs to perform some code formatting.
+
+The supported code formatting systems are these.
+<ul>
+<li><a href="https://github.com/psf/black">Black</a></li>
+</ul
+"""
--- a/src/eric7/JediInterface/AssistantJedi.py	Mon Jul 11 16:09:04 2022 +0200
+++ b/src/eric7/JediInterface/AssistantJedi.py	Mon Jul 11 16:42:50 2022 +0200
@@ -129,8 +129,7 @@
         if menu is not None:
             checkAction = editor.getMenu("Checks").menuAction()
             act = menu.insertMenu(checkAction, self.__menu)
-            sep = menu.insertSeparator(checkAction)
-            self.__menuActions[editor] = [act, sep]
+            self.__menuActions[editor] = [act]
         editor.showMenu.connect(self.__editorShowMenu)
     
     def __disconnectEditor(self, editor):
--- a/src/eric7/Project/Project.py	Mon Jul 11 16:09:04 2022 +0200
+++ b/src/eric7/Project/Project.py	Mon Jul 11 16:42:50 2022 +0200
@@ -52,6 +52,8 @@
 
 from Tasks.TasksFile import TasksFile
 
+from CodeFormatting.BlackFormattingAction import BlackFormattingAction
+
 
 class Project(QObject):
     """
@@ -2346,6 +2348,7 @@
             self.menuMakeAct.setEnabled(
                 self.pdata["MAKEPARAMS"]["MakeEnabled"])
             self.menuOtherToolsAct.setEnabled(True)
+            self.menuFormattingAct.setEnabled(True)
             
             self.projectAboutToBeCreated.emit()
             
@@ -3039,6 +3042,7 @@
                     self.menuMakeAct.setEnabled(
                         self.pdata["MAKEPARAMS"]["MakeEnabled"])
                     self.menuOtherToolsAct.setEnabled(True)
+                    self.menuFormattingAct.setEnabled(True)
                     
                     # open a project debugger properties file being quiet
                     # about errors
@@ -3296,6 +3300,7 @@
         self.makeGrp.setEnabled(False)
         self.menuMakeAct.setEnabled(False)
         self.menuOtherToolsAct.setEnabled(False)
+        self.menuFormattingAct.setEnabled(False)
         
         self.__model.projectClosed()
         self.projectClosedHooks.emit()
@@ -3505,10 +3510,14 @@
         pwl = ""
         if self.pdata["SPELLWORDS"]:
             pwl = os.path.join(self.ppath, self.pdata["SPELLWORDS"])
+            if not os.path.isfile(pwl):
+                pwl = ""
         
         pel = ""
         if self.pdata["SPELLEXCLUDES"]:
             pel = os.path.join(self.ppath, self.pdata["SPELLEXCLUDES"])
+            if not os.path.isfile(pel):
+                pel = ""
         
         return (pwl, pel)
         
@@ -3923,6 +3932,10 @@
         """
         self.actions = []
         
+        ###################################################################
+        ## Project actions
+        ###################################################################
+        
         self.actGrp1 = createActionGroup(self)
         
         act = EricAction(
@@ -3988,7 +4001,11 @@
         ))
         self.saveasAct.triggered.connect(self.saveProjectAs)
         self.actions.append(self.saveasAct)
-
+        
+        ###################################################################
+        ## Project management actions
+        ###################################################################
+        
         self.actGrp2 = createActionGroup(self)
         
         self.addFilesAct = EricAction(
@@ -4128,7 +4145,11 @@
         ))
         self.lexersAct.triggered.connect(self.__showLexerAssociations)
         self.actions.append(self.lexersAct)
-
+        
+        ###################################################################
+        ## Project debug actions
+        ###################################################################
+        
         self.dbgActGrp = createActionGroup(self)
         
         act = EricAction(
@@ -4193,8 +4214,12 @@
         act.triggered.connect(self.__initDebugProperties)
         self.actions.append(act)
         
+        ###################################################################
+        ## Project session actions
+        ###################################################################
+        
         self.sessActGrp = createActionGroup(self)
-
+        
         act = EricAction(
             self.tr('Load session'),
             self.tr('Load session'), 0, 0,
@@ -4243,6 +4268,10 @@
         act.triggered.connect(self.__deleteSession)
         self.actions.append(act)
         
+        ###################################################################
+        ## Project Tools - check actions
+        ###################################################################
+        
         self.chkGrp = createActionGroup(self)
 
         self.codeMetricsAct = EricAction(
@@ -4285,6 +4314,10 @@
         ))
         self.codeProfileAct.triggered.connect(self.__showProfileData)
         self.actions.append(self.codeProfileAct)
+        
+        ###################################################################
+        ## Project Tools - graphics actions
+        ###################################################################
 
         self.graphicsGrp = createActionGroup(self)
 
@@ -4314,9 +4347,13 @@
         ))
         self.loadDiagramAct.triggered.connect(self.__loadDiagram)
         self.actions.append(self.loadDiagramAct)
-
+        
+        ###################################################################
+        ## Project Tools - plugin packaging actions
+        ###################################################################
+        
         self.pluginGrp = createActionGroup(self)
-
+        
         self.pluginPkgListAct = EricAction(
             self.tr('Create Package List'),
             UI.PixmapCache.getIcon("pluginArchiveList"),
@@ -4368,6 +4405,10 @@
         self.pluginSArchiveAct.triggered.connect(
             self.__pluginCreateSnapshotArchives)
         self.actions.append(self.pluginSArchiveAct)
+        
+        ###################################################################
+        ## Project Tools - make actions
+        ###################################################################
 
         self.makeGrp = createActionGroup(self)
 
@@ -4400,10 +4441,16 @@
             lambda: self.__executeMake(questionOnly=True))
         self.actions.append(self.makeTestAct)
         
+        ###################################################################
+        ## Project Tools - other tools actions
+        ###################################################################
+        
+        self.othersGrp = createActionGroup(self)
+        
         self.createSBOMAct = EricAction(
             self.tr('Create SBOM File'),
             self.tr('Create &SBOM File'), 0, 0,
-            self, 'project_create_sbom')
+            self.othersGrp, 'project_create_sbom')
         self.createSBOMAct.setStatusTip(
             self.tr("Create a SBOM file of the project dependencies."))
         self.createSBOMAct.setWhatsThis(self.tr(
@@ -4414,7 +4461,96 @@
         ))
         self.createSBOMAct.triggered.connect(self.__createSBOMFile)
         self.actions.append(self.createSBOMAct)
-
+        
+        ###################################################################
+        ## Project Tools - code formatting actions
+        ###################################################################
+        
+        self.blackFormattingGrp = createActionGroup(self)
+        
+        self.blackAboutAct = EricAction(
+            self.tr("About Black"),
+            self.tr("&Black"),
+            0, 0,
+            self.blackFormattingGrp, "project_black_about"
+        )
+        self.blackAboutAct.setStatusTip(
+            self.tr("Show some information about 'Black'.")
+        )
+        self.blackAboutAct.setWhatsThis(self.tr(
+            "<b>Black</b>"
+            "<p>This shows some information about the installed 'Black' tool.</p>"
+        ))
+        self.blackAboutAct.triggered.connect(self.__aboutBlack)
+        self.actions.append(self.blackAboutAct)
+        font = self.blackAboutAct.font()
+        font.setBold(True)
+        self.blackAboutAct.setFont(font)
+        
+        self.blackFormatAct = EricAction(
+            self.tr("Format Code"),
+            self.tr("&Format Code"),
+            0, 0,
+            self.blackFormattingGrp, "project_black_format_code"
+        )
+        self.blackFormatAct.setStatusTip(
+            self.tr("Format the project sources with 'Black'.")
+        )
+        self.blackFormatAct.setWhatsThis(self.tr(
+            "<b>Format Code</b>"
+            "<p>This shows a dialog to enter parameters for the formatting run and"
+            " reformats the project sources using 'Black'.</p>"
+        ))
+        self.blackFormatAct.triggered.connect(
+            lambda: self.__performFormatWithBlack(BlackFormattingAction.Format)
+        )
+        self.actions.append(self.blackFormatAct)
+        
+        self.blackCheckFormattingAct = EricAction(
+            self.tr("Check Code Formatting"),
+            self.tr("&Check Code Formatting"),
+            0, 0,
+            self.blackFormattingGrp, "project_black_check_code"
+        )
+        self.blackCheckFormattingAct.setStatusTip(
+            self.tr(
+                "Check, if the project sources need to be reformatted with 'Black'."
+            )
+        )
+        self.blackCheckFormattingAct.setWhatsThis(self.tr(
+            "<b>Check Code Formatting</b>"
+            "<p>This shows a dialog to enter parameters for the format check run and"
+            " performs a check, if the project sources need to be reformatted using"
+            " 'Black'.</p>"
+        ))
+        self.blackCheckFormattingAct.triggered.connect(
+            lambda: self.__performFormatWithBlack(BlackFormattingAction.Check)
+        )
+        self.actions.append(self.blackCheckFormattingAct)
+        
+        self.blackDiffFormattingAct = EricAction(
+            self.tr("Code Formatting Diff"),
+            self.tr("Code Formatting &Diff"),
+            0, 0,
+            self.blackFormattingGrp, "project_black_diff_code"
+        )
+        self.blackDiffFormattingAct.setStatusTip(
+            self.tr(
+                "Generate a unified diff of potential project source reformatting"
+                " with 'Black'."
+            )
+        )
+        self.blackDiffFormattingAct.setWhatsThis(self.tr(
+            "<b>Diff Code Formatting</b>"
+            "<p>This shows a dialog to enter parameters for the format diff run and"
+            " generates a unified diff of potential project source reformatting using"
+            " 'Black'.</p>"
+        ))
+        self.blackDiffFormattingAct.triggered.connect(
+            lambda: self.__performFormatWithBlack(BlackFormattingAction.Diff)
+        )
+        self.actions.append(self.blackDiffFormattingAct)
+        
         self.closeAct.setEnabled(False)
         self.saveAct.setEnabled(False)
         self.saveasAct.setEnabled(False)
@@ -4446,6 +4582,8 @@
         self.vcsMenu.setEnabled(self.vcsSoftwareAvailable())
         self.checksMenu = QMenu(self.tr('Chec&k'), toolsMenu)
         self.checksMenu.setTearOffEnabled(True)
+        self.formattingMenu = QMenu(self.tr("Code &Formatting"), toolsMenu)
+        self.formattingMenu.setTearOffEnabled(True)
         self.menuShow = QMenu(self.tr('Sho&w'), toolsMenu)
         self.graphicsMenu = QMenu(self.tr('&Diagrams'), toolsMenu)
         self.packagersMenu = QMenu(self.tr('Pac&kagers'), toolsMenu)
@@ -4467,6 +4605,7 @@
             "Packagers": self.packagersMenu,
             "Make": self.makeMenu,
             "OtherTools": self.othersMenu,
+            "Formatting": self.formattingMenu,
         }
         
         # connect the aboutToShow signals
@@ -4482,6 +4621,7 @@
         self.debuggerMenu.aboutToShow.connect(self.__showContextMenuDebugger)
         self.makeMenu.aboutToShow.connect(self.__showContextMenuMake)
         self.othersMenu.aboutToShow.connect(self.__showContextMenuOthers)
+        self.formattingMenu.aboutToShow.connect(self.__showContextMenuFormat)
         menu.aboutToShow.connect(self.__showMenu)
         
         # build the show menu
@@ -4516,7 +4656,13 @@
         
         # build the 'Other Tools' menu
         self.othersMenu.setTearOffEnabled(True)
-        self.othersMenu.addAction(self.createSBOMAct)
+        self.othersMenu.addActions(self.othersGrp.actions())
+        self.othersMenu.addSeparator()
+        
+        # build the 'Code Formatting' menu
+        self.formattingMenu.setTearOffEnabled(True)
+        self.formattingMenu.addActions(self.blackFormattingGrp.actions())
+        self.formattingMenu.addSeparator()
         
         # build the project main menu
         menu.setTearOffEnabled(True)
@@ -4545,6 +4691,8 @@
         toolsMenu.addSeparator()
         self.menuCheckAct = toolsMenu.addMenu(self.checksMenu)
         toolsMenu.addSeparator()
+        self.menuFormattingAct = toolsMenu.addMenu(self.formattingMenu)
+        toolsMenu.addSeparator()
         self.menuMakeAct = toolsMenu.addMenu(self.makeMenu)
         toolsMenu.addSeparator()
         self.menuDiagramAct = toolsMenu.addMenu(self.graphicsMenu)
@@ -4566,6 +4714,7 @@
         self.menuPackagersAct.setEnabled(False)
         self.menuMakeAct.setEnabled(False)
         self.menuOtherToolsAct.setEnabled(False)
+        self.menuFormattingAct.setEnabled(False)
         
         self.__menu = menu
         self.__toolsMenu = toolsMenu
@@ -5956,7 +6105,7 @@
     #########################################################################
     ## Below are methods implementing the 'SBOM' support
     #########################################################################
-     
+    
     def __showContextMenuOthers(self):
         """
         Private slot called before the 'Other Tools' menu is shown.
@@ -5971,6 +6120,63 @@
         import CycloneDXInterface
         
         CycloneDXInterface.createCycloneDXFile("<project>")
+    
+    #########################################################################
+    ## Below are methods implementing the 'Code Formatting' support
+    #########################################################################
+    
+    def __showContextMenuFormat(self):
+        """
+        Private slot called before the 'Code Formatting' menu is shown.
+        """
+        self.showMenu.emit("Formatting", self.othersMenu)
+    
+    @pyqtSlot()
+    def __aboutBlack(self):
+        """
+        Private slot to show some information about the installed 'Black' tool.
+        """
+        import black
+        
+        EricMessageBox.information(
+            None,
+            self.tr("About Black"),
+            self.tr("""<p><b>Black Version {0}</b></p>"""
+                    """<p><i>Black</i> is the uncompromising Python code"""
+                    """ formatter.</p>""").format(black.__version__)
+        )
+    
+    def __performFormatWithBlack(self, action):
+        """
+        Private method to format the project sources using the 'Black' tool.
+        
+        Following actions are supported.
+        <ul>
+        <li>BlackFormattingAction.Format - the code reformatting is performed</li>
+        <li>BlackFormattingAction.Check - a check is performed, if code formatting
+            is necessary</li>
+        <li>BlackFormattingAction.Diff - a unified diff of potential code formatting
+            changes is generated</li>
+        </ul>
+        
+        @param action formatting operation to be performed
+        @type BlackFormattingAction
+        """
+        from CodeFormatting.BlackConfigurationDialog import BlackConfigurationDialog
+        from CodeFormatting.BlackFormattingDialog import BlackFormattingDialog
+        
+        if ericApp().getObject("ViewManager").checkAllDirty():
+            dlg = BlackConfigurationDialog(withProject=True)
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                config = dlg.getConfiguration()
+                
+                formattingDialog = BlackFormattingDialog(
+                    config,
+                    self.getProjectFiles("SOURCES", normalized=True),
+                    project=self,
+                    action=action
+                )
+                formattingDialog.exec()
 
 #
 # eflag: noqa = M601
--- a/src/eric7/Project/ProjectSourcesBrowser.py	Mon Jul 11 16:09:04 2022 +0200
+++ b/src/eric7/Project/ProjectSourcesBrowser.py	Mon Jul 11 16:42:50 2022 +0200
@@ -30,6 +30,8 @@
 import Utilities
 import UI.PixmapCache
 
+from CodeFormatting.BlackFormattingAction import BlackFormattingAction
+
 
 class ProjectSourcesBrowser(ProjectBaseBrowser):
     """
@@ -118,6 +120,21 @@
         self.checksMenu = QMenu(self.tr('Check'))
         self.checksMenu.aboutToShow.connect(self.__showContextMenuCheck)
         
+        self.formattingMenu = QMenu(self.tr("Code Formatting"))
+        self.formattingMenu.addAction(
+            self.tr("Format Code"),
+            lambda: self.__performFormatWithBlack(BlackFormattingAction.Format)
+        )
+        self.formattingMenu.addAction(
+            self.tr("Check Formatting"),
+            lambda: self.__performFormatWithBlack(BlackFormattingAction.Check)
+        )
+        self.formattingMenu.addAction(
+            self.tr("Formatting Diff"),
+            lambda: self.__performFormatWithBlack(BlackFormattingAction.Diff)
+        )
+        self.formattingMenu.aboutToShow.connect(self.__showContextMenuFormatting)
+        
         self.menuShow = QMenu(self.tr('Show'))
         self.menuShow.addAction(
             self.tr('Code metrics...'), self.__showCodeMetrics)
@@ -181,10 +198,9 @@
         self.sourceMenu.addAction(
             self.tr('Add source directory...'), self.__addSourceDirectory)
         self.sourceMenu.addSeparator()
-        act = self.sourceMenu.addMenu(self.graphicsMenu)
-        self.sourceMenu.addSeparator()
+        self.sourceMenu.addMenu(self.graphicsMenu)
         self.sourceMenu.addMenu(self.checksMenu)
-        self.sourceMenu.addSeparator()
+        self.sourceMenu.addMenu(self.formattingMenu)
         self.sourceMenuActions["Show"] = self.sourceMenu.addMenu(self.menuShow)
         self.sourceMenu.addSeparator()
         self.__startAct = self.sourceMenu.addMenu(self.__startMenu)
@@ -262,6 +278,7 @@
         self.multiMenuActions.append(act)
         self.multiMenu.addSeparator()
         self.multiMenu.addMenu(self.checksMenu)
+        self.multiMenu.addMenu(self.formattingMenu)
         self.multiMenu.addSeparator()
         self.multiMenu.addAction(
             self.tr('Expand all directories'), self._expandAllDirs)
@@ -286,7 +303,6 @@
             self.tr('Add source directory...'), self.__addSourceDirectory)
         self.dirMenu.addSeparator()
         act = self.dirMenu.addMenu(self.graphicsMenu)
-        self.dirMenu.addSeparator()
         self.dirMenu.addMenu(self.checksMenu)
         self.dirMenu.addSeparator()
         self.dirMenu.addAction(
@@ -1175,3 +1191,54 @@
         """
         fn = self.model().item(self.currentIndex()).fileName()
         ericApp().getObject("DebugUI").doCoverage(False, script=fn)
+    
+    ###########################################################################
+    ## Methods for the Code Formatting submenu
+    ###########################################################################
+    
+    def __showContextMenuFormatting(self):
+        """
+        Private slot called before the Code Formatting menu is shown.
+        """
+        self.showMenu.emit("Formatting", self.formattingMenu)
+    
+    def __performFormatWithBlack(self, action):
+        """
+        Private method to format the selected project sources using the 'Black' tool.
+        
+        Following actions are supported.
+        <ul>
+        <li>BlackFormattingAction.Format - the code reformatting is performed</li>
+        <li>BlackFormattingAction.Check - a check is performed, if code formatting
+            is necessary</li>
+        <li>BlackFormattingAction.Diff - a unified diff of potential code formatting
+            changes is generated</li>
+        </ul>
+        
+        @param action formatting operation to be performed
+        @type BlackFormattingAction
+        """
+        from CodeFormatting.BlackConfigurationDialog import BlackConfigurationDialog
+        from CodeFormatting.BlackFormattingDialog import BlackFormattingDialog
+        
+        files = [
+            itm.fileName()
+            for itm in self.getSelectedItems(
+                [BrowserFileItem, BrowserClassItem, BrowserMethodItem,
+                 BrowserClassAttributeItem, BrowserImportItem]
+            )
+            if itm.isPython3File()
+        ]
+        
+        if ericApp().getObject("ViewManager").checkAllDirty():
+            dlg = BlackConfigurationDialog(withProject=True)
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                config = dlg.getConfiguration()
+                
+                formattingDialog = BlackFormattingDialog(
+                    config,
+                    files,
+                    project=self.project,
+                    action=action
+                )
+                formattingDialog.exec()
--- a/src/eric7/QScintilla/Editor.py	Mon Jul 11 16:09:04 2022 +0200
+++ b/src/eric7/QScintilla/Editor.py	Mon Jul 11 16:42:50 2022 +0200
@@ -48,6 +48,8 @@
 
 from UI import PythonDisViewer
 
+from CodeFormatting.BlackFormattingAction import BlackFormattingAction
+
 EditorAutoCompletionListID = 1
 TemplateCompletionListID = 2
 ReferencesListID = 3
@@ -816,10 +818,12 @@
             self.menuShow = self.__initContextMenuShow()
             self.graphicsMenu = self.__initContextMenuGraphics()
             self.autocompletionMenu = self.__initContextMenuAutocompletion()
+            self.codeFormattingMenu = self.__initContextMenuFormatting()
             self.__menus["Checks"] = self.checksMenu
             self.__menus["Show"] = self.menuShow
             self.__menus["Graphics"] = self.graphicsMenu
             self.__menus["Autocompletion"] = self.autocompletionMenu
+            self.__menus["Formatting"] = self.codeFormattingMenu
         self.toolsMenu = self.__initContextMenuTools()
         self.__menus["Tools"] = self.toolsMenu
         self.eolMenu = self.__initContextMenuEol()
@@ -917,9 +921,8 @@
             self.menu.addMenu(self.resourcesMenu)
         else:
             self.menuActs["Check"] = self.menu.addMenu(self.checksMenu)
-            self.menu.addSeparator()
+            self.menuActs["Formatting"] = self.menu.addMenu(self.codeFormattingMenu)
             self.menuActs["Show"] = self.menu.addMenu(self.menuShow)
-            self.menu.addSeparator()
             self.menuActs["Diagrams"] = self.menu.addMenu(self.graphicsMenu)
         self.menu.addSeparator()
         self.menuActs["Tools"] = self.menu.addMenu(self.toolsMenu)
@@ -992,6 +995,32 @@
         menu.aboutToShow.connect(self.__showContextMenuChecks)
         return menu
 
+    def __initContextMenuFormatting(self):
+        """
+        Private method used to setup the Code Formatting context sub menu.
+        
+        @return reference to the generated menu
+        @rtype QMenu
+        """
+        menu = QMenu(self.tr("Code Formatting"))
+        
+        menu.addAction(
+            self.tr("Format Code"),
+            lambda: self.__performFormatWithBlack(BlackFormattingAction.Format)
+        )
+        menu.addAction(
+            self.tr("Check Formatting"),
+            lambda: self.__performFormatWithBlack(BlackFormattingAction.Check)
+        )
+        menu.addAction(
+            self.tr("Formatting Diff"),
+            lambda: self.__performFormatWithBlack(BlackFormattingAction.Diff)
+        )
+        
+        menu.aboutToShow.connect(self.__showContextMenuFormatting)
+        
+        return menu
+    
     def __initContextMenuTools(self):
         """
         Private method used to setup the Tools context sub menu.
@@ -5799,7 +5828,14 @@
         menu.
         """
         self.showMenu.emit("Tools", self.toolsMenu, self)
-        
+    
+    def __showContextMenuFormatting(self):
+        """
+        Private slot handling the aboutToShow signal of the code formatting context
+        menu.
+        """
+        self.showMenu.emit("Formatting", self.codeFormattingMenu, self)
+    
     def __reopenWithEncodingMenuTriggered(self, act):
         """
         Private method to handle the rereading of the file with a selected
@@ -8874,7 +8910,12 @@
         @param y y-value of mouse screen position
         @type int
         """
-        if not self.isCallTipActive() and not self.isListActive():
+        if (
+            not self.isCallTipActive()
+            and not self.isListActive()
+            and not self.menu.isVisible()
+            and not self.spellingMenu.isVisible()
+        ):
             if self.__mouseHoverHelp is not None and pos > 0 and y > 0:
                 line, index = self.lineIndexFromPosition(pos)
                 if index > 0:
@@ -8934,3 +8975,44 @@
             self.__showingMouseHoverHelp = True
         else:
             self.__cancelMouseHoverHelp()
+    
+    #######################################################################
+    ## Methods implementing the Black code formatting interface
+    #######################################################################
+    
+    def __performFormatWithBlack(self, action):
+        """
+        Private method to format the source code using the 'Black' tool.
+        
+        Following actions are supported.
+        <ul>
+        <li>BlackFormattingAction.Format - the code reformatting is performed</li>
+        <li>BlackFormattingAction.Check - a check is performed, if code formatting
+            is necessary</li>
+        <li>BlackFormattingAction.Diff - a unified diff of potential code formatting
+            changes is generated</li>
+        </ul>
+        
+        @param action formatting operation to be performed
+        @type BlackFormattingAction
+        """
+        from CodeFormatting.BlackConfigurationDialog import BlackConfigurationDialog
+        from CodeFormatting.BlackFormattingDialog import BlackFormattingDialog
+        
+        if not self.isModified() or self.saveFile():
+            withProject = (
+                self.fileName and
+                self.project.isOpen() and
+                self.project.isProjectSource(self.fileName)
+            )
+            dlg = BlackConfigurationDialog(withProject=withProject)
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                config = dlg.getConfiguration()
+                
+                formattingDialog = BlackFormattingDialog(
+                    config,
+                    [self.fileName],
+                    project=self.project,
+                    action=action
+                )
+                formattingDialog.exec()
--- a/src/eric7/Utilities/__init__.py	Mon Jul 11 16:09:04 2022 +0200
+++ b/src/eric7/Utilities/__init__.py	Mon Jul 11 16:42:50 2022 +0200
@@ -15,6 +15,7 @@
 import getpass
 import glob
 import os
+import pathlib
 import re
 import shlex
 import subprocess           # secok
@@ -720,20 +721,24 @@
     """
     Function returning a path, that is using native separator characters.
     
-    @param path path to be converted (string)
-    @return path with converted separator characters (string)
+    @param path path to be converted
+    @type str
+    @return path with converted separator characters
+    @rtype str
     """
-    return QDir.toNativeSeparators(path)
+    return str(pathlib.PurePath(path))
 
 
 def fromNativeSeparators(path):
     """
     Function returning a path, that is using "/" separator characters.
     
-    @param path path to be converted (string)
-    @return path with converted separator characters (string)
+    @param path path to be converted
+    @type str
+    @return path with converted separator characters
+    @rtype str
     """
-    return QDir.fromNativeSeparators(path)
+    return pathlib.PurePath(path).as_posix()
 
 
 def normcasepath(path):
Binary file src/eric7/icons/oxygen/jedi.png has changed

eric ide

mercurial