Mon, 31 Oct 2022 15:29:18 +0100
Code Formatting
- added an interface to resort the import statements of Python source files with the 'isort' utility
--- a/docs/changelog.md Mon Oct 31 14:09:07 2022 +0100 +++ b/docs/changelog.md Mon Oct 31 15:29:18 2022 +0100 @@ -2,6 +2,9 @@ ### Version 22.12 - bug fixes +- Code Formatting + - added an interface to resort the import statements of Python source files with + the 'isort' utility - Previewers - added a button to copy the contents of the HTML previewer to the clipboard
--- a/eric7.epj Mon Oct 31 14:09:07 2022 +0100 +++ b/eric7.epj Mon Oct 31 15:29:18 2022 +0100 @@ -274,8 +274,10 @@ }, "FORMS": [ "src/eric7/CodeFormatting/BlackConfigurationDialog.ui", - "src/eric7/CodeFormatting/BlackDiffWidget.ui", "src/eric7/CodeFormatting/BlackFormattingDialog.ui", + "src/eric7/CodeFormatting/FormattingDiffWidget.ui", + "src/eric7/CodeFormatting/IsortConfigurationDialog.ui", + "src/eric7/CodeFormatting/IsortFormattingDialog.ui", "src/eric7/CondaInterface/CondaExecDialog.ui", "src/eric7/CondaInterface/CondaExportDialog.ui", "src/eric7/CondaInterface/CondaInfoDialog.ui", @@ -994,6 +996,32 @@ }, "RadonCodeMetrics": { "ExcludeFiles": "*/ThirdParty/*, */coverage/*, Ui_*.py, *_rc.py" + }, + "isort": { + "config_source": "project", + "extend_skip_glob": [ + "*/Examples/*", + "*/ThirdParty/*", + "*/coverage/*", + "*/Ui_*.py", + "*/pycodestyle.py", + "*/pyflakes/checker.py", + "*/mccabe.py", + "*/eradicate.py", + "*/ast_unparse.py", + "*/piplicenses.py", + "*/pipdeptree.py" + ], + "lines_between_types": 1, + "profile": "black", + "sort_order": "natural", + "supported_extensions": [ + "py", + "pyi", + "pyx", + "pxd", + "pyw" + ] } }, "PACKAGERSPARMS": {}, @@ -1023,10 +1051,14 @@ "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/FormattingDiffWidget.py", + "src/eric7/CodeFormatting/IsortConfigurationDialog.py", + "src/eric7/CodeFormatting/IsortFormattingAction.py", + "src/eric7/CodeFormatting/IsortFormattingDialog.py", + "src/eric7/CodeFormatting/IsortUtilities.py", "src/eric7/CodeFormatting/__init__.py", "src/eric7/CondaInterface/Conda.py", "src/eric7/CondaInterface/CondaExecDialog.py",
--- a/pyproject.toml Mon Oct 31 14:09:07 2022 +0100 +++ b/pyproject.toml Mon Oct 31 15:29:18 2022 +0100 @@ -69,6 +69,7 @@ "cyclonedx-bom", "trove-classifiers", "black>=22.6.0", + "isort>=5.10.0", "pywin32>=1.0;platform_system=='Windows'", ] dynamic = ["version"]
--- a/scripts/install.py Mon Oct 31 14:09:07 2022 +0100 +++ b/scripts/install.py Mon Oct 31 15:29:18 2022 +0100 @@ -1698,6 +1698,7 @@ "cyclonedx-bom": ("cyclonedx_py", ""), "trove-classifiers": ("trove_classifiers", ""), "black": ("black", ">=22.6.0"), + "isort": ("isort", ">=5.10.0"), } optionalModulesList = { # key is pip project name
--- a/src/eric7/CodeFormatting/BlackConfigurationDialog.ui Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/CodeFormatting/BlackConfigurationDialog.ui Mon Oct 31 15:29:18 2022 +0100 @@ -194,8 +194,8 @@ <slot>accept()</slot> <hints> <hint type="sourcelabel"> - <x>248</x> - <y>254</y> + <x>257</x> + <y>490</y> </hint> <hint type="destinationlabel"> <x>157</x> @@ -210,8 +210,8 @@ <slot>reject()</slot> <hints> <hint type="sourcelabel"> - <x>316</x> - <y>260</y> + <x>325</x> + <y>490</y> </hint> <hint type="destinationlabel"> <x>286</x>
--- a/src/eric7/CodeFormatting/BlackDiffWidget.py Mon Oct 31 14:09:07 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -# -*- 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 eric7.UI.DiffHighlighter import DiffHighlighter - -from eric7 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()
--- a/src/eric7/CodeFormatting/BlackDiffWidget.ui Mon Oct 31 14:09:07 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -<?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>
--- a/src/eric7/CodeFormatting/BlackFormattingAction.py Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/CodeFormatting/BlackFormattingAction.py Mon Oct 31 15:29:18 2022 +0100 @@ -4,7 +4,7 @@ # """ -Module implementing an enum defining the various code formatting actions. +Module implementing an enum defining the various Black code formatting actions. """ import enum @@ -12,7 +12,7 @@ class BlackFormattingAction(enum.Enum): """ - Class defining the various code formatting actions. + Class defining the various Black code formatting actions. """ Format = 0
--- a/src/eric7/CodeFormatting/BlackFormattingDialog.py Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/CodeFormatting/BlackFormattingDialog.py Mon Oct 31 15:29:18 2022 +0100 @@ -4,7 +4,7 @@ # """ -Module implementing a dialog showing the code formatting progress and the result. +Module implementing a dialog showing the Black code formatting progress and the results. """ import copy @@ -30,7 +30,7 @@ from .Ui_BlackFormattingDialog import Ui_BlackFormattingDialog from . import BlackUtilities -from .BlackDiffWidget import BlackDiffWidget +from .FormattingDiffWidget import FormattingDiffWidget from .BlackFormattingAction import BlackFormattingAction from eric7 import Preferences, Utilities @@ -38,7 +38,8 @@ class BlackFormattingDialog(QDialog, Ui_BlackFormattingDialog): """ - Class implementing a dialog showing the code formatting progress and the result. + Class implementing a dialog showing the Black code formatting progress and the + results. """ DataTypeRole = Qt.ItemDataRole.UserRole @@ -98,7 +99,7 @@ def __performAction(self): """ - Private method to exceute the requested formatting action. + Private method to execute the requested formatting action. """ self.progressBar.setValue(0) self.progressBar.setVisible(True) @@ -277,7 +278,7 @@ ) elif dataType == "diff": if self.__diffDialog is None: - self.__diffDialog = BlackDiffWidget() + self.__diffDialog = FormattingDiffWidget() self.__diffDialog.showDiff(item.data(0, BlackFormattingDialog.DataRole)) def __formatManyFiles(self, files): @@ -520,8 +521,8 @@ """ Private slot to handle the result of a black reformatting action. - @param status status of the performed action (one of 'changed', 'unchanged', - 'unmodified', 'failed' or 'ignored') + @param status status of the performed action (one of 'changed', 'failed', + 'ignored', 'unchanged' or 'unmodified') @type str @param filename name of the processed file @type str
--- a/src/eric7/CodeFormatting/BlackFormattingDialog.ui Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/CodeFormatting/BlackFormattingDialog.ui Mon Oct 31 15:29:18 2022 +0100 @@ -240,8 +240,8 @@ </layout> </widget> <tabstops> + <tabstop>resultsList</tabstop> <tabstop>statusFilterComboBox</tabstop> - <tabstop>resultsList</tabstop> </tabstops> <resources/> <connections/>
--- a/src/eric7/CodeFormatting/BlackUtilities.py Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/CodeFormatting/BlackUtilities.py Mon Oct 31 15:29:18 2022 +0100 @@ -98,6 +98,6 @@ "BlackUtilities", """<p><b>Black Version {0}</b></p>""" """<p><i>Black</i> is the uncompromising Python code""" - """ formatter.</p>""" + """ formatter.</p>""", ).format(black.__version__), )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/CodeFormatting/FormattingDiffWidget.py Mon Oct 31 15:29:18 2022 +0100 @@ -0,0 +1,67 @@ +# -*- 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_FormattingDiffWidget import Ui_FormattingDiffWidget + +from eric7.UI.DiffHighlighter import DiffHighlighter + +from eric7 import Preferences + + +class FormattingDiffWidget(QWidget, Ui_FormattingDiffWidget): + """ + 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/FormattingDiffWidget.ui Mon Oct 31 15:29:18 2022 +0100 @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FormattingDiffWidget</class> + <widget class="QWidget" name="FormattingDiffWidget"> + <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>FormattingDiffWidget</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/IsortConfigurationDialog.py Mon Oct 31 15:29:18 2022 +0100 @@ -0,0 +1,427 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to enter the parameters for an isort formatting run. +""" + +import contextlib +import copy +import pathlib + +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtGui import QGuiApplication +from PyQt6.QtWidgets import QDialog, QDialogButtonBox + +from eric7.EricWidgets import EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp + +from isort import Config +from isort.profiles import profiles +from isort.settings import VALID_PY_TARGETS +from isort.wrap_modes import WrapModes + +import tomlkit + + +from .Ui_IsortConfigurationDialog import Ui_IsortConfigurationDialog + + +class IsortConfigurationDialog(QDialog, Ui_IsortConfigurationDialog): + """ + Class implementing a dialog to enter the parameters for an isort formatting run. + """ + + def __init__(self, withProject=True, onlyProject=False, parent=None): + """ + Constructor + + @param withProject flag indicating to look for project configurations + (defaults to True) + @type bool (optional) + @param onlyProject flag indicating to only look for project configurations + (defaults to False) + @type bool (optional) + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + self.profileComboBox.lineEdit().setClearButtonEnabled(True) + + self.__parameterWidgetMapping = { + "profile": self.profileComboBox, + "py_version": self.pythonComboBox, + "multi_line_output": self.multiLineComboBox, + "sort_order": self.sortOrderComboBox, + "supported_extensions": self.extensionsEdit, + "line_length": self.lineLengthSpinBox, + "lines_before_imports": self.linesBeforeImportsSpinBox, + "lines_after_imports": self.linesAfterImportsSpinBox, + "lines_between_sections": self.linesBetweenSectionsSpinBox, + "lines_between_types": self.linesBetweenTypesSpinBox, + "include_trailing_comma": self.trailingCommaCheckBox, + "use_parentheses": self.parenthesesCheckBox, + "sections": self.sectionsEdit, + "extend_skip_glob": self.excludeEdit, + "case_sensitive": self.sortCaseSensitiveCheckBox, + } + + self.__project = ( + ericApp().getObject("Project") if (withProject or onlyProject) else None + ) + self.__onlyProject = onlyProject + + self.__pyprojectData = {} + self.__projectData = {} + + self.__defaultConfig = Config() + + 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) + + self.profileComboBox.addItem("") + self.profileComboBox.addItems(sorted(profiles.keys())) + + self.pythonComboBox.addItem("", "") + self.pythonComboBox.addItem(self.tr("All Versions"), "all") + for pyTarget in VALID_PY_TARGETS: + if pyTarget.startswith("3"): + self.pythonComboBox.addItem( + self.tr("Python {0}").format(pyTarget) + if len(pyTarget) == 1 + else self.tr("Python {0}.{1}").format(pyTarget[0], pyTarget[1:]), + pyTarget, + ) + + self.sortOrderComboBox.addItem("", "") + self.sortOrderComboBox.addItem("Natural", "natural") + self.sortOrderComboBox.addItem("Native Python", "native") + + self.__populateMultiLineComboBox() + + # 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("isort", {}) + if config: + self.__pyprojectData = { + k.replace("--", ""): v for k, v in config.items() + } + self.sourceComboBox.addItem("pyproject.toml", "pyproject") + if self.__project.getData("OTHERTOOLSPARMS", "isort") is not None: + self.__projectData = copy.deepcopy( + self.__project.getData("OTHERTOOLSPARMS", "isort") + ) + self.sourceComboBox.addItem(self.tr("Project File"), "project") + elif onlyProject: + self.sourceComboBox.addItem(self.tr("Project File"), "project") + if not onlyProject: + self.sourceComboBox.addItem(self.tr("Defaults"), "default") + self.sourceComboBox.addItem(self.tr("Configuration Below"), "dialog") + + if self.__projectData: + source = self.__projectData.get("config_source", "") + self.sourceComboBox.setCurrentIndex(self.sourceComboBox.findData(source)) + elif onlyProject: + self.sourceComboBox.setCurrentIndex(self.sourceComboBox.findData("project")) + + def __populateMultiLineComboBox(self): + """ + Private method to populate the multi line output selector. + """ + self.multiLineComboBox.addItem("", -1) + for entry, wrapMode in ( + (self.tr("Grid"), WrapModes.GRID), + (self.tr("Vertical"), WrapModes.VERTICAL), + (self.tr("Hanging Indent"), WrapModes.HANGING_INDENT), + ( + self.tr("Vertical Hanging Indent"), + WrapModes.VERTICAL_HANGING_INDENT, + ), + (self.tr("Hanging Grid"), WrapModes.VERTICAL_GRID), + (self.tr("Hanging Grid Grouped"), WrapModes.VERTICAL_GRID_GROUPED), + (self.tr("NOQA"), WrapModes.NOQA), + ( + self.tr("Vertical Hanging Indent Bracket"), + WrapModes.VERTICAL_HANGING_INDENT_BRACKET, + ), + ( + self.tr("Vertical Prefix From Module Import"), + WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, + ), + ( + self.tr("Hanging Indent With Parentheses"), + WrapModes.HANGING_INDENT_WITH_PARENTHESES, + ), + (self.tr("Backslash Grid"), WrapModes.BACKSLASH_GRID), + ): + self.multiLineComboBox.addItem(entry, wrapMode.value) + + def __loadConfiguration(self, confDict): + """ + Private method to load the configuration section with data of the given + dictionary. + + Note: Default values will be loaded for missing parameters. + + @param confDict reference to the data to be loaded + @type dict + """ + self.pythonComboBox.setCurrentIndex( + self.pythonComboBox.findData( + str(confDict["py_version"]) + if "py_version" in confDict + else self.__defaultConfig.py_version.replace("py", "") + ) + ) + self.multiLineComboBox.setCurrentIndex( + self.multiLineComboBox.findData( + int(confDict["multi_line_output"]) + if "multi_line_output" in confDict + else self.__defaultConfig.multi_line_output.value + ) + ) + self.sortOrderComboBox.setCurrentIndex( + self.sortOrderComboBox.findData( + str(confDict["sort_order"]) + if "sort_order" in confDict + else self.__defaultConfig.sort_order + ) + ) + self.extensionsEdit.setText( + " ".join( + confDict["supported_extensions"] + if "supported_extensions" in confDict + else self.__defaultConfig.supported_extensions + ) + ) + for parameter in ( + "line_length", + "lines_before_imports", + "lines_after_imports", + "lines_between_sections", + "lines_between_types", + ): + # set spin box values + self.__parameterWidgetMapping[parameter].setValue( + confDict[parameter] + if parameter in confDict + else getattr(self.__defaultConfig, parameter) + ) + for parameter in ( + "include_trailing_comma", + "use_parentheses", + "case_sensitive", + ): + # set check box values + self.__parameterWidgetMapping[parameter].setChecked( + confDict[parameter] + if parameter in confDict + else getattr(self.__defaultConfig, parameter) + ) + for parameter in ( + "sections", + "extend_skip_glob", + ): + # set the plain text edits + self.__parameterWidgetMapping[parameter].setPlainText( + "\n".join( + confDict[parameter] + if parameter in confDict + else getattr(self.__defaultConfig, parameter) + ) + ) + # set the profile combo box last because it may change other entries + self.profileComboBox.setEditText( + confDict["profile"] + if "profile" in confDict + else self.__defaultConfig.profile + ) + + @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) or self.__onlyProject + ) + + source = self.sourceComboBox.currentData() + if source != "dialog": + # reset the profile combo box first + self.profileComboBox.setCurrentIndex(0) + + if source == "pyproject": + self.__loadConfiguration(self.__pyprojectData) + elif source == "project": + self.__loadConfiguration(self.__projectData) + elif source == "default": + self.__loadConfiguration({}) # loads the default values + elif source == "dialog": + # just leave the current entries + pass + + @pyqtSlot(str) + def on_profileComboBox_editTextChanged(self, profileName): + """ + Private slot to react upon changes of the selected/entered profile. + + @param profileName name of the current profile + @type str + """ + if profileName and profileName in profiles: + confDict = self.__getConfigurationDict() + confDict["profile"] = profileName + confDict.update(profiles[profileName]) + self.__loadConfiguration(confDict) + + for parameter in self.__parameterWidgetMapping: + self.__parameterWidgetMapping[parameter].setEnabled( + parameter not in profiles[profileName] + ) + else: + for widget in self.__parameterWidgetMapping.values(): + widget.setEnabled(True) + + @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. + """ + configDict = self.__getConfigurationDict() + + isort = tomlkit.table() + for key, value in configDict.items(): + isort[key] = value + + doc = tomlkit.document() + doc["tool"] = tomlkit.table(is_super_table=True) + doc["tool"]["isort"] = isort + + QGuiApplication.clipboard().setText(tomlkit.dumps(doc)) + + EricMessageBox.information( + self, + self.tr("Create TOML snippet"), + self.tr( + """The 'pyproject.toml' snippet was copied to the clipboard""" + """ successfully.""" + ), + ) + + def __getConfigurationDict(self): + """ + Private method to assemble and return a dictionary containing the entered + non-default configuration parameters. + + @return dictionary containing the non-default configuration parameters + @rtype dict + """ + configDict = {} + + if self.profileComboBox.currentText(): + configDict["profile"] = self.profileComboBox.currentText() + if ( + self.pythonComboBox.currentText() + and self.pythonComboBox.currentData() + != self.__defaultConfig.py_version.replace("py", "") + ): + configDict["py_version"] = self.pythonComboBox.currentData() + if self.multiLineComboBox.isEnabled() and self.multiLineComboBox.currentText(): + configDict["multi_line_output"] = self.multiLineComboBox.currentData() + if self.sortOrderComboBox.isEnabled() and self.sortOrderComboBox.currentText(): + configDict["sort_order"] = self.sortOrderComboBox.currentData() + if self.extensionsEdit.isEnabled() and self.extensionsEdit.text(): + configDict["supported_extensions"] = [ + e.lstrip(".") + for e in self.extensionsEdit.text().strip().split() + if e.lstrip(".") + ] + + for parameter in ( + "line_length", + "lines_before_imports", + "lines_after_imports", + "lines_between_sections", + "lines_between_types", + ): + if self.__parameterWidgetMapping[ + parameter + ].isEnabled() and self.__parameterWidgetMapping[ + parameter + ].value() != getattr( + self.__defaultConfig, parameter + ): + configDict[parameter] = self.__parameterWidgetMapping[parameter].value() + + for parameter in ( + "include_trailing_comma", + "use_parentheses", + "case_sensitive", + ): + if self.__parameterWidgetMapping[ + parameter + ].isEnabled() and self.__parameterWidgetMapping[ + parameter + ].isChecked() != getattr( + self.__defaultConfig, parameter + ): + configDict[parameter] = self.__parameterWidgetMapping[ + parameter + ].isChecked() + + for parameter in ( + "sections", + "extend_skip_glob", + ): + if self.__parameterWidgetMapping[parameter].isEnabled(): + value = ( + self.__parameterWidgetMapping[parameter].toPlainText().splitlines() + ) + if value != list(getattr(self.__defaultConfig, parameter)): + configDict[parameter] = value + + return configDict + + def getConfiguration(self, saveToProject=False): + """ + Public method to get the current configuration parameters. + + @param saveToProject flag indicating to save the configuration data in the + project file (defaults to False) + @type bool (optional) + @return dictionary containing the configuration parameters + @rtype dict + """ + configuration = self.__getConfigurationDict() + + if saveToProject and self.__project: + configuration["config_source"] = self.sourceComboBox.currentData() + self.__project.setData("OTHERTOOLSPARMS", "isort", configuration) + + return configuration
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/CodeFormatting/IsortConfigurationDialog.ui Mon Oct 31 15:29:18 2022 +0100 @@ -0,0 +1,479 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>IsortConfigurationDialog</class> + <widget class="QDialog" name="IsortConfigurationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>700</height> + </rect> + </property> + <property name="windowTitle"> + <string>isort 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="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> + <item row="1" column="0" colspan="2"> + <widget class="QGroupBox" name="configurationGroup"> + <property name="title"> + <string>Configuration</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Profile:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="profileComboBox"> + <property name="toolTip"> + <string>Enter a profile to be used (empty for none) (see isort documentation).</string> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="label_13"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string><a href="https://pycqa.github.io/isort/docs/configuration/profiles.html">Built-In Profiles</a></string> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Target Version:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="pythonComboBox"> + <property name="toolTip"> + <string>Select the target Python version.</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Multi Line Output:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QComboBox" name="multiLineComboBox"> + <property name="toolTip"> + <string>Select the type of multi line import statements (see isort documentation).</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QLabel" name="label_12"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string><a href="https://pycqa.github.io/isort/docs/configuration/multi_line_output_modes.html">Defined Multi Line Output Modes</a></string> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_14"> + <property name="text"> + <string>Sort Order:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QComboBox" name="sortOrderComboBox"> + <property name="toolTip"> + <string>Select the sort order (empty for default).</string> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string><a href="https://pycqa.github.io/isort/docs/configuration/options.html#sort-order">Defined Sort Orders</a></string> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="4" column="0" colspan="3"> + <widget class="QCheckBox" name="sortCaseSensitiveCheckBox"> + <property name="toolTip"> + <string>Select to sort imports observing the case.</string> + </property> + <property name="text"> + <string>Sort Case Sensitively</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_16"> + <property name="text"> + <string>Supported Extensions:</string> + </property> + </widget> + </item> + <item row="5" column="1" colspan="2"> + <widget class="QLineEdit" name="extensionsEdit"> + <property name="toolTip"> + <string>Enter the supported extensions separated by space (empty for default).</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Line Length:</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QSpinBox" name="lineLengthSpinBox"> + <property name="toolTip"> + <string>Enter the allowed maximum line length.</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>70</number> + </property> + <property name="maximum"> + <number>120</number> + </property> + <property name="value"> + <number>79</number> + </property> + </widget> + </item> + <item row="0" column="3"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>55</width> + <height>17</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Lines Before Imports:</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QSpinBox" name="linesBeforeImportsSpinBox"> + <property name="toolTip"> + <string>Enter the number of lines before import statements.</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="specialValueText"> + <string>automatic</string> + </property> + <property name="minimum"> + <number>-1</number> + </property> + <property name="maximum"> + <number>5</number> + </property> + <property name="value"> + <number>-1</number> + </property> + </widget> + </item> + <item row="1" column="4" colspan="2"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Lines After Imports:</string> + </property> + </widget> + </item> + <item row="1" column="6"> + <widget class="QSpinBox" name="linesAfterImportsSpinBox"> + <property name="toolTip"> + <string>Enter the number of lines after import statements.</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="specialValueText"> + <string>automatic</string> + </property> + <property name="minimum"> + <number>-1</number> + </property> + <property name="maximum"> + <number>120</number> + </property> + <property name="value"> + <number>-1</number> + </property> + </widget> + </item> + <item row="1" column="7"> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>87</width> + <height>17</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Lines Between Sections:</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QSpinBox" name="linesBetweenSectionsSpinBox"> + <property name="toolTip"> + <string>Enter the number of lines between import sections.</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>0</number> + </property> + <property name="maximum"> + <number>5</number> + </property> + <property name="value"> + <number>1</number> + </property> + </widget> + </item> + <item row="2" column="4" colspan="2"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Lines Between Types:</string> + </property> + </widget> + </item> + <item row="2" column="6"> + <widget class="QSpinBox" name="linesBetweenTypesSpinBox"> + <property name="toolTip"> + <string>Enter the number of lines between import types.</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>0</number> + </property> + <property name="maximum"> + <number>5</number> + </property> + <property name="value"> + <number>0</number> + </property> + </widget> + </item> + <item row="3" column="0" colspan="4"> + <widget class="QCheckBox" name="trailingCommaCheckBox"> + <property name="toolTip"> + <string>Select to include a trailing comma.</string> + </property> + <property name="text"> + <string>Include Trailing Comma</string> + </property> + </widget> + </item> + <item row="3" column="4" colspan="4"> + <widget class="QCheckBox" name="parenthesesCheckBox"> + <property name="toolTip"> + <string>Select for parenthesized import statements.</string> + </property> + <property name="text"> + <string>Use Parentheses</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Sections:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="4" column="1" colspan="3"> + <widget class="QPlainTextEdit" name="sectionsEdit"> + <property name="toolTip"> + <string>Enter the order of sections (one per line).</string> + </property> + </widget> + </item> + <item row="4" column="4"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Exclude:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="4" column="5" colspan="3"> + <widget class="QPlainTextEdit" name="excludeEdit"> + <property name="toolTip"> + <string>Enter glob patterns for files to be skipped.</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>sourceComboBox</tabstop> + <tabstop>profileComboBox</tabstop> + <tabstop>pythonComboBox</tabstop> + <tabstop>multiLineComboBox</tabstop> + <tabstop>sortOrderComboBox</tabstop> + <tabstop>sortCaseSensitiveCheckBox</tabstop> + <tabstop>extensionsEdit</tabstop> + <tabstop>lineLengthSpinBox</tabstop> + <tabstop>linesBeforeImportsSpinBox</tabstop> + <tabstop>linesAfterImportsSpinBox</tabstop> + <tabstop>linesBetweenSectionsSpinBox</tabstop> + <tabstop>linesBetweenTypesSpinBox</tabstop> + <tabstop>trailingCommaCheckBox</tabstop> + <tabstop>parenthesesCheckBox</tabstop> + <tabstop>sectionsEdit</tabstop> + <tabstop>excludeEdit</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>IsortConfigurationDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>353</x> + <y>453</y> + </hint> + <hint type="destinationlabel"> + <x>376</x> + <y>371</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>IsortConfigurationDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>166</x> + <y>452</y> + </hint> + <hint type="destinationlabel"> + <x>186</x> + <y>365</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/CodeFormatting/IsortFormattingAction.py Mon Oct 31 15:29:18 2022 +0100 @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an enum defining the various isort code formatting actions. +""" + +import enum + + +class IsortFormattingAction(enum.Enum): + """ + Class defining the various isort code formatting actions. + """ + + Sort = 0 + Check = 1 + Diff = 2
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/CodeFormatting/IsortFormattingDialog.py Mon Oct 31 15:29:18 2022 +0100 @@ -0,0 +1,577 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog showing the isort code formatting progress and the results. +""" + +import copy +import io +import multiprocessing +import pathlib + +from dataclasses import dataclass + +from isort.settings import Config +from isort.api import check_file, sort_file + +from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication +from PyQt6.QtWidgets import ( + QAbstractButton, + QDialog, + QDialogButtonBox, + QHeaderView, + QTreeWidgetItem, +) + +from eric7 import Preferences +from eric7.EricWidgets import EricMessageBox + +from .FormattingDiffWidget import FormattingDiffWidget +from .IsortFormattingAction import IsortFormattingAction +from .IsortUtilities import suppressStderr + +from .Ui_IsortFormattingDialog import Ui_IsortFormattingDialog + + +class IsortFormattingDialog(QDialog, Ui_IsortFormattingDialog): + """ + Class implementing a dialog showing the isort code formatting progress and the + results. + """ + + DataTypeRole = Qt.ItemDataRole.UserRole + DataRole = Qt.ItemDataRole.UserRole + 1 + + StatusColumn = 0 + FileNameColumn = 1 + + def __init__( + self, + configuration, + filesList, + project=None, + action=IsortFormattingAction.Sort, + 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 IsortFormattingAction.Sort) + @type IsortFormattingAction (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.resultsList.header().setSortIndicator(1, Qt.SortOrder.AscendingOrder) + + self.__config = copy.deepcopy(configuration) + self.__config["quiet"] = True # we don't want extra output + self.__config["overwrite_in_place"] = True # we want to overwrite the files + if "config_source" in self.__config: + del self.__config["config_source"] + self.__isortConfig = Config(**self.__config) + self.__config["__action__"] = action # needed by the workers + self.__project = project + + self.__filesList = filesList[:] + + self.__diffDialog = None + + self.__allFilter = self.tr("<all>") + + self.__sortImportsButton = self.buttonBox.addButton( + self.tr("Sort Imports"), QDialogButtonBox.ButtonRole.ActionRole + ) + self.__sortImportsButton.setVisible(False) + + self.show() + QCoreApplication.processEvents() + + self.__performAction() + + def __performAction(self): + """ + Private method to execute the requested formatting action. + """ + self.progressBar.setValue(0) + self.progressBar.setVisible(True) + + self.statisticsGroup.setVisible(False) + self.__statistics = IsortStatistics() + + self.__cancelled = False + + self.statusFilterComboBox.clear() + self.resultsList.clear() + + self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setDefault(True) + + files = self.__filterFiles(self.__filesList) + if len(files) > 1: + self.__formatManyFiles(files) + elif len(files) == 1: + self.__formatOneFile(files[0]) + + 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 + """ + files = [] + for file in filesList: + if not self.__isortConfig.is_supported_filetype( + file + ) or self.__isortConfig.is_skipped(pathlib.Path(file)): + self.__handleIsortResult(file, "skipped") + 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 __populateStatusFilterCombo(self): + """ + Private method to populate the status filter combo box with allowed selections. + """ + allowedSelections = set() + for row in range(self.resultsList.topLevelItemCount()): + allowedSelections.add( + self.resultsList.topLevelItem(row).text( + IsortFormattingDialog.StatusColumn + ) + ) + + self.statusFilterComboBox.addItem(self.__allFilter) + self.statusFilterComboBox.addItems(sorted(allowedSelections)) + + 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.__sortImportsButton.setVisible( + self.__config["__action__"] is not IsortFormattingAction.Sort + and self.__statistics.changeCount > 0 + ) + + self.__updateStatistics() + self.__populateStatusFilterCombo() + + def __updateStatistics(self): + """ + Private method to update the statistics about the recent formatting run and + make them visible. + """ + self.reformattedLabel.setText( + self.tr("Reformatted:") + if self.__config["__action__"] is IsortFormattingAction.Sort + else self.tr("Would Reformat:") + ) + + total = self.progressBar.maximum() + + self.totalCountLabel.setText("{0:n}".format(total)) + self.skippedCountLabel.setText("{0:n}".format(self.__statistics.skippedCount)) + self.failuresCountLabel.setText("{0:n}".format(self.__statistics.failureCount)) + self.processedCountLabel.setText( + "{0:n}".format(self.__statistics.processedCount) + ) + self.reformattedCountLabel.setText( + "{0:n}".format(self.__statistics.changeCount) + ) + self.unchangedCountLabel.setText("{0:n}".format(self.__statistics.sameCount)) + + self.statisticsGroup.setVisible(True) + + @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() + elif button is self.__sortImportsButton: + self.__sortImportsButtonClicked() + + @pyqtSlot() + def __sortImportsButtonClicked(self): + """ + Private slot handling the selection of the 'Sort Imports' button. + """ + self.__config["__action__"] = IsortFormattingAction.Sort + + self.__performAction() + + @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, IsortFormattingDialog.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, IsortFormattingDialog.DataRole) + ), + ) + elif dataType == "diff": + if self.__diffDialog is None: + self.__diffDialog = FormattingDiffWidget() + self.__diffDialog.showDiff(item.data(0, IsortFormattingDialog.DataRole)) + + @pyqtSlot(str) + def on_statusFilterComboBox_currentTextChanged(self, status): + """ + Private slot handling the selection of a status for items to be shown. + + @param status selected status + @type str + """ + for row in range(self.resultsList.topLevelItemCount()): + itm = self.resultsList.topLevelItem(row) + itm.setHidden( + status != self.__allFilter + and itm.text(IsortFormattingDialog.StatusColumn) != status + ) + + 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() + + def __handleIsortResult(self, filename, status, data=""): + """ + Private method to handle an isort formatting result. + + @param filename name of the processed file + @type str + @param status status of the performed action (one of 'changed', 'failed', + 'skipped' or 'unchanged') + @type str + @param data action data (error message or unified diff) (defaults to "") + @type str (optional) + """ + isError = False + + if status == "changed": + statusMsg = ( + self.tr("would resort") + if self.__config["__action__"] + in (IsortFormattingAction.Check, IsortFormattingAction.Diff) + else self.tr("resorted") + ) + self.__statistics.changeCount += 1 + + elif status == "unchanged": + statusMsg = self.tr("unchanged") + self.__statistics.sameCount += 1 + + elif status == "skipped": + statusMsg = self.tr("skipped") + self.__statistics.skippedCount += 1 + + elif status == "failed": + statusMsg = self.tr("failed") + self.__statistics.failureCount += 1 + isError = True + + elif status == "unsupported": + statusMsg = self.tr("error") + data = self.tr("Unsupported 'isort' action ({0}) given.").format( + self.__config["__action__"] + ) + self.__statistics.failureCount += 1 + isError = True + + else: + statusMsg = self.tr("invalid status ({0})").format(status) + self.__statistics.failureCount += 1 + isError = True + + if status != "skipped": + self.__statistics.processedCount += 1 + + if self.__project: + filename = self.__project.getRelativePath(filename) + + itm = QTreeWidgetItem(self.resultsList, [statusMsg, filename]) + if data: + itm.setData( + 0, IsortFormattingDialog.DataTypeRole, "error" if isError else "diff" + ) + itm.setData(0, IsortFormattingDialog.DataRole, data) + + self.progressBar.setValue(self.progressBar.value() + 1) + + QCoreApplication.processEvents() + + 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 (initially two times the number of processes) + tasks = len(files) + initialTasks = min(2 * NumberOfProcesses, tasks) + for _ in range(initialTasks): + file = files.pop(0) + taskQueue.put((file, self.__config["__action__"])) + + # Start worker processes + workers = [ + multiprocessing.Process( + target=self.formattingWorkerTask, + args=(taskQueue, doneQueue, self.__isortConfig), + ) + for _ in range(NumberOfProcesses) + ] + for worker in workers: + worker.start() + + # Get the results from the worker tasks + for _ in range(tasks): + result = doneQueue.get() + self.__handleIsortResult(result.filename, result.status, data=result.data) + + if self.__cancelled: + break + + if files: + file = files.pop(0) + taskQueue.put((file, self.__config["__action__"])) + + # 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, isortConfig): + """ + 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 isortConfig config object for isort + @type isort.Config + """ + for file, action in iter(inputQueue.get, "STOP"): + if action == IsortFormattingAction.Diff: + result = IsortFormattingDialog.__isortCheckFile( + file, + isortConfig, + withDiff=True, + ) + + elif action == IsortFormattingAction.Sort: + result = IsortFormattingDialog.__isortSortFile( + file, + isortConfig, + ) + + else: + result = IsortResult( + status="unsupported", + filename=file, + ) + + outputQueue.put(result) + + 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 + """ + if self.__config["__action__"] == IsortFormattingAction.Diff: + result = IsortFormattingDialog.__isortCheckFile( + file, + self.__isortConfig, + withDiff=True, + ) + + elif self.__config["__action__"] == IsortFormattingAction.Sort: + result = IsortFormattingDialog.__isortSortFile( + file, + self.__isortConfig, + ) + + else: + result = IsortResult( + status="unsupported", + filename=file, + ) + + self.__handleIsortResult(result.filename, result.status, data=result.data) + + self.__finish() + + @staticmethod + def __isortCheckFile(filename, isortConfig, withDiff=True): + """ + Static method to check, if a file's import statements need to be changed. + + @param filename name of the file to be processed + @type str + @param isortConfig config object for isort + @type isort.Config + @param withDiff flag indicating to return a unified diff, if the file needs to + be changed (defaults to True) + @type bool (optional) + @return result object + @rtype IsortResult + """ + diffIO = io.StringIO() if withDiff else False + with suppressStderr(): + ok = check_file(filename, show_diff=diffIO, config=isortConfig) + if withDiff: + data = "" if ok else diffIO.getvalue() + diffIO.close() + else: + data = "" + + status = "unchanged" if ok else "changed" + + return IsortResult(status=status, filename=filename, data=data) + + @staticmethod + def __isortSortFile(filename, isortConfig): + """ + Static method to sort the import statements of a file. + + @param filename name of the file to be processed + @type str + @param isortConfig config object for isort + @type isort.Config + @return result object + @rtype IsortResult + """ + with suppressStderr(): + ok = sort_file( + filename, + config=isortConfig, + ask_to_apply=False, + write_to_stdout=False, + show_diff=False, + ) + + status = "changed" if ok else "unchanged" + + return IsortResult(status=status, filename=filename) + + +@dataclass +class IsortStatistics: + """ + Class containing the isort reformatting statistic data. + """ + + skippedCount: int = 0 + changeCount: int = 0 + sameCount: int = 0 + failureCount: int = 0 + processedCount: int = 0 + + +@dataclass +class IsortResult: + """ + Class containing the isort reformatting result data. + """ + + status: str = "" + filename: str = "" + data: str = ""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/CodeFormatting/IsortFormattingDialog.ui Mon Oct 31 15:29:18 2022 +0100 @@ -0,0 +1,248 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>IsortFormattingDialog</class> + <widget class="QDialog" name="IsortFormattingDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>700</width> + <height>650</height> + </rect> + </property> + <property name="windowTitle"> + <string>Formatting Imports with isort</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Status Filter:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="statusFilterComboBox"> + <property name="minimumSize"> + <size> + <width>150</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Select the status of items to be shown (empty for all).</string> + </property> + <property name="sizeAdjustPolicy"> + <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum> + </property> + </widget> + </item> + </layout> + </item> + <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="statisticsGroup"> + <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>Skipped</string> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QLabel" name="skippedCountLabel"> + <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>Resorted</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> + <tabstops> + <tabstop>resultsList</tabstop> + <tabstop>statusFilterComboBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/CodeFormatting/IsortUtilities.py Mon Oct 31 15:29:18 2022 +0100 @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some utility functions for the isort import statement formatting +tool. +""" + +import contextlib +import os +import sys + +import isort + +from PyQt6.QtCore import QCoreApplication, pyqtSlot + +from eric7.EricWidgets import EricMessageBox + + +@pyqtSlot() +def aboutIsort(): + """ + Slot to show an 'About isort' dialog. + """ + EricMessageBox.information( + None, + QCoreApplication.translate("IsortUtilities", "About isort"), + QCoreApplication.translate( + "IsortUtilities", + """<p><b>isort Version {0}</b></p>""" + """<p><i>isort</i> is a Python utility / library to sort imports""" + """ alphabetically, and automatically separated into sections and by""" + """ type.</p>""", + ).format(isort.__version__), + ) + + +@contextlib.contextmanager +def suppressStderr(): + """ + Function to suppress output to stderr. + """ + stderr = sys.stderr + with open(os.devnull, "w") as devnull: + sys.stderr = devnull + yield + sys.stderr = stderr
--- a/src/eric7/CodeFormatting/__init__.py Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/CodeFormatting/__init__.py Mon Oct 31 15:29:18 2022 +0100 @@ -9,5 +9,6 @@ The supported code formatting systems are these. <ul> <li><a href="https://github.com/psf/black">Black</a></li> +<li><a href="https://pycqa.github.io/isort/">isort</a></li> </ul """
--- a/src/eric7/Documentation/Source/eric7.CodeFormatting.BlackDiffWidget.html Mon Oct 31 14:09:07 2022 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,120 +0,0 @@ -<!DOCTYPE html> -<html><head> -<title>eric7.CodeFormatting.BlackDiffWidget</title> -<meta charset="UTF-8"> -<link rel="stylesheet" href="styles.css"> -</head> -<body> -<a NAME="top" ID="top"></a> -<h1>eric7.CodeFormatting.BlackDiffWidget</h1> - -<p> -Module implementing a window to show a unified diff.. -</p> -<h3>Global Attributes</h3> - -<table> -<tr><td>None</td></tr> -</table> -<h3>Classes</h3> - -<table> - -<tr> -<td><a href="#BlackDiffWidget">BlackDiffWidget</a></td> -<td>Class implementing a window to show a unified diff..</td> -</tr> -</table> -<h3>Functions</h3> - -<table> -<tr><td>None</td></tr> -</table> -<hr /> -<hr /> -<a NAME="BlackDiffWidget" ID="BlackDiffWidget"></a> -<h2>BlackDiffWidget</h2> - -<p> - Class implementing a window to show a unified diff.. -</p> -<h3>Derived from</h3> -QWidget, Ui_BlackDiffWidget -<h3>Class Attributes</h3> - -<table> -<tr><td>None</td></tr> -</table> -<h3>Class Methods</h3> - -<table> -<tr><td>None</td></tr> -</table> -<h3>Methods</h3> - -<table> - -<tr> -<td><a href="#BlackDiffWidget.__init__">BlackDiffWidget</a></td> -<td>Constructor</td> -</tr> -<tr> -<td><a href="#BlackDiffWidget.closeEvent">closeEvent</a></td> -<td>Protected slot implementing a close event handler.</td> -</tr> -<tr> -<td><a href="#BlackDiffWidget.showDiff">showDiff</a></td> -<td>Public method to show the given diff.</td> -</tr> -</table> -<h3>Static Methods</h3> - -<table> -<tr><td>None</td></tr> -</table> - -<a NAME="BlackDiffWidget.__init__" ID="BlackDiffWidget.__init__"></a> -<h4>BlackDiffWidget (Constructor)</h4> -<b>BlackDiffWidget</b>(<i>parent=None</i>) - -<p> - Constructor -</p> -<dl> - -<dt><i>parent</i> (QWidget (optional))</dt> -<dd> -reference to the parent widget (defaults to None) -</dd> -</dl> -<a NAME="BlackDiffWidget.closeEvent" ID="BlackDiffWidget.closeEvent"></a> -<h4>BlackDiffWidget.closeEvent</h4> -<b>closeEvent</b>(<i>evt</i>) - -<p> - Protected slot implementing a close event handler. -</p> -<dl> - -<dt><i>evt</i> (QCloseEvent)</dt> -<dd> -reference to the close event -</dd> -</dl> -<a NAME="BlackDiffWidget.showDiff" ID="BlackDiffWidget.showDiff"></a> -<h4>BlackDiffWidget.showDiff</h4> -<b>showDiff</b>(<i>diff</i>) - -<p> - Public method to show the given diff. -</p> -<dl> - -<dt><i>diff</i> (str)</dt> -<dd> -text containing the unified diff -</dd> -</dl> -<div align="right"><a href="#top">Up</a></div> -<hr /> -</body></html> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/Documentation/Source/eric7.CodeFormatting.FormattingDiffWidget.html Mon Oct 31 15:29:18 2022 +0100 @@ -0,0 +1,120 @@ +<!DOCTYPE html> +<html><head> +<title>eric7.CodeFormatting.BlackDiffWidget</title> +<meta charset="UTF-8"> +<link rel="stylesheet" href="styles.css"> +</head> +<body> +<a NAME="top" ID="top"></a> +<h1>eric7.CodeFormatting.BlackDiffWidget</h1> + +<p> +Module implementing a window to show a unified diff.. +</p> +<h3>Global Attributes</h3> + +<table> +<tr><td>None</td></tr> +</table> +<h3>Classes</h3> + +<table> + +<tr> +<td><a href="#BlackDiffWidget">BlackDiffWidget</a></td> +<td>Class implementing a window to show a unified diff..</td> +</tr> +</table> +<h3>Functions</h3> + +<table> +<tr><td>None</td></tr> +</table> +<hr /> +<hr /> +<a NAME="BlackDiffWidget" ID="BlackDiffWidget"></a> +<h2>BlackDiffWidget</h2> + +<p> + Class implementing a window to show a unified diff.. +</p> +<h3>Derived from</h3> +QWidget, Ui_BlackDiffWidget +<h3>Class Attributes</h3> + +<table> +<tr><td>None</td></tr> +</table> +<h3>Class Methods</h3> + +<table> +<tr><td>None</td></tr> +</table> +<h3>Methods</h3> + +<table> + +<tr> +<td><a href="#BlackDiffWidget.__init__">BlackDiffWidget</a></td> +<td>Constructor</td> +</tr> +<tr> +<td><a href="#BlackDiffWidget.closeEvent">closeEvent</a></td> +<td>Protected slot implementing a close event handler.</td> +</tr> +<tr> +<td><a href="#BlackDiffWidget.showDiff">showDiff</a></td> +<td>Public method to show the given diff.</td> +</tr> +</table> +<h3>Static Methods</h3> + +<table> +<tr><td>None</td></tr> +</table> + +<a NAME="BlackDiffWidget.__init__" ID="BlackDiffWidget.__init__"></a> +<h4>BlackDiffWidget (Constructor)</h4> +<b>BlackDiffWidget</b>(<i>parent=None</i>) + +<p> + Constructor +</p> +<dl> + +<dt><i>parent</i> (QWidget (optional))</dt> +<dd> +reference to the parent widget (defaults to None) +</dd> +</dl> +<a NAME="BlackDiffWidget.closeEvent" ID="BlackDiffWidget.closeEvent"></a> +<h4>BlackDiffWidget.closeEvent</h4> +<b>closeEvent</b>(<i>evt</i>) + +<p> + Protected slot implementing a close event handler. +</p> +<dl> + +<dt><i>evt</i> (QCloseEvent)</dt> +<dd> +reference to the close event +</dd> +</dl> +<a NAME="BlackDiffWidget.showDiff" ID="BlackDiffWidget.showDiff"></a> +<h4>BlackDiffWidget.showDiff</h4> +<b>showDiff</b>(<i>diff</i>) + +<p> + Public method to show the given diff. +</p> +<dl> + +<dt><i>diff</i> (str)</dt> +<dd> +text containing the unified diff +</dd> +</dl> +<div align="right"><a href="#top">Up</a></div> +<hr /> +</body></html> \ No newline at end of file
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Imports/ImportsChecker.py Mon Oct 31 15:29:18 2022 +0100 @@ -396,6 +396,8 @@ ## adapted from: flake8-alphabetize v0.0.17 ####################################################################### + # TODO: upgrade to v0.0.18 + # TODO: introduce sort type iaw. isort def __checkImportOrder(self): """ Private method to check the order of import statements.
--- a/src/eric7/Project/Project.py Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/Project/Project.py Mon Oct 31 15:29:18 2022 +0100 @@ -59,6 +59,9 @@ from eric7.CodeFormatting.BlackFormattingAction import BlackFormattingAction from eric7.CodeFormatting.BlackUtilities import aboutBlack +from eric7.CodeFormatting.IsortFormattingAction import IsortFormattingAction +from eric7.CodeFormatting.IsortUtilities import aboutIsort + class Project(QObject): """ @@ -4867,7 +4870,7 @@ self.actions.append(self.createSBOMAct) ################################################################### - ## Project Tools - code formatting actions + ## Project Tools - code formatting actions - Black ################################################################### self.blackFormattingGrp = createActionGroup(self) @@ -4993,6 +4996,108 @@ self.actions.append(self.blackConfigureAct) ################################################################### + ## Project Tools - code formatting actions - isort + ################################################################### + + self.isortFormattingGrp = createActionGroup(self) + + self.isortAboutAct = EricAction( + self.tr("About isort"), + self.tr("&isort"), + 0, + 0, + self.isortFormattingGrp, + "project_isort_about", + ) + self.isortAboutAct.setStatusTip(self.tr("Show some information about 'isort'.")) + self.isortAboutAct.setWhatsThis( + self.tr( + "<b>isort</b>" + "<p>This shows some information about the installed 'isort' tool.</p>" + ) + ) + self.isortAboutAct.triggered.connect(aboutIsort) + self.actions.append(self.isortAboutAct) + font = self.isortAboutAct.font() + font.setBold(True) + self.isortAboutAct.setFont(font) + + self.isortSortImportsAct = EricAction( + self.tr("Sort Imports"), + self.tr("Sort Imports"), + 0, + 0, + self.isortFormattingGrp, + "project_isort_sort_imports", + ) + self.isortSortImportsAct.setStatusTip( + self.tr("Sort the import statements of the project sources with 'isort'.") + ) + self.isortSortImportsAct.setWhatsThis( + self.tr( + "<b>Sort Imports</b>" + "<p>This shows a dialog to enter parameters for the imports sorting" + " run and sorts the import statements of the project sources using" + " 'isort'.</p>" + ) + ) + self.isortSortImportsAct.triggered.connect( + lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Sort) + ) + self.actions.append(self.isortSortImportsAct) + + self.isortDiffSortingAct = EricAction( + self.tr("Imports Sorting Diff"), + self.tr("Imports Sorting Diff"), + 0, + 0, + self.isortFormattingGrp, + "project_isort_diff_code", + ) + self.isortDiffSortingAct.setStatusTip( + self.tr( + "Generate a unified diff of potential project source imports" + " resorting with 'isort'." + ) + ) + self.isortDiffSortingAct.setWhatsThis( + self.tr( + "<b>Imports Sorting Diff</b>" + "<p>This shows a dialog to enter parameters for the imports sorting" + " diff run and generates a unified diff of potential project source" + " changes using 'isort'.</p>" + ) + ) + self.isortDiffSortingAct.triggered.connect( + lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Diff) + ) + self.actions.append(self.isortDiffSortingAct) + + self.isortConfigureAct = EricAction( + self.tr("Configure"), + self.tr("Configure"), + 0, + 0, + self.isortFormattingGrp, + "project_isort_configure", + ) + self.isortConfigureAct.setStatusTip( + self.tr( + "Enter the parameters for resorting the project sources import" + " statements with 'isort'." + ) + ) + self.isortConfigureAct.setWhatsThis( + self.tr( + "<b>Configure</b>" + "<p>This shows a dialog to enter the parameters for resorting the" + " import statements of the project sources with 'isort'.</p>" + ) + ) + self.isortConfigureAct.triggered.connect(self.__configureIsort) + self.actions.append(self.isortConfigureAct) + + ################################################################### ## Project - embedded environment actions ################################################################### @@ -5205,6 +5310,8 @@ self.formattingMenu.setTearOffEnabled(True) self.formattingMenu.addActions(self.blackFormattingGrp.actions()) self.formattingMenu.addSeparator() + self.formattingMenu.addActions(self.isortFormattingGrp.actions()) + self.formattingMenu.addSeparator() # build the project main menu menu.setTearOffEnabled(True) @@ -6787,6 +6894,56 @@ dlg.getConfiguration(saveToProject=True) # The data is saved to the project as a side effect. + def __performImportSortingWithIsort(self, action): + """ + Private method to format the project sources import statements using the + 'isort' tool. + + Following actions are supported. + <ul> + <li>IsortFormattingAction.Format - the imports reformatting is performed</li> + <li>IsortFormattingAction.Check - a check is performed, if imports formatting + is necessary</li> + <li>IsortFormattingAction.Diff - a unified diff of potential imports formatting + changes is generated</li> + </ul> + + @param action formatting operation to be performed + @type IsortFormattingAction + """ + from eric7.CodeFormatting.IsortConfigurationDialog import ( + IsortConfigurationDialog, + ) + from eric7.CodeFormatting.IsortFormattingDialog import IsortFormattingDialog + + if ericApp().getObject("ViewManager").checkAllDirty(): + dlg = IsortConfigurationDialog(withProject=True) + if dlg.exec() == QDialog.DialogCode.Accepted: + config = dlg.getConfiguration(saveToProject=True) + + isortDialog = IsortFormattingDialog( + config, + self.getProjectFiles("SOURCES", normalized=True), + project=self, + action=action, + ) + isortDialog.exec() + + @pyqtSlot() + def __configureIsort(self): + """ + Private slot to enter the parameters for formatting the import statements of the + project sources with 'isort'. + """ + from eric7.CodeFormatting.IsortConfigurationDialog import ( + IsortConfigurationDialog, + ) + + dlg = IsortConfigurationDialog(withProject=True, onlyProject=True) + if dlg.exec() == QDialog.DialogCode.Accepted: + dlg.getConfiguration(saveToProject=True) + # The data is saved to the project as a side effect. + ######################################################################### ## Below are methods implementing the 'Embedded Environment' support #########################################################################
--- a/src/eric7/Project/ProjectSourcesBrowser.py Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/Project/ProjectSourcesBrowser.py Mon Oct 31 15:29:18 2022 +0100 @@ -38,6 +38,9 @@ from eric7.CodeFormatting.BlackFormattingAction import BlackFormattingAction from eric7.CodeFormatting.BlackUtilities import aboutBlack +from eric7.CodeFormatting.IsortFormattingAction import IsortFormattingAction +from eric7.CodeFormatting.IsortUtilities import aboutIsort + class ProjectSourcesBrowser(ProjectBaseBrowser): """ @@ -147,6 +150,20 @@ self.tr("Formatting Diff"), lambda: self.__performFormatWithBlack(BlackFormattingAction.Diff), ) + self.formattingMenu.addSeparator() + act = self.formattingMenu.addAction(self.tr("isort"), aboutIsort) + font = act.font() + font.setBold(True) + act.setFont(font) + self.formattingMenu.addAction( + self.tr("Sort Imports"), + lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Sort), + ) + self.formattingMenu.addAction( + self.tr("Imports Sorting Diff"), + lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Diff), + ) + self.formattingMenu.addSeparator() self.formattingMenu.aboutToShow.connect(self.__showContextMenuFormatting) self.menuShow = QMenu(self.tr("Show")) @@ -1271,15 +1288,7 @@ files = [ itm.fileName() - for itm in self.getSelectedItems( - [ - BrowserFileItem, - BrowserClassItem, - BrowserMethodItem, - BrowserClassAttributeItem, - BrowserImportItem, - ] - ) + for itm in self.getSelectedItems([BrowserFileItem]) if itm.isPython3File() ] if not files: @@ -1310,3 +1319,59 @@ self.tr("Code Formatting"), self.tr("""There are no files left for reformatting."""), ) + + def __performImportSortingWithIsort(self, action): + """ + Private method to sort the import statements of the selected project sources + using the 'isort' tool. + + Following actions are supported. + <ul> + <li>IsortFormattingAction.Sort - the import statement sorting is performed</li> + <li>IsortFormattingAction.Check - a check is performed, if import statement + resorting is necessary</li> + <li>IsortFormattingAction.Diff - a unified diff of potential import statement + changes is generated</li> + </ul> + + @param action sorting operation to be performed + @type IsortFormattingAction + """ + from eric7.CodeFormatting.IsortConfigurationDialog import ( + IsortConfigurationDialog, + ) + from eric7.CodeFormatting.IsortFormattingDialog import IsortFormattingDialog + + files = [ + itm.fileName() + for itm in self.getSelectedItems([BrowserFileItem]) + if itm.isPython3File() + ] + if not files: + # called for a directory + itm = self.model().item(self.currentIndex()) + dirName = itm.dirName() + files = [ + f + for f in self.project.getProjectFiles("SOURCES", normalized=True) + if f.startswith(dirName) + ] + + vm = ericApp().getObject("ViewManager") + files = [fn for fn in files if vm.checkFileDirty(fn)] + + if files: + dlg = IsortConfigurationDialog(withProject=True) + if dlg.exec() == QDialog.DialogCode.Accepted: + config = dlg.getConfiguration() + + formattingDialog = IsortFormattingDialog( + config, files, project=self.project, action=action + ) + formattingDialog.exec() + else: + EricMessageBox.information( + self, + self.tr("Import Sorting"), + self.tr("""There are no files left for import statement sorting."""), + )
--- a/src/eric7/QScintilla/DocstringGenerator/PyDocstringGenerator.py Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/QScintilla/DocstringGenerator/PyDocstringGenerator.py Mon Oct 31 15:29:18 2022 +0100 @@ -216,9 +216,7 @@ docstringList.append(self.__quote3) for index, line in enumerate(docstringList): docstringList[index] = ( - indentation + line - if bool(line.strip()) - else "" + indentation + line if bool(line.strip()) else "" ) return sep.join(docstringList) + sep, (insertLine, 0), newCursorLine @@ -324,9 +322,7 @@ docstringList.append(self.__quote3) for index, line in enumerate(docstringList): docstringList[index] = ( - indentation + line - if bool(line.strip()) - else "" + indentation + line if bool(line.strip()) else "" ) docstring = sep.join(docstringList) + indentation return docstring, cursorPosition, newCursorLine
--- a/src/eric7/QScintilla/Editor.py Mon Oct 31 14:09:07 2022 +0100 +++ b/src/eric7/QScintilla/Editor.py Mon Oct 31 15:29:18 2022 +0100 @@ -57,6 +57,9 @@ from eric7.CodeFormatting.BlackFormattingAction import BlackFormattingAction from eric7.CodeFormatting.BlackUtilities import aboutBlack +from eric7.CodeFormatting.IsortFormattingAction import IsortFormattingAction +from eric7.CodeFormatting.IsortUtilities import aboutIsort + EditorAutoCompletionListID = 1 TemplateCompletionListID = 2 ReferencesListID = 3 @@ -1046,6 +1049,10 @@ """ menu = QMenu(self.tr("Code Formatting")) + ####################################################################### + ## Black related entries + ####################################################################### + act = menu.addAction(self.tr("Black"), aboutBlack) font = act.font() font.setBold(True) @@ -1062,6 +1069,25 @@ self.tr("Formatting Diff"), lambda: self.__performFormatWithBlack(BlackFormattingAction.Diff), ) + menu.addSeparator() + + ####################################################################### + ## isort related entries + ####################################################################### + + act = menu.addAction(self.tr("isort"), aboutIsort) + font = act.font() + font.setBold(True) + act.setFont(font) + menu.addAction( + self.tr("Sort Imports"), + lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Sort), + ) + menu.addAction( + self.tr("Imports Sorting Diff"), + lambda: self.__performImportSortingWithIsort(IsortFormattingAction.Diff), + ) + menu.addSeparator() menu.aboutToShow.connect(self.__showContextMenuFormatting) @@ -9061,7 +9087,7 @@ self.__cancelMouseHoverHelp() ####################################################################### - ## Methods implementing the Black code formatting interface + ## Methods implementing the code formatting interface ####################################################################### def __performFormatWithBlack(self, action): @@ -9099,3 +9125,39 @@ config, [self.fileName], project=self.project, action=action ) formattingDialog.exec() + + def __performImportSortingWithIsort(self, action): + """ + Private method to sort the import statements using the 'isort' tool. + + Following actions are supported. + <ul> + <li>IsortFormattingAction.Sort - the import statement sorting is performed</li> + <li>IsortFormattingAction.Check - a check is performed, if import statement + resorting is necessary</li> + <li>IsortFormattingAction.Diff - a unified diff of potential import statement + changes is generated</li> + </ul> + + @param action sorting operation to be performed + @type IsortFormattingAction + """ + from eric7.CodeFormatting.IsortConfigurationDialog import ( + IsortConfigurationDialog, + ) + from eric7.CodeFormatting.IsortFormattingDialog import IsortFormattingDialog + + if not self.isModified() or self.saveFile(): + withProject = ( + self.fileName + and self.project.isOpen() + and self.project.isProjectSource(self.fileName) + ) + dlg = IsortConfigurationDialog(withProject=withProject) + if dlg.exec() == QDialog.DialogCode.Accepted: + config = dlg.getConfiguration() + + formattingDialog = IsortFormattingDialog( + config, [self.fileName], project=self.project, action=action + ) + formattingDialog.exec()