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
# -*- 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