Repository: napari/napari

Issue:
KeyError: 'version' when installing a plugin
## 🐛 Bug

KeyError: 'version' when installing a plugin (that is on pypi but not yet on napari hub).
This error is maybe the reason why the warning message  'When installing/uninstalling npe2 plugins, you must restart napari for UI changes to take effect.' is not displaying...

## To Reproduce

Steps to reproduce the behavior:

1. Install the `napari-deepfinder` plugin from the UI by typing `napari-deepfinder` in the text bar.
2. The 

```
KeyError                                  Traceback (most recent call last)
File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/superqt/utils/_qthreading.py:616, in create_worker.<locals>.reraise(e=KeyError('version'))
    615 def reraise(e):
--> 616     raise e
        e = KeyError('version')

File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/superqt/utils/_qthreading.py:176, in WorkerBase.run(self=<napari._qt.qthreading.GeneratorWorker object>)
    174     warnings.filterwarnings("always")
    175     warnings.showwarning = lambda *w: self.warned.emit(w)
--> 176     result = self.work()
        self = <napari._qt.qthreading.GeneratorWorker object at 0x7faf71497790>
    177 if isinstance(result, Exception):
    178     if isinstance(result, RuntimeError):
    179         # The Worker object has likely been deleted.
    180         # A deleted wrapped C/C++ object may result in a runtime
    181         # error that will cause segfault if we try to do much other
    182         # than simply notify the user.

File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/superqt/utils/_qthreading.py:443, in GeneratorWorker.work(self=<napari._qt.qthreading.GeneratorWorker object>)
    441 try:
    442     input = self._next_value()
--> 443     output = self._gen.send(input)
        self = <napari._qt.qthreading.GeneratorWorker object at 0x7faf71497790>
        input = None
        output = (PackageMetadata(metadata_version='1.0', name='zarpaint', version='0.2.0', dynamic=None, platform=None, supported_platform=None, summary='Paint segmentations directly to on-disk/remote zarr arrays', description=None, description_content_type=None, keywords=None, home_page='https://github.com/jni/zarpaint', download_url=None, author='Abigail McGovern and Juan Nunez-Iglesias', author_email=None, maintainer=None, maintainer_email=None, license='BSD-3-Clause', classifier=None, requires_dist=None, requires_python=None, requires_external=None, project_url=None, provides_extra=None, provides_dist=None, obsoletes_dist=None), True)
        self._gen = <generator object iter_hub_plugin_info at 0x7faf714d2c80>
    444     self.yielded.emit(output)
    445 except StopIteration as exc:

File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/napari/plugins/hub.py:101, in iter_hub_plugin_info(skip={}, conda_forge=False)
     94 futures = [
     95     executor.submit(hub_plugin_info, name, conda_forge=conda_forge)
     96     for name in sorted(plugins)
     97     if name not in skip
     98 ]
    100 for future in as_completed(futures):
--> 101     info, is_available_in_conda_forge = future.result()
        future = <Future at 0x7faf7270c9d0 state=finished raised KeyError>
        info, is_available_in_conda_forge = (PackageMetadata(metadata_version='1.0', name='zarpaint', version='0.2.0', dynamic=None, platform=None, supported_platform=None, summary='Paint segmentations directly to on-disk/remote zarr arrays', description=None, description_content_type=None, keywords=None, home_page='https://github.com/jni/zarpaint', download_url=None, author='Abigail McGovern and Juan Nunez-Iglesias', author_email=None, maintainer=None, maintainer_email=None, license='BSD-3-Clause', classifier=None, requires_dist=None, requires_python=None, requires_external=None, project_url=None, provides_extra=None, provides_dist=None, obsoletes_dist=None), True)
        info = PackageMetadata(metadata_version='1.0', name='zarpaint', version='0.2.0', dynamic=None, platform=None, supported_platform=None, summary='Paint segmentations directly to on-disk/remote zarr arrays', description=None, description_content_type=None, keywords=None, home_page='https://github.com/jni/zarpaint', download_url=None, author='Abigail McGovern and Juan Nunez-Iglesias', author_email=None, maintainer=None, maintainer_email=None, license='BSD-3-Clause', classifier=None, requires_dist=None, requires_python=None, requires_external=None, project_url=None, provides_extra=None, provides_dist=None, obsoletes_dist=None)
        is_available_in_conda_forge = True
    102     if info and info not in already_yielded:
    103         already_yielded.append(info)

File /usr/lib64/python3.9/concurrent/futures/_base.py:439, in Future.result(self=None, timeout=None)
    437     raise CancelledError()
    438 elif self._state == FINISHED:
--> 439     return self.__get_result()
        self = None
    441 self._condition.wait(timeout)
    443 if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:

File /usr/lib64/python3.9/concurrent/futures/_base.py:391, in Future.__get_result(self=None)
    389 if self._exception:
    390     try:
--> 391         raise self._exception
        self = None
    392     finally:
    393         # Break a reference cycle with the exception in self._exception
    394         self = None

File /usr/lib64/python3.9/concurrent/futures/thread.py:58, in _WorkItem.run(self=None)
     55     return
     57 try:
---> 58     result = self.fn(*self.args, **self.kwargs)
        self = None
     59 except BaseException as exc:
     60     self.future.set_exception(exc)

File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/napari/plugins/hub.py:47, in hub_plugin_info(name='napari-deepfinder', min_dev_status=3, conda_forge=False)
     44 except error.HTTPError:
     45     return None, False
---> 47 version = info["version"]
        info = {'conda': [], 'display_name': '', 'plugin_types': [], 'reader_file_extensions': [], 'writer_file_extensions': [], 'writer_save_layers': []}
     48 norm_name = normalized_name(info["name"])
     49 is_available_in_conda_forge = True

KeyError: 'version'


```

