|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the pip packages management widget. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 |
|
12 import textwrap |
|
13 import os |
|
14 |
|
15 from PyQt5.QtCore import pyqtSlot, Qt, QEventLoop, QRegExp |
|
16 from PyQt5.QtGui import QCursor |
|
17 from PyQt5.QtWidgets import QWidget, QToolButton, QApplication, QHeaderView, \ |
|
18 QTreeWidgetItem, QInputDialog, QMenu, QDialog |
|
19 |
|
20 from E5Gui.E5Application import e5App |
|
21 from E5Gui import E5MessageBox |
|
22 |
|
23 from E5Network.E5XmlRpcClient import E5XmlRpcClient |
|
24 |
|
25 from .Ui_PipPackagesWidget import Ui_PipPackagesWidget |
|
26 |
|
27 import UI.PixmapCache |
|
28 |
|
29 from .Pip import Pip |
|
30 |
|
31 |
|
32 class PipPackagesWidget(QWidget, Ui_PipPackagesWidget): |
|
33 """ |
|
34 Class implementing the pip packages management widget. |
|
35 """ |
|
36 ShowProcessGeneralMode = 0 |
|
37 ShowProcessClassifiersMode = 1 |
|
38 ShowProcessEntryPointsMode = 2 |
|
39 ShowProcessFilesListMode = 3 |
|
40 |
|
41 SearchStopwords = { |
|
42 "a", "and", "are", "as", "at", "be", "but", "by", |
|
43 "for", "if", "in", "into", "is", "it", |
|
44 "no", "not", "of", "on", "or", "such", |
|
45 "that", "the", "their", "then", "there", "these", |
|
46 "they", "this", "to", "was", "will", |
|
47 } |
|
48 SearchVersionRole = Qt.UserRole + 1 |
|
49 |
|
50 def __init__(self, parent=None): |
|
51 """ |
|
52 Constructor |
|
53 |
|
54 @param parent reference to the parent widget |
|
55 @type QWidget |
|
56 """ |
|
57 super(PipPackagesWidget, self).__init__(parent) |
|
58 self.setupUi(self) |
|
59 |
|
60 self.pipMenuButton.setObjectName( |
|
61 "navigation_supermenu_button") |
|
62 self.pipMenuButton.setIcon(UI.PixmapCache.getIcon("superMenu")) |
|
63 self.pipMenuButton.setToolTip(self.tr("pip Menu")) |
|
64 self.pipMenuButton.setPopupMode(QToolButton.InstantPopup) |
|
65 self.pipMenuButton.setToolButtonStyle(Qt.ToolButtonIconOnly) |
|
66 self.pipMenuButton.setFocusPolicy(Qt.NoFocus) |
|
67 self.pipMenuButton.setAutoRaise(True) |
|
68 self.pipMenuButton.setShowMenuInside(True) |
|
69 |
|
70 self.refreshButton.setIcon(UI.PixmapCache.getIcon("reload")) |
|
71 self.upgradeButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) |
|
72 self.upgradeAllButton.setIcon(UI.PixmapCache.getIcon("2uparrow")) |
|
73 self.uninstallButton.setIcon(UI.PixmapCache.getIcon("minus")) |
|
74 self.showPackageDetailsButton.setIcon(UI.PixmapCache.getIcon("info")) |
|
75 self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find")) |
|
76 self.searchButton.setIcon(UI.PixmapCache.getIcon("findNext")) |
|
77 self.installButton.setIcon(UI.PixmapCache.getIcon("plus")) |
|
78 self.installUserSiteButton.setIcon(UI.PixmapCache.getIcon("addUser")) |
|
79 self.showDetailsButton.setIcon(UI.PixmapCache.getIcon("info")) |
|
80 |
|
81 self.__pip = Pip(self) |
|
82 self.__client = E5XmlRpcClient(self.__pip.getIndexUrlXml(), self) |
|
83 |
|
84 self.packagesList.header().setSortIndicator(0, Qt.AscendingOrder) |
|
85 |
|
86 self.__infoLabels = { |
|
87 "name": self.tr("Name:"), |
|
88 "version": self.tr("Version:"), |
|
89 "location": self.tr("Location:"), |
|
90 "requires": self.tr("Requires:"), |
|
91 "summary": self.tr("Summary:"), |
|
92 "home-page": self.tr("Homepage:"), |
|
93 "author": self.tr("Author:"), |
|
94 "author-email": self.tr("Author Email:"), |
|
95 "license": self.tr("License:"), |
|
96 "metadata-version": self.tr("Metadata Version:"), |
|
97 "installer": self.tr("Installer:"), |
|
98 "classifiers": self.tr("Classifiers:"), |
|
99 "entry-points": self.tr("Entry Points:"), |
|
100 "files": self.tr("Files:"), |
|
101 } |
|
102 self.infoWidget.setHeaderLabels(["Key", "Value"]) |
|
103 |
|
104 venvManager = e5App().getObject("VirtualEnvManager") |
|
105 venvManager.virtualEnvironmentAdded.connect( |
|
106 self.on_refreshButton_clicked) |
|
107 venvManager.virtualEnvironmentRemoved.connect( |
|
108 self.on_refreshButton_clicked) |
|
109 |
|
110 project = e5App().getObject("Project") |
|
111 project.projectOpened.connect( |
|
112 self.on_refreshButton_clicked) |
|
113 project.projectClosed.connect( |
|
114 self.on_refreshButton_clicked) |
|
115 |
|
116 self.__initPipMenu() |
|
117 self.__populateEnvironments() |
|
118 self.__updateActionButtons() |
|
119 |
|
120 self.statusLabel.hide() |
|
121 self.searchWidget.hide() |
|
122 |
|
123 self.__queryName = [] |
|
124 self.__querySummary = [] |
|
125 |
|
126 self.__packageDetailsDialog = None |
|
127 |
|
128 def __populateEnvironments(self): |
|
129 """ |
|
130 Private method to get a list of environments and populate the selector. |
|
131 """ |
|
132 self.environmentsComboBox.addItem("") |
|
133 projectVenv = self.__pip.getProjectEnvironmentString() |
|
134 if projectVenv: |
|
135 self.environmentsComboBox.addItem(projectVenv) |
|
136 self.environmentsComboBox.addItems(self.__pip.getVirtualenvNames()) |
|
137 |
|
138 def __isPipAvailable(self): |
|
139 """ |
|
140 Private method to check, if the pip package is available for the |
|
141 selected environment. |
|
142 |
|
143 @return flag indicating availability |
|
144 @rtype bool |
|
145 """ |
|
146 available = False |
|
147 |
|
148 venvName = self.environmentsComboBox.currentText() |
|
149 if venvName: |
|
150 available = len(self.packagesList.findItems( |
|
151 "pip", Qt.MatchExactly | Qt.MatchCaseSensitive)) == 1 |
|
152 |
|
153 return available |
|
154 |
|
155 ####################################################################### |
|
156 ## Slots handling widget signals below |
|
157 ####################################################################### |
|
158 |
|
159 def __selectedUpdateableItems(self): |
|
160 """ |
|
161 Private method to get a list of selected items that can be updated. |
|
162 |
|
163 @return list of selected items that can be updated |
|
164 @rtype list of QTreeWidgetItem |
|
165 """ |
|
166 return [ |
|
167 itm for itm in self.packagesList.selectedItems() |
|
168 if bool(itm.text(2)) |
|
169 ] |
|
170 |
|
171 def __allUpdateableItems(self): |
|
172 """ |
|
173 Private method to get a list of all items that can be updated. |
|
174 |
|
175 @return list of all items that can be updated |
|
176 @rtype list of QTreeWidgetItem |
|
177 """ |
|
178 updateableItems = [] |
|
179 for index in range(self.packagesList.topLevelItemCount()): |
|
180 itm = self.packagesList.topLevelItem(index) |
|
181 if itm.text(2): |
|
182 updateableItems.append(itm) |
|
183 |
|
184 return updateableItems |
|
185 |
|
186 def __updateActionButtons(self): |
|
187 """ |
|
188 Private method to set the state of the action buttons. |
|
189 """ |
|
190 if self.__isPipAvailable(): |
|
191 self.upgradeButton.setEnabled( |
|
192 bool(self.__selectedUpdateableItems())) |
|
193 self.uninstallButton.setEnabled( |
|
194 bool(self.packagesList.selectedItems())) |
|
195 self.upgradeAllButton.setEnabled( |
|
196 bool(self.__allUpdateableItems())) |
|
197 self.showPackageDetailsButton.setEnabled( |
|
198 len(self.packagesList.selectedItems()) == 1) |
|
199 else: |
|
200 self.upgradeButton.setEnabled(False) |
|
201 self.uninstallButton.setEnabled(False) |
|
202 self.upgradeAllButton.setEnabled(False) |
|
203 self.showPackageDetailsButton.setEnabled(False) |
|
204 |
|
205 def __refreshPackagesList(self): |
|
206 """ |
|
207 Private method to referesh the packages list. |
|
208 """ |
|
209 self.packagesList.clear() |
|
210 venvName = self.environmentsComboBox.currentText() |
|
211 if venvName: |
|
212 interpreter = self.__pip.getVirtualenvInterpreter(venvName) |
|
213 if interpreter: |
|
214 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) |
|
215 self.statusLabel.show() |
|
216 self.statusLabel.setText( |
|
217 self.tr("Getting installed packages...")) |
|
218 QApplication.processEvents() |
|
219 |
|
220 # 1. populate with installed packages |
|
221 self.packagesList.setUpdatesEnabled(False) |
|
222 installedPackages = self.__pip.getInstalledPackages( |
|
223 venvName, |
|
224 localPackages=self.localCheckBox.isChecked(), |
|
225 notRequired=self.notRequiredCheckBox.isChecked(), |
|
226 usersite=self.userCheckBox.isChecked(), |
|
227 ) |
|
228 for package, version in installedPackages: |
|
229 QTreeWidgetItem(self.packagesList, [package, version]) |
|
230 self.packagesList.setUpdatesEnabled(True) |
|
231 self.statusLabel.setText( |
|
232 self.tr("Getting outdated packages...")) |
|
233 QApplication.processEvents() |
|
234 |
|
235 # 2. update with update information |
|
236 self.packagesList.setUpdatesEnabled(False) |
|
237 outdatedPackages = self.__pip.getOutdatedPackages( |
|
238 venvName, |
|
239 localPackages=self.localCheckBox.isChecked(), |
|
240 notRequired=self.notRequiredCheckBox.isChecked(), |
|
241 usersite=self.userCheckBox.isChecked(), |
|
242 ) |
|
243 for package, _version, latest in outdatedPackages: |
|
244 items = self.packagesList.findItems( |
|
245 package, Qt.MatchExactly | Qt.MatchCaseSensitive) |
|
246 if items: |
|
247 itm = items[0] |
|
248 itm.setText(2, latest) |
|
249 |
|
250 self.packagesList.sortItems(0, Qt.AscendingOrder) |
|
251 for col in range(self.packagesList.columnCount()): |
|
252 self.packagesList.resizeColumnToContents(col) |
|
253 self.packagesList.setUpdatesEnabled(True) |
|
254 QApplication.restoreOverrideCursor() |
|
255 self.statusLabel.hide() |
|
256 |
|
257 self.__updateActionButtons() |
|
258 self.__updateSearchActionButtons() |
|
259 self.__updateSearchButton() |
|
260 |
|
261 @pyqtSlot(int) |
|
262 def on_environmentsComboBox_currentIndexChanged(self, index): |
|
263 """ |
|
264 Private slot handling the selection of a conda environment. |
|
265 |
|
266 @param index index of the selected conda environment |
|
267 @type int |
|
268 """ |
|
269 self.__refreshPackagesList() |
|
270 |
|
271 @pyqtSlot(bool) |
|
272 def on_localCheckBox_clicked(self, checked): |
|
273 """ |
|
274 Private slot handling the switching of the local mode. |
|
275 |
|
276 @param checked state of the local check box |
|
277 @type bool |
|
278 """ |
|
279 self.__refreshPackagesList() |
|
280 |
|
281 @pyqtSlot(bool) |
|
282 def on_notRequiredCheckBox_clicked(self, checked): |
|
283 """ |
|
284 Private slot handling the switching of the 'not required' mode. |
|
285 |
|
286 @param checked state of the 'not required' check box |
|
287 @type bool |
|
288 """ |
|
289 self.__refreshPackagesList() |
|
290 |
|
291 @pyqtSlot(bool) |
|
292 def on_userCheckBox_clicked(self, checked): |
|
293 """ |
|
294 Private slot handling the switching of the 'user-site' mode. |
|
295 |
|
296 @param checked state of the 'user-site' check box |
|
297 @type bool |
|
298 """ |
|
299 self.__refreshPackagesList() |
|
300 |
|
301 @pyqtSlot() |
|
302 def on_packagesList_itemSelectionChanged(self): |
|
303 """ |
|
304 Private slot handling the selection of a package. |
|
305 """ |
|
306 self.infoWidget.clear() |
|
307 |
|
308 if len(self.packagesList.selectedItems()) == 1: |
|
309 itm = self.packagesList.selectedItems()[0] |
|
310 |
|
311 environment = self.environmentsComboBox.currentText() |
|
312 interpreter = self.__pip.getVirtualenvInterpreter(environment) |
|
313 if not interpreter: |
|
314 return |
|
315 |
|
316 QApplication.setOverrideCursor(Qt.WaitCursor) |
|
317 |
|
318 args = ["-m", "pip", "show"] |
|
319 if self.verboseCheckBox.isChecked(): |
|
320 args.append("--verbose") |
|
321 if self.installedFilesCheckBox.isChecked(): |
|
322 args.append("--files") |
|
323 args.append(itm.text(0)) |
|
324 success, output = self.__pip.runProcess(args, interpreter) |
|
325 |
|
326 if success and output: |
|
327 mode = self.ShowProcessGeneralMode |
|
328 for line in output.splitlines(): |
|
329 line = line.rstrip() |
|
330 if line != "---": |
|
331 if mode != self.ShowProcessGeneralMode: |
|
332 if line[0] == " ": |
|
333 QTreeWidgetItem( |
|
334 self.infoWidget, |
|
335 [" ", line.strip()]) |
|
336 else: |
|
337 mode = self.ShowProcessGeneralMode |
|
338 if mode == self.ShowProcessGeneralMode: |
|
339 try: |
|
340 label, info = line.split(": ", 1) |
|
341 except ValueError: |
|
342 label = line[:-1] |
|
343 info = "" |
|
344 label = label.lower() |
|
345 if label in self.__infoLabels: |
|
346 QTreeWidgetItem( |
|
347 self.infoWidget, |
|
348 [self.__infoLabels[label], info]) |
|
349 if label == "files": |
|
350 mode = self.ShowProcessFilesListMode |
|
351 elif label == "classifiers": |
|
352 mode = self.ShowProcessClassifiersMode |
|
353 elif label == "entry-points": |
|
354 mode = self.ShowProcessEntryPointsMode |
|
355 self.infoWidget.scrollToTop() |
|
356 |
|
357 header = self.infoWidget.header() |
|
358 header.setStretchLastSection(False) |
|
359 header.resizeSections(QHeaderView.ResizeToContents) |
|
360 if header.sectionSize(0) + header.sectionSize(1) < header.width(): |
|
361 header.setStretchLastSection(True) |
|
362 |
|
363 QApplication.restoreOverrideCursor() |
|
364 |
|
365 self.__updateActionButtons() |
|
366 |
|
367 @pyqtSlot(QTreeWidgetItem, int) |
|
368 def on_packagesList_itemActivated(self, item, column): |
|
369 """ |
|
370 Private slot reacting on a package item activation. |
|
371 |
|
372 @param item reference to the activated item |
|
373 @type QTreeWidgetItem |
|
374 @param column activated column |
|
375 @type int |
|
376 """ |
|
377 packageName = item.text(0) |
|
378 if column == 1: |
|
379 # show details for installed version |
|
380 packageVersion = item.text(1) |
|
381 else: |
|
382 # show details for available version or installed one |
|
383 if item.text(2): |
|
384 packageVersion = item.text(2) |
|
385 else: |
|
386 packageVersion = item.text(1) |
|
387 |
|
388 self.__showPackageDetails(packageName, packageVersion) |
|
389 |
|
390 @pyqtSlot(bool) |
|
391 def on_verboseCheckBox_clicked(self, checked): |
|
392 """ |
|
393 Private slot to handle a change of the verbose package information |
|
394 checkbox. |
|
395 |
|
396 @param checked state of the checkbox |
|
397 @type bool |
|
398 """ |
|
399 self.on_packagesList_itemSelectionChanged() |
|
400 |
|
401 @pyqtSlot(bool) |
|
402 def on_installedFilesCheckBox_clicked(self, checked): |
|
403 """ |
|
404 Private slot to handle a change of the installed files information |
|
405 checkbox. |
|
406 |
|
407 @param checked state of the checkbox |
|
408 @type bool |
|
409 """ |
|
410 self.on_packagesList_itemSelectionChanged() |
|
411 |
|
412 @pyqtSlot() |
|
413 def on_refreshButton_clicked(self): |
|
414 """ |
|
415 Private slot to refresh the display. |
|
416 """ |
|
417 currentEnvironment = self.environmentsComboBox.currentText() |
|
418 self.environmentsComboBox.clear() |
|
419 self.packagesList.clear() |
|
420 |
|
421 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) |
|
422 QApplication.processEvents() |
|
423 |
|
424 self.__populateEnvironments() |
|
425 |
|
426 index = self.environmentsComboBox.findText( |
|
427 currentEnvironment, Qt.MatchExactly | Qt.MatchCaseSensitive) |
|
428 if index != -1: |
|
429 self.environmentsComboBox.setCurrentIndex(index) |
|
430 |
|
431 QApplication.restoreOverrideCursor() |
|
432 self.__updateActionButtons() |
|
433 |
|
434 @pyqtSlot() |
|
435 def on_upgradeButton_clicked(self): |
|
436 """ |
|
437 Private slot to upgrade selected packages of the selected environment. |
|
438 """ |
|
439 packages = [itm.text(0) for itm in self.__selectedUpdateableItems()] |
|
440 if packages: |
|
441 ok = self.__executeUpgradePackages(packages) |
|
442 if ok: |
|
443 self.on_refreshButton_clicked() |
|
444 |
|
445 @pyqtSlot() |
|
446 def on_upgradeAllButton_clicked(self): |
|
447 """ |
|
448 Private slot to upgrade all packages of the selected environment. |
|
449 """ |
|
450 packages = [itm.text(0) for itm in self.__allUpdateableItems()] |
|
451 if packages: |
|
452 ok = self.__executeUpgradePackages(packages) |
|
453 if ok: |
|
454 self.on_refreshButton_clicked() |
|
455 |
|
456 @pyqtSlot() |
|
457 def on_uninstallButton_clicked(self): |
|
458 """ |
|
459 Private slot to remove selected packages of the selected environment. |
|
460 """ |
|
461 packages = [itm.text(0) for itm in self.packagesList.selectedItems()] |
|
462 if packages: |
|
463 ok = self.__pip.uninstallPackages( |
|
464 packages, |
|
465 venvName=self.environmentsComboBox.currentText()) |
|
466 if ok: |
|
467 self.on_refreshButton_clicked() |
|
468 |
|
469 def __executeUpgradePackages(self, packages): |
|
470 """ |
|
471 Private method to execute the pip upgrade command. |
|
472 |
|
473 @param packages list of package names to be upgraded |
|
474 @type list of str |
|
475 @return flag indicating success |
|
476 @rtype bool |
|
477 """ |
|
478 ok = self.__pip.upgradePackages( |
|
479 packages, venvName=self.environmentsComboBox.currentText(), |
|
480 userSite=self.userCheckBox.isChecked()) |
|
481 return ok |
|
482 |
|
483 @pyqtSlot() |
|
484 def on_showPackageDetailsButton_clicked(self): |
|
485 """ |
|
486 Private slot to show information for the selected package. |
|
487 """ |
|
488 item = self.packagesList.selectedItems()[0] |
|
489 if item: |
|
490 packageName = item.text(0) |
|
491 # show details for available version or installed one |
|
492 if item.text(2): |
|
493 packageVersion = item.text(2) |
|
494 else: |
|
495 packageVersion = item.text(1) |
|
496 |
|
497 self.__showPackageDetails(packageName, packageVersion) |
|
498 |
|
499 ####################################################################### |
|
500 ## Search widget related methods below |
|
501 ####################################################################### |
|
502 |
|
503 def __updateSearchActionButtons(self): |
|
504 """ |
|
505 Private method to update the action button states of the search widget. |
|
506 """ |
|
507 installEnable = ( |
|
508 len(self.searchResultList.selectedItems()) > 0 and |
|
509 self.environmentsComboBox.currentIndex() > 0 and |
|
510 self.__isPipAvailable() |
|
511 ) |
|
512 self.installButton.setEnabled(installEnable) |
|
513 self.installUserSiteButton.setEnabled(installEnable) |
|
514 |
|
515 self.showDetailsButton.setEnabled( |
|
516 len(self.searchResultList.selectedItems()) == 1 and |
|
517 self.__isPipAvailable() |
|
518 ) |
|
519 |
|
520 def __updateSearchButton(self): |
|
521 """ |
|
522 Private method to update the state of the search button. |
|
523 """ |
|
524 self.searchButton.setEnabled( |
|
525 (bool(self.searchEditName.text()) or |
|
526 bool(self.searchEditSummary.text())) and |
|
527 self.__isPipAvailable() |
|
528 ) |
|
529 |
|
530 @pyqtSlot(bool) |
|
531 def on_searchToggleButton_toggled(self, checked): |
|
532 """ |
|
533 Private slot to togle the search widget. |
|
534 |
|
535 @param checked state of the search widget button |
|
536 @type bool |
|
537 """ |
|
538 self.searchWidget.setVisible(checked) |
|
539 |
|
540 if checked: |
|
541 self.searchEditName.setFocus(Qt.OtherFocusReason) |
|
542 self.searchEditName.selectAll() |
|
543 |
|
544 self.__updateSearchActionButtons() |
|
545 self.__updateSearchButton() |
|
546 |
|
547 @pyqtSlot(str) |
|
548 def on_searchEditName_textChanged(self, txt): |
|
549 """ |
|
550 Private slot handling a change of the search term. |
|
551 |
|
552 @param txt search term |
|
553 @type str |
|
554 """ |
|
555 self.__updateSearchButton() |
|
556 |
|
557 @pyqtSlot() |
|
558 def on_searchEditName_returnPressed(self): |
|
559 """ |
|
560 Private slot initiating a search via a press of the Return key. |
|
561 """ |
|
562 self.__search() |
|
563 |
|
564 @pyqtSlot(str) |
|
565 def on_searchEditSummary_textChanged(self, txt): |
|
566 """ |
|
567 Private slot handling a change of the search term. |
|
568 |
|
569 @param txt search term |
|
570 @type str |
|
571 """ |
|
572 self.__updateSearchButton() |
|
573 |
|
574 @pyqtSlot() |
|
575 def on_searchEditSummary_returnPressed(self): |
|
576 """ |
|
577 Private slot initiating a search via a press of the Return key. |
|
578 """ |
|
579 self.__search() |
|
580 |
|
581 @pyqtSlot() |
|
582 def on_searchButton_clicked(self): |
|
583 """ |
|
584 Private slot handling a press of the search button. |
|
585 """ |
|
586 self.__search() |
|
587 |
|
588 @pyqtSlot() |
|
589 def on_searchResultList_itemSelectionChanged(self): |
|
590 """ |
|
591 Private slot handling changes of the search result selection. |
|
592 """ |
|
593 self.__updateSearchActionButtons() |
|
594 |
|
595 def __search(self): |
|
596 """ |
|
597 Private method to perform the search. |
|
598 """ |
|
599 self.searchResultList.clear() |
|
600 self.searchInfoLabel.clear() |
|
601 |
|
602 self.searchButton.setEnabled(False) |
|
603 QApplication.setOverrideCursor(Qt.WaitCursor) |
|
604 QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) |
|
605 |
|
606 self.__queryName = [ |
|
607 term for term in self.searchEditName.text().strip().split() |
|
608 if term not in self.SearchStopwords |
|
609 ] |
|
610 self.__querySummary = [ |
|
611 term for term in self.searchEditSummary.text().strip().split() |
|
612 if term not in self.SearchStopwords |
|
613 ] |
|
614 self.__client.call( |
|
615 "search", |
|
616 ({"name": self.__queryName, |
|
617 "summary": self.__querySummary}, |
|
618 self.searchTermCombineComboBox.currentText()), |
|
619 self.__processSearchResult, |
|
620 self.__searchError |
|
621 ) |
|
622 |
|
623 def __processSearchResult(self, data): |
|
624 """ |
|
625 Private method to process the search result data from PyPI. |
|
626 |
|
627 @param data result data with hits in the first element |
|
628 @type tuple |
|
629 """ |
|
630 if data: |
|
631 packages = self.__transformHits(data[0]) |
|
632 if packages: |
|
633 self.searchInfoLabel.setText( |
|
634 self.tr("%n package(s) found.", "", len(packages))) |
|
635 wrapper = textwrap.TextWrapper(width=80) |
|
636 count = 0 |
|
637 total = 0 |
|
638 for package in packages: |
|
639 itm = QTreeWidgetItem( |
|
640 self.searchResultList, [ |
|
641 package['name'].strip(), |
|
642 "{0:4d}".format(package['score']), |
|
643 "\n".join([ |
|
644 wrapper.fill(line) for line in |
|
645 package['summary'].strip().splitlines() |
|
646 ]) |
|
647 ]) |
|
648 itm.setData(0, self.SearchVersionRole, package['version']) |
|
649 count += 1 |
|
650 total += 1 |
|
651 if count == 100: |
|
652 count = 0 |
|
653 QApplication.processEvents() |
|
654 else: |
|
655 QApplication.restoreOverrideCursor() |
|
656 E5MessageBox.warning( |
|
657 self, |
|
658 self.tr("Search PyPI"), |
|
659 self.tr("""<p>The package search did not return""" |
|
660 """ anything.</p>""")) |
|
661 self.searchInfoLabel.setText( |
|
662 self.tr("""<p>The package search did not return""" |
|
663 """ anything.</p>""")) |
|
664 else: |
|
665 QApplication.restoreOverrideCursor() |
|
666 E5MessageBox.warning( |
|
667 self, |
|
668 self.tr("Search PyPI"), |
|
669 self.tr("""<p>The package search did not return anything.""" |
|
670 """</p>""")) |
|
671 self.searchInfoLabel.setText( |
|
672 self.tr("""<p>The package search did not return anything.""" |
|
673 """</p>""")) |
|
674 |
|
675 header = self.searchResultList.header() |
|
676 self.searchResultList.sortItems(1, Qt.DescendingOrder) |
|
677 header.setStretchLastSection(False) |
|
678 header.resizeSections(QHeaderView.ResizeToContents) |
|
679 headerSize = 0 |
|
680 for col in range(header.count()): |
|
681 headerSize += header.sectionSize(col) |
|
682 if headerSize < header.width(): |
|
683 header.setStretchLastSection(True) |
|
684 |
|
685 self.__finishSearch() |
|
686 |
|
687 def __finishSearch(self): |
|
688 """ |
|
689 Private slot performing the search finishing actions. |
|
690 """ |
|
691 QApplication.restoreOverrideCursor() |
|
692 |
|
693 self.__updateSearchActionButtons() |
|
694 self.__updateSearchButton() |
|
695 |
|
696 self.searchEditName.setFocus(Qt.OtherFocusReason) |
|
697 |
|
698 def __searchError(self, errorCode, errorString): |
|
699 """ |
|
700 Private method handling a search error. |
|
701 |
|
702 @param errorCode code of the error |
|
703 @type int |
|
704 @param errorString error message |
|
705 @type str |
|
706 """ |
|
707 self.__finish() |
|
708 E5MessageBox.warning( |
|
709 self, |
|
710 self.tr("Search PyPI"), |
|
711 self.tr("""<p>The package search failed.</p><p>Reason: {0}</p>""") |
|
712 .format(errorString)) |
|
713 self.searchInfoLabel.setText(self.tr("Error: {0}").format(errorString)) |
|
714 |
|
715 def __transformHits(self, hits): |
|
716 """ |
|
717 Private method to convert the list returned from pypi into a |
|
718 packages list. |
|
719 |
|
720 @param hits list returned from pypi |
|
721 @type list of dict |
|
722 @return list of packages |
|
723 @rtype list of dict |
|
724 """ |
|
725 # we only include the record with the highest score |
|
726 packages = {} |
|
727 for hit in hits: |
|
728 name = hit['name'].strip() |
|
729 summary = (hit['summary'] or "").strip() |
|
730 version = hit['version'].strip() |
|
731 score = self.__score(name, summary) |
|
732 # cleanup the summary |
|
733 if summary in ["UNKNOWN", "."]: |
|
734 summary = "" |
|
735 |
|
736 if name not in packages: |
|
737 packages[name] = { |
|
738 'name': name, |
|
739 'summary': summary, |
|
740 'version': [version.strip()], |
|
741 'score': score} |
|
742 else: |
|
743 if score > packages[name]['score']: |
|
744 packages[name]['score'] = score |
|
745 packages[name]['summary'] = summary |
|
746 packages[name]['version'].append(version.strip()) |
|
747 |
|
748 return list(packages.values()) |
|
749 |
|
750 def __score(self, name, summary): |
|
751 """ |
|
752 Private method to calculate some score for a search result. |
|
753 |
|
754 @param name name of the returned package |
|
755 @type str |
|
756 @param summary summary text for the package |
|
757 @type str |
|
758 @return score value |
|
759 @rtype int |
|
760 """ |
|
761 score = 0 |
|
762 for queryTerm in self.__queryName: |
|
763 if queryTerm.lower() in name.lower(): |
|
764 score += 4 |
|
765 if queryTerm.lower() == name.lower(): |
|
766 score += 4 |
|
767 |
|
768 for queryTerm in self.__querySummary: |
|
769 if queryTerm.lower() in summary.lower(): |
|
770 if QRegExp(r'\b{0}\b'.format(QRegExp.escape(queryTerm)), |
|
771 Qt.CaseInsensitive).indexIn(summary) != -1: |
|
772 # word match gets even higher score |
|
773 score += 2 |
|
774 else: |
|
775 score += 1 |
|
776 |
|
777 return score |
|
778 |
|
779 @pyqtSlot() |
|
780 def on_installButton_clicked(self): |
|
781 """ |
|
782 Private slot to handle pressing the Install button.. |
|
783 """ |
|
784 self.__install() |
|
785 |
|
786 @pyqtSlot() |
|
787 def on_installUserSiteButton_clicked(self): |
|
788 """ |
|
789 Private slot to handle pressing the Install to User-Site button.. |
|
790 """ |
|
791 self.__install(userSite=True) |
|
792 |
|
793 def __install(self, userSite=False): |
|
794 """ |
|
795 Private slot to install the selected packages. |
|
796 |
|
797 @param userSite flag indicating to install to the user directory |
|
798 @type bool |
|
799 """ |
|
800 venvName = self.environmentsComboBox.currentText() |
|
801 if venvName: |
|
802 packages = [] |
|
803 for itm in self.searchResultList.selectedItems(): |
|
804 packages.append(itm.text(0).strip()) |
|
805 if packages: |
|
806 self.__pip.installPackages(packages, venvName=venvName, |
|
807 userSite=userSite) |
|
808 |
|
809 @pyqtSlot() |
|
810 def on_showDetailsButton_clicked(self): |
|
811 """ |
|
812 Private slot to handle pressing the Show Details button. |
|
813 """ |
|
814 self.__showSearchedDetails() |
|
815 |
|
816 @pyqtSlot(QTreeWidgetItem, int) |
|
817 def on_searchResultList_itemActivated(self, item, column): |
|
818 """ |
|
819 Private slot reacting on an search result item activation. |
|
820 |
|
821 @param item reference to the activated item |
|
822 @type QTreeWidgetItem |
|
823 @param column activated column |
|
824 @type int |
|
825 """ |
|
826 self.__showDetails(item) |
|
827 |
|
828 def __showSearchedDetails(self, item=None): |
|
829 """ |
|
830 Private slot to show details about the selected search result package. |
|
831 |
|
832 @param item reference to the search result item to show details for |
|
833 @type QTreeWidgetItem |
|
834 """ |
|
835 self.showDetailsButton.setEnabled(False) |
|
836 |
|
837 if not item: |
|
838 item = self.searchResultList.selectedItems()[0] |
|
839 |
|
840 packageVersions = item.data(0, self.SearchVersionRole) |
|
841 if len(packageVersions) == 1: |
|
842 packageVersion = packageVersions[0] |
|
843 elif len(packageVersions) == 0: |
|
844 packageVersion = "" |
|
845 else: |
|
846 packageVersion, ok = QInputDialog.getItem( |
|
847 self, |
|
848 self.tr("Show Package Details"), |
|
849 self.tr("Select the package version:"), |
|
850 packageVersions, |
|
851 0, False) |
|
852 if not ok: |
|
853 return |
|
854 packageName = item.text(0) |
|
855 |
|
856 self.__showPackageDetails(packageName, packageVersion) |
|
857 |
|
858 def __showPackageDetails(self, packageName, packageVersion): |
|
859 """ |
|
860 Private method to populate the package details dialog. |
|
861 |
|
862 @param packageName name of the package to show details for |
|
863 @type str |
|
864 @param packageVersion version of the package |
|
865 @type str |
|
866 """ |
|
867 QApplication.setOverrideCursor(Qt.WaitCursor) |
|
868 QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) |
|
869 |
|
870 packageData = self.__pip.getPackageDetails(packageName, packageVersion) |
|
871 |
|
872 QApplication.restoreOverrideCursor() |
|
873 if packageData: |
|
874 from .PipPackageDetailsDialog import PipPackageDetailsDialog |
|
875 |
|
876 self.showDetailsButton.setEnabled(True) |
|
877 |
|
878 if self.__packageDetailsDialog is not None: |
|
879 self.__packageDetailsDialog.close() |
|
880 |
|
881 self.__packageDetailsDialog = \ |
|
882 PipPackageDetailsDialog(packageData, self) |
|
883 self.__packageDetailsDialog.show() |
|
884 else: |
|
885 E5MessageBox.warning( |
|
886 self, |
|
887 self.tr("Search PyPI"), |
|
888 self.tr("""<p>No package details info for <b>{0}</b>""" |
|
889 """ available.</p>""").format(packageName)) |
|
890 |
|
891 ####################################################################### |
|
892 ## Menu related methods below |
|
893 ####################################################################### |
|
894 |
|
895 def __initPipMenu(self): |
|
896 """ |
|
897 Private method to create the super menu and attach it to the super |
|
898 menu button. |
|
899 """ |
|
900 self.__pipMenu = QMenu() |
|
901 self.__installPipAct = self.__pipMenu.addAction( |
|
902 self.tr("Install Pip"), |
|
903 self.__installPip) |
|
904 self.__installPipUserAct = self.__pipMenu.addAction( |
|
905 self.tr("Install Pip to User-Site"), |
|
906 self.__installPipUser) |
|
907 self.__repairPipAct = self.__pipMenu.addAction( |
|
908 self.tr("Repair Pip"), |
|
909 self.__repairPip) |
|
910 self.__pipMenu.addSeparator() |
|
911 self.__installPackagesAct = self.__pipMenu.addAction( |
|
912 self.tr("Install Packages"), |
|
913 self.__installPackages) |
|
914 self.__installLocalPackageAct = self.__pipMenu.addAction( |
|
915 self.tr("Install Local Package"), |
|
916 self.__installLocalPackage) |
|
917 self.__pipMenu.addSeparator() |
|
918 self.__installRequirementsAct = self.__pipMenu.addAction( |
|
919 self.tr("Install Requirements"), |
|
920 self.__installRequirements) |
|
921 self.__uninstallRequirementsAct = self.__pipMenu.addAction( |
|
922 self.tr("Uninstall Requirements"), |
|
923 self.__uninstallRequirements) |
|
924 self.__generateRequirementsAct = self.__pipMenu.addAction( |
|
925 self.tr("Generate Requirements..."), |
|
926 self.__generateRequirements) |
|
927 self.__pipMenu.addSeparator() |
|
928 # editUserConfigAct |
|
929 self.__pipMenu.addAction( |
|
930 self.tr("Edit User Configuration..."), |
|
931 self.__editUserConfiguration) |
|
932 self.__editVirtualenvConfigAct = self.__pipMenu.addAction( |
|
933 self.tr("Edit Environment Configuration..."), |
|
934 self.__editVirtualenvConfiguration) |
|
935 self.__pipMenu.addSeparator() |
|
936 # pipConfigAct |
|
937 self.__pipMenu.addAction( |
|
938 self.tr("Configure..."), |
|
939 self.__pipConfigure) |
|
940 |
|
941 self.__pipMenu.aboutToShow.connect(self.__aboutToShowPipMenu) |
|
942 |
|
943 self.pipMenuButton.setMenu(self.__pipMenu) |
|
944 |
|
945 def __aboutToShowPipMenu(self): |
|
946 """ |
|
947 Private slot to set the action enabled status. |
|
948 """ |
|
949 enable = bool(self.environmentsComboBox.currentText()) |
|
950 enablePip = self.__isPipAvailable() |
|
951 |
|
952 self.__installPipAct.setEnabled(not enablePip) |
|
953 self.__installPipUserAct.setEnabled(not enablePip) |
|
954 self.__repairPipAct.setEnabled(enablePip) |
|
955 |
|
956 self.__installPackagesAct.setEnabled(enablePip) |
|
957 self.__installLocalPackageAct.setEnabled(enablePip) |
|
958 |
|
959 self.__installRequirementsAct.setEnabled(enablePip) |
|
960 self.__uninstallRequirementsAct.setEnabled(enablePip) |
|
961 self.__generateRequirementsAct.setEnabled(enablePip) |
|
962 |
|
963 self.__editVirtualenvConfigAct.setEnabled(enable) |
|
964 |
|
965 @pyqtSlot() |
|
966 def __installPip(self): |
|
967 """ |
|
968 Private slot to install pip into the selected environment. |
|
969 """ |
|
970 venvName = self.environmentsComboBox.currentText() |
|
971 if venvName: |
|
972 self.__pip.installPip(venvName) |
|
973 |
|
974 @pyqtSlot() |
|
975 def __installPipUser(self): |
|
976 """ |
|
977 Private slot to install pip into the user site for the selected |
|
978 environment. |
|
979 """ |
|
980 venvName = self.environmentsComboBox.currentText() |
|
981 if venvName: |
|
982 self.__pip.installPip(venvName, userSite=True) |
|
983 |
|
984 @pyqtSlot() |
|
985 def __repairPip(self): |
|
986 """ |
|
987 Private slot to repair the pip installation of the selected |
|
988 environment. |
|
989 """ |
|
990 venvName = self.environmentsComboBox.currentText() |
|
991 if venvName: |
|
992 self.__pip.repairPip(venvName) |
|
993 |
|
994 @pyqtSlot() |
|
995 def __installPackages(self): |
|
996 """ |
|
997 Private slot to install packages to be given by the user. |
|
998 """ |
|
999 venvName = self.environmentsComboBox.currentText() |
|
1000 if venvName: |
|
1001 from .PipPackagesInputDialog import PipPackagesInputDialog |
|
1002 dlg = PipPackagesInputDialog(self, self.tr("Install Packages")) |
|
1003 if dlg.exec_() == QDialog.Accepted: |
|
1004 packages, user = dlg.getData() |
|
1005 if packages: |
|
1006 self.__pip.installPackages(packages, venvName=venvName, |
|
1007 userSite=user) |
|
1008 |
|
1009 @pyqtSlot() |
|
1010 def __installLocalPackage(self): |
|
1011 """ |
|
1012 Private slot to install a package available on local storage. |
|
1013 """ |
|
1014 venvName = self.environmentsComboBox.currentText() |
|
1015 if venvName: |
|
1016 from .PipFileSelectionDialog import PipFileSelectionDialog |
|
1017 dlg = PipFileSelectionDialog(self, "package") |
|
1018 if dlg.exec_() == QDialog.Accepted: |
|
1019 package, user = dlg.getData() |
|
1020 if package and os.path.exists(package): |
|
1021 self.__pip.installPackages([package], venvName=venvName, |
|
1022 userSite=user) |
|
1023 |
|
1024 @pyqtSlot() |
|
1025 def __installRequirements(self): |
|
1026 """ |
|
1027 Private slot to install packages as given in a requirements file. |
|
1028 """ |
|
1029 venvName = self.environmentsComboBox.currentText() |
|
1030 if venvName: |
|
1031 self.__pip.installRequirements(venvName) |
|
1032 |
|
1033 @pyqtSlot() |
|
1034 def __uninstallRequirements(self): |
|
1035 """ |
|
1036 Private slot to uninstall packages as given in a requirements file. |
|
1037 """ |
|
1038 venvName = self.environmentsComboBox.currentText() |
|
1039 if venvName: |
|
1040 self.__pip.uninstallRequirements(venvName) |
|
1041 |
|
1042 @pyqtSlot() |
|
1043 def __generateRequirements(self): |
|
1044 """ |
|
1045 Private slot to generate the contents for a requirements file. |
|
1046 """ |
|
1047 venvName = self.environmentsComboBox.currentText() |
|
1048 if venvName: |
|
1049 from .PipFreezeDialog import PipFreezeDialog |
|
1050 self.__freezeDialog = PipFreezeDialog(self.__pip, self) |
|
1051 self.__freezeDialog.show() |
|
1052 self.__freezeDialog.start(venvName) |
|
1053 |
|
1054 @pyqtSlot() |
|
1055 def __editUserConfiguration(self): |
|
1056 """ |
|
1057 Private slot to edit the user configuration. |
|
1058 """ |
|
1059 self.__editConfiguration() |
|
1060 |
|
1061 @pyqtSlot() |
|
1062 def __editVirtualenvConfiguration(self): |
|
1063 """ |
|
1064 Private slot to edit the configuration of the selected environment. |
|
1065 """ |
|
1066 venvName = self.environmentsComboBox.currentText() |
|
1067 if venvName: |
|
1068 self.__editConfiguration(venvName=venvName) |
|
1069 |
|
1070 def __editConfiguration(self, venvName=""): |
|
1071 """ |
|
1072 Private method to edit a configuration. |
|
1073 |
|
1074 @param venvName name of the environment to act upon |
|
1075 @type str |
|
1076 """ |
|
1077 from QScintilla.MiniEditor import MiniEditor |
|
1078 if venvName: |
|
1079 cfgFile = self.__pip.getVirtualenvConfig(venvName) |
|
1080 if not cfgFile: |
|
1081 return |
|
1082 else: |
|
1083 cfgFile = self.__pip.getUserConfig() |
|
1084 cfgDir = os.path.dirname(cfgFile) |
|
1085 if not cfgDir: |
|
1086 E5MessageBox.critical( |
|
1087 None, |
|
1088 self.tr("Edit Configuration"), |
|
1089 self.tr("""No valid configuration path determined.""" |
|
1090 """ Aborting""")) |
|
1091 return |
|
1092 |
|
1093 try: |
|
1094 if not os.path.isdir(cfgDir): |
|
1095 os.makedirs(cfgDir) |
|
1096 except OSError: |
|
1097 E5MessageBox.critical( |
|
1098 None, |
|
1099 self.tr("Edit Configuration"), |
|
1100 self.tr("""No valid configuration path determined.""" |
|
1101 """ Aborting""")) |
|
1102 return |
|
1103 |
|
1104 if not os.path.exists(cfgFile): |
|
1105 try: |
|
1106 f = open(cfgFile, "w") |
|
1107 f.write("[global]\n") |
|
1108 f.close() |
|
1109 except (IOError, OSError): |
|
1110 # ignore these |
|
1111 pass |
|
1112 |
|
1113 # check, if the destination is writeable |
|
1114 if not os.access(cfgFile, os.W_OK): |
|
1115 E5MessageBox.critical( |
|
1116 None, |
|
1117 self.tr("Edit Configuration"), |
|
1118 self.tr("""No valid configuration path determined.""" |
|
1119 """ Aborting""")) |
|
1120 return |
|
1121 |
|
1122 self.__editor = MiniEditor(cfgFile, "Properties") |
|
1123 self.__editor.show() |
|
1124 |
|
1125 def __pipConfigure(self): |
|
1126 """ |
|
1127 Private slot to open the configuration page. |
|
1128 """ |
|
1129 e5App().getObject("UserInterface").showPreferences("pipPage") |