Repository: napari/napari

Issue:
Plugin update in bundle does not work
## 🐛 Bug

<!-- A clear and concise description of what the bug is. -->

To update the plugin I need to uninstall it and then install it again. Clicking update trigger a processing event but ends without a result

![Zrzut ekranu z 2022-04-26 18-44-25](https://user-images.githubusercontent.com/3826210/165351187-700dfbce-e556-48e2-9750-50b835b7a2d5.png)



## To Reproduce

Steps to reproduce the behavior:

1. install bundled (new with conda) napari
2. install old version plugin
3. try to update it from GUI 

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

Update should work

<!-- A clear and concise description of what you expected to happen. -->

## Environment
```
napari: 0.4.15
Platform: Linux-5.13.0-40-generic-x86_64-with-glibc2.34
System: Ubuntu 21.10
Python: 3.9.10 | packaged by conda-forge | (main, Feb 1 2022, 21:24:11) [GCC 9.4.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

OpenGL:
- GL version: 4.6.0 NVIDIA 510.60.02
- MAX_TEXTURE_SIZE: 32768

Screens:
- screen 1: resolution 2560x1440, scale 1.0
- screen 2: resolution 1920x1080, scale 1.0
- screen 3: resolution 1920x1080, scale 1.0

Plugins:
- PartSeg Analysis project reader: 0.14.0
- PartSeg Image reader: 0.14.0
- PartSeg Image tiff writer: 0.14.0
- PartSeg Mask project reader: 0.14.0
- PartSeg Mask project writer: 0.14.0
- PartSeg Masked Tiff reader: 0.14.0
- PartSeg widgets: 0.14.0
- console: 0.0.4
- scikit-image: 0.4.15
- svg: 0.1.5
```
## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
@jaimergp @goanpeca have you observed this too? any ideas on a fix?
Hm, I am curious where 0.1.6 is coming from if `napari-svg` only has [0.1.5](https://anaconda.org/conda-forge/napari-svg/files) on conda-forge?
But has 0.1.6 on pypi.

I also observe same error with PartSeg update after new release land on conda. 

I need to uninstall old one and install new because update does not work. 
I thought the bundle only accepted conda packages at this point, so PyPI updates shouldn't be visible? @goanpeca can you help clarify?
Ok, nevermind, I see that part of the UI now. It just takes quite a while to show up.

<img width="1091" alt="image" src="https://user-images.githubusercontent.com/2559438/166450844-897f6b66-2f60-4a9d-bdf7-353153b5a258.png">


I can try reproducing now!
These are the full logs (verbosity=3, lots of noise!) for the update operation.

[issue-4427.osx-arm64.txt](https://github.com/napari/napari/files/8610694/issue-4427.osx-arm64.txt)

You can see the mamba operation is successful. After cleaning up:

```

                  __    __    __    __
                 /  \  /  \  /  \  /  \
                /    \/    \/    \/    \
███████████████/  /██/  /██/  /██/  /████████████████████████
              /  / \   / \   / \   / \  \____
             /  /   \_/   \_/   \_/   \    o \__,
            / _/                       \_____/  `
            |/
        ███╗   ███╗ █████╗ ███╗   ███╗██████╗  █████╗
        ████╗ ████║██╔══██╗████╗ ████║██╔══██╗██╔══██╗
        ██╔████╔██║███████║██╔████╔██║██████╔╝███████║
        ██║╚██╔╝██║██╔══██║██║╚██╔╝██║██╔══██╗██╔══██║
        ██║ ╚═╝ ██║██║  ██║██║ ╚═╝ ██║██████╔╝██║  ██║
        ╚═╝     ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝╚═════╝ ╚═╝  ╚═╝

        mamba (0.22.1) supported by @QuantStack

        GitHub:  https://github.com/mamba-org/mamba
        Twitter: https://twitter.com/QuantStack

█████████████████████████████████████████████████████████████


Looking for: ['napari-svg']

Pinned packages:
  - python 3.9.*
  - napari 0.4.15.*

Transaction

  Prefix: /Users/jrodriguez/Library/napari-0.4.15/envs/napari-0.4.15

  Updating specs:

   - napari-svg
   - ca-certificates
   - certifi
   - openssl


  Package      Version  Build           Channel                    Size
─────────────────────────────────────────────────────────────────────────
  Change:
─────────────────────────────────────────────────────────────────────────

  - certifi  2021.10.8  py39h2804cbe_1  installed
  + certifi  2021.10.8  py39h2804cbe_2  conda-forge/osx-arm64     149kB

  Upgrade:
─────────────────────────────────────────────────────────────────────────

  - openssl     1.1.1l  h3422bc3_0      installed
  + openssl     1.1.1n  h90dfc92_0      conda-forge/osx-arm64       2MB

  Summary:

  Change: 1 packages
  Upgrade: 1 packages

  Total download: 2MB

─────────────────────────────────────────────────────────────────────────


Preparing transaction: done
Verifying transaction: done
Executing transaction: done

```

... but napari-svg is not updated after all. I think we either need to use `mamba update napari-svg` OR force an update `mamba install napari-svg=<newversion>`. If we just use `mamba install napari-svg` it will consider it already satisfied because, well, it's indeed installed, but not the last version.


***

However, we shouldn't be seeing this update :/ It's not on conda-forge (yet, I am building the new version as we speak!

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
@@ -557,20 +557,18 @@ def addItem(
         enabled=True,
         npe_version=1,
     ):
+        pkg_name = project_info.name
         # don't add duplicates
-        if (
-            self.findItems(project_info.name, Qt.MatchFixedString)
-            and not plugin_name
-        ):
+        if self.findItems(pkg_name, Qt.MatchFixedString) and not plugin_name:
             return
 
         # including summary here for sake of filtering below.
-        searchable_text = project_info.name + " " + project_info.summary
+        searchable_text = pkg_name + " " + project_info.summary
         item = QListWidgetItem(searchable_text, parent=self)
         item.version = project_info.version
         super().addItem(item)
         widg = PluginListItem(
-            package_name=project_info.name,
+            package_name=pkg_name,
             version=project_info.version,
             url=project_info.home_page,
             summary=project_info.summary,
@@ -596,17 +594,14 @@ def addItem(
             )
         else:
             widg.help_button.setVisible(False)
-
         widg.action_button.clicked.connect(
-            lambda: self.handle_action(item, project_info.name, action_name)
+            lambda: self.handle_action(item, pkg_name, action_name)
         )
         widg.update_btn.clicked.connect(
-            lambda: self.handle_action(
-                item, project_info.name, "install", update=True
-            )
+            lambda: self.handle_action(item, pkg_name, "install", update=True)
         )
         widg.cancel_btn.clicked.connect(
-            lambda: self.handle_action(item, project_info.name, "cancel")
+            lambda: self.handle_action(item, pkg_name, "cancel")
         )
         item.setSizeHint(widg.sizeHint())
         self.setItemWidget(item, widg)
@@ -635,6 +630,9 @@ def handle_action(self, item, pkg_name, action_name, update=False):
 
         if action_name == "install":
             if update:
+                if hasattr(item, 'latest_version'):
+                    pkg_name += f"=={item.latest_version}"
+
                 widget.set_busy(trans._("updating..."), update)
                 widget.action_button.setDisabled(True)
             else:
@@ -655,8 +653,11 @@ def handle_action(self, item, pkg_name, action_name, update=False):
             widget.set_busy(trans._("cancelling..."), update)
             method((pkg_name,))
 
-    @Slot(PackageMetadata)
-    def tag_outdated(self, project_info: PackageMetadata):
+    @Slot(PackageMetadata, bool)
+    def tag_outdated(self, project_info: PackageMetadata, is_available: bool):
+        if not is_available:
+            return
+
         for item in self.findItems(project_info.name, Qt.MatchStartsWith):
             current = item.version
             latest = project_info.version
@@ -667,6 +668,7 @@ def tag_outdated(self, project_info: PackageMetadata):
                 continue
 
             item.outdated = True
+            item.latest_version = latest
             widg = self.itemWidget(item)
             widg.update_btn.setVisible(True)
             widg.update_btn.setText(
@@ -998,7 +1000,7 @@ def _install_packages(self, packages: Sequence[str] = ()):
     def _handle_yield(self, data: Tuple[PackageMetadata, bool]):
         project_info, is_available = data
         if project_info.name in self.already_installed:
-            self.installed_list.tag_outdated(project_info)
+            self.installed_list.tag_outdated(project_info, is_available)
         else:
             self.available_list.addItem(project_info)
             if not is_available:
diff --git a/napari/plugins/hub.py b/napari/plugins/hub.py
--- a/napari/plugins/hub.py
+++ b/napari/plugins/hub.py
@@ -55,9 +55,12 @@ def hub_plugin_info(
         try:
             with request.urlopen(anaconda_api) as resp_api:
                 anaconda_info = json.loads(resp_api.read().decode())
-                if version in anaconda_info.get("versions", []):
-                    is_available_in_conda_forge = True
+                versions = anaconda_info.get("versions", [])
+                if versions:
+                    if version not in versions:
+                        version = versions[-1]
 
+                    is_available_in_conda_forge = True
         except error.HTTPError:
             pass
 


Test patch:
diff --git a/napari/plugins/_tests/test_hub.py b/napari/plugins/_tests/test_hub.py
new file mode 100644
--- /dev/null
+++ b/napari/plugins/_tests/test_hub.py
@@ -0,0 +1,121 @@
+from unittest import mock
+from urllib import error
+
+from napari.plugins import hub
+
+# Mock data
+# ----------------------------------------------------------------------------
+HUB_REPLY = b'''{"authors": [{"email": "sofroniewn@gmail.com", "name": "Nicholas Sofroniew"}],
+"development_status": ["Development Status :: 4 - Beta"],
+"license": "BSD-3-Clause",
+"name": "napari-svg",
+"project_site": "https://github.com/napari/napari-svg",
+"summary": "A plugin",
+"version": "0.1.6",
+"visibility": "public"}'''
+ANACONDA_REPLY_DIFFERENT_PYPI = b'{"versions": ["0.1.5"]}'
+ANACONDA_REPLY_SAME_PYPI = b'{"versions": ["0.1.5", "0.1.6"]}'
+ANACONDA_REPLY_EMPTY = b'{"versions": []}'
+
+
+# Mocks
+# ----------------------------------------------------------------------------
+class FakeResponse:
+    def __init__(self, *, data: bytes, _error=None):
+        self.data = data
+        self._error = _error
+
+    def read(self):
+        if self._error:
+            raise self._error
+
+        return self.data
+
+    def close(self):
+        pass
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *exc):
+        return
+
+
+def mocked_urlopen_valid_different(*args, **kwargs):
+    if "https://api.anaconda.org" in args[0]:
+        return FakeResponse(data=ANACONDA_REPLY_DIFFERENT_PYPI)
+    return FakeResponse(data=HUB_REPLY)
+
+
+def mocked_urlopen_valid_same(*args, **kwargs):
+    if "https://api.anaconda.org" in args[0]:
+        return FakeResponse(data=ANACONDA_REPLY_SAME_PYPI)
+    return FakeResponse(data=HUB_REPLY)
+
+
+def mocked_urlopen_valid_empty(*args, **kwargs):
+    if "https://api.anaconda.org" in args[0]:
+        return FakeResponse(data=ANACONDA_REPLY_EMPTY)
+    return FakeResponse(data=HUB_REPLY)
+
+
+def mocked_urlopen_valid_not_in_forge(*args, **kwargs):
+    if "https://api.anaconda.org" in args[0]:
+        return FakeResponse(
+            data=ANACONDA_REPLY_EMPTY,
+            _error=error.HTTPError('', 1, '', '', None),
+        )
+    return FakeResponse(data=HUB_REPLY)
+
+
+# Tests
+# ----------------------------------------------------------------------------
+@mock.patch('urllib.request.urlopen', new=mocked_urlopen_valid_different)
+def test_hub_plugin_info_different_pypi():
+    hub.hub_plugin_info.cache_clear()
+    info, is_available_in_conda_forge = hub.hub_plugin_info(
+        'napari-SVG', conda_forge=True
+    )
+    assert is_available_in_conda_forge
+    assert info.name == 'napari-svg'
+    assert info.version == '0.1.5'
+
+
+@mock.patch('urllib.request.urlopen', new=mocked_urlopen_valid_same)
+def test_hub_plugin_info_same_as_pypi():
+    hub.hub_plugin_info.cache_clear()
+    info, is_available_in_conda_forge = hub.hub_plugin_info(
+        'napari-SVG', conda_forge=True
+    )
+    assert is_available_in_conda_forge
+    assert info.version == '0.1.6'
+
+
+@mock.patch('urllib.request.urlopen', new=mocked_urlopen_valid_empty)
+def test_hub_plugin_info_empty():
+    hub.hub_plugin_info.cache_clear()
+    info, is_available_in_conda_forge = hub.hub_plugin_info(
+        'napari-SVG', conda_forge=True
+    )
+    assert not is_available_in_conda_forge
+    assert info.version == '0.1.6'
+
+
+@mock.patch('urllib.request.urlopen', new=mocked_urlopen_valid_empty)
+def test_hub_plugin_info_forge_false():
+    hub.hub_plugin_info.cache_clear()
+    info, is_available_in_conda_forge = hub.hub_plugin_info(
+        'napari-SVG', conda_forge=False
+    )
+    assert is_available_in_conda_forge
+    assert info.version == '0.1.6'
+
+
+@mock.patch('urllib.request.urlopen', new=mocked_urlopen_valid_not_in_forge)
+def test_hub_plugin_info_not_in_forge():
+    hub.hub_plugin_info.cache_clear()
+    info, is_available_in_conda_forge = hub.hub_plugin_info(
+        'napari-SVG', conda_forge=True
+    )
+    assert not is_available_in_conda_forge
+    assert info.version == '0.1.6'