## Expected behavior

This error should not happen!

## Environment

 napari: 0.4.16
Platform: Linux-5.18.10-100.fc35.x86_64-x86_64-with-glibc2.34
System: Fedora Linux 35 (Workstation Edition)
Python: 3.9.13 (main, Jun 9 2022, 00:00:00) [GCC 11.3.1 20220421 (Red Hat 11.3.1-2)]
Qt: 5.15.2
PyQt5: 5.15.7
NumPy: 1.23.1
SciPy: 1.8.1
Dask: 2022.7.1
VisPy: 0.10.0

OpenGL:
- GL version: 4.6.0 NVIDIA 515.48.07
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1920x1200, scale 1.0

Plugins:
- console: 0.0.4
- napari-svg: 0.1.6
- scikit-image: 0.4.16

KeyError: 'version' when installing a plugin
## 🐛 Bug

KeyError: 'version' when installing a plugin (that is on pypi but not yet on napari hub).
This error is maybe the reason why the warning message  'When installing/uninstalling npe2 plugins, you must restart napari for UI changes to take effect.' is not displaying...

## To Reproduce

Steps to reproduce the behavior:

1. Install the `napari-deepfinder` plugin from the UI by typing `napari-deepfinder` in the text bar.
2. The 

```
KeyError                                  Traceback (most recent call last)
File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/superqt/utils/_qthreading.py:616, in create_worker.<locals>.reraise(e=KeyError('version'))
    615 def reraise(e):
--> 616     raise e
        e = KeyError('version')

File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/superqt/utils/_qthreading.py:176, in WorkerBase.run(self=<napari._qt.qthreading.GeneratorWorker object>)
    174     warnings.filterwarnings("always")
    175     warnings.showwarning = lambda *w: self.warned.emit(w)
--> 176     result = self.work()
        self = <napari._qt.qthreading.GeneratorWorker object at 0x7faf71497790>
    177 if isinstance(result, Exception):
    178     if isinstance(result, RuntimeError):
    179         # The Worker object has likely been deleted.
    180         # A deleted wrapped C/C++ object may result in a runtime
    181         # error that will cause segfault if we try to do much other
    182         # than simply notify the user.

File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/superqt/utils/_qthreading.py:443, in GeneratorWorker.work(self=<napari._qt.qthreading.GeneratorWorker object>)
    441 try:
    442     input = self._next_value()
--> 443     output = self._gen.send(input)
        self = <napari._qt.qthreading.GeneratorWorker object at 0x7faf71497790>
        input = None
        output = (PackageMetadata(metadata_version='1.0', name='zarpaint', version='0.2.0', dynamic=None, platform=None, supported_platform=None, summary='Paint segmentations directly to on-disk/remote zarr arrays', description=None, description_content_type=None, keywords=None, home_page='https://github.com/jni/zarpaint', download_url=None, author='Abigail McGovern and Juan Nunez-Iglesias', author_email=None, maintainer=None, maintainer_email=None, license='BSD-3-Clause', classifier=None, requires_dist=None, requires_python=None, requires_external=None, project_url=None, provides_extra=None, provides_dist=None, obsoletes_dist=None), True)
        self._gen = <generator object iter_hub_plugin_info at 0x7faf714d2c80>
    444     self.yielded.emit(output)
    445 except StopIteration as exc:

File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/napari/plugins/hub.py:101, in iter_hub_plugin_info(skip={}, conda_forge=False)
     94 futures = [
     95     executor.submit(hub_plugin_info, name, conda_forge=conda_forge)
     96     for name in sorted(plugins)
     97     if name not in skip
     98 ]
    100 for future in as_completed(futures):
--> 101     info, is_available_in_conda_forge = future.result()
        future = <Future at 0x7faf7270c9d0 state=finished raised KeyError>
        info, is_available_in_conda_forge = (PackageMetadata(metadata_version='1.0', name='zarpaint', version='0.2.0', dynamic=None, platform=None, supported_platform=None, summary='Paint segmentations directly to on-disk/remote zarr arrays', description=None, description_content_type=None, keywords=None, home_page='https://github.com/jni/zarpaint', download_url=None, author='Abigail McGovern and Juan Nunez-Iglesias', author_email=None, maintainer=None, maintainer_email=None, license='BSD-3-Clause', classifier=None, requires_dist=None, requires_python=None, requires_external=None, project_url=None, provides_extra=None, provides_dist=None, obsoletes_dist=None), True)
        info = PackageMetadata(metadata_version='1.0', name='zarpaint', version='0.2.0', dynamic=None, platform=None, supported_platform=None, summary='Paint segmentations directly to on-disk/remote zarr arrays', description=None, description_content_type=None, keywords=None, home_page='https://github.com/jni/zarpaint', download_url=None, author='Abigail McGovern and Juan Nunez-Iglesias', author_email=None, maintainer=None, maintainer_email=None, license='BSD-3-Clause', classifier=None, requires_dist=None, requires_python=None, requires_external=None, project_url=None, provides_extra=None, provides_dist=None, obsoletes_dist=None)
        is_available_in_conda_forge = True
    102     if info and info not in already_yielded:
    103         already_yielded.append(info)

File /usr/lib64/python3.9/concurrent/futures/_base.py:439, in Future.result(self=None, timeout=None)
    437     raise CancelledError()
    438 elif self._state == FINISHED:
--> 439     return self.__get_result()
        self = None
    441 self._condition.wait(timeout)
    443 if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:

File /usr/lib64/python3.9/concurrent/futures/_base.py:391, in Future.__get_result(self=None)
    389 if self._exception:
    390     try:
--> 391         raise self._exception
        self = None
    392     finally:
    393         # Break a reference cycle with the exception in self._exception
    394         self = None

File /usr/lib64/python3.9/concurrent/futures/thread.py:58, in _WorkItem.run(self=None)
     55     return
     57 try:
---> 58     result = self.fn(*self.args, **self.kwargs)
        self = None
     59 except BaseException as exc:
     60     self.future.set_exception(exc)

File /home/cnstt/test-nap/venv1/lib/python3.9/site-packages/napari/plugins/hub.py:47, in hub_plugin_info(name='napari-deepfinder', min_dev_status=3, conda_forge=False)
     44 except error.HTTPError:
     45     return None, False
---> 47 version = info["version"]
        info = {'conda': [], 'display_name': '', 'plugin_types': [], 'reader_file_extensions': [], 'writer_file_extensions': [], 'writer_save_layers': []}
     48 norm_name = normalized_name(info["name"])
     49 is_available_in_conda_forge = True

KeyError: 'version'


```

