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