Mon, 11 Jul 2022 16:42:50 +0200
Code Formatting
- added an interface to reformat Python source code with the 'Black' utility
# -*- coding: utf-8 -*- # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a dialog to enter the parameters for a Black formatting run. """ import contextlib import copy import pathlib import black import tomlkit from PyQt6.QtCore import pyqtSlot, Qt from PyQt6.QtGui import QFontMetricsF, QGuiApplication from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem from EricWidgets import EricMessageBox from EricWidgets.EricApplication import ericApp from .Ui_BlackConfigurationDialog import Ui_BlackConfigurationDialog from . import BlackUtilities class BlackConfigurationDialog(QDialog, Ui_BlackConfigurationDialog): """ Class implementing a dialog to enter the parameters for a Black formatting run. """ def __init__(self, withProject=True, parent=None): """ Constructor @param withProject flag indicating to look for project configurations (defaults to True) @type bool @param parent reference to the parent widget (defaults to None) @type QWidget (optional) """ super().__init__(parent) self.setupUi(self) self.__project = ericApp().getObject("Project") if withProject else None indentTabWidth = ( QFontMetricsF(self.excludeEdit.font()).horizontalAdvance(" ") * 2 ) self.excludeEdit.document().setIndentWidth(indentTabWidth) self.excludeEdit.setTabStopDistance(indentTabWidth) self.__pyprojectData = {} self.__projectData = {} self.__tomlButton = self.buttonBox.addButton( self.tr("Generate TOML"), QDialogButtonBox.ButtonRole.ActionRole ) self.__tomlButton.setToolTip(self.tr( "Place a code snippet for 'pyproject.toml' into the clipboard." )) self.__tomlButton.clicked.connect(self.__createTomlSnippet) # setup the source combobox self.sourceComboBox.addItem("", "") if self.__project: pyprojectPath = pathlib.Path( self.__project.getProjectPath() ) / "pyproject.toml" if pyprojectPath.exists(): with contextlib.suppress(tomlkit.exceptions.ParseError, OSError): with pyprojectPath.open("r", encoding="utf-8") as f: data = tomlkit.load(f) config = data.get("tool", {}).get("black", {}) if config: self.__pyprojectData = { k.replace("--", "").replace("-", "_"): v for k, v in config.items() } self.sourceComboBox.addItem("pyproject.toml", "pyproject") if self.__project.getData("OTHERTOOLSPARMS", "Black") is not None: self.__projectData = copy.deepcopy( self.__project.getData("OTHERTOOLSPARMS", "Black") ) self.sourceComboBox.addItem(self.tr("Project File"), "project") self.sourceComboBox.addItem(self.tr("Defaults"), "default") self.sourceComboBox.addItem(self.tr("Configuration Below"), "dialog") self.__populateTargetVersionsList() if self.__projectData: source = self.__projectData.get("source", "") self.sourceComboBox.setCurrentIndex(self.sourceComboBox.findData(source)) def __populateTargetVersionsList(self): """ Private method to populate the target versions list widget with checkable Python version entries. """ targets = [ (int(t[2]), int(t[3:]), t) for t in dir(black.TargetVersion) if t.startswith("PY") ] for target in sorted(targets): itm = QListWidgetItem( "Python {0}.{1}".format(target[0], target[1]), self.targetVersionsList ) itm.setData(Qt.ItemDataRole.UserRole, target[2]) itm.setFlags(itm.flags() | Qt.ItemFlag.ItemIsUserCheckable) itm.setCheckState(Qt.CheckState.Unchecked) def __loadConfiguration(self, configurationDict): """ Private method to load the configuration section with data of the given dictionary. @param configurationDict reference to the data to be loaded @type dict """ confDict = copy.deepcopy(BlackUtilities.getDefaultConfiguration()) confDict.update(configurationDict) self.lineLengthSpinBox.setValue(int(confDict["line-length"])) self.skipStringNormalCheckBox.setChecked(confDict["skip-string-normalization"]) self.skipMagicCommaCheckBox.setChecked(confDict["skip-magic-trailing-comma"]) self.excludeEdit.setPlainText(confDict["extend-exclude"]) for row in range(self.targetVersionsList.count()): itm = self.targetVersionsList.item(row) itm.setCheckState( Qt.CheckState.Checked if itm.data(Qt.ItemDataRole.UserRole).lower() in confDict["target-version"] else Qt.CheckState.Unchecked ) @pyqtSlot(str) def on_sourceComboBox_currentTextChanged(self, selection): """ Private slot to handle the selection of a configuration source. @param selection text of the currently selected item @type str """ self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled( bool(selection) ) source = self.sourceComboBox.currentData() if source == "pyproject": self.__loadConfiguration(self.__pyprojectData) elif source == "project": self.__loadConfiguration(self.__projectData) elif source == "default": self.__loadConfiguration(BlackUtilities.getDefaultConfiguration()) elif source == "dialog": # just leave the current entries pass @pyqtSlot() def on_excludeEdit_textChanged(self): """ Private slot to enable the validate button depending on the exclude text. """ self.validateButton.setEnabled(bool(self.excludeEdit.toPlainText())) @pyqtSlot() def on_validateButton_clicked(self): """ Private slot to validate the entered exclusion regular expression. """ regexp = self.excludeEdit.toPlainText() valid, error = BlackUtilities.validateRegExp(regexp) if valid: EricMessageBox.information( self, self.tr("Validation"), self.tr("""The exclusion expression is valid.""") ) else: EricMessageBox.critical( self, self.tr("Validation Error"), error ) def __getTargetList(self): """ Private method to get the list of checked target versions. @return list of target versions @rtype list of str """ targets = [] for row in range(self.targetVersionsList.count()): itm = self.targetVersionsList.item(row) if itm.checkState() == Qt.CheckState.Checked: targets.append(itm.data(Qt.ItemDataRole.UserRole).lower()) return targets @pyqtSlot() def __createTomlSnippet(self): """ Private slot to generate a TOML snippet of the current configuration. Note: Only non-default values are included in this snippet. The code snippet is copied to the clipboard and may be placed inside the 'pyproject.toml' file. """ doc = tomlkit.document() black = tomlkit.table() targetList = self.__getTargetList() if targetList: black["target-version"] = targetList black["line-length"] = self.lineLengthSpinBox.value() if self.skipStringNormalCheckBox.isChecked(): black["skip-string-normalization"] = True if self.skipMagicCommaCheckBox.isChecked(): black["skip-magic-trailing-comma"] = True excludeRegexp = self.excludeEdit.toPlainText() if excludeRegexp and BlackUtilities.validateRegExp(excludeRegexp)[0]: black["extend-exclude"] = tomlkit.string( "\n{0}\n".format(excludeRegexp.strip()), literal=True, multiline=True ) doc["tool"] = tomlkit.table(is_super_table=True) doc["tool"]["black"] = black QGuiApplication.clipboard().setText(tomlkit.dumps(doc)) EricMessageBox.information( self, self.tr("Create TOML snipper"), self.tr("""The 'pyproject.toml' snippet was copied to the clipboard""" """ successfully.""") ) def getConfiguration(self): """ Public method to get the current configuration parameters. @return dictionary containing the configuration parameters @rtype dict """ configuration = BlackUtilities.getDefaultConfiguration() configuration["source"] = self.sourceComboBox.currentData() configuration["target-version"] = self.__getTargetList() configuration["line-length"] = self.lineLengthSpinBox.value() configuration["skip-string-normalization"] = ( self.skipStringNormalCheckBox.isChecked() ) configuration["skip-magic-trailing-comma"] = ( self.skipMagicCommaCheckBox.isChecked() ) configuration["extend-exclude"] = self.excludeEdit.toPlainText().strip() if self.__project: self.__project.setData("OTHERTOOLSPARMS", "Black", configuration) return configuration