## Expected behavior

This error should not happen!

## Environment

 napari: 0.4.16
Platform: Linux-5.18.10-100.fc35.x86_64-x86_64-with-glibc2.34
System: Fedora Linux 35 (Workstation Edition)
Python: 3.9.13 (main, Jun 9 2022, 00:00:00) [GCC 11.3.1 20220421 (Red Hat 11.3.1-2)]
Qt: 5.15.2
PyQt5: 5.15.7
NumPy: 1.23.1
SciPy: 1.8.1
Dask: 2022.7.1
VisPy: 0.10.0

OpenGL:
- GL version: 4.6.0 NVIDIA 515.48.07
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1920x1200, scale 1.0

Plugins:
- console: 0.0.4
- napari-svg: 0.1.6
- scikit-image: 0.4.16



Discussion:
The issue is likely caused by this try/except section here: https://github.com/napari/napari/blob/975a2de6bff1f92c27820237fdcfa38ff2adc713/napari/plugins/hub.py#L41-L45

An additionnal check should be made, to verify that the if `info` is empty, we quit the we also `return None, False`
The issue is likely caused by this try/except section here: https://github.com/napari/napari/blob/975a2de6bff1f92c27820237fdcfa38ff2adc713/napari/plugins/hub.py#L41-L45

An additionnal check should be made, to verify that the if `info` is empty, we quit the we also `return None, False`

Resolving patch:
diff --git a/napari/plugins/hub.py b/napari/plugins/hub.py
--- a/napari/plugins/hub.py
+++ b/napari/plugins/hub.py
@@ -44,6 +44,21 @@ def hub_plugin_info(
     except error.HTTPError:
         return None, False
 
+    # If the napari hub returns an info dict missing the required keys,
+    # simply return None, False like the above except
+    if (
+        not {
+            'name',
+            'version',
+            'authors',
+            'summary',
+            'license',
+            'project_site',
+        }
+        <= info.keys()
+    ):
+        return None, False
+
     version = info["version"]
     norm_name = normalized_name(info["name"])
     is_available_in_conda_forge = True


Test patch: