|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2019 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to show the output of a conda execution. |
|
8 """ |
|
9 |
|
10 import json |
|
11 |
|
12 from PyQt6.QtCore import pyqtSlot, QProcess, QTimer |
|
13 from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton |
|
14 |
|
15 from EricWidgets import EricMessageBox |
|
16 |
|
17 from .Ui_CondaExecDialog import Ui_CondaExecDialog |
|
18 |
|
19 import Preferences |
|
20 import Globals |
|
21 |
|
22 |
|
23 class CondaExecDialog(QDialog, Ui_CondaExecDialog): |
|
24 """ |
|
25 Class implementing a dialog to show the output of a conda execution. |
|
26 """ |
|
27 def __init__(self, command, parent=None): |
|
28 """ |
|
29 Constructor |
|
30 |
|
31 @param command conda command executed |
|
32 @type str |
|
33 @param parent reference to the parent widget |
|
34 @type QWidget |
|
35 """ |
|
36 super().__init__(parent) |
|
37 self.setupUi(self) |
|
38 |
|
39 self.buttonBox.button( |
|
40 QDialogButtonBox.StandardButton.Close).setEnabled(False) |
|
41 self.buttonBox.button( |
|
42 QDialogButtonBox.StandardButton.Cancel).setDefault(True) |
|
43 |
|
44 self.__condaCommand = command |
|
45 |
|
46 self.__process = None |
|
47 self.__condaExe = Preferences.getConda("CondaExecutable") |
|
48 if not self.__condaExe: |
|
49 self.__condaExe = "conda" |
|
50 |
|
51 @pyqtSlot(QAbstractButton) |
|
52 def on_buttonBox_clicked(self, button): |
|
53 """ |
|
54 Private slot called by a button of the button box clicked. |
|
55 |
|
56 @param button button that was clicked |
|
57 @type QAbstractButton |
|
58 """ |
|
59 if button == self.buttonBox.button( |
|
60 QDialogButtonBox.StandardButton.Close |
|
61 ): |
|
62 self.accept() |
|
63 elif button == self.buttonBox.button( |
|
64 QDialogButtonBox.StandardButton.Cancel |
|
65 ): |
|
66 self.__finish(1, 0) |
|
67 |
|
68 def start(self, arguments): |
|
69 """ |
|
70 Public slot to start the conda command. |
|
71 |
|
72 @param arguments commandline arguments for conda program |
|
73 @type list of str |
|
74 """ |
|
75 self.errorGroup.hide() |
|
76 self.progressLabel.hide() |
|
77 self.progressBar.hide() |
|
78 |
|
79 self.contents.clear() |
|
80 self.errors.clear() |
|
81 self.progressLabel.clear() |
|
82 self.progressBar.setValue(0) |
|
83 |
|
84 self.__bufferedStdout = None |
|
85 self.__json = "--json" in arguments |
|
86 self.__firstProgress = True |
|
87 self.__lastFetchFile = "" |
|
88 |
|
89 self.__statusOk = False |
|
90 self.__result = None |
|
91 |
|
92 self.__logOutput(self.__condaExe + " " + " ".join(arguments) + "\n\n") |
|
93 |
|
94 self.__process = QProcess() |
|
95 self.__process.readyReadStandardOutput.connect(self.__readStdout) |
|
96 self.__process.readyReadStandardError.connect(self.__readStderr) |
|
97 self.__process.finished.connect(self.__finish) |
|
98 |
|
99 self.__process.start(self.__condaExe, arguments) |
|
100 procStarted = self.__process.waitForStarted(5000) |
|
101 if not procStarted: |
|
102 EricMessageBox.critical( |
|
103 self, |
|
104 self.tr("Conda Execution"), |
|
105 self.tr("""The conda executable could not be started. Is it""" |
|
106 """ configured correctly?""")) |
|
107 self.__finish(1, 0) |
|
108 else: |
|
109 self.__logOutput(self.tr("Operation started.\n")) |
|
110 |
|
111 def __finish(self, exitCode, exitStatus, giveUp=False): |
|
112 """ |
|
113 Private slot called when the process finished. |
|
114 |
|
115 It is called when the process finished or |
|
116 the user pressed the button. |
|
117 |
|
118 @param exitCode exit code of the process |
|
119 @type int |
|
120 @param exitStatus exit status of the process |
|
121 @type QProcess.ExitStatus |
|
122 @param giveUp flag indicating to not start another attempt |
|
123 @type bool |
|
124 """ |
|
125 if (self.__process is not None and |
|
126 self.__process.state() != QProcess.ProcessState.NotRunning): |
|
127 self.__process.terminate() |
|
128 QTimer.singleShot(2000, self.__process.kill) |
|
129 self.__process.waitForFinished(3000) |
|
130 |
|
131 self.buttonBox.button( |
|
132 QDialogButtonBox.StandardButton.Close).setEnabled(True) |
|
133 self.buttonBox.button( |
|
134 QDialogButtonBox.StandardButton.Cancel).setEnabled(False) |
|
135 self.buttonBox.button( |
|
136 QDialogButtonBox.StandardButton.Close).setDefault(True) |
|
137 |
|
138 self.progressLabel.hide() |
|
139 self.progressBar.hide() |
|
140 |
|
141 self.__statusOk = exitCode == 0 |
|
142 |
|
143 self.__logOutput(self.tr("Operation finished.\n")) |
|
144 if not self.__json and self.__bufferedStdout: |
|
145 self.__logOutput(self.__bufferedStdout) |
|
146 |
|
147 if self.__json and self.__bufferedStdout: |
|
148 index = self.__bufferedStdout.find("{") |
|
149 rindex = self.__bufferedStdout.rfind("}") |
|
150 self.__bufferedStdout = self.__bufferedStdout[index:rindex + 1] |
|
151 try: |
|
152 self.__result = json.loads(self.__bufferedStdout) |
|
153 except Exception as error: |
|
154 self.__result = {} |
|
155 self.__logError(str(error)) |
|
156 return |
|
157 |
|
158 if "error" in self.__result: |
|
159 self.__logError(self.__result["error"]) |
|
160 self.__statusOk = False |
|
161 elif ("success" in self.__result and |
|
162 not self.__result["success"]): |
|
163 self.__logError( |
|
164 self.tr("Conda command '{0}' did not return success.") |
|
165 .format(self.__condaCommand)) |
|
166 if "message" in self.__result: |
|
167 self.__logError("\n") |
|
168 self.__logError( |
|
169 self.tr("\nConda Message: {0}").format( |
|
170 self.__result["message"])) |
|
171 self.__statusOk = False |
|
172 elif "message" in self.__result: |
|
173 self.__logOutput( |
|
174 self.tr("\nConda Message: {0}").format( |
|
175 self.__result["message"])) |
|
176 |
|
177 def getResult(self): |
|
178 """ |
|
179 Public method to the result of the command execution. |
|
180 |
|
181 @return tuple containing a flag indicating success and the result data. |
|
182 @rtype tuple of (bool, dict) |
|
183 """ |
|
184 return self.__statusOk, self.__result |
|
185 |
|
186 def __setProgressValues(self, jsonDict, progressType): |
|
187 """ |
|
188 Private method to set the value of the progress bar. |
|
189 |
|
190 @param jsonDict dictionary containing the progress info |
|
191 @type dict |
|
192 @param progressType action type to check for |
|
193 @type str |
|
194 @return flag indicating success |
|
195 @rtype bool |
|
196 """ |
|
197 if progressType in jsonDict and "progress" in jsonDict: |
|
198 if jsonDict["maxval"] == 1: |
|
199 self.progressBar.setMaximum(100) |
|
200 # percent values |
|
201 self.progressBar.setValue( |
|
202 int(jsonDict["progress"] * 100)) |
|
203 parts = jsonDict["fetch"].split("|") |
|
204 filename = parts[0].strip() |
|
205 filesize = parts[1].strip() |
|
206 else: |
|
207 self.progressBar.setMaximum(jsonDict["maxval"]) |
|
208 self.progressBar.setValue(jsonDict["progress"]) |
|
209 filename = jsonDict["fetch"].strip() |
|
210 filesize = Globals.dataString(int(jsonDict["maxval"])) |
|
211 |
|
212 self.progressLabel.setText( |
|
213 self.tr("{0} (Size: {1})").format(filename, filesize)) |
|
214 |
|
215 if progressType == "fetch": |
|
216 if filename != self.__lastFetchFile: |
|
217 self.__logOutput( |
|
218 self.tr("Fetching {0} ...").format(filename)) |
|
219 self.__lastFetchFile = filename |
|
220 elif jsonDict["finished"]: |
|
221 self.__logOutput(self.tr(" Done.\n")) |
|
222 |
|
223 if self.__firstProgress: |
|
224 self.progressLabel.show() |
|
225 self.progressBar.show() |
|
226 self.__firstProgress = False |
|
227 |
|
228 return True |
|
229 |
|
230 return False |
|
231 |
|
232 def __readStdout(self): |
|
233 """ |
|
234 Private slot to handle the readyReadStandardOutput signal. |
|
235 |
|
236 It reads the output of the process, formats it and inserts it into |
|
237 the contents pane. |
|
238 """ |
|
239 all_stdout = str(self.__process.readAllStandardOutput(), |
|
240 Preferences.getSystem("IOEncoding"), |
|
241 'replace') |
|
242 all_stdout = all_stdout.replace("\x00", "") |
|
243 if self.__json: |
|
244 for stdout in all_stdout.splitlines(): |
|
245 try: |
|
246 jsonDict = json.loads(stdout.replace("\x00", "").strip()) |
|
247 if self.__setProgressValues(jsonDict, "fetch"): |
|
248 # nothing to do anymore |
|
249 pass |
|
250 elif "progress" not in jsonDict: |
|
251 if self.__bufferedStdout is None: |
|
252 self.__bufferedStdout = stdout |
|
253 else: |
|
254 self.__bufferedStdout += stdout |
|
255 except (TypeError, ValueError): |
|
256 if self.__bufferedStdout is None: |
|
257 self.__bufferedStdout = stdout |
|
258 else: |
|
259 self.__bufferedStdout += stdout |
|
260 else: |
|
261 self.__logOutput(all_stdout) |
|
262 |
|
263 def __readStderr(self): |
|
264 """ |
|
265 Private slot to handle the readyReadStandardError signal. |
|
266 |
|
267 It reads the error output of the process and inserts it into the |
|
268 error pane. |
|
269 """ |
|
270 self.__process.setReadChannel(QProcess.ProcessChannel.StandardError) |
|
271 |
|
272 while self.__process.canReadLine(): |
|
273 stderr = str(self.__process.readLine(), |
|
274 Preferences.getSystem("IOEncoding"), |
|
275 'replace') |
|
276 self.__logError(stderr) |
|
277 |
|
278 def __logOutput(self, stdout): |
|
279 """ |
|
280 Private method to log some output. |
|
281 |
|
282 @param stdout output string to log |
|
283 @type str |
|
284 """ |
|
285 self.contents.insertPlainText(stdout) |
|
286 self.contents.ensureCursorVisible() |
|
287 |
|
288 def __logError(self, stderr): |
|
289 """ |
|
290 Private method to log an error. |
|
291 |
|
292 @param stderr error string to log |
|
293 @type str |
|
294 """ |
|
295 self.errorGroup.show() |
|
296 self.errors.insertPlainText(stderr) |
|
297 self.errors.ensureCursorVisible() |