From 8460d18e2af876986f16e6da637959ad5cebf1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Wed, 10 Apr 2024 18:13:03 -0500 Subject: [PATCH 1/5] Add exception handling --- napari_plugin_manager/qt_plugin_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/napari_plugin_manager/qt_plugin_dialog.py b/napari_plugin_manager/qt_plugin_dialog.py index 3568846..69855a6 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -447,6 +447,7 @@ def addItem( project_info, versions_pypi, versions_conda = project_info_versions pkg_name = project_info.name + pkg_name = project_info.display_name # TODO: Add exception handling # don't add duplicates if ( self.findItems(pkg_name, Qt.MatchFlag.MatchFixedString) From 7e823f64fa497ea857a12bae9af3ceb6e01ec1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Mon, 15 Apr 2024 08:52:09 -0500 Subject: [PATCH 2/5] Add display name on available plugin lists --- napari_plugin_manager/qt_plugin_dialog.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/napari_plugin_manager/qt_plugin_dialog.py b/napari_plugin_manager/qt_plugin_dialog.py index 69855a6..5e5c9ed 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -437,17 +437,17 @@ def _count_visible(self) -> int: def addItem( self, project_info_versions: Tuple[ - npe2.PackageMetadata, List[str], List[str] + npe2.PackageMetadata, str, List[str], List[str] ], installed=False, plugin_name=None, enabled=True, npe_version=None, ): - project_info, versions_pypi, versions_conda = project_info_versions - + project_info, display_name, versions_pypi, versions_conda = ( + project_info_versions + ) pkg_name = project_info.name - pkg_name = project_info.display_name # TODO: Add exception handling # don't add duplicates if ( self.findItems(pkg_name, Qt.MatchFlag.MatchFixedString) @@ -456,12 +456,12 @@ def addItem( return # including summary here for sake of filtering below. - searchable_text = f"{pkg_name} {project_info.summary}" + searchable_text = f"{pkg_name} {display_name} {project_info.summary}" item = QListWidgetItem(searchable_text, self) item.version = project_info.version super().addItem(item) widg = PluginListItem( - package_name=pkg_name, + package_name=display_name or pkg_name, version=project_info.version, url=project_info.home_page, summary=project_info.summary, @@ -782,6 +782,7 @@ def _add_to_installed(distname, enabled, npe_version=1): author=meta.get('author', ''), license=meta.get('license', ''), ), + norm_name, [], [], ), @@ -1021,6 +1022,7 @@ def _add_items(self): data = self._plugin_data.pop(0) project_info, is_available_in_conda, extra_info = data + display_name = extra_info.get('display_name', project_info.name) if project_info.name in self.already_installed: self.installed_list.tag_outdated( project_info, is_available_in_conda @@ -1031,6 +1033,7 @@ def _add_items(self): self.available_list.addItem( ( project_info, + display_name, extra_info['pypi_versions'], extra_info['conda_versions'], ) From cc6b6035169898b403fa27918b95c285013297f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Sat, 27 Apr 2024 20:57:02 -0500 Subject: [PATCH 3/5] Add named tuple and refactor code to clean up names and use namedtuple --- napari_plugin_manager/qt_plugin_dialog.py | 77 +++++++++++------------ 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/napari_plugin_manager/qt_plugin_dialog.py b/napari_plugin_manager/qt_plugin_dialog.py index 5e5c9ed..169f88a 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -3,7 +3,7 @@ from enum import Enum, auto from functools import partial from pathlib import Path -from typing import Dict, List, Literal, Optional, Sequence, Tuple +from typing import Dict, List, Literal, NamedTuple, Optional, Sequence, Tuple import napari.plugins import napari.resources @@ -53,13 +53,19 @@ # Scaling factor for each list widget item when expanding. SCALE = 1.6 - CONDA = 'Conda' PYPI = 'PyPI' ON_BUNDLE = running_as_constructor_app() IS_NAPARI_CONDA_INSTALLED = is_conda_package('napari') +class ProjectInfoVersions(NamedTuple): + metadata: npe2.PackageMetadata + display_name: str + pypi_versions: List[str] + conda_versions: List[str] + + class PluginListItem(QFrame): """An entry in the plugin dialog. This will include the package name, summary, author, source, version, and buttons to update, install/uninstall, etc.""" @@ -436,18 +442,13 @@ def _count_visible(self) -> int: @Slot(tuple) def addItem( self, - project_info_versions: Tuple[ - npe2.PackageMetadata, str, List[str], List[str] - ], + project_info: ProjectInfoVersions, installed=False, plugin_name=None, enabled=True, npe_version=None, ): - project_info, display_name, versions_pypi, versions_conda = ( - project_info_versions - ) - pkg_name = project_info.name + pkg_name = project_info.metadata.name # don't add duplicates if ( self.findItems(pkg_name, Qt.MatchFlag.MatchFixedString) @@ -456,24 +457,24 @@ def addItem( return # including summary here for sake of filtering below. - searchable_text = f"{pkg_name} {display_name} {project_info.summary}" + searchable_text = f"{pkg_name} {project_info.display_name} {project_info.metadata.summary}" item = QListWidgetItem(searchable_text, self) - item.version = project_info.version + item.version = project_info.metadata.version super().addItem(item) widg = PluginListItem( - package_name=display_name or pkg_name, - version=project_info.version, - url=project_info.home_page, - summary=project_info.summary, - author=project_info.author, - license=project_info.license, + package_name=project_info.display_name or pkg_name, + version=project_info.metadata.version, + url=project_info.metadata.home_page, + summary=project_info.metadata.summary, + author=project_info.metadata.author, + license=project_info.metadata.license, parent=self, plugin_name=plugin_name, enabled=enabled, installed=installed, npe_version=npe_version, - versions_conda=versions_conda, - versions_pypi=versions_pypi, + versions_conda=project_info.conda_versions, + versions_pypi=project_info.pypi_versions, ) item.widget = widg item.npe_version = npe_version @@ -481,12 +482,12 @@ def addItem( item.setSizeHint(widg.sizeHint()) self.setItemWidget(item, widg) - if project_info.home_page: + if project_info.metadata.home_page: import webbrowser # FIXME: Partial may lead to leak memory when connecting to Qt signals. widg.plugin_name.clicked.connect( - partial(webbrowser.open, project_info.home_page) + partial(webbrowser.open, project_info.metadata.home_page) ) # FIXME: Partial may lead to leak memory when connecting to Qt signals. @@ -621,9 +622,7 @@ def handle_action( widget.setProperty("current_job_id", None) @Slot(npe2.PackageMetadata, bool) - def tag_outdated( - self, project_info: npe2.PackageMetadata, is_available: bool - ): + def tag_outdated(self, metadata: npe2.PackageMetadata, is_available: bool): """Determines if an installed plugin is up to date with the latest version. If it is not, the latest version will be displayed on the update button. """ @@ -631,10 +630,10 @@ def tag_outdated( return for item in self.findItems( - project_info.name, Qt.MatchFlag.MatchStartsWith + metadata.name, Qt.MatchFlag.MatchStartsWith ): current = item.version - latest = project_info.version + latest = metadata.version if parse_version(current) >= parse_version(latest): continue if hasattr(item, 'outdated'): @@ -649,7 +648,7 @@ def tag_outdated( trans._("update (v{latest})", latest=latest) ) - def tag_unavailable(self, project_info: npe2.PackageMetadata): + def tag_unavailable(self, metadata: npe2.PackageMetadata): """ Tag list items as unavailable for install with conda-forge. @@ -657,7 +656,7 @@ def tag_unavailable(self, project_info: npe2.PackageMetadata): icon with a hover tooltip. """ for item in self.findItems( - project_info.name, Qt.MatchFlag.MatchStartsWith + metadata.name, Qt.MatchFlag.MatchStartsWith ): widget = self.itemWidget(item) widget.show_warning( @@ -772,7 +771,7 @@ def _add_to_installed(distname, enabled, npe_version=1): meta = {} self.installed_list.addItem( - ( + ProjectInfoVersions( npe2.PackageMetadata( metadata_version="1.0", name=norm_name, @@ -1021,25 +1020,23 @@ def _add_items(self): return data = self._plugin_data.pop(0) - project_info, is_available_in_conda, extra_info = data - display_name = extra_info.get('display_name', project_info.name) - if project_info.name in self.already_installed: - self.installed_list.tag_outdated( - project_info, is_available_in_conda - ) + metadata, is_available_in_conda, extra_info = data + display_name = extra_info.get('display_name', metadata.name) + if metadata.name in self.already_installed: + self.installed_list.tag_outdated(metadata, is_available_in_conda) else: - if project_info.name not in self.available_set: - self.available_set.add(project_info.name) + if metadata.name not in self.available_set: + self.available_set.add(metadata.name) self.available_list.addItem( - ( - project_info, + ProjectInfoVersions( + metadata, display_name, extra_info['pypi_versions'], extra_info['conda_versions'], ) ) if ON_BUNDLE and not is_available_in_conda: - self.available_list.tag_unavailable(project_info) + self.available_list.tag_unavailable(metadata) self.filter() From 4d27debc51b0b89d63ea7ab1af266b89cb12bf99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Sat, 27 Apr 2024 21:08:35 -0500 Subject: [PATCH 4/5] Add package name also so display name and pakcage name are always shown --- napari_plugin_manager/qt_plugin_dialog.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/napari_plugin_manager/qt_plugin_dialog.py b/napari_plugin_manager/qt_plugin_dialog.py index 169f88a..6464ae2 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -73,6 +73,7 @@ class PluginListItem(QFrame): def __init__( self, package_name: str, + display_name: str, version: str = '', url: str = '', summary: str = '', @@ -92,7 +93,12 @@ def __init__( self._versions_conda = versions_conda self._versions_pypi = versions_pypi self.setup_ui(enabled) - self.plugin_name.setText(package_name) + if package_name == display_name: + name = package_name + else: + name = f"{display_name} ({package_name})" + + self.plugin_name.setText(name) if len(versions_pypi) > 0: self._populate_version_dropdown(PYPI) @@ -462,7 +468,8 @@ def addItem( item.version = project_info.metadata.version super().addItem(item) widg = PluginListItem( - package_name=project_info.display_name or pkg_name, + package_name=pkg_name, + display_name=project_info.display_name, version=project_info.metadata.version, url=project_info.metadata.home_page, summary=project_info.metadata.summary, From 31944fac16c3460fc2719361226f7c51fbb3b13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Thu, 23 May 2024 18:35:02 -0500 Subject: [PATCH 5/5] Use ClickableLabel (QLabel subclass) instead of button to style plugin name --- napari_plugin_manager/qt_plugin_dialog.py | 5 +++-- napari_plugin_manager/qt_widgets.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 napari_plugin_manager/qt_widgets.py diff --git a/napari_plugin_manager/qt_plugin_dialog.py b/napari_plugin_manager/qt_plugin_dialog.py index 6464ae2..dd9d9a8 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -47,6 +47,7 @@ InstallerQueue, InstallerTools, ) +from napari_plugin_manager.qt_widgets import ClickableLabel from napari_plugin_manager.utils import is_conda_package # TODO: add error icon and handle pip install errors @@ -96,7 +97,7 @@ def __init__( if package_name == display_name: name = package_name else: - name = f"{display_name} ({package_name})" + name = f"{display_name} ({package_name})" self.plugin_name.setText(name) @@ -200,7 +201,7 @@ def setup_ui(self, enabled=True): self.enabled_checkbox.setMinimumSize(QSize(20, 0)) self.enabled_checkbox.setText("") self.row1.addWidget(self.enabled_checkbox) - self.plugin_name = QPushButton(self) + self.plugin_name = ClickableLabel(self) # To style content # Do not want to highlight on hover unless there is a website. if self.url and self.url != 'UNKNOWN': self.plugin_name.setObjectName('plugin_name_web') diff --git a/napari_plugin_manager/qt_widgets.py b/napari_plugin_manager/qt_widgets.py new file mode 100644 index 0000000..f115071 --- /dev/null +++ b/napari_plugin_manager/qt_widgets.py @@ -0,0 +1,14 @@ +from qtpy.QtCore import Signal +from qtpy.QtGui import QMouseEvent +from qtpy.QtWidgets import QLabel + + +class ClickableLabel(QLabel): + clicked = Signal() + + def __init__(self, parent=None): + super().__init__(parent=parent) + + def mouseReleaseEvent(self, event: QMouseEvent): + super().mouseReleaseEvent(event) + self.clicked.emit()