diff -r 8f69edb4ad73 -r 1f7d52f024b1 src/eric7/PluginManager/PluginRepositoryDialog.py --- a/src/eric7/PluginManager/PluginRepositoryDialog.py Thu Nov 16 13:58:04 2023 +0100 +++ b/src/eric7/PluginManager/PluginRepositoryDialog.py Thu Nov 16 15:56:12 2023 +0100 @@ -7,11 +7,14 @@ Module implementing a dialog showing the available plugins. """ +import enum import glob import os import re import zipfile +from collections import ChainMap, defaultdict + from PyQt6.QtCore import ( QCoreApplication, QFile, @@ -66,6 +69,18 @@ from .Ui_PluginRepositoryDialog import Ui_PluginRepositoryDialog +class PluginStatus(enum.Enum): + """ + Class defining the various plugin status. + """ + + UpToDate = 0 + New = 1 + LocalUpdate = 2 + RemoteUpdate = 3 + Error = 4 + + class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog): """ Class implementing a dialog showing the available plugins. @@ -81,12 +96,6 @@ FilenameRole = Qt.ItemDataRole.UserRole + 2 AuthorRole = Qt.ItemDataRole.UserRole + 3 - PluginStatusUpToDate = 0 - PluginStatusNew = 1 - PluginStatusLocalUpdate = 2 - PluginStatusRemoteUpdate = 3 - PluginStatusError = 4 - def __init__(self, pluginManager, integrated=False, parent=None): """ Constructor @@ -111,6 +120,14 @@ self.__integratedWidget = integrated + self.__statusTranslations = { + "stable": self.tr("Stable"), + "unstable": self.tr("Unstable"), + "obsolete": self.tr("Obsolete"), + "unknown": self.tr("Unknown"), + } + self.__initHeaderItemsCache() + if self.__integratedWidget: self.layout().setContentsMargins(0, 3, 0, 0) @@ -272,7 +289,8 @@ """ Private slot to handle the click of a button of the button box. - @param button reference to the button pressed (QAbstractButton) + @param button reference to the button pressed + @type QAbstractButton """ if button == self.__updateButton: self.__updateList() @@ -306,8 +324,10 @@ """ Private method to format the description. - @param lines lines of the description (list of strings) - @return formatted description (string) + @param lines lines of the description + @type list of str + @return formatted description + @rtype str """ # remove empty line at start and end newlines = lines[:] @@ -333,6 +353,7 @@ @param url URL to be modified @type str @param newScheme scheme to be set for the given URL + @type str @return modified URL @rtype str """ @@ -350,7 +371,8 @@ """ Private slot to show the context menu. - @param pos position to show the menu (QPoint) + @param pos position to show the menu + @type QPoint """ self.__hideAct.setEnabled( self.repositoryList.currentItem() is not None @@ -365,8 +387,10 @@ """ Private slot to handle the change of the current item. - @param current reference to the new current item (QTreeWidgetItem) - @param previous reference to the old current item (QTreeWidgetItem) + @param current reference to the new current item + @type QTreeWidgetItem + @param previous reference to the old current item + @type QTreeWidgetItem """ if self.__repositoryMissing or current is None: self.descriptionEdit.clear() @@ -389,16 +413,20 @@ def __selectedItems(self): """ - Private method to get all selected items without the toplevel ones. + Private method to get all selected items without the status and category items. - @return list of selected items (list) + @return list of selected items without header items + @rtype list of QTreeWidgetItem """ - ql = self.repositoryList.selectedItems() - for index in range(self.repositoryList.topLevelItemCount()): - ti = self.repositoryList.topLevelItem(index) - if ti in ql: - ql.remove(ti) - return ql + selectedItems = [] + allCategoryItems = ChainMap(*self.__categoryItems.values()) + for itm in self.repositoryList.selectedItems(): + if ( + itm not in self.__statusItems.values() + and itm not in allCategoryItems.values() + ): + selectedItems.append(itm) + return selectedItems @pyqtSlot() def on_repositoryList_itemSelectionChanged(self): @@ -426,21 +454,14 @@ url = url.replace("https://", "http://") self.__pluginManager.downLoadRepositoryFile(url=url) - def __downloadRepositoryFileDone(self, status, filename): # noqa: U100 - """ - Private method called after the repository file was downloaded. - - @param status flaging indicating a successful download (boolean) - @param filename full path of the downloaded file (string) - """ - self.__populateList() - def __downloadPluginDone(self, status, filename): """ Private method called, when the download of a plugin is finished. - @param status flag indicating a successful download (boolean) - @param filename full path of the downloaded file (string) + @param status flag indicating a successful download + @type bool + @param filename full path of the downloaded file + @type str """ if status: self.__pluginsDownloaded.append(filename) @@ -480,21 +501,15 @@ if Preferences.getPluginManager("ForceHttpPluginDownload") else self.repositoryUrlEdit.text().split("//", 1)[0] ) - for itm in self.repositoryList.selectedItems(): - if itm not in [ - self.__stableItem, - self.__unstableItem, - self.__unknownItem, - self.__obsoleteItem, - ]: - url = self.__changeScheme( - itm.data(0, PluginRepositoryWidget.UrlRole), newScheme - ) - filename = os.path.join( - Preferences.getPluginManager("DownloadPath"), - itm.data(0, PluginRepositoryWidget.FilenameRole), - ) - self.__pluginsToDownload.append((url, filename)) + for itm in self.__selectedItems(): + url = self.__changeScheme( + itm.data(0, PluginRepositoryWidget.UrlRole), newScheme + ) + filename = os.path.join( + Preferences.getPluginManager("DownloadPath"), + itm.data(0, PluginRepositoryWidget.FilenameRole), + ) + self.__pluginsToDownload.append((url, filename)) if self.__pluginsToDownload: self.__downloadPlugin() @@ -532,20 +547,30 @@ """ Private method to resort the tree. """ - self.repositoryList.sortItems( - self.repositoryList.sortColumn(), - self.repositoryList.header().sortIndicatorOrder(), - ) + if self.__integratedWidget: + self.repositoryList.sortItems( + self.repositoryList.sortColumn(), + Qt.SortOrder.AscendingOrder, + ) + else: + self.repositoryList.sortItems( + self.repositoryList.sortColumn(), + self.repositoryList.header().sortIndicatorOrder(), + ) + + def __initHeaderItemsCache(self): + """ + Private method to initialize the cache variables for the header items. + """ + self.__statusItems = defaultdict(lambda: None) + self.__categoryItems = defaultdict(dict) def __populateList(self): """ Private method to populate the list of available plugins. """ + self.__initHeaderItemsCache() self.repositoryList.clear() - self.__stableItem = None - self.__unstableItem = None - self.__unknownItem = None - self.__obsoleteItem = None self.__newItems = 0 self.__updateLocalItems = 0 @@ -604,9 +629,12 @@ """ Private slot to download the given file. - @param url URL for the download (string) - @param filename local name of the file (string) + @param url URL for the download + @type str + @param filename local name of the file + @type str @param doneMethod method to be called when done + @type function """ if self.__online: self.__updateButton.setEnabled(False) @@ -652,7 +680,7 @@ @param fileName local name of the file @type str @param doneMethod method to be called when done - @type func + @type function """ self.__updateButton.setEnabled(True) if not self.__integratedWidget: @@ -720,69 +748,87 @@ """ Private slot to show the download progress. - @param done number of bytes downloaded so far (integer) - @param total total bytes to be downloaded (integer) + @param done number of bytes downloaded so far + @type int + @param total total bytes to be downloaded + @type int """ if total: self.downloadProgress.setMaximum(total) self.downloadProgress.setValue(done) def addEntry( - self, name, short, description, url, author, version, filename, status + self, + name, + short, + description, + url, + author, + version, + filename, + status, + category, ): """ Public method to add an entry to the list. - @param name data for the name field (string) - @param short data for the short field (string) - @param description data for the description field (list of strings) - @param url data for the url field (string) - @param author data for the author field (string) - @param version data for the version field (string) - @param filename data for the filename field (string) - @param status status of the plugin (string [stable, unstable, unknown]) + @param name data for the name field + @type str + @param short data for the short field + @type str + @param description data for the description field + @type list of str + @param url data for the url field + @type str + @param author data for the author field + @type str + @param version data for the version field + @type str + @param filename data for the filename field + @type str + @param status status of the plugin (one of stable, unstable, unknown) + @type str + @param category category designation of the plugin + @type str """ pluginName = filename.rsplit("-", 1)[0] if pluginName in self.__hiddenPlugins: return - if status == "stable": - if self.__stableItem is None: - self.__stableItem = QTreeWidgetItem( - self.repositoryList, [self.tr("Stable")] - ) - self.__stableItem.setExpanded(True) - parent = self.__stableItem - elif status == "unstable": - if self.__unstableItem is None: - self.__unstableItem = QTreeWidgetItem( - self.repositoryList, [self.tr("Unstable")] - ) - self.__unstableItem.setExpanded(True) - parent = self.__unstableItem - elif status == "obsolete": - if self.__obsoleteItem is None: - self.__obsoleteItem = QTreeWidgetItem( - self.repositoryList, [self.tr("Obsolete")] - ) - self.__obsoleteItem.setExpanded(True) - parent = self.__obsoleteItem - else: - if self.__unknownItem is None: - self.__unknownItem = QTreeWidgetItem( - self.repositoryList, [self.tr("Unknown")] - ) - self.__unknownItem.setExpanded(True) - parent = self.__unknownItem + # 1. determine and create the status item + statusItem = self.__statusItems[status] + if statusItem is None: + statusItem = QTreeWidgetItem( + self.repositoryList, + [ + self.__statusTranslations.get( + status, self.__statusTranslations["unknown"] + ) + ], + ) + statusItem.setExpanded(True) + statusItem.setFirstColumnSpanned(True) + self.__statusItems[status] = statusItem + # 2. determine and create the category item + try: + categoryItem = self.__categoryItems[status][category] + except KeyError: + # create the category item + categoryItem = QTreeWidgetItem(statusItem, [category]) + categoryItem.setExpanded(True) + categoryItem.setFirstColumnSpanned(True) + self.__categoryItems[status][category] = categoryItem + + # 3. create the plugin item if self.__integratedWidget: entryFormat = "<b>{0}</b> - Version: <i>{1}</i><br/>{2}" - itm = QTreeWidgetItem(parent) + itm = QTreeWidgetItem(categoryItem) itm.setFirstColumnSpanned(True) label = QLabel(entryFormat.format(name, version, short)) self.repositoryList.setItemWidget(itm, 0, label) else: - itm = QTreeWidgetItem(parent, [name, version, short]) + itm = QTreeWidgetItem(categoryItem, [name, version, short]) itm.setData(0, PluginRepositoryWidget.UrlRole, url) itm.setData(0, PluginRepositoryWidget.FilenameRole, filename) @@ -791,22 +837,22 @@ iconColumn = 0 if self.__integratedWidget else 1 updateStatus = self.__updateStatus(filename, version) - if updateStatus == PluginRepositoryWidget.PluginStatusUpToDate: + if updateStatus == PluginStatus.UpToDate: itm.setIcon(iconColumn, EricPixmapCache.getIcon("empty")) itm.setToolTip(iconColumn, self.tr("up-to-date")) - elif updateStatus == PluginRepositoryWidget.PluginStatusNew: + elif updateStatus == PluginStatus.New: itm.setIcon(iconColumn, EricPixmapCache.getIcon("download")) itm.setToolTip(iconColumn, self.tr("new download available")) self.__newItems += 1 - elif updateStatus == PluginRepositoryWidget.PluginStatusLocalUpdate: + elif updateStatus == PluginStatus.LocalUpdate: itm.setIcon(iconColumn, EricPixmapCache.getIcon("updateLocal")) itm.setToolTip(iconColumn, self.tr("update installable")) self.__updateLocalItems += 1 - elif updateStatus == PluginRepositoryWidget.PluginStatusRemoteUpdate: + elif updateStatus == PluginStatus.RemoteUpdate: itm.setIcon(iconColumn, EricPixmapCache.getIcon("updateRemote")) itm.setToolTip(iconColumn, self.tr("updated download available")) self.__updateRemoteItems += 1 - elif updateStatus == PluginRepositoryWidget.PluginStatusError: + elif updateStatus == PluginStatus.Error: itm.setIcon(iconColumn, EricPixmapCache.getIcon("warning")) itm.setToolTip(iconColumn, self.tr("error determining status")) @@ -814,11 +860,13 @@ """ Private method to check the given archive update status. - @param filename data for the filename field (string) - @param version data for the version field (string) - @return plug-in update status (integer, one of PluginStatusNew, - PluginStatusUpToDate, PluginStatusLocalUpdate, - PluginStatusRemoteUpdate) + @param filename data for the filename field + @type str + @param version data for the version field + @type str + @return plug-in update status + @rtype int (one of PluginStatusNew, PluginStatusUpToDate, + PluginStatusLocalUpdate, PluginStatusRemoteUpdate) """ archive = os.path.join(Preferences.getPluginManager("DownloadPath"), filename) @@ -830,23 +878,23 @@ pluginName = filename.rsplit("-", 1)[0] pluginDetails = self.__pluginManager.getPluginDetails(pluginName) if pluginDetails is None or pluginDetails["moduleName"] != pluginName: - return PluginRepositoryWidget.PluginStatusNew + return PluginStatus.New if pluginDetails["error"]: - return PluginRepositoryWidget.PluginStatusError + return PluginStatus.Error pluginVersionTuple = Globals.versionToTuple(pluginDetails["version"])[:3] versionTuple = Globals.versionToTuple(version)[:3] if pluginVersionTuple < versionTuple: - return PluginRepositoryWidget.PluginStatusRemoteUpdate + return PluginStatus.RemoteUpdate else: - return PluginRepositoryWidget.PluginStatusUpToDate + return PluginStatus.UpToDate # check, if the archive exists if not os.path.exists(archive): - return PluginRepositoryWidget.PluginStatusRemoteUpdate + return PluginStatus.RemoteUpdate # check, if the archive is a valid zip file if not zipfile.is_zipfile(archive): - return PluginRepositoryWidget.PluginStatusRemoteUpdate + return PluginStatus.RemoteUpdate zipFile = zipfile.ZipFile(archive, "r") try: @@ -860,23 +908,25 @@ pluginName = filename.rsplit("-", 1)[0] pluginDetails = self.__pluginManager.getPluginDetails(pluginName) if pluginDetails is None: - return PluginRepositoryWidget.PluginStatusLocalUpdate + return PluginStatus.LocalUpdate if ( Globals.versionToTuple(pluginDetails["version"])[:3] < Globals.versionToTuple(version)[:3] ): - return PluginRepositoryWidget.PluginStatusLocalUpdate + return PluginStatus.LocalUpdate else: - return PluginRepositoryWidget.PluginStatusUpToDate + return PluginStatus.UpToDate else: - return PluginRepositoryWidget.PluginStatusRemoteUpdate + return PluginStatus.RemoteUpdate def __sslErrors(self, reply, errors): """ Private slot to handle SSL errors. - @param reply reference to the reply object (QNetworkReply) - @param errors list of SSL errors (list of QSslError) + @param reply reference to the reply object + @type QNetworkReply + @param errors list of SSL errors + @type list of QSslError """ ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0] if ignored == EricSslErrorState.NOT_IGNORED: @@ -886,7 +936,8 @@ """ Public method to get the list of recently downloaded plugin files. - @return list of plugin filenames (list of strings) + @return list of plugin filenames + @rtype list of str """ return self.__pluginsDownloaded @@ -896,7 +947,8 @@ Private slot to set the read only status of the repository URL line edit. - @param checked state of the push button (boolean) + @param checked state of the push button + @type bool """ self.repositoryUrlEdit.setReadOnly(not checked) @@ -944,7 +996,8 @@ """ Private method to check, if there are any hidden plug-ins. - @return flag indicating the presence of hidden plug-ins (boolean) + @return flag indicating the presence of hidden plug-ins + @rtype bool """ return bool(self.__hiddenPlugins) @@ -953,7 +1006,7 @@ Private method to store the list of hidden plug-ins to the settings. @param hideList list of plug-ins to add to the list of hidden ones - (list of string) + @type list of str """ if hideList: self.__hiddenPlugins.extend( @@ -1010,7 +1063,8 @@ """ Public method to get the list of recently downloaded plugin files. - @return list of plugin filenames (list of strings) + @return list of plugin filenames + @rtype list of str """ return self.cw.getDownloadedPlugins() @@ -1024,7 +1078,8 @@ """ Constructor - @param parent reference to the parent widget (QWidget) + @param parent reference to the parent widget + @type QWidget """ super().__init__(parent) self.cw = PluginRepositoryWidget(None, parent=self) @@ -1076,19 +1131,37 @@ pluginsRegister = [] # list of plug-ins contained in the repository def registerPlugin( - name, short, description, url, author, version, filename, status # noqa: U100 + name, # noqa: U100 + short, # noqa: U100 + description, # noqa: U100 + url, + author, # noqa: U100 + version, # noqa: U100 + filename, # noqa: U100 + status, # noqa: U100 + category, # noqa: U100 ): """ Method to register a plug-in's data. - @param name data for the name field (string) - @param short data for the short field (string) - @param description data for the description field (list of strings) - @param url data for the url field (string) - @param author data for the author field (string) - @param version data for the version field (string) - @param filename data for the filename field (string) - @param status status of the plugin (string [stable, unstable, unknown]) + @param name data for the name field + @type str + @param short data for the short field + @type str + @param description data for the description field + @type list of str + @param url data for the url field + @type str + @param author data for the author field + @type str + @param version data for the version field + @type str + @param filename data for the filename field + @type str + @param status status of the plugin (one of stable, unstable, unknown) + @type str + @param category category designation of the plugin + @type str """ pluginName = os.path.splitext(url.rsplit("/", 1)[1])[0] if pluginName not in pluginsRegister: