Repository: napari/napari

Issue:
Uninstall is not happening for some plugins in mac
## 🐛 Bug

Plugins cannot be uninstalled

## To Reproduce

Steps to reproduce the behavior:

1. Instal several plugins
2. Click on Uninstall button for a bunch of them


https://user-images.githubusercontent.com/63799148/157497851-56f78252-8ad4-4794-89ac-5cd3ea4c1813.mov


## Expected behavior

Plugins should be uninstalled

## Environment

 napari: 0.4.15rc3
Platform: macOS-11.6.4-x86_64-i386-64bit
System: MacOS 11.6.4
Python: 3.9.10 | packaged by conda-forge | (main, Feb 1 2022, 21:28:27) [Clang 11.1.0 ]
Qt: 5.12.9
PySide2: 5.13.2
NumPy: 1.22.3
SciPy: 1.8.0
Dask: 2022.02.1
VisPy: 0.9.6



Discussion:

Resolving patch:
diff --git a/napari/_qt/dialogs/qt_plugin_dialog.py b/napari/_qt/dialogs/qt_plugin_dialog.py
--- a/napari/_qt/dialogs/qt_plugin_dialog.py
+++ b/napari/_qt/dialogs/qt_plugin_dialog.py
@@ -1,6 +1,7 @@
 import os
 import sys
-from importlib.metadata import metadata
+from enum import Enum, auto
+from importlib.metadata import PackageNotFoundError, metadata
 from pathlib import Path
 from typing import Callable, Dict, List, Optional, Sequence, Tuple
 
@@ -696,9 +697,16 @@ def filter(self, text: str):
                 item.setHidden(False)
 
 
+class RefreshState(Enum):
+    REFRESHING = auto()
+    OUTDATED = auto()
+    DONE = auto()
+
+
 class QtPluginDialog(QDialog):
     def __init__(self, parent=None):
         super().__init__(parent)
+        self.refresh_state = RefreshState.DONE
         self.already_installed = set()
 
         installer_type = "pip"
@@ -734,6 +742,10 @@ def closeEvent(self, event):
         event.ignore()
 
     def refresh(self):
+        if self.refresh_state != RefreshState.DONE:
+            self.refresh_state = RefreshState.OUTDATED
+            return
+        self.refresh_state = RefreshState.REFRESHING
         self.installed_list.clear()
         self.available_list.clear()
 
@@ -749,7 +761,11 @@ def refresh(self):
         def _add_to_installed(distname, enabled, npe_version=1):
             norm_name = normalized_name(distname or '')
             if distname:
-                meta = metadata(distname)
+                try:
+                    meta = metadata(distname)
+                except PackageNotFoundError:
+                    self.refresh_state = RefreshState.OUTDATED
+                    return  # a race condition has occurred and the package is uninstalled by another thread
                 if len(meta) == 0:
                     # will not add builtins.
                     return
@@ -819,6 +835,7 @@ def _add_to_installed(distname, enabled, npe_version=1):
         self.worker.yielded.connect(self._handle_yield)
         self.worker.finished.connect(self.working_indicator.hide)
         self.worker.finished.connect(self._update_count_in_label)
+        self.worker.finished.connect(self._end_refresh)
         self.worker.start()
 
     def setup_ui(self):
@@ -930,6 +947,12 @@ def _update_count_in_label(self):
             trans._("Available Plugins ({count})", count=count)
         )
 
+    def _end_refresh(self):
+        refresh_state = self.refresh_state
+        self.refresh_state = RefreshState.DONE
+        if refresh_state == RefreshState.OUTDATED:
+            self.refresh()
+
     def eventFilter(self, watched, event):
         if event.type() == QEvent.DragEnter:
             # we need to accept this event explicitly to be able
diff --git a/napari/plugins/hub.py b/napari/plugins/hub.py
--- a/napari/plugins/hub.py
+++ b/napari/plugins/hub.py
@@ -16,7 +16,7 @@
 ANACONDA_ORG = 'https://api.anaconda.org/package/{channel}/{package_name}'
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=1024)
 def hub_plugin_info(
     name: str,
     min_dev_status=3,
diff --git a/napari/plugins/pypi.py b/napari/plugins/pypi.py
--- a/napari/plugins/pypi.py
+++ b/napari/plugins/pypi.py
@@ -23,7 +23,7 @@
 )
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=1024)
 def get_packages_by_prefix(prefix: str) -> Dict[str, str]:
     """Search for packages starting with ``prefix`` on pypi.
 
@@ -48,7 +48,7 @@ def get_packages_by_prefix(prefix: str) -> Dict[str, str]:
     }
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=1024)
 def get_packages_by_classifier(classifier: str) -> List[str]:
     """Search for packages declaring ``classifier`` on PyPI
 
@@ -72,7 +72,7 @@ def get_packages_by_classifier(classifier: str) -> List[str]:
     return packages
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=1024)
 def get_package_versions(name: str) -> List[str]:
     """Get available versions of a package on pypi
 
@@ -92,7 +92,7 @@ def get_package_versions(name: str) -> List[str]:
     return re.findall(f'>{name}-(.+).tar', html.decode())
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=1024)
 def ensure_published_at_pypi(
     name: str, min_dev_status=3
 ) -> Optional[PackageMetadata]:


Test patch: