We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `napari/plugins/pypi.py`
Content:
```
1 """
2 These convenience functions will be useful for searching pypi for packages
3 that match the plugin naming convention, and retrieving related metadata.
4 """
5 import json
6 import re
7 from concurrent.futures import ThreadPoolExecutor, as_completed
8 from functools import lru_cache
9 from typing import Dict, Generator, List, NamedTuple, Optional
10 from urllib import error, parse, request
11 
12 PYPI_SIMPLE_API_URL = 'https://pypi.org/simple/'
13 
14 setup_py_entrypoint = re.compile(
15     r"entry_points\s?=\s?([^}]*napari.plugin[^}]*)}"
16 )
17 setup_py_pypi_name = re.compile(
18     r"setup\s?\(.*name\s?=\s?['\"]([^'\"]+)['\"]", re.DOTALL
19 )
20 
21 
22 class ProjectInfo(NamedTuple):
23     """Info associated with a PyPI Project."""
24 
25     name: str
26     version: str
27     url: str
28     summary: str
29     author: str
30     license: str
31 
32 
33 @lru_cache(maxsize=128)
34 def get_packages_by_prefix(prefix: str) -> Dict[str, str]:
35     """Search for packages starting with ``prefix`` on pypi.
36 
37     Packages using naming convention: http://bit.ly/pynaming-convention
38     can be autodiscovered on pypi using the SIMPLE API:
39     https://www.python.org/dev/peps/pep-0503/
40 
41     Returns
42     -------
43     dict
44         {name: url} for all packages at pypi that start with ``prefix``
45     """
46 
47     with request.urlopen(PYPI_SIMPLE_API_URL) as response:
48         html = response.read().decode()
49 
50     return {
51         name: PYPI_SIMPLE_API_URL + url
52         for url, name in re.findall(
53             f'<a href="/simple/(.+)">({prefix}.*)</a>', html
54         )
55     }
56 
57 
58 @lru_cache(maxsize=128)
59 def get_packages_by_classifier(classifier: str) -> List[str]:
60     """Search for packages declaring ``classifier`` on PyPI
61 
62     Yields
63     ------
64     name : str
65         name of all packages at pypi that declare ``classifier``
66     """
67 
68     url = f"https://pypi.org/search/?c={parse.quote_plus(classifier)}"
69     with request.urlopen(url) as response:
70         html = response.read().decode()
71 
72     return re.findall('class="package-snippet__name">(.+)</span>', html)
73 
74 
75 @lru_cache(maxsize=128)
76 def get_package_versions(name: str) -> List[str]:
77     """Get available versions of a package on pypi
78 
79     Parameters
80     ----------
81     name : str
82         name of the package
83 
84     Returns
85     -------
86     tuple
87         versions available on pypi
88     """
89     with request.urlopen(PYPI_SIMPLE_API_URL + name) as response:
90         html = response.read()
91 
92     return re.findall(f'>{name}-(.+).tar', html.decode())
93 
94 
95 @lru_cache(maxsize=128)
96 def ensure_published_at_pypi(
97     name: str, min_dev_status=3
98 ) -> Optional[ProjectInfo]:
99     """Return name if ``name`` is a package in PyPI with dev_status > min."""
100     try:
101         with request.urlopen(f'https://pypi.org/pypi/{name}/json') as resp:
102             info = json.loads(resp.read().decode()).get("info")
103     except error.HTTPError:
104         return None
105     classifiers = info.get("classifiers")
106     for i in range(1, min_dev_status):
107         if any(f'Development Status :: {1}' in x for x in classifiers):
108             return None
109 
110     return ProjectInfo(
111         name=normalized_name(info["name"]),
112         version=info["version"],
113         url=info["home_page"],
114         summary=info["summary"],
115         author=info["author"],
116         license=info["license"] or "UNKNOWN",
117     )
118 
119 
120 def iter_napari_plugin_info(
121     skip={'napari-plugin-engine'},
122 ) -> Generator[ProjectInfo, None, None]:
123     """Return a generator that yields ProjectInfo of available napari plugins.
124 
125     By default, requires that packages are at least "Alpha" stage of
126     development.  to allow lower, change the ``min_dev_status`` argument to
127     ``ensure_published_at_pypi``.
128     """
129     already_yielded = set()
130     with ThreadPoolExecutor(max_workers=8) as executor:
131         futures = [
132             executor.submit(ensure_published_at_pypi, name)
133             for name in get_packages_by_classifier("Framework :: napari")
134             if name not in skip
135         ]
136 
137         for future in as_completed(futures):
138             info = future.result()
139             if info and info not in already_yielded:
140                 already_yielded.add(info)
141                 yield info
142 
143 
144 def normalized_name(name) -> str:
145     return re.sub(r"[-_.]+", "-", name).lower()
146
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
         {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 @@
     """
 
     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 @@
     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 @@
 ) -> 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

{"golden_diff": "diff --git a/napari/plugins/pypi.py b/napari/plugins/pypi.py\n--- a/napari/plugins/pypi.py\n+++ b/napari/plugins/pypi.py\n@@ -4,11 +4,14 @@\n \"\"\"\n import json\n import re\n+import ssl\n from concurrent.futures import ThreadPoolExecutor, as_completed\n-from functools import lru_cache\n+from functools import lru_cache, partial\n from typing import Dict, Generator, List, NamedTuple, Optional\n from urllib import error, parse, request\n \n+import certifi\n+\n PYPI_SIMPLE_API_URL = 'https://pypi.org/simple/'\n \n setup_py_entrypoint = re.compile(\n@@ -18,6 +21,9 @@\n     r\"setup\\s?\\(.*name\\s?=\\s?['\\\"]([^'\\\"]+)['\\\"]\", re.DOTALL\n )\n \n+context = ssl.create_default_context(cafile=certifi.where())\n+urlopen = partial(request.urlopen, context=context)\n+\n \n class ProjectInfo(NamedTuple):\n     \"\"\"Info associated with a PyPI Project.\"\"\"\n@@ -44,7 +50,7 @@\n         {name: url} for all packages at pypi that start with ``prefix``\n     \"\"\"\n \n-    with request.urlopen(PYPI_SIMPLE_API_URL) as response:\n+    with urlopen(PYPI_SIMPLE_API_URL) as response:\n         html = response.read().decode()\n \n     return {\n@@ -66,7 +72,7 @@\n     \"\"\"\n \n     url = f\"https://pypi.org/search/?c={parse.quote_plus(classifier)}\"\n-    with request.urlopen(url) as response:\n+    with urlopen(url) as response:\n         html = response.read().decode()\n \n     return re.findall('class=\"package-snippet__name\">(.+)</span>', html)\n@@ -86,7 +92,7 @@\n     tuple\n         versions available on pypi\n     \"\"\"\n-    with request.urlopen(PYPI_SIMPLE_API_URL + name) as response:\n+    with urlopen(PYPI_SIMPLE_API_URL + name) as response:\n         html = response.read()\n \n     return re.findall(f'>{name}-(.+).tar', html.decode())\n@@ -98,7 +104,7 @@\n ) -> Optional[ProjectInfo]:\n     \"\"\"Return name if ``name`` is a package in PyPI with dev_status > min.\"\"\"\n     try:\n-        with request.urlopen(f'https://pypi.org/pypi/{name}/json') as resp:\n+        with urlopen(f'https://pypi.org/pypi/{name}/json') as resp:\n             info = json.loads(resp.read().decode()).get(\"info\")\n     except error.HTTPError:\n         return None\n", "issue": "Plugin discovery appears to be broken\n## \ud83d\udc1b Bug\r\n\r\nIf I go to \"Plugins > Install/Uninstall packages\", I now see an empty list, and no progress widget:\r\n\r\n<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\">\r\n\r\n## Environment\r\n\r\nThis is using the latest nightly bundle build, but I also experienced it on master.\r\n\r\nnapari: 0.4.5.dev9+g79ab358\r\nPlatform: macOS-10.15.7-x86_64-i386-64bit\r\nSystem: MacOS 10.15.7\r\nPython: 3.8.7 (default, Jan 2 2021, 04:16:43) [Clang 11.0.0 (clang-1100.0.33.17)]\r\nQt: 5.15.2\r\nPySide2: 5.15.2\r\nNumPy: 1.19.3\r\nSciPy: 1.6.0\r\nDask: 2021.01.1\r\nVisPy: 0.6.6\r\n\r\nOpenGL:\r\n- GL version: 2.1 ATI-3.10.19\r\n- MAX_TEXTURE_SIZE: 16384\r\n\r\nScreens:\r\n- screen #1: resolution 2048x1152, scale 2.0\r\n- screen #2: resolution 1536x960, scale 2.0\r\n\r\nPlugins:\r\n- svg: 0.1.4\n", "before_files": [{"content": "\"\"\"\nThese convenience functions will be useful for searching pypi for packages\nthat match the plugin naming convention, and retrieving related metadata.\n\"\"\"\nimport json\nimport re\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom functools import lru_cache\nfrom typing import Dict, Generator, List, NamedTuple, Optional\nfrom urllib import error, parse, request\n\nPYPI_SIMPLE_API_URL = 'https://pypi.org/simple/'\n\nsetup_py_entrypoint = re.compile(\n    r\"entry_points\\s?=\\s?([^}]*napari.plugin[^}]*)}\"\n)\nsetup_py_pypi_name = re.compile(\n    r\"setup\\s?\\(.*name\\s?=\\s?['\\\"]([^'\\\"]+)['\\\"]\", re.DOTALL\n)\n\n\nclass ProjectInfo(NamedTuple):\n    \"\"\"Info associated with a PyPI Project.\"\"\"\n\n    name: str\n    version: str\n    url: str\n    summary: str\n    author: str\n    license: str\n\n\n@lru_cache(maxsize=128)\ndef get_packages_by_prefix(prefix: str) -> Dict[str, str]:\n    \"\"\"Search for packages starting with ``prefix`` on pypi.\n\n    Packages using naming convention: http://bit.ly/pynaming-convention\n    can be autodiscovered on pypi using the SIMPLE API:\n    https://www.python.org/dev/peps/pep-0503/\n\n    Returns\n    -------\n    dict\n        {name: url} for all packages at pypi that start with ``prefix``\n    \"\"\"\n\n    with request.urlopen(PYPI_SIMPLE_API_URL) as response:\n        html = response.read().decode()\n\n    return {\n        name: PYPI_SIMPLE_API_URL + url\n        for url, name in re.findall(\n            f'<a href=\"/simple/(.+)\">({prefix}.*)</a>', html\n        )\n    }\n\n\n@lru_cache(maxsize=128)\ndef get_packages_by_classifier(classifier: str) -> List[str]:\n    \"\"\"Search for packages declaring ``classifier`` on PyPI\n\n    Yields\n    ------\n    name : str\n        name of all packages at pypi that declare ``classifier``\n    \"\"\"\n\n    url = f\"https://pypi.org/search/?c={parse.quote_plus(classifier)}\"\n    with request.urlopen(url) as response:\n        html = response.read().decode()\n\n    return re.findall('class=\"package-snippet__name\">(.+)</span>', html)\n\n\n@lru_cache(maxsize=128)\ndef get_package_versions(name: str) -> List[str]:\n    \"\"\"Get available versions of a package on pypi\n\n    Parameters\n    ----------\n    name : str\n        name of the package\n\n    Returns\n    -------\n    tuple\n        versions available on pypi\n    \"\"\"\n    with request.urlopen(PYPI_SIMPLE_API_URL + name) as response:\n        html = response.read()\n\n    return re.findall(f'>{name}-(.+).tar', html.decode())\n\n\n@lru_cache(maxsize=128)\ndef ensure_published_at_pypi(\n    name: str, min_dev_status=3\n) -> Optional[ProjectInfo]:\n    \"\"\"Return name if ``name`` is a package in PyPI with dev_status > min.\"\"\"\n    try:\n        with request.urlopen(f'https://pypi.org/pypi/{name}/json') as resp:\n            info = json.loads(resp.read().decode()).get(\"info\")\n    except error.HTTPError:\n        return None\n    classifiers = info.get(\"classifiers\")\n    for i in range(1, min_dev_status):\n        if any(f'Development Status :: {1}' in x for x in classifiers):\n            return None\n\n    return ProjectInfo(\n        name=normalized_name(info[\"name\"]),\n        version=info[\"version\"],\n        url=info[\"home_page\"],\n        summary=info[\"summary\"],\n        author=info[\"author\"],\n        license=info[\"license\"] or \"UNKNOWN\",\n    )\n\n\ndef iter_napari_plugin_info(\n    skip={'napari-plugin-engine'},\n) -> Generator[ProjectInfo, None, None]:\n    \"\"\"Return a generator that yields ProjectInfo of available napari plugins.\n\n    By default, requires that packages are at least \"Alpha\" stage of\n    development.  to allow lower, change the ``min_dev_status`` argument to\n    ``ensure_published_at_pypi``.\n    \"\"\"\n    already_yielded = set()\n    with ThreadPoolExecutor(max_workers=8) as executor:\n        futures = [\n            executor.submit(ensure_published_at_pypi, name)\n            for name in get_packages_by_classifier(\"Framework :: napari\")\n            if name not in skip\n        ]\n\n        for future in as_completed(futures):\n            info = future.result()\n            if info and info not in already_yielded:\n                already_yielded.add(info)\n                yield info\n\n\ndef normalized_name(name) -> str:\n    return re.sub(r\"[-_.]+\", \"-\", name).lower()\n", "path": "napari/plugins/pypi.py"}], "after_files": [{"content": "\"\"\"\nThese convenience functions will be useful for searching pypi for packages\nthat match the plugin naming convention, and retrieving related metadata.\n\"\"\"\nimport json\nimport re\nimport ssl\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom functools import lru_cache, partial\nfrom typing import Dict, Generator, List, NamedTuple, Optional\nfrom urllib import error, parse, request\n\nimport certifi\n\nPYPI_SIMPLE_API_URL = 'https://pypi.org/simple/'\n\nsetup_py_entrypoint = re.compile(\n    r\"entry_points\\s?=\\s?([^}]*napari.plugin[^}]*)}\"\n)\nsetup_py_pypi_name = re.compile(\n    r\"setup\\s?\\(.*name\\s?=\\s?['\\\"]([^'\\\"]+)['\\\"]\", re.DOTALL\n)\n\ncontext = ssl.create_default_context(cafile=certifi.where())\nurlopen = partial(request.urlopen, context=context)\n\n\nclass ProjectInfo(NamedTuple):\n    \"\"\"Info associated with a PyPI Project.\"\"\"\n\n    name: str\n    version: str\n    url: str\n    summary: str\n    author: str\n    license: str\n\n\n@lru_cache(maxsize=128)\ndef get_packages_by_prefix(prefix: str) -> Dict[str, str]:\n    \"\"\"Search for packages starting with ``prefix`` on pypi.\n\n    Packages using naming convention: http://bit.ly/pynaming-convention\n    can be autodiscovered on pypi using the SIMPLE API:\n    https://www.python.org/dev/peps/pep-0503/\n\n    Returns\n    -------\n    dict\n        {name: url} for all packages at pypi that start with ``prefix``\n    \"\"\"\n\n    with urlopen(PYPI_SIMPLE_API_URL) as response:\n        html = response.read().decode()\n\n    return {\n        name: PYPI_SIMPLE_API_URL + url\n        for url, name in re.findall(\n            f'<a href=\"/simple/(.+)\">({prefix}.*)</a>', html\n        )\n    }\n\n\n@lru_cache(maxsize=128)\ndef get_packages_by_classifier(classifier: str) -> List[str]:\n    \"\"\"Search for packages declaring ``classifier`` on PyPI\n\n    Yields\n    ------\n    name : str\n        name of all packages at pypi that declare ``classifier``\n    \"\"\"\n\n    url = f\"https://pypi.org/search/?c={parse.quote_plus(classifier)}\"\n    with urlopen(url) as response:\n        html = response.read().decode()\n\n    return re.findall('class=\"package-snippet__name\">(.+)</span>', html)\n\n\n@lru_cache(maxsize=128)\ndef get_package_versions(name: str) -> List[str]:\n    \"\"\"Get available versions of a package on pypi\n\n    Parameters\n    ----------\n    name : str\n        name of the package\n\n    Returns\n    -------\n    tuple\n        versions available on pypi\n    \"\"\"\n    with urlopen(PYPI_SIMPLE_API_URL + name) as response:\n        html = response.read()\n\n    return re.findall(f'>{name}-(.+).tar', html.decode())\n\n\n@lru_cache(maxsize=128)\ndef ensure_published_at_pypi(\n    name: str, min_dev_status=3\n) -> Optional[ProjectInfo]:\n    \"\"\"Return name if ``name`` is a package in PyPI with dev_status > min.\"\"\"\n    try:\n        with urlopen(f'https://pypi.org/pypi/{name}/json') as resp:\n            info = json.loads(resp.read().decode()).get(\"info\")\n    except error.HTTPError:\n        return None\n    classifiers = info.get(\"classifiers\")\n    for i in range(1, min_dev_status):\n        if any(f'Development Status :: {1}' in x for x in classifiers):\n            return None\n\n    return ProjectInfo(\n        name=normalized_name(info[\"name\"]),\n        version=info[\"version\"],\n        url=info[\"home_page\"],\n        summary=info[\"summary\"],\n        author=info[\"author\"],\n        license=info[\"license\"] or \"UNKNOWN\",\n    )\n\n\ndef iter_napari_plugin_info(\n    skip={'napari-plugin-engine'},\n) -> Generator[ProjectInfo, None, None]:\n    \"\"\"Return a generator that yields ProjectInfo of available napari plugins.\n\n    By default, requires that packages are at least \"Alpha\" stage of\n    development.  to allow lower, change the ``min_dev_status`` argument to\n    ``ensure_published_at_pypi``.\n    \"\"\"\n    already_yielded = set()\n    with ThreadPoolExecutor(max_workers=8) as executor:\n        futures = [\n            executor.submit(ensure_published_at_pypi, name)\n            for name in get_packages_by_classifier(\"Framework :: napari\")\n            if name not in skip\n        ]\n\n        for future in as_completed(futures):\n            info = future.result()\n            if info and info not in already_yielded:\n                already_yielded.add(info)\n                yield info\n\n\ndef normalized_name(name) -> str:\n    return re.sub(r\"[-_.]+\", \"-\", name).lower()\n", "path": "napari/plugins/pypi.py"}]}