Repository: napari/napari

Issue:
Plugin discovery appears to be broken
## 🐛 Bug

If I go to "Plugins > Install/Uninstall packages", I now see an empty list, and no progress widget:

<img width="1192" alt="Screen Shot 2021-02-04 at 3 15 02 pm" src="https://user-images.githubusercontent.com/492549/106844231-1977b780-66fc-11eb-812b-b3d581d255f9.png">

## Environment

This is using the latest nightly bundle build, but I also experienced it on master.

napari: 0.4.5.dev9+g79ab358
Platform: macOS-10.15.7-x86_64-i386-64bit
System: MacOS 10.15.7
Python: 3.8.7 (default, Jan 2 2021, 04:16:43) [Clang 11.0.0 (clang-1100.0.33.17)]
Qt: 5.15.2
PySide2: 5.15.2
NumPy: 1.19.3
SciPy: 1.6.0
Dask: 2021.01.1
VisPy: 0.6.6

OpenGL:
- GL version: 2.1 ATI-3.10.19
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen #1: resolution 2048x1152, scale 2.0
- screen #2: resolution 1536x960, scale 2.0

Plugins:
- svg: 0.1.4


Discussion:
Hmm, I take it back, I can't reproduce on master now... =\ But the problem in the bundle persists. 🤔 
I can confirm I saw this before, and the reason was that the ssl certificate not valid when napari tries to access the pypi, this is more of a error message not showing up anywhere but the console 
> the reason was that the ssl certificate not valid when napari tries to access the pypi

@ziyangczi I suspected I was missing an error message, but what's the fix?
the error message is something similar to `SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed`, probably not exact, the solution I had is overwriting the SSL certificate in the run environment with the one from certifi: 
see https://stackoverflow.com/questions/44649449/brew-installation-of-python-3-6-1-ssl-certificate-verify-failed-certificate/44649450#44649450

```
find out default cafile:
python -c 'import ssl; print(ssl.get_default_verify_paths().openssl_cafile)'
/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/etc/ssl/cert.pem

find out ca file of certifi
python -c 'import certifi; print(certifi.where())'
'/usr/local/lib/python3.7/site-packages/certifi/cacert.pem'

copy to
sudo cp /usr/local/lib/python3.7/site-packages/certifi/cacert.pem
/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/etc/ssl/certs/cert.pem
```

Resolving patch:
diff --git a/napari/plugins/pypi.py b/napari/plugins/pypi.py
--- a/napari/plugins/pypi.py
+++ b/napari/plugins/pypi.py
@@ -4,11 +4,14 @@
 """
 import json
 import re
+import ssl
 from concurrent.futures import ThreadPoolExecutor, as_completed
-from functools import lru_cache
+from functools import lru_cache, partial
 from typing import Dict, Generator, List, NamedTuple, Optional
 from urllib import error, parse, request
 
+import certifi
+
 PYPI_SIMPLE_API_URL = 'https://pypi.org/simple/'
 
 setup_py_entrypoint = re.compile(
@@ -18,6 +21,9 @@
     r"setup\s?\(.*name\s?=\s?['\"]([^'\"]+)['\"]", re.DOTALL
 )
 
+context = ssl.create_default_context(cafile=certifi.where())
+urlopen = partial(request.urlopen, context=context)
+
 
 class ProjectInfo(NamedTuple):
     """Info associated with a PyPI Project."""
@@ -44,7 +50,7 @@ def get_packages_by_prefix(prefix: str) -> Dict[str, str]:
         {name: url} for all packages at pypi that start with ``prefix``
     """
 
-    with request.urlopen(PYPI_SIMPLE_API_URL) as response:
+    with urlopen(PYPI_SIMPLE_API_URL) as response:
         html = response.read().decode()
 
     return {
@@ -66,7 +72,7 @@ def get_packages_by_classifier(classifier: str) -> List[str]:
     """
 
     url = f"https://pypi.org/search/?c={parse.quote_plus(classifier)}"
-    with request.urlopen(url) as response:
+    with urlopen(url) as response:
         html = response.read().decode()
 
     return re.findall('class="package-snippet__name">(.+)</span>', html)
@@ -86,7 +92,7 @@ def get_package_versions(name: str) -> List[str]:
     tuple
         versions available on pypi
     """
-    with request.urlopen(PYPI_SIMPLE_API_URL + name) as response:
+    with urlopen(PYPI_SIMPLE_API_URL + name) as response:
         html = response.read()
 
     return re.findall(f'>{name}-(.+).tar', html.decode())
@@ -98,7 +104,7 @@ def ensure_published_at_pypi(
 ) -> Optional[ProjectInfo]:
     """Return name if ``name`` is a package in PyPI with dev_status > min."""
     try:
-        with request.urlopen(f'https://pypi.org/pypi/{name}/json') as resp:
+        with urlopen(f'https://pypi.org/pypi/{name}/json') as resp:
             info = json.loads(resp.read().decode()).get("info")
     except error.HTTPError:
         return None


Test patch:
diff --git a/napari/plugins/_tests/test_pypi.py b/napari/plugins/_tests/test_pypi.py
--- a/napari/plugins/_tests/test_pypi.py
+++ b/napari/plugins/_tests/test_pypi.py
@@ -35,7 +35,7 @@ def __exit__(self, *exc):
 
 
 @mock.patch(
-    'napari.plugins.pypi.request.urlopen', return_value=FakeResponse(data=txtA)
+    'napari.plugins.pypi.urlopen', return_value=FakeResponse(data=txtA)
 )
 def test_get_packages_by_prefix(mock_get):
     urls = pypi.get_packages_by_prefix('package')
@@ -44,7 +44,7 @@ def test_get_packages_by_prefix(mock_get):
 
 
 @mock.patch(
-    'napari.plugins.pypi.request.urlopen', return_value=FakeResponse(data=txtB)
+    'napari.plugins.pypi.urlopen', return_value=FakeResponse(data=txtB)
 )
 def test_get_package_versions(mock_get):
     versions = pypi.get_package_versions('package')