|
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 an isort formatting run. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 import copy |
|
12 import pathlib |
|
13 |
|
14 from PyQt6.QtCore import pyqtSlot |
|
15 from PyQt6.QtGui import QGuiApplication |
|
16 from PyQt6.QtWidgets import QDialog, QDialogButtonBox |
|
17 |
|
18 from eric7.EricWidgets import EricMessageBox |
|
19 from eric7.EricWidgets.EricApplication import ericApp |
|
20 |
|
21 from isort import Config |
|
22 from isort.profiles import profiles |
|
23 from isort.settings import VALID_PY_TARGETS |
|
24 from isort.wrap_modes import WrapModes |
|
25 |
|
26 import tomlkit |
|
27 |
|
28 |
|
29 from .Ui_IsortConfigurationDialog import Ui_IsortConfigurationDialog |
|
30 |
|
31 |
|
32 class IsortConfigurationDialog(QDialog, Ui_IsortConfigurationDialog): |
|
33 """ |
|
34 Class implementing a dialog to enter the parameters for an isort formatting run. |
|
35 """ |
|
36 |
|
37 def __init__(self, withProject=True, onlyProject=False, parent=None): |
|
38 """ |
|
39 Constructor |
|
40 |
|
41 @param withProject flag indicating to look for project configurations |
|
42 (defaults to True) |
|
43 @type bool (optional) |
|
44 @param onlyProject flag indicating to only look for project configurations |
|
45 (defaults to False) |
|
46 @type bool (optional) |
|
47 @param parent reference to the parent widget (defaults to None) |
|
48 @type QWidget (optional) |
|
49 """ |
|
50 super().__init__(parent) |
|
51 self.setupUi(self) |
|
52 |
|
53 self.profileComboBox.lineEdit().setClearButtonEnabled(True) |
|
54 |
|
55 self.__parameterWidgetMapping = { |
|
56 "profile": self.profileComboBox, |
|
57 "py_version": self.pythonComboBox, |
|
58 "multi_line_output": self.multiLineComboBox, |
|
59 "sort_order": self.sortOrderComboBox, |
|
60 "supported_extensions": self.extensionsEdit, |
|
61 "line_length": self.lineLengthSpinBox, |
|
62 "lines_before_imports": self.linesBeforeImportsSpinBox, |
|
63 "lines_after_imports": self.linesAfterImportsSpinBox, |
|
64 "lines_between_sections": self.linesBetweenSectionsSpinBox, |
|
65 "lines_between_types": self.linesBetweenTypesSpinBox, |
|
66 "include_trailing_comma": self.trailingCommaCheckBox, |
|
67 "use_parentheses": self.parenthesesCheckBox, |
|
68 "sections": self.sectionsEdit, |
|
69 "extend_skip_glob": self.excludeEdit, |
|
70 "case_sensitive": self.sortCaseSensitiveCheckBox, |
|
71 } |
|
72 |
|
73 self.__project = ( |
|
74 ericApp().getObject("Project") if (withProject or onlyProject) else None |
|
75 ) |
|
76 self.__onlyProject = onlyProject |
|
77 |
|
78 self.__pyprojectData = {} |
|
79 self.__projectData = {} |
|
80 |
|
81 self.__defaultConfig = Config() |
|
82 |
|
83 self.__tomlButton = self.buttonBox.addButton( |
|
84 self.tr("Generate TOML"), QDialogButtonBox.ButtonRole.ActionRole |
|
85 ) |
|
86 self.__tomlButton.setToolTip( |
|
87 self.tr("Place a code snippet for 'pyproject.toml' into the clipboard.") |
|
88 ) |
|
89 self.__tomlButton.clicked.connect(self.__createTomlSnippet) |
|
90 |
|
91 self.profileComboBox.addItem("") |
|
92 self.profileComboBox.addItems(sorted(profiles.keys())) |
|
93 |
|
94 self.pythonComboBox.addItem("", "") |
|
95 self.pythonComboBox.addItem(self.tr("All Versions"), "all") |
|
96 for pyTarget in VALID_PY_TARGETS: |
|
97 if pyTarget.startswith("3"): |
|
98 self.pythonComboBox.addItem( |
|
99 self.tr("Python {0}").format(pyTarget) |
|
100 if len(pyTarget) == 1 |
|
101 else self.tr("Python {0}.{1}").format(pyTarget[0], pyTarget[1:]), |
|
102 pyTarget, |
|
103 ) |
|
104 |
|
105 self.sortOrderComboBox.addItem("", "") |
|
106 self.sortOrderComboBox.addItem("Natural", "natural") |
|
107 self.sortOrderComboBox.addItem("Native Python", "native") |
|
108 |
|
109 self.__populateMultiLineComboBox() |
|
110 |
|
111 # setup the source combobox |
|
112 self.sourceComboBox.addItem("", "") |
|
113 if self.__project: |
|
114 pyprojectPath = ( |
|
115 pathlib.Path(self.__project.getProjectPath()) / "pyproject.toml" |
|
116 ) |
|
117 if pyprojectPath.exists(): |
|
118 with contextlib.suppress(tomlkit.exceptions.ParseError, OSError): |
|
119 with pyprojectPath.open("r", encoding="utf-8") as f: |
|
120 data = tomlkit.load(f) |
|
121 config = data.get("tool", {}).get("isort", {}) |
|
122 if config: |
|
123 self.__pyprojectData = { |
|
124 k.replace("--", ""): v for k, v in config.items() |
|
125 } |
|
126 self.sourceComboBox.addItem("pyproject.toml", "pyproject") |
|
127 if self.__project.getData("OTHERTOOLSPARMS", "isort") is not None: |
|
128 self.__projectData = copy.deepcopy( |
|
129 self.__project.getData("OTHERTOOLSPARMS", "isort") |
|
130 ) |
|
131 self.sourceComboBox.addItem(self.tr("Project File"), "project") |
|
132 elif onlyProject: |
|
133 self.sourceComboBox.addItem(self.tr("Project File"), "project") |
|
134 if not onlyProject: |
|
135 self.sourceComboBox.addItem(self.tr("Defaults"), "default") |
|
136 self.sourceComboBox.addItem(self.tr("Configuration Below"), "dialog") |
|
137 |
|
138 if self.__projectData: |
|
139 source = self.__projectData.get("config_source", "") |
|
140 self.sourceComboBox.setCurrentIndex(self.sourceComboBox.findData(source)) |
|
141 elif onlyProject: |
|
142 self.sourceComboBox.setCurrentIndex(self.sourceComboBox.findData("project")) |
|
143 |
|
144 def __populateMultiLineComboBox(self): |
|
145 """ |
|
146 Private method to populate the multi line output selector. |
|
147 """ |
|
148 self.multiLineComboBox.addItem("", -1) |
|
149 for entry, wrapMode in ( |
|
150 (self.tr("Grid"), WrapModes.GRID), |
|
151 (self.tr("Vertical"), WrapModes.VERTICAL), |
|
152 (self.tr("Hanging Indent"), WrapModes.HANGING_INDENT), |
|
153 ( |
|
154 self.tr("Vertical Hanging Indent"), |
|
155 WrapModes.VERTICAL_HANGING_INDENT, |
|
156 ), |
|
157 (self.tr("Hanging Grid"), WrapModes.VERTICAL_GRID), |
|
158 (self.tr("Hanging Grid Grouped"), WrapModes.VERTICAL_GRID_GROUPED), |
|
159 (self.tr("NOQA"), WrapModes.NOQA), |
|
160 ( |
|
161 self.tr("Vertical Hanging Indent Bracket"), |
|
162 WrapModes.VERTICAL_HANGING_INDENT_BRACKET, |
|
163 ), |
|
164 ( |
|
165 self.tr("Vertical Prefix From Module Import"), |
|
166 WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, |
|
167 ), |
|
168 ( |
|
169 self.tr("Hanging Indent With Parentheses"), |
|
170 WrapModes.HANGING_INDENT_WITH_PARENTHESES, |
|
171 ), |
|
172 (self.tr("Backslash Grid"), WrapModes.BACKSLASH_GRID), |
|
173 ): |
|
174 self.multiLineComboBox.addItem(entry, wrapMode.value) |
|
175 |
|
176 def __loadConfiguration(self, confDict): |
|
177 """ |
|
178 Private method to load the configuration section with data of the given |
|
179 dictionary. |
|
180 |
|
181 Note: Default values will be loaded for missing parameters. |
|
182 |
|
183 @param confDict reference to the data to be loaded |
|
184 @type dict |
|
185 """ |
|
186 self.pythonComboBox.setCurrentIndex( |
|
187 self.pythonComboBox.findData( |
|
188 str(confDict["py_version"]) |
|
189 if "py_version" in confDict |
|
190 else self.__defaultConfig.py_version.replace("py", "") |
|
191 ) |
|
192 ) |
|
193 self.multiLineComboBox.setCurrentIndex( |
|
194 self.multiLineComboBox.findData( |
|
195 int(confDict["multi_line_output"]) |
|
196 if "multi_line_output" in confDict |
|
197 else self.__defaultConfig.multi_line_output.value |
|
198 ) |
|
199 ) |
|
200 self.sortOrderComboBox.setCurrentIndex( |
|
201 self.sortOrderComboBox.findData( |
|
202 str(confDict["sort_order"]) |
|
203 if "sort_order" in confDict |
|
204 else self.__defaultConfig.sort_order |
|
205 ) |
|
206 ) |
|
207 self.extensionsEdit.setText( |
|
208 " ".join( |
|
209 confDict["supported_extensions"] |
|
210 if "supported_extensions" in confDict |
|
211 else self.__defaultConfig.supported_extensions |
|
212 ) |
|
213 ) |
|
214 for parameter in ( |
|
215 "line_length", |
|
216 "lines_before_imports", |
|
217 "lines_after_imports", |
|
218 "lines_between_sections", |
|
219 "lines_between_types", |
|
220 ): |
|
221 # set spin box values |
|
222 self.__parameterWidgetMapping[parameter].setValue( |
|
223 confDict[parameter] |
|
224 if parameter in confDict |
|
225 else getattr(self.__defaultConfig, parameter) |
|
226 ) |
|
227 for parameter in ( |
|
228 "include_trailing_comma", |
|
229 "use_parentheses", |
|
230 "case_sensitive", |
|
231 ): |
|
232 # set check box values |
|
233 self.__parameterWidgetMapping[parameter].setChecked( |
|
234 confDict[parameter] |
|
235 if parameter in confDict |
|
236 else getattr(self.__defaultConfig, parameter) |
|
237 ) |
|
238 for parameter in ( |
|
239 "sections", |
|
240 "extend_skip_glob", |
|
241 ): |
|
242 # set the plain text edits |
|
243 self.__parameterWidgetMapping[parameter].setPlainText( |
|
244 "\n".join( |
|
245 confDict[parameter] |
|
246 if parameter in confDict |
|
247 else getattr(self.__defaultConfig, parameter) |
|
248 ) |
|
249 ) |
|
250 # set the profile combo box last because it may change other entries |
|
251 self.profileComboBox.setEditText( |
|
252 confDict["profile"] |
|
253 if "profile" in confDict |
|
254 else self.__defaultConfig.profile |
|
255 ) |
|
256 |
|
257 @pyqtSlot(str) |
|
258 def on_sourceComboBox_currentTextChanged(self, selection): |
|
259 """ |
|
260 Private slot to handle the selection of a configuration source. |
|
261 |
|
262 @param selection text of the currently selected item |
|
263 @type str |
|
264 """ |
|
265 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled( |
|
266 bool(selection) or self.__onlyProject |
|
267 ) |
|
268 |
|
269 source = self.sourceComboBox.currentData() |
|
270 if source != "dialog": |
|
271 # reset the profile combo box first |
|
272 self.profileComboBox.setCurrentIndex(0) |
|
273 |
|
274 if source == "pyproject": |
|
275 self.__loadConfiguration(self.__pyprojectData) |
|
276 elif source == "project": |
|
277 self.__loadConfiguration(self.__projectData) |
|
278 elif source == "default": |
|
279 self.__loadConfiguration({}) # loads the default values |
|
280 elif source == "dialog": |
|
281 # just leave the current entries |
|
282 pass |
|
283 |
|
284 @pyqtSlot(str) |
|
285 def on_profileComboBox_editTextChanged(self, profileName): |
|
286 """ |
|
287 Private slot to react upon changes of the selected/entered profile. |
|
288 |
|
289 @param profileName name of the current profile |
|
290 @type str |
|
291 """ |
|
292 if profileName and profileName in profiles: |
|
293 confDict = self.__getConfigurationDict() |
|
294 confDict["profile"] = profileName |
|
295 confDict.update(profiles[profileName]) |
|
296 self.__loadConfiguration(confDict) |
|
297 |
|
298 for parameter in self.__parameterWidgetMapping: |
|
299 self.__parameterWidgetMapping[parameter].setEnabled( |
|
300 parameter not in profiles[profileName] |
|
301 ) |
|
302 else: |
|
303 for widget in self.__parameterWidgetMapping.values(): |
|
304 widget.setEnabled(True) |
|
305 |
|
306 @pyqtSlot() |
|
307 def __createTomlSnippet(self): |
|
308 """ |
|
309 Private slot to generate a TOML snippet of the current configuration. |
|
310 |
|
311 Note: Only non-default values are included in this snippet. |
|
312 |
|
313 The code snippet is copied to the clipboard and may be placed inside the |
|
314 'pyproject.toml' file. |
|
315 """ |
|
316 configDict = self.__getConfigurationDict() |
|
317 |
|
318 isort = tomlkit.table() |
|
319 for key, value in configDict.items(): |
|
320 isort[key] = value |
|
321 |
|
322 doc = tomlkit.document() |
|
323 doc["tool"] = tomlkit.table(is_super_table=True) |
|
324 doc["tool"]["isort"] = isort |
|
325 |
|
326 QGuiApplication.clipboard().setText(tomlkit.dumps(doc)) |
|
327 |
|
328 EricMessageBox.information( |
|
329 self, |
|
330 self.tr("Create TOML snippet"), |
|
331 self.tr( |
|
332 """The 'pyproject.toml' snippet was copied to the clipboard""" |
|
333 """ successfully.""" |
|
334 ), |
|
335 ) |
|
336 |
|
337 def __getConfigurationDict(self): |
|
338 """ |
|
339 Private method to assemble and return a dictionary containing the entered |
|
340 non-default configuration parameters. |
|
341 |
|
342 @return dictionary containing the non-default configuration parameters |
|
343 @rtype dict |
|
344 """ |
|
345 configDict = {} |
|
346 |
|
347 if self.profileComboBox.currentText(): |
|
348 configDict["profile"] = self.profileComboBox.currentText() |
|
349 if ( |
|
350 self.pythonComboBox.currentText() |
|
351 and self.pythonComboBox.currentData() |
|
352 != self.__defaultConfig.py_version.replace("py", "") |
|
353 ): |
|
354 configDict["py_version"] = self.pythonComboBox.currentData() |
|
355 if self.multiLineComboBox.isEnabled() and self.multiLineComboBox.currentText(): |
|
356 configDict["multi_line_output"] = self.multiLineComboBox.currentData() |
|
357 if self.sortOrderComboBox.isEnabled() and self.sortOrderComboBox.currentText(): |
|
358 configDict["sort_order"] = self.sortOrderComboBox.currentData() |
|
359 if self.extensionsEdit.isEnabled() and self.extensionsEdit.text(): |
|
360 configDict["supported_extensions"] = [ |
|
361 e.lstrip(".") |
|
362 for e in self.extensionsEdit.text().strip().split() |
|
363 if e.lstrip(".") |
|
364 ] |
|
365 |
|
366 for parameter in ( |
|
367 "line_length", |
|
368 "lines_before_imports", |
|
369 "lines_after_imports", |
|
370 "lines_between_sections", |
|
371 "lines_between_types", |
|
372 ): |
|
373 if self.__parameterWidgetMapping[ |
|
374 parameter |
|
375 ].isEnabled() and self.__parameterWidgetMapping[ |
|
376 parameter |
|
377 ].value() != getattr( |
|
378 self.__defaultConfig, parameter |
|
379 ): |
|
380 configDict[parameter] = self.__parameterWidgetMapping[parameter].value() |
|
381 |
|
382 for parameter in ( |
|
383 "include_trailing_comma", |
|
384 "use_parentheses", |
|
385 "case_sensitive", |
|
386 ): |
|
387 if self.__parameterWidgetMapping[ |
|
388 parameter |
|
389 ].isEnabled() and self.__parameterWidgetMapping[ |
|
390 parameter |
|
391 ].isChecked() != getattr( |
|
392 self.__defaultConfig, parameter |
|
393 ): |
|
394 configDict[parameter] = self.__parameterWidgetMapping[ |
|
395 parameter |
|
396 ].isChecked() |
|
397 |
|
398 for parameter in ( |
|
399 "sections", |
|
400 "extend_skip_glob", |
|
401 ): |
|
402 if self.__parameterWidgetMapping[parameter].isEnabled(): |
|
403 value = ( |
|
404 self.__parameterWidgetMapping[parameter].toPlainText().splitlines() |
|
405 ) |
|
406 if value != list(getattr(self.__defaultConfig, parameter)): |
|
407 configDict[parameter] = value |
|
408 |
|
409 return configDict |
|
410 |
|
411 def getConfiguration(self, saveToProject=False): |
|
412 """ |
|
413 Public method to get the current configuration parameters. |
|
414 |
|
415 @param saveToProject flag indicating to save the configuration data in the |
|
416 project file (defaults to False) |
|
417 @type bool (optional) |
|
418 @return dictionary containing the configuration parameters |
|
419 @rtype dict |
|
420 """ |
|
421 configuration = self.__getConfigurationDict() |
|
422 |
|
423 if saveToProject and self.__project: |
|
424 configuration["config_source"] = self.sourceComboBox.currentData() |
|
425 self.__project.setData("OTHERTOOLSPARMS", "isort", configuration) |
|
426 |
|
427 return configuration |