29 """ |
34 """ |
30 ShowProcessGeneralMode = 0 |
35 ShowProcessGeneralMode = 0 |
31 ShowProcessClassifiersMode = 1 |
36 ShowProcessClassifiersMode = 1 |
32 ShowProcessEntryPointsMode = 2 |
37 ShowProcessEntryPointsMode = 2 |
33 ShowProcessFilesListMode = 3 |
38 ShowProcessFilesListMode = 3 |
|
39 |
|
40 SearchStopwords = { |
|
41 "a", "and", "are", "as", "at", "be", "but", "by", |
|
42 "for", "if", "in", "into", "is", "it", |
|
43 "no", "not", "of", "on", "or", "such", |
|
44 "that", "the", "their", "then", "there", "these", |
|
45 "they", "this", "to", "was", "will", |
|
46 } |
|
47 SearchVersionRole = Qt.UserRole + 1 |
34 |
48 |
35 def __init__(self, parent=None): |
49 def __init__(self, parent=None): |
36 """ |
50 """ |
37 Constructor |
51 Constructor |
38 |
52 |
407 |
445 |
408 def __updateSearchActionButtons(self): |
446 def __updateSearchActionButtons(self): |
409 """ |
447 """ |
410 Private method to update the action button states of the search widget. |
448 Private method to update the action button states of the search widget. |
411 """ |
449 """ |
412 # TODO: adjust this like search dialog |
450 installEnable = ( |
413 enable = len(self.searchResultList.selectedItems()) == 1 |
451 len(self.searchResultList.selectedItems()) > 0 and |
414 self.installButton.setEnabled( |
452 self.environmentsComboBox.currentIndex() > 0 and |
415 enable and self.environmentsComboBox.currentIndex() > 0) |
453 self.__isPipAvailable() |
|
454 ) |
|
455 self.installButton.setEnabled(installEnable) |
|
456 self.installUserSiteButton.setEnabled(installEnable) |
|
457 |
416 self.showDetailsButton.setEnabled( |
458 self.showDetailsButton.setEnabled( |
417 enable and bool(self.searchResultList.selectedItems()[0].parent())) |
459 len(self.searchResultList.selectedItems()) == 1 and |
|
460 self.__isPipAvailable() |
|
461 ) |
|
462 |
|
463 def __updateSearchButton(self): |
|
464 """ |
|
465 Private method to update the state of the search button. |
|
466 """ |
|
467 self.searchButton.setEnabled( |
|
468 bool(self.searchEdit.text()) and |
|
469 self.__isPipAvailable() |
|
470 ) |
418 |
471 |
419 @pyqtSlot(bool) |
472 @pyqtSlot(bool) |
420 def on_searchToggleButton_toggled(self, checked): |
473 def on_searchToggleButton_toggled(self, checked): |
421 """ |
474 """ |
422 Private slot to togle the search widget. |
475 Private slot to togle the search widget. |
429 if checked: |
482 if checked: |
430 self.searchEdit.setFocus(Qt.OtherFocusReason) |
483 self.searchEdit.setFocus(Qt.OtherFocusReason) |
431 self.searchEdit.selectAll() |
484 self.searchEdit.selectAll() |
432 |
485 |
433 self.__updateSearchActionButtons() |
486 self.__updateSearchActionButtons() |
|
487 self.__updateSearchButton() |
|
488 |
|
489 @pyqtSlot(str) |
|
490 def on_searchEdit_textChanged(self, txt): |
|
491 """ |
|
492 Private slot handling a change of the search term. |
|
493 |
|
494 @param txt search term |
|
495 @type str |
|
496 """ |
|
497 self.__updateSearchButton() |
|
498 |
|
499 @pyqtSlot() |
|
500 def on_searchButton_clicked(self): |
|
501 """ |
|
502 Private slot handling a press of the search button. |
|
503 """ |
|
504 self.__search() |
|
505 |
|
506 @pyqtSlot() |
|
507 def on_searchResultList_itemSelectionChanged(self): |
|
508 """ |
|
509 Private slot handling changes of the search result selection. |
|
510 """ |
|
511 self.__updateSearchActionButtons() |
|
512 |
|
513 def __search(self): |
|
514 """ |
|
515 Private method to perform the search. |
|
516 """ |
|
517 self.searchResultList.clear() |
|
518 self.searchInfoLabel.clear() |
|
519 |
|
520 self.searchButton.setEnabled(False) |
|
521 QApplication.setOverrideCursor(Qt.WaitCursor) |
|
522 QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) |
|
523 |
|
524 self.__query = [term for term in self.searchEdit.text().strip().split() |
|
525 if term not in self.SearchStopwords] |
|
526 self.__client.call( |
|
527 "search", |
|
528 ({"name": self.__query, "summary": self.__query}, "or"), |
|
529 self.__processSearchResult, |
|
530 self.__searchError |
|
531 ) |
|
532 |
|
533 def __processSearchResult(self, data): |
|
534 """ |
|
535 Private method to process the search result data from PyPI. |
|
536 |
|
537 @param data result data with hits in the first element |
|
538 @type tuple |
|
539 """ |
|
540 if data: |
|
541 packages = self.__transformHits(data[0]) |
|
542 if packages: |
|
543 self.searchInfoLabel.setText( |
|
544 self.tr("%n package(s) found.", "", len(packages))) |
|
545 wrapper = textwrap.TextWrapper(width=80) |
|
546 count = 0 |
|
547 total = 0 |
|
548 for package in packages: |
|
549 itm = QTreeWidgetItem( |
|
550 self.searchResultList, [ |
|
551 package['name'].strip(), |
|
552 "{0:4d}".format(package['score']), |
|
553 "\n".join([ |
|
554 wrapper.fill(line) for line in |
|
555 package['summary'].strip().splitlines() |
|
556 ]) |
|
557 ]) |
|
558 itm.setData(0, self.SearchVersionRole, package['version']) |
|
559 count += 1 |
|
560 total += 1 |
|
561 if count == 100: |
|
562 count = 0 |
|
563 QApplication.processEvents() |
|
564 else: |
|
565 QApplication.restoreOverrideCursor() |
|
566 E5MessageBox.warning( |
|
567 self, |
|
568 self.tr("Search PyPI"), |
|
569 self.tr("""<p>The package search did not return""" |
|
570 """ anything.</p>""")) |
|
571 self.searchInfoLabel.setText( |
|
572 self.tr("""<p>The package search did not return""" |
|
573 """ anything.</p>""")) |
|
574 else: |
|
575 QApplication.restoreOverrideCursor() |
|
576 E5MessageBox.warning( |
|
577 self, |
|
578 self.tr("Search PyPI"), |
|
579 self.tr("""<p>The package search did not return anything.""" |
|
580 """</p>""")) |
|
581 self.searchInfoLabel.setText( |
|
582 self.tr("""<p>The package search did not return anything.""" |
|
583 """</p>""")) |
|
584 |
|
585 header = self.searchResultList.header() |
|
586 self.searchResultList.sortItems(1, Qt.DescendingOrder) |
|
587 header.setStretchLastSection(False) |
|
588 header.resizeSections(QHeaderView.ResizeToContents) |
|
589 headerSize = 0 |
|
590 for col in range(header.count()): |
|
591 headerSize += header.sectionSize(col) |
|
592 if headerSize < header.width(): |
|
593 header.setStretchLastSection(True) |
|
594 |
|
595 self.__finishSearch() |
|
596 |
|
597 def __finishSearch(self): |
|
598 """ |
|
599 Private slot performing the search finishing actions. |
|
600 """ |
|
601 QApplication.restoreOverrideCursor() |
|
602 |
|
603 self.__updateSearchActionButtons() |
|
604 self.__updateSearchButton() |
|
605 |
|
606 self.searchEdit.setFocus(Qt.OtherFocusReason) |
|
607 |
|
608 def __searchError(self, errorCode, errorString): |
|
609 """ |
|
610 Private method handling a search error. |
|
611 |
|
612 @param errorCode code of the error |
|
613 @type int |
|
614 @param errorString error message |
|
615 @type str |
|
616 """ |
|
617 self.__finish() |
|
618 E5MessageBox.warning( |
|
619 self, |
|
620 self.tr("Search PyPI"), |
|
621 self.tr("""<p>The package search failed.</p><p>Reason: {0}</p>""") |
|
622 .format(errorString)) |
|
623 self.searchInfoLabel.setText(self.tr("Error: {0}").format(errorString)) |
|
624 |
|
625 def __transformHits(self, hits): |
|
626 """ |
|
627 Private method to convert the list returned from pypi into a |
|
628 packages list. |
|
629 |
|
630 @param hits list returned from pypi |
|
631 @type list of dict |
|
632 @return list of packages |
|
633 @rtype list of dict |
|
634 """ |
|
635 # we only include the record with the highest score |
|
636 packages = {} |
|
637 for hit in hits: |
|
638 name = hit['name'].strip() |
|
639 summary = (hit['summary'] or "").strip() |
|
640 version = hit['version'].strip() |
|
641 score = self.__score(name, summary) |
|
642 # cleanup the summary |
|
643 if summary in ["UNKNOWN", "."]: |
|
644 summary = "" |
|
645 |
|
646 if name not in packages: |
|
647 packages[name] = { |
|
648 'name': name, |
|
649 'summary': summary, |
|
650 'version': [version.strip()], |
|
651 'score': score} |
|
652 else: |
|
653 if score > packages[name]['score']: |
|
654 packages[name]['score'] = score |
|
655 packages[name]['summary'] = summary |
|
656 packages[name]['version'].append(version.strip()) |
|
657 |
|
658 return list(packages.values()) |
|
659 |
|
660 def __score(self, name, summary): |
|
661 """ |
|
662 Private method to calculate some score for a search result. |
|
663 |
|
664 @param name name of the returned package |
|
665 @type str |
|
666 @param summary summary text for the package |
|
667 @type str |
|
668 @return score value |
|
669 @rtype int |
|
670 """ |
|
671 score = 0 |
|
672 for queryTerm in self.__query: |
|
673 if queryTerm.lower() in name.lower(): |
|
674 score += 4 |
|
675 if queryTerm.lower() == name.lower(): |
|
676 score += 4 |
|
677 |
|
678 if queryTerm.lower() in summary.lower(): |
|
679 if QRegExp(r'\b{0}\b'.format(QRegExp.escape(queryTerm)), |
|
680 Qt.CaseInsensitive).indexIn(summary) != -1: |
|
681 # word match gets even higher score |
|
682 score += 2 |
|
683 else: |
|
684 score += 1 |
|
685 |
|
686 return score |
434 |
687 |
435 ####################################################################### |
688 ####################################################################### |
436 ## Menu related methods below |
689 ## Menu related methods below |
437 ####################################################################### |
690 ####################################################################### |
438 |
691 |