|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to enter the parameters for a Black formatting run. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 import copy |
|
12 import pathlib |
|
13 |
|
14 import black |
|
15 import tomlkit |
|
16 |
|
17 from PyQt6.QtCore import pyqtSlot, Qt |
|
18 from PyQt6.QtGui import QFontMetricsF, QGuiApplication |
|
19 from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem |
|
20 |
|
21 from EricWidgets import EricMessageBox |
|
22 from EricWidgets.EricApplication import ericApp |
|
23 |
|
24 from .Ui_BlackConfigurationDialog import Ui_BlackConfigurationDialog |
|
25 |
|
26 from . import BlackUtilities |
|
27 |
|
28 |
|
29 class BlackConfigurationDialog(QDialog, Ui_BlackConfigurationDialog): |
|
30 """ |
|
31 Class implementing a dialog to enter the parameters for a Black formatting run. |
|
32 """ |
|
33 def __init__(self, withProject=True, parent=None): |
|
34 """ |
|
35 Constructor |
|
36 |
|
37 @param withProject flag indicating to look for project configurations |
|
38 (defaults to True) |
|
39 @type bool |
|
40 @param parent reference to the parent widget (defaults to None) |
|
41 @type QWidget (optional) |
|
42 """ |
|
43 super().__init__(parent) |
|
44 self.setupUi(self) |
|
45 |
|
46 self.__project = ericApp().getObject("Project") if withProject else None |
|
47 |
|
48 indentTabWidth = ( |
|
49 QFontMetricsF(self.excludeEdit.font()).horizontalAdvance(" ") * 2 |
|
50 ) |
|
51 self.excludeEdit.document().setIndentWidth(indentTabWidth) |
|
52 self.excludeEdit.setTabStopDistance(indentTabWidth) |
|
53 |
|
54 self.__pyprojectData = {} |
|
55 self.__projectData = {} |
|
56 |
|
57 self.__tomlButton = self.buttonBox.addButton( |
|
58 self.tr("Generate TOML"), |
|
59 QDialogButtonBox.ButtonRole.ActionRole |
|
60 ) |
|
61 self.__tomlButton.setToolTip(self.tr( |
|
62 "Place a code snippet for 'pyproject.toml' into the clipboard." |
|
63 )) |
|
64 self.__tomlButton.clicked.connect(self.__createTomlSnippet) |
|
65 |
|
66 # setup the source combobox |
|
67 self.sourceComboBox.addItem("", "") |
|
68 if self.__project: |
|
69 pyprojectPath = pathlib.Path( |
|
70 self.__project.getProjectPath() |
|
71 ) / "pyproject.toml" |
|
72 if pyprojectPath.exists(): |
|
73 with contextlib.suppress(tomlkit.exceptions.ParseError, OSError): |
|
74 with pyprojectPath.open("r", encoding="utf-8") as f: |
|
75 data = tomlkit.load(f) |
|
76 config = data.get("tool", {}).get("black", {}) |
|
77 if config: |
|
78 self.__pyprojectData = { |
|
79 k.replace("--", "").replace("-", "_"): v |
|
80 for k, v in config.items() |
|
81 } |
|
82 self.sourceComboBox.addItem("pyproject.toml", "pyproject") |
|
83 if self.__project.getData("OTHERTOOLSPARMS", "Black") is not None: |
|
84 self.__projectData = copy.deepcopy( |
|
85 self.__project.getData("OTHERTOOLSPARMS", "Black") |
|
86 ) |
|
87 self.sourceComboBox.addItem(self.tr("Project File"), "project") |
|
88 self.sourceComboBox.addItem(self.tr("Defaults"), "default") |
|
89 self.sourceComboBox.addItem(self.tr("Configuration Below"), "dialog") |
|
90 |
|
91 self.__populateTargetVersionsList() |
|
92 |
|
93 if self.__projectData: |
|
94 source = self.__projectData.get("source", "") |
|
95 self.sourceComboBox.setCurrentIndex(self.sourceComboBox.findData(source)) |
|
96 |
|
97 def __populateTargetVersionsList(self): |
|
98 """ |
|
99 Private method to populate the target versions list widget with checkable |
|
100 Python version entries. |
|
101 """ |
|
102 targets = [ |
|
103 (int(t[2]), int(t[3:]), t) |
|
104 for t in dir(black.TargetVersion) |
|
105 if t.startswith("PY") |
|
106 ] |
|
107 for target in sorted(targets): |
|
108 itm = QListWidgetItem( |
|
109 "Python {0}.{1}".format(target[0], target[1]), self.targetVersionsList |
|
110 ) |
|
111 itm.setData(Qt.ItemDataRole.UserRole, target[2]) |
|
112 itm.setFlags(itm.flags() | Qt.ItemFlag.ItemIsUserCheckable) |
|
113 itm.setCheckState(Qt.CheckState.Unchecked) |
|
114 |
|
115 def __loadConfiguration(self, configurationDict): |
|
116 """ |
|
117 Private method to load the configuration section with data of the given |
|
118 dictionary. |
|
119 |
|
120 @param configurationDict reference to the data to be loaded |
|
121 @type dict |
|
122 """ |
|
123 confDict = copy.deepcopy(BlackUtilities.getDefaultConfiguration()) |
|
124 confDict.update(configurationDict) |
|
125 |
|
126 self.lineLengthSpinBox.setValue(int(confDict["line-length"])) |
|
127 self.skipStringNormalCheckBox.setChecked(confDict["skip-string-normalization"]) |
|
128 self.skipMagicCommaCheckBox.setChecked(confDict["skip-magic-trailing-comma"]) |
|
129 self.excludeEdit.setPlainText(confDict["extend-exclude"]) |
|
130 for row in range(self.targetVersionsList.count()): |
|
131 itm = self.targetVersionsList.item(row) |
|
132 itm.setCheckState( |
|
133 Qt.CheckState.Checked |
|
134 if itm.data(Qt.ItemDataRole.UserRole).lower() |
|
135 in confDict["target-version"] else |
|
136 Qt.CheckState.Unchecked |
|
137 ) |
|
138 |
|
139 @pyqtSlot(str) |
|
140 def on_sourceComboBox_currentTextChanged(self, selection): |
|
141 """ |
|
142 Private slot to handle the selection of a configuration source. |
|
143 |
|
144 @param selection text of the currently selected item |
|
145 @type str |
|
146 """ |
|
147 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled( |
|
148 bool(selection) |
|
149 ) |
|
150 |
|
151 source = self.sourceComboBox.currentData() |
|
152 if source == "pyproject": |
|
153 self.__loadConfiguration(self.__pyprojectData) |
|
154 elif source == "project": |
|
155 self.__loadConfiguration(self.__projectData) |
|
156 elif source == "default": |
|
157 self.__loadConfiguration(BlackUtilities.getDefaultConfiguration()) |
|
158 elif source == "dialog": |
|
159 # just leave the current entries |
|
160 pass |
|
161 |
|
162 @pyqtSlot() |
|
163 def on_excludeEdit_textChanged(self): |
|
164 """ |
|
165 Private slot to enable the validate button depending on the exclude text. |
|
166 """ |
|
167 self.validateButton.setEnabled(bool(self.excludeEdit.toPlainText())) |
|
168 |
|
169 @pyqtSlot() |
|
170 def on_validateButton_clicked(self): |
|
171 """ |
|
172 Private slot to validate the entered exclusion regular expression. |
|
173 """ |
|
174 regexp = self.excludeEdit.toPlainText() |
|
175 valid, error = BlackUtilities.validateRegExp(regexp) |
|
176 if valid: |
|
177 EricMessageBox.information( |
|
178 self, |
|
179 self.tr("Validation"), |
|
180 self.tr("""The exclusion expression is valid.""") |
|
181 ) |
|
182 else: |
|
183 EricMessageBox.critical( |
|
184 self, |
|
185 self.tr("Validation Error"), |
|
186 error |
|
187 ) |
|
188 |
|
189 def __getTargetList(self): |
|
190 """ |
|
191 Private method to get the list of checked target versions. |
|
192 |
|
193 @return list of target versions |
|
194 @rtype list of str |
|
195 """ |
|
196 targets = [] |
|
197 for row in range(self.targetVersionsList.count()): |
|
198 itm = self.targetVersionsList.item(row) |
|
199 if itm.checkState() == Qt.CheckState.Checked: |
|
200 targets.append(itm.data(Qt.ItemDataRole.UserRole).lower()) |
|
201 |
|
202 return targets |
|
203 |
|
204 @pyqtSlot() |
|
205 def __createTomlSnippet(self): |
|
206 """ |
|
207 Private slot to generate a TOML snippet of the current configuration. |
|
208 |
|
209 Note: Only non-default values are included in this snippet. |
|
210 |
|
211 The code snippet is copied to the clipboard and may be placed inside the |
|
212 'pyproject.toml' file. |
|
213 """ |
|
214 doc = tomlkit.document() |
|
215 |
|
216 black = tomlkit.table() |
|
217 targetList = self.__getTargetList() |
|
218 if targetList: |
|
219 black["target-version"] = targetList |
|
220 black["line-length"] = self.lineLengthSpinBox.value() |
|
221 if self.skipStringNormalCheckBox.isChecked(): |
|
222 black["skip-string-normalization"] = True |
|
223 if self.skipMagicCommaCheckBox.isChecked(): |
|
224 black["skip-magic-trailing-comma"] = True |
|
225 |
|
226 excludeRegexp = self.excludeEdit.toPlainText() |
|
227 if excludeRegexp and BlackUtilities.validateRegExp(excludeRegexp)[0]: |
|
228 black["extend-exclude"] = tomlkit.string( |
|
229 "\n{0}\n".format(excludeRegexp.strip()), |
|
230 literal=True, |
|
231 multiline=True |
|
232 ) |
|
233 |
|
234 doc["tool"] = tomlkit.table(is_super_table=True) |
|
235 doc["tool"]["black"] = black |
|
236 |
|
237 QGuiApplication.clipboard().setText(tomlkit.dumps(doc)) |
|
238 |
|
239 EricMessageBox.information( |
|
240 self, |
|
241 self.tr("Create TOML snipper"), |
|
242 self.tr("""The 'pyproject.toml' snippet was copied to the clipboard""" |
|
243 """ successfully.""") |
|
244 ) |
|
245 |
|
246 def getConfiguration(self): |
|
247 """ |
|
248 Public method to get the current configuration parameters. |
|
249 |
|
250 @return dictionary containing the configuration parameters |
|
251 @rtype dict |
|
252 """ |
|
253 configuration = BlackUtilities.getDefaultConfiguration() |
|
254 |
|
255 configuration["source"] = self.sourceComboBox.currentData() |
|
256 configuration["target-version"] = self.__getTargetList() |
|
257 configuration["line-length"] = self.lineLengthSpinBox.value() |
|
258 configuration["skip-string-normalization"] = ( |
|
259 self.skipStringNormalCheckBox.isChecked() |
|
260 ) |
|
261 configuration["skip-magic-trailing-comma"] = ( |
|
262 self.skipMagicCommaCheckBox.isChecked() |
|
263 ) |
|
264 configuration["extend-exclude"] = self.excludeEdit.toPlainText().strip() |
|
265 |
|
266 if self.__project: |
|
267 self.__project.setData("OTHERTOOLSPARMS", "Black", configuration) |
|
268 |
|
269 return configuration |