1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2015 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to list installed packages. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 try: |
|
12 str = unicode # __IGNORE_EXCEPTION__ |
|
13 except NameError: |
|
14 pass |
|
15 |
|
16 import json |
|
17 |
|
18 from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer |
|
19 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \ |
|
20 QApplication, QTreeWidgetItem, QHeaderView |
|
21 |
|
22 from E5Gui import E5MessageBox |
|
23 |
|
24 from .Ui_PipListDialog import Ui_PipListDialog |
|
25 |
|
26 import Preferences |
|
27 |
|
28 |
|
29 class PipListDialog(QDialog, Ui_PipListDialog): |
|
30 """ |
|
31 Class implementing a dialog to list installed packages. |
|
32 """ |
|
33 CommandArguments = { |
|
34 "list": ["list", "--format=json"], |
|
35 "uptodate": ["list", "--uptodate", "--format=json"], |
|
36 "outdated": ["list", "--outdated", "--format=json"], |
|
37 } |
|
38 |
|
39 ShowProcessGeneralMode = 0 |
|
40 ShowProcessClassifiersMode = 1 |
|
41 ShowProcessEntryPointsMode = 2 |
|
42 ShowProcessFilesListMode = 3 |
|
43 |
|
44 def __init__(self, pip, mode, indexUrl, title, parent=None): |
|
45 """ |
|
46 Constructor |
|
47 |
|
48 @param pip reference to the master object |
|
49 @type Pip |
|
50 @param mode list command mode (one of 'list', 'uptodate', 'outdated') |
|
51 @type str |
|
52 @param indexUrl URL of the pypi index |
|
53 @type str |
|
54 @param title title of the dialog |
|
55 @type str |
|
56 @param parent reference to the parent widget |
|
57 @type QWidget |
|
58 """ |
|
59 assert mode in PipListDialog.CommandArguments |
|
60 |
|
61 super(PipListDialog, self).__init__(parent) |
|
62 self.setupUi(self) |
|
63 self.setWindowFlags(Qt.Window) |
|
64 |
|
65 self.setWindowTitle(title) |
|
66 |
|
67 self.__refreshButton = self.buttonBox.addButton( |
|
68 self.tr("&Refresh"), QDialogButtonBox.ActionRole) |
|
69 self.__refreshButton.setEnabled(False) |
|
70 if mode == "outdated": |
|
71 self.__upgradeButton = self.buttonBox.addButton( |
|
72 self.tr("Up&grade"), QDialogButtonBox.ActionRole) |
|
73 self.__upgradeButton.setEnabled(False) |
|
74 self.__upgradeAllButton = self.buttonBox.addButton( |
|
75 self.tr("Upgrade &All"), QDialogButtonBox.ActionRole) |
|
76 self.__upgradeAllButton.setEnabled(False) |
|
77 else: |
|
78 self.__upgradeButton = None |
|
79 self.__upgradeAllButton = None |
|
80 self.__uninstallButton = self.buttonBox.addButton( |
|
81 self.tr("&Uninstall"), QDialogButtonBox.ActionRole) |
|
82 self.__uninstallButton.setEnabled(False) |
|
83 |
|
84 self.__pip = pip |
|
85 self.__mode = mode |
|
86 self.__ioEncoding = Preferences.getSystem("IOEncoding") |
|
87 self.__indexUrl = indexUrl |
|
88 self.__errors = "" |
|
89 self.__output = [] |
|
90 |
|
91 self.__nothingStrings = { |
|
92 "list": self.tr("Nothing to show"), |
|
93 "uptodate": self.tr("All packages outdated"), |
|
94 "outdated": self.tr("All packages up-to-date"), |
|
95 } |
|
96 |
|
97 self.venvComboBox.addItem(self.__pip.getDefaultEnvironmentString()) |
|
98 projectVenv = self.__pip.getProjectEnvironmentString() |
|
99 if projectVenv: |
|
100 self.venvComboBox.addItem(projectVenv) |
|
101 self.venvComboBox.addItems(self.__pip.getVirtualenvNames()) |
|
102 |
|
103 if mode == "list": |
|
104 self.infoLabel.setText(self.tr("Installed Packages:")) |
|
105 self.packageList.setHeaderLabels([ |
|
106 self.tr("Package"), |
|
107 self.tr("Version"), |
|
108 ]) |
|
109 elif mode == "uptodate": |
|
110 self.infoLabel.setText(self.tr("Up-to-date Packages:")) |
|
111 self.packageList.setHeaderLabels([ |
|
112 self.tr("Package"), |
|
113 self.tr("Version"), |
|
114 ]) |
|
115 elif mode == "outdated": |
|
116 self.infoLabel.setText(self.tr("Outdated Packages:")) |
|
117 self.packageList.setHeaderLabels([ |
|
118 self.tr("Package"), |
|
119 self.tr("Current Version"), |
|
120 self.tr("Latest Version"), |
|
121 self.tr("Package Type"), |
|
122 ]) |
|
123 |
|
124 self.packageList.header().setSortIndicator(0, Qt.AscendingOrder) |
|
125 |
|
126 self.__infoLabels = { |
|
127 "name": self.tr("Name:"), |
|
128 "version": self.tr("Version:"), |
|
129 "location": self.tr("Location:"), |
|
130 "requires": self.tr("Requires:"), |
|
131 "summary": self.tr("Summary:"), |
|
132 "home-page": self.tr("Homepage:"), |
|
133 "author": self.tr("Author:"), |
|
134 "author-email": self.tr("Author Email:"), |
|
135 "license": self.tr("License:"), |
|
136 "metadata-version": self.tr("Metadata Version:"), |
|
137 "installer": self.tr("Installer:"), |
|
138 "classifiers": self.tr("Classifiers:"), |
|
139 "entry-points": self.tr("Entry Points:"), |
|
140 "files": self.tr("Files:"), |
|
141 } |
|
142 self.infoWidget.setHeaderLabels(["Key", "Value"]) |
|
143 |
|
144 self.process = QProcess() |
|
145 self.process.finished.connect(self.__procFinished) |
|
146 self.process.readyReadStandardOutput.connect(self.__readStdout) |
|
147 self.process.readyReadStandardError.connect(self.__readStderr) |
|
148 |
|
149 self.show() |
|
150 QApplication.processEvents() |
|
151 |
|
152 def __stopProcess(self): |
|
153 """ |
|
154 Private slot to stop the running process. |
|
155 """ |
|
156 if self.process.state() != QProcess.NotRunning: |
|
157 self.process.terminate() |
|
158 QTimer.singleShot(2000, self.process.kill) |
|
159 self.process.waitForFinished(3000) |
|
160 |
|
161 QApplication.restoreOverrideCursor() |
|
162 |
|
163 def closeEvent(self, e): |
|
164 """ |
|
165 Protected slot implementing a close event handler. |
|
166 |
|
167 @param e close event |
|
168 @type QCloseEvent |
|
169 """ |
|
170 self.__stopProcess() |
|
171 e.accept() |
|
172 |
|
173 def __finish(self): |
|
174 """ |
|
175 Private slot called when the process finished or the user pressed |
|
176 the cancel button. |
|
177 """ |
|
178 self.__stopProcess() |
|
179 |
|
180 self.__processOutput() |
|
181 |
|
182 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) |
|
183 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) |
|
184 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) |
|
185 self.buttonBox.button(QDialogButtonBox.Close).setFocus( |
|
186 Qt.OtherFocusReason) |
|
187 self.__refreshButton.setEnabled(True) |
|
188 |
|
189 if self.packageList.topLevelItemCount() == 0: |
|
190 QTreeWidgetItem(self.packageList, |
|
191 [self.__nothingStrings[self.__mode]]) |
|
192 if self.__errors and not self.__errors.startswith("DEPRECATION"): |
|
193 E5MessageBox.critical( |
|
194 self, |
|
195 self.windowTitle(), |
|
196 self.tr("""<p>The command failed.</p>""" |
|
197 """<p>Reason: {0}</p>""").format( |
|
198 self.__errors.replace("\r\n", "<br/>") |
|
199 .replace("\n", "<br/>").replace("\r", "<br/>") |
|
200 .replace(" ", " "))) |
|
201 if self.__upgradeAllButton is not None: |
|
202 self.__upgradeAllButton.setEnabled(False) |
|
203 else: |
|
204 if self.__upgradeAllButton is not None: |
|
205 self.__upgradeAllButton.setEnabled(True) |
|
206 |
|
207 self.packageList.sortItems( |
|
208 0, |
|
209 self.packageList.header().sortIndicatorOrder()) |
|
210 self.packageList.header().resizeSections( |
|
211 QHeaderView.ResizeToContents) |
|
212 self.packageList.header().setStretchLastSection(True) |
|
213 |
|
214 @pyqtSlot(QAbstractButton) |
|
215 def on_buttonBox_clicked(self, button): |
|
216 """ |
|
217 Private slot called by a button of the button box clicked. |
|
218 |
|
219 @param button button that was clicked |
|
220 @type QAbstractButton |
|
221 """ |
|
222 if button == self.buttonBox.button(QDialogButtonBox.Close): |
|
223 self.close() |
|
224 elif button == self.buttonBox.button(QDialogButtonBox.Cancel): |
|
225 self.__finish() |
|
226 elif button == self.__refreshButton: |
|
227 self.__refresh() |
|
228 elif button == self.__upgradeButton: |
|
229 self.__upgradePackages() |
|
230 elif button == self.__upgradeAllButton: |
|
231 self.__upgradeAllPackages() |
|
232 elif button == self.__uninstallButton: |
|
233 self.__uninstallPackages() |
|
234 |
|
235 def __procFinished(self, exitCode, exitStatus): |
|
236 """ |
|
237 Private slot connected to the finished signal. |
|
238 |
|
239 @param exitCode exit code of the process |
|
240 @type int |
|
241 @param exitStatus exit status of the process |
|
242 @type QProcess.ExitStatus |
|
243 """ |
|
244 self.__finish() |
|
245 |
|
246 def __refresh(self): |
|
247 """ |
|
248 Private slot to refresh the displayed list. |
|
249 """ |
|
250 self.__stopProcess() |
|
251 self.start() |
|
252 |
|
253 def start(self): |
|
254 """ |
|
255 Public method to start the command. |
|
256 """ |
|
257 self.packageList.clear() |
|
258 self.__errors = "" |
|
259 self.__output = [] |
|
260 |
|
261 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) |
|
262 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) |
|
263 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) |
|
264 self.__refreshButton.setEnabled(False) |
|
265 if self.__upgradeAllButton is not None: |
|
266 self.__upgradeAllButton.setEnabled(False) |
|
267 QApplication.processEvents() |
|
268 |
|
269 QApplication.setOverrideCursor(Qt.WaitCursor) |
|
270 QApplication.processEvents() |
|
271 |
|
272 venvName = self.venvComboBox.currentText() |
|
273 interpreter = self.__pip.getVirtualenvInterpreter(venvName) |
|
274 if not interpreter: |
|
275 return |
|
276 |
|
277 args = ["-m", "pip"] + PipListDialog.CommandArguments[self.__mode] |
|
278 if self.localCheckBox.isChecked(): |
|
279 args.append("--local") |
|
280 if self.notRequiredCheckBox.isChecked(): |
|
281 args.append("--not-required") |
|
282 if self.userCheckBox.isChecked(): |
|
283 args.append("--user") |
|
284 |
|
285 if self.__indexUrl: |
|
286 args.append("--index-url") |
|
287 args.append(self.__indexUrl + "/simple") |
|
288 |
|
289 self.process.start(interpreter, args) |
|
290 procStarted = self.process.waitForStarted(5000) |
|
291 if not procStarted: |
|
292 self.buttonBox.setFocus() |
|
293 self.__stopProcess() |
|
294 E5MessageBox.critical( |
|
295 self, |
|
296 self.tr('Process Generation Error'), |
|
297 self.tr( |
|
298 'The process {0} could not be started.' |
|
299 ).format(interpreter)) |
|
300 self.__finish() |
|
301 |
|
302 def __processOutput(self): |
|
303 """ |
|
304 Private method to process the captured output. |
|
305 """ |
|
306 if self.__output: |
|
307 try: |
|
308 packageData = json.loads("\n".join(self.__output)) |
|
309 for package in packageData: |
|
310 data = [ |
|
311 package["name"], |
|
312 package["version"], |
|
313 ] |
|
314 if self.__mode == "outdated": |
|
315 data.extend([ |
|
316 package["latest_version"], |
|
317 package["latest_filetype"], |
|
318 ]) |
|
319 QTreeWidgetItem(self.packageList, data) |
|
320 except ValueError as err: |
|
321 self.__errors += str(err) + "\n" |
|
322 self.__errors += "received output:\n" |
|
323 self.__errors += "\n".join(self.__output) |
|
324 |
|
325 def __readStdout(self): |
|
326 """ |
|
327 Private slot to handle the readyReadStandardOutput signal. |
|
328 |
|
329 It reads the output of the process, formats it and inserts it into |
|
330 the contents pane. |
|
331 """ |
|
332 self.process.setReadChannel(QProcess.StandardOutput) |
|
333 |
|
334 while self.process.canReadLine(): |
|
335 line = str(self.process.readLine(), self.__ioEncoding, |
|
336 'replace').strip() |
|
337 self.__output.append(line) |
|
338 |
|
339 def __readStderr(self): |
|
340 """ |
|
341 Private slot to handle the readyReadStandardError signal. |
|
342 |
|
343 It reads the error output of the process and inserts it into the |
|
344 error pane. |
|
345 """ |
|
346 self.__errors += str(self.process.readAllStandardError(), |
|
347 self.__ioEncoding, 'replace') |
|
348 |
|
349 @pyqtSlot(str) |
|
350 def on_venvComboBox_activated(self, txt): |
|
351 """ |
|
352 Private slot handling the selection of a virtual environment. |
|
353 |
|
354 @param txt virtual environment |
|
355 @type str |
|
356 """ |
|
357 self.__refresh() |
|
358 |
|
359 @pyqtSlot(bool) |
|
360 def on_localCheckBox_clicked(self, checked): |
|
361 """ |
|
362 Private slot handling the switching of the local mode. |
|
363 |
|
364 @param checked state of the local check box |
|
365 @type bool |
|
366 """ |
|
367 self.__refresh() |
|
368 |
|
369 @pyqtSlot(bool) |
|
370 def on_notRequiredCheckBox_clicked(self, checked): |
|
371 """ |
|
372 Private slot handling the switching of the 'not required' mode. |
|
373 |
|
374 @param checked state of the 'not required' check box |
|
375 @type bool |
|
376 """ |
|
377 self.__refresh() |
|
378 |
|
379 @pyqtSlot(bool) |
|
380 def on_userCheckBox_clicked(self, checked): |
|
381 """ |
|
382 Private slot handling the switching of the 'user-site' mode. |
|
383 |
|
384 @param checked state of the 'user-site' check box |
|
385 @type bool |
|
386 """ |
|
387 self.__refresh() |
|
388 |
|
389 @pyqtSlot() |
|
390 def on_packageList_itemSelectionChanged(self): |
|
391 """ |
|
392 Private slot handling the selection of a package. |
|
393 """ |
|
394 self.infoWidget.clear() |
|
395 |
|
396 if len(self.packageList.selectedItems()) == 1: |
|
397 itm = self.packageList.selectedItems()[0] |
|
398 |
|
399 environment = self.venvComboBox.currentText() |
|
400 interpreter = self.__pip.getVirtualenvInterpreter(environment) |
|
401 if not interpreter: |
|
402 return |
|
403 |
|
404 QApplication.setOverrideCursor(Qt.WaitCursor) |
|
405 |
|
406 args = ["-m", "pip", "show"] |
|
407 if self.verboseCheckBox.isChecked(): |
|
408 args.append("--verbose") |
|
409 if self.installedFilesCheckBox.isChecked(): |
|
410 args.append("--files") |
|
411 args.append(itm.text(0)) |
|
412 success, output = self.__pip.runProcess(args, interpreter) |
|
413 |
|
414 if success and output: |
|
415 mode = PipListDialog.ShowProcessGeneralMode |
|
416 for line in output.splitlines(): |
|
417 line = line.rstrip() |
|
418 if line != "---": |
|
419 if mode != PipListDialog.ShowProcessGeneralMode: |
|
420 if line[0] == " ": |
|
421 QTreeWidgetItem( |
|
422 self.infoWidget, |
|
423 [" ", line.strip()]) |
|
424 else: |
|
425 mode = PipListDialog.ShowProcessGeneralMode |
|
426 if mode == PipListDialog.ShowProcessGeneralMode: |
|
427 try: |
|
428 label, info = line.split(": ", 1) |
|
429 except ValueError: |
|
430 label = line[:-1] |
|
431 info = "" |
|
432 label = label.lower() |
|
433 if label in self.__infoLabels: |
|
434 QTreeWidgetItem( |
|
435 self.infoWidget, |
|
436 [self.__infoLabels[label], info]) |
|
437 if label == "files": |
|
438 mode = PipListDialog.ShowProcessFilesListMode |
|
439 elif label == "classifiers": |
|
440 mode = PipListDialog.ShowProcessClassifiersMode |
|
441 elif label == "entry-points": |
|
442 mode = PipListDialog.ShowProcessEntryPointsMode |
|
443 self.infoWidget.scrollToTop() |
|
444 |
|
445 header = self.infoWidget.header() |
|
446 header.setStretchLastSection(False) |
|
447 header.resizeSections(QHeaderView.ResizeToContents) |
|
448 if header.sectionSize(0) + header.sectionSize(1) < header.width(): |
|
449 header.setStretchLastSection(True) |
|
450 |
|
451 QApplication.restoreOverrideCursor() |
|
452 |
|
453 enable = (len(self.packageList.selectedItems()) > 1 or |
|
454 (len(self.packageList.selectedItems()) == 1 and |
|
455 self.packageList.selectedItems()[0].text(0) not in |
|
456 self.__nothingStrings.values())) |
|
457 self.__upgradeButton and self.__upgradeButton.setEnabled(enable) |
|
458 self.__uninstallButton.setEnabled(enable) |
|
459 |
|
460 @pyqtSlot(bool) |
|
461 def on_verboseCheckBox_clicked(self, checked): |
|
462 """ |
|
463 Private slot to handle a change of the verbose package information |
|
464 checkbox. |
|
465 |
|
466 @param checked state of the checkbox |
|
467 @type bool |
|
468 """ |
|
469 self.on_packageList_itemSelectionChanged() |
|
470 |
|
471 @pyqtSlot(bool) |
|
472 def on_installedFilesCheckBox_clicked(self, checked): |
|
473 """ |
|
474 Private slot to handle a change of the installed files information |
|
475 checkbox. |
|
476 |
|
477 @param checked state of the checkbox |
|
478 @type bool |
|
479 """ |
|
480 self.on_packageList_itemSelectionChanged() |
|
481 |
|
482 def __upgradePackages(self): |
|
483 """ |
|
484 Private slot to upgrade the selected packages. |
|
485 """ |
|
486 packages = [] |
|
487 for itm in self.packageList.selectedItems(): |
|
488 packages.append(itm.text(0)) |
|
489 |
|
490 if packages: |
|
491 if "pip" in packages: |
|
492 self.__upgradePip() |
|
493 else: |
|
494 self.__executeUpgradePackages(packages) |
|
495 |
|
496 def __upgradeAllPackages(self): |
|
497 """ |
|
498 Private slot to upgrade all listed packages. |
|
499 """ |
|
500 packages = [] |
|
501 for index in range(self.packageList.topLevelItemCount()): |
|
502 itm = self.packageList.topLevelItem(index) |
|
503 packages.append(itm.text(0)) |
|
504 |
|
505 if packages: |
|
506 if "pip" in packages: |
|
507 self.__upgradePip() |
|
508 else: |
|
509 self.__executeUpgradePackages(packages) |
|
510 |
|
511 def __upgradePip(self): |
|
512 """ |
|
513 Private slot to upgrade pip itself. |
|
514 """ |
|
515 res = self.__pip.upgradePip( |
|
516 venvName=self.venvComboBox.currentText(), |
|
517 userSite=self.userCheckBox.isChecked()) |
|
518 if res: |
|
519 self.__refresh() |
|
520 |
|
521 def __executeUpgradePackages(self, packages): |
|
522 """ |
|
523 Private method to execute the pip upgrade command. |
|
524 |
|
525 @param packages list of package names to be upgraded |
|
526 @type list of str |
|
527 """ |
|
528 res = self.__pip.upgradePackages( |
|
529 packages, venvName=self.venvComboBox.currentText(), |
|
530 userSite=self.userCheckBox.isChecked()) |
|
531 if res: |
|
532 self.activateWindow() |
|
533 self.raise_() |
|
534 self.__refresh() |
|
535 |
|
536 def __uninstallPackages(self): |
|
537 """ |
|
538 Private slot to uninstall the selected packages. |
|
539 """ |
|
540 packages = [] |
|
541 for itm in self.packageList.selectedItems(): |
|
542 packages.append(itm.text(0)) |
|
543 |
|
544 if packages: |
|
545 res = self.__pip.uninstallPackages( |
|
546 packages, |
|
547 venvName=self.venvComboBox.currentText()) |
|
548 if res: |
|
549 self.activateWindow() |
|
550 self.raise_() |
|
551 self.__refresh() |
|