We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Version selection for packages availble both on PyPi and in Pyodide
For packages not built in pyodide, version selection works as expected. For instance,
```py
>>> import micropip
>>> micropip.install('idna==2.9')   # version before last on PyPi, package not in pyodide
Installed idna
>>> import idna
>>> idna.__version__
2.9
```

However, when one specifies the version for a package available in the pyodide distribution, it is ignored and the version from pyodide is installed regardless if PyPi includes the requested version,
```py
>>> import micropip
>>> micropip.install('pytz==2020.1')
Installed pytz
>>> import pytz
>>> pytz.__version__
2019.3
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `packages/micropip/micropip/micropip.py`
Content:
```
1 import asyncio
2 import hashlib
3 import importlib
4 import io
5 import json
6 from pathlib import Path
7 import zipfile
8 from typing import Dict, Any, Union, List, Tuple
9 
10 from distlib import markers, util, version
11 
12 # Provide stubs for testing in native python
13 try:
14     import pyodide_js
15 
16     IN_BROWSER = True
17 except ImportError:
18     IN_BROWSER = False
19 
20 if IN_BROWSER:
21     # In practice, this is the `site-packages` directory.
22     WHEEL_BASE = Path(__file__).parent
23 else:
24     WHEEL_BASE = Path(".") / "wheels"
25 
26 if IN_BROWSER:
27     from js import fetch
28 
29     async def _get_url(url):
30         resp = await fetch(url)
31         if not resp.ok:
32             raise OSError(
33                 f"Request for {url} failed with status {resp.status}: {resp.statusText}"
34             )
35         return io.BytesIO(await resp.arrayBuffer())
36 
37 
38 else:
39     from urllib.request import urlopen
40 
41     async def _get_url(url):
42         with urlopen(url) as fd:
43             content = fd.read()
44         return io.BytesIO(content)
45 
46 
47 if IN_BROWSER:
48     from asyncio import gather
49 else:
50     # asyncio.gather will schedule any coroutines to run on the event loop but
51     # we want to avoid using the event loop at all. Instead just run the
52     # coroutines in sequence.
53     async def gather(*coroutines):  # type: ignore
54         result = []
55         for coroutine in coroutines:
56             result.append(await coroutine)
57         return result
58 
59 
60 async def _get_pypi_json(pkgname):
61     url = f"https://pypi.org/pypi/{pkgname}/json"
62     fd = await _get_url(url)
63     return json.load(fd)
64 
65 
66 def _parse_wheel_url(url: str) -> Tuple[str, Dict[str, Any], str]:
67     """Parse wheels URL and extract available metadata
68 
69     See https://www.python.org/dev/peps/pep-0427/#file-name-convention
70     """
71     file_name = Path(url).name
72     # also strip '.whl' extension.
73     wheel_name = Path(url).stem
74     tokens = wheel_name.split("-")
75     # TODO: support optional build tags in the filename (cf PEP 427)
76     if len(tokens) < 5:
77         raise ValueError(f"{file_name} is not a valid wheel file name.")
78     version, python_tag, abi_tag, platform = tokens[-4:]
79     name = "-".join(tokens[:-4])
80     wheel = {
81         "digests": None,  # checksums not available
82         "filename": file_name,
83         "packagetype": "bdist_wheel",
84         "python_version": python_tag,
85         "abi_tag": abi_tag,
86         "platform": platform,
87         "url": url,
88     }
89 
90     return name, wheel, version
91 
92 
93 def _extract_wheel(fd):
94     with zipfile.ZipFile(fd) as zf:
95         zf.extractall(WHEEL_BASE)
96 
97 
98 def _validate_wheel(data, fileinfo):
99     if fileinfo.get("digests") is None:
100         # No checksums available, e.g. because installing
101         # from a different location than PyPi.
102         return
103     sha256 = fileinfo["digests"]["sha256"]
104     m = hashlib.sha256()
105     m.update(data.getvalue())
106     if m.hexdigest() != sha256:
107         raise ValueError("Contents don't match hash")
108 
109 
110 async def _install_wheel(name, fileinfo):
111     url = fileinfo["url"]
112     wheel = await _get_url(url)
113     _validate_wheel(wheel, fileinfo)
114     _extract_wheel(wheel)
115 
116 
117 class _PackageManager:
118     version_scheme = version.get_scheme("normalized")
119 
120     def __init__(self):
121         if IN_BROWSER:
122             self.builtin_packages = pyodide_js._module.packages.dependencies.to_py()
123         else:
124             self.builtin_packages = {}
125         self.installed_packages = {}
126 
127     async def install(self, requirements: Union[str, List[str]], ctx=None):
128         if ctx is None:
129             ctx = {"extra": None}
130 
131         complete_ctx = dict(markers.DEFAULT_CONTEXT)
132         complete_ctx.update(ctx)
133 
134         if isinstance(requirements, str):
135             requirements = [requirements]
136 
137         transaction: Dict[str, Any] = {
138             "wheels": [],
139             "pyodide_packages": set(),
140             "locked": dict(self.installed_packages),
141         }
142         requirement_promises = []
143         for requirement in requirements:
144             requirement_promises.append(
145                 self.add_requirement(requirement, complete_ctx, transaction)
146             )
147 
148         await gather(*requirement_promises)
149 
150         wheel_promises = []
151 
152         # Install built-in packages
153         pyodide_packages = transaction["pyodide_packages"]
154         if len(pyodide_packages):
155             # Note: branch never happens in out-of-browser testing because we
156             # report that all dependencies are empty.
157             self.installed_packages.update(dict((k, None) for k in pyodide_packages))
158             wheel_promises.append(pyodide_js.loadPackage(list(pyodide_packages)))
159 
160         # Now install PyPI packages
161         for name, wheel, ver in transaction["wheels"]:
162             wheel_promises.append(_install_wheel(name, wheel))
163             self.installed_packages[name] = ver
164         await gather(*wheel_promises)
165         return f'Installed {", ".join(self.installed_packages.keys())}'
166 
167     async def add_requirement(self, requirement: str, ctx, transaction):
168         if requirement.endswith(".whl"):
169             # custom download location
170             name, wheel, version = _parse_wheel_url(requirement)
171             transaction["wheels"].append((name, wheel, version))
172             return
173 
174         req = util.parse_requirement(requirement)
175 
176         # If it's a Pyodide package, use that instead of the one on PyPI
177         if req.name in self.builtin_packages:
178             transaction["pyodide_packages"].add(req.name)
179             return
180 
181         if req.marker:
182             if not markers.evaluator.evaluate(req.marker, ctx):
183                 return
184 
185         matcher = self.version_scheme.matcher(req.requirement)
186 
187         # If we already have something that will work, don't
188         # fetch again
189         for name, ver in transaction["locked"].items():
190             if name == req.name:
191                 if matcher.match(ver):
192                     break
193                 else:
194                     raise ValueError(
195                         f"Requested '{requirement}', "
196                         f"but {name}=={ver} is already installed"
197                     )
198         else:
199             metadata = await _get_pypi_json(req.name)
200             wheel, ver = self.find_wheel(metadata, req)
201             transaction["locked"][req.name] = ver
202 
203             recurs_reqs = metadata.get("info", {}).get("requires_dist") or []
204             for recurs_req in recurs_reqs:
205                 await self.add_requirement(recurs_req, ctx, transaction)
206 
207             transaction["wheels"].append((req.name, wheel, ver))
208 
209     def find_wheel(self, metadata, req):
210         releases = []
211         for ver, files in metadata.get("releases", {}).items():
212             ver = self.version_scheme.suggest(ver)
213             if ver is not None:
214                 releases.append((ver, files))
215 
216         def version_number(release):
217             return version.NormalizedVersion(release[0])
218 
219         releases = sorted(releases, key=version_number, reverse=True)
220         matcher = self.version_scheme.matcher(req.requirement)
221         for ver, meta in releases:
222             if matcher.match(ver):
223                 for fileinfo in meta:
224                     if fileinfo["filename"].endswith("py3-none-any.whl"):
225                         return fileinfo, ver
226 
227         raise ValueError(f"Couldn't find a pure Python 3 wheel for '{req.requirement}'")
228 
229 
230 # Make PACKAGE_MANAGER singleton
231 PACKAGE_MANAGER = _PackageManager()
232 del _PackageManager
233 
234 
235 def install(requirements: Union[str, List[str]]):
236     """Install the given package and all of its dependencies.
237 
238     See :ref:`loading packages <loading_packages>` for more information.
239 
240     This only works for packages that are either pure Python or for packages
241     with C extensions that are built in Pyodide. If a pure Python package is not
242     found in the Pyodide repository it will be loaded from PyPi.
243 
244     Parameters
245     ----------
246     requirements : ``str | List[str]``
247 
248         A requirement or list of requirements to install. Each requirement is a string, which should be either
249         a package name or URL to a wheel:
250 
251         - If the requirement ends in ``.whl`` it will be interpreted as a URL.
252           The file must be a wheel named in compliance with the
253           `PEP 427 naming convention <https://www.python.org/dev/peps/pep-0427/#file-format>`_.
254 
255         - If the requirement does not end in ``.whl``, it will interpreted as the
256           name of a package. A package by this name must either be present in the
257           Pyodide repository at `indexURL <globalThis.loadPyodide>` or on PyPi
258 
259     Returns
260     -------
261     ``Future``
262 
263         A ``Future`` that resolves to ``None`` when all packages have
264         been downloaded and installed.
265     """
266     importlib.invalidate_caches()
267     return asyncio.ensure_future(PACKAGE_MANAGER.install(requirements))
268 
269 
270 __all__ = ["install"]
271 
272 
273 if __name__ == "__main__":
274     install("snowballstemmer")
275
```
--- 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/packages/micropip/micropip/micropip.py b/packages/micropip/micropip/micropip.py
--- a/packages/micropip/micropip/micropip.py
+++ b/packages/micropip/micropip/micropip.py
@@ -57,6 +57,14 @@
         return result
 
 
+if IN_BROWSER:
+    from pyodide_js import loadedPackages
+else:
+
+    class loadedPackages:  # type: ignore
+        pass
+
+
 async def _get_pypi_json(pkgname):
     url = f"https://pypi.org/pypi/{pkgname}/json"
     fd = await _get_url(url)
@@ -112,6 +120,7 @@
     wheel = await _get_url(url)
     _validate_wheel(wheel, fileinfo)
     _extract_wheel(wheel)
+    setattr(loadedPackages, name, url)
 
 
 class _PackageManager:
@@ -155,14 +164,15 @@
             # Note: branch never happens in out-of-browser testing because we
             # report that all dependencies are empty.
             self.installed_packages.update(dict((k, None) for k in pyodide_packages))
-            wheel_promises.append(pyodide_js.loadPackage(list(pyodide_packages)))
+            wheel_promises.append(
+                asyncio.ensure_future(pyodide_js.loadPackage(list(pyodide_packages)))
+            )
 
         # Now install PyPI packages
         for name, wheel, ver in transaction["wheels"]:
             wheel_promises.append(_install_wheel(name, wheel))
             self.installed_packages[name] = ver
         await gather(*wheel_promises)
-        return f'Installed {", ".join(self.installed_packages.keys())}'
 
     async def add_requirement(self, requirement: str, ctx, transaction):
         if requirement.endswith(".whl"):
@@ -245,8 +255,8 @@
     ----------
     requirements : ``str | List[str]``
 
-        A requirement or list of requirements to install. Each requirement is a string, which should be either
-        a package name or URL to a wheel:
+        A requirement or list of requirements to install. Each requirement is a
+        string, which should be either a package name or URL to a wheel:
 
         - If the requirement ends in ``.whl`` it will be interpreted as a URL.
           The file must be a wheel named in compliance with the
@@ -260,8 +270,8 @@
     -------
     ``Future``
 
-        A ``Future`` that resolves to ``None`` when all packages have
-        been downloaded and installed.
+        A ``Future`` that resolves to ``None`` when all packages have been
+        downloaded and installed.
     """
     importlib.invalidate_caches()
     return asyncio.ensure_future(PACKAGE_MANAGER.install(requirements))

{"golden_diff": "diff --git a/packages/micropip/micropip/micropip.py b/packages/micropip/micropip/micropip.py\n--- a/packages/micropip/micropip/micropip.py\n+++ b/packages/micropip/micropip/micropip.py\n@@ -57,6 +57,14 @@\n         return result\n \n \n+if IN_BROWSER:\n+    from pyodide_js import loadedPackages\n+else:\n+\n+    class loadedPackages:  # type: ignore\n+        pass\n+\n+\n async def _get_pypi_json(pkgname):\n     url = f\"https://pypi.org/pypi/{pkgname}/json\"\n     fd = await _get_url(url)\n@@ -112,6 +120,7 @@\n     wheel = await _get_url(url)\n     _validate_wheel(wheel, fileinfo)\n     _extract_wheel(wheel)\n+    setattr(loadedPackages, name, url)\n \n \n class _PackageManager:\n@@ -155,14 +164,15 @@\n             # Note: branch never happens in out-of-browser testing because we\n             # report that all dependencies are empty.\n             self.installed_packages.update(dict((k, None) for k in pyodide_packages))\n-            wheel_promises.append(pyodide_js.loadPackage(list(pyodide_packages)))\n+            wheel_promises.append(\n+                asyncio.ensure_future(pyodide_js.loadPackage(list(pyodide_packages)))\n+            )\n \n         # Now install PyPI packages\n         for name, wheel, ver in transaction[\"wheels\"]:\n             wheel_promises.append(_install_wheel(name, wheel))\n             self.installed_packages[name] = ver\n         await gather(*wheel_promises)\n-        return f'Installed {\", \".join(self.installed_packages.keys())}'\n \n     async def add_requirement(self, requirement: str, ctx, transaction):\n         if requirement.endswith(\".whl\"):\n@@ -245,8 +255,8 @@\n     ----------\n     requirements : ``str | List[str]``\n \n-        A requirement or list of requirements to install. Each requirement is a string, which should be either\n-        a package name or URL to a wheel:\n+        A requirement or list of requirements to install. Each requirement is a\n+        string, which should be either a package name or URL to a wheel:\n \n         - If the requirement ends in ``.whl`` it will be interpreted as a URL.\n           The file must be a wheel named in compliance with the\n@@ -260,8 +270,8 @@\n     -------\n     ``Future``\n \n-        A ``Future`` that resolves to ``None`` when all packages have\n-        been downloaded and installed.\n+        A ``Future`` that resolves to ``None`` when all packages have been\n+        downloaded and installed.\n     \"\"\"\n     importlib.invalidate_caches()\n     return asyncio.ensure_future(PACKAGE_MANAGER.install(requirements))\n", "issue": "Version selection for packages availble both on PyPi and in Pyodide\nFor packages not built in pyodide, version selection works as expected. For instance,\r\n```py\r\n>>> import micropip\r\n>>> micropip.install('idna==2.9')   # version before last on PyPi, package not in pyodide\r\nInstalled idna\r\n>>> import idna\r\n>>> idna.__version__\r\n2.9\r\n```\r\n\r\nHowever, when one specifies the version for a package available in the pyodide distribution, it is ignored and the version from pyodide is installed regardless if PyPi includes the requested version,\r\n```py\r\n>>> import micropip\r\n>>> micropip.install('pytz==2020.1')\r\nInstalled pytz\r\n>>> import pytz\r\n>>> pytz.__version__\r\n2019.3\r\n```\n", "before_files": [{"content": "import asyncio\nimport hashlib\nimport importlib\nimport io\nimport json\nfrom pathlib import Path\nimport zipfile\nfrom typing import Dict, Any, Union, List, Tuple\n\nfrom distlib import markers, util, version\n\n# Provide stubs for testing in native python\ntry:\n    import pyodide_js\n\n    IN_BROWSER = True\nexcept ImportError:\n    IN_BROWSER = False\n\nif IN_BROWSER:\n    # In practice, this is the `site-packages` directory.\n    WHEEL_BASE = Path(__file__).parent\nelse:\n    WHEEL_BASE = Path(\".\") / \"wheels\"\n\nif IN_BROWSER:\n    from js import fetch\n\n    async def _get_url(url):\n        resp = await fetch(url)\n        if not resp.ok:\n            raise OSError(\n                f\"Request for {url} failed with status {resp.status}: {resp.statusText}\"\n            )\n        return io.BytesIO(await resp.arrayBuffer())\n\n\nelse:\n    from urllib.request import urlopen\n\n    async def _get_url(url):\n        with urlopen(url) as fd:\n            content = fd.read()\n        return io.BytesIO(content)\n\n\nif IN_BROWSER:\n    from asyncio import gather\nelse:\n    # asyncio.gather will schedule any coroutines to run on the event loop but\n    # we want to avoid using the event loop at all. Instead just run the\n    # coroutines in sequence.\n    async def gather(*coroutines):  # type: ignore\n        result = []\n        for coroutine in coroutines:\n            result.append(await coroutine)\n        return result\n\n\nasync def _get_pypi_json(pkgname):\n    url = f\"https://pypi.org/pypi/{pkgname}/json\"\n    fd = await _get_url(url)\n    return json.load(fd)\n\n\ndef _parse_wheel_url(url: str) -> Tuple[str, Dict[str, Any], str]:\n    \"\"\"Parse wheels URL and extract available metadata\n\n    See https://www.python.org/dev/peps/pep-0427/#file-name-convention\n    \"\"\"\n    file_name = Path(url).name\n    # also strip '.whl' extension.\n    wheel_name = Path(url).stem\n    tokens = wheel_name.split(\"-\")\n    # TODO: support optional build tags in the filename (cf PEP 427)\n    if len(tokens) < 5:\n        raise ValueError(f\"{file_name} is not a valid wheel file name.\")\n    version, python_tag, abi_tag, platform = tokens[-4:]\n    name = \"-\".join(tokens[:-4])\n    wheel = {\n        \"digests\": None,  # checksums not available\n        \"filename\": file_name,\n        \"packagetype\": \"bdist_wheel\",\n        \"python_version\": python_tag,\n        \"abi_tag\": abi_tag,\n        \"platform\": platform,\n        \"url\": url,\n    }\n\n    return name, wheel, version\n\n\ndef _extract_wheel(fd):\n    with zipfile.ZipFile(fd) as zf:\n        zf.extractall(WHEEL_BASE)\n\n\ndef _validate_wheel(data, fileinfo):\n    if fileinfo.get(\"digests\") is None:\n        # No checksums available, e.g. because installing\n        # from a different location than PyPi.\n        return\n    sha256 = fileinfo[\"digests\"][\"sha256\"]\n    m = hashlib.sha256()\n    m.update(data.getvalue())\n    if m.hexdigest() != sha256:\n        raise ValueError(\"Contents don't match hash\")\n\n\nasync def _install_wheel(name, fileinfo):\n    url = fileinfo[\"url\"]\n    wheel = await _get_url(url)\n    _validate_wheel(wheel, fileinfo)\n    _extract_wheel(wheel)\n\n\nclass _PackageManager:\n    version_scheme = version.get_scheme(\"normalized\")\n\n    def __init__(self):\n        if IN_BROWSER:\n            self.builtin_packages = pyodide_js._module.packages.dependencies.to_py()\n        else:\n            self.builtin_packages = {}\n        self.installed_packages = {}\n\n    async def install(self, requirements: Union[str, List[str]], ctx=None):\n        if ctx is None:\n            ctx = {\"extra\": None}\n\n        complete_ctx = dict(markers.DEFAULT_CONTEXT)\n        complete_ctx.update(ctx)\n\n        if isinstance(requirements, str):\n            requirements = [requirements]\n\n        transaction: Dict[str, Any] = {\n            \"wheels\": [],\n            \"pyodide_packages\": set(),\n            \"locked\": dict(self.installed_packages),\n        }\n        requirement_promises = []\n        for requirement in requirements:\n            requirement_promises.append(\n                self.add_requirement(requirement, complete_ctx, transaction)\n            )\n\n        await gather(*requirement_promises)\n\n        wheel_promises = []\n\n        # Install built-in packages\n        pyodide_packages = transaction[\"pyodide_packages\"]\n        if len(pyodide_packages):\n            # Note: branch never happens in out-of-browser testing because we\n            # report that all dependencies are empty.\n            self.installed_packages.update(dict((k, None) for k in pyodide_packages))\n            wheel_promises.append(pyodide_js.loadPackage(list(pyodide_packages)))\n\n        # Now install PyPI packages\n        for name, wheel, ver in transaction[\"wheels\"]:\n            wheel_promises.append(_install_wheel(name, wheel))\n            self.installed_packages[name] = ver\n        await gather(*wheel_promises)\n        return f'Installed {\", \".join(self.installed_packages.keys())}'\n\n    async def add_requirement(self, requirement: str, ctx, transaction):\n        if requirement.endswith(\".whl\"):\n            # custom download location\n            name, wheel, version = _parse_wheel_url(requirement)\n            transaction[\"wheels\"].append((name, wheel, version))\n            return\n\n        req = util.parse_requirement(requirement)\n\n        # If it's a Pyodide package, use that instead of the one on PyPI\n        if req.name in self.builtin_packages:\n            transaction[\"pyodide_packages\"].add(req.name)\n            return\n\n        if req.marker:\n            if not markers.evaluator.evaluate(req.marker, ctx):\n                return\n\n        matcher = self.version_scheme.matcher(req.requirement)\n\n        # If we already have something that will work, don't\n        # fetch again\n        for name, ver in transaction[\"locked\"].items():\n            if name == req.name:\n                if matcher.match(ver):\n                    break\n                else:\n                    raise ValueError(\n                        f\"Requested '{requirement}', \"\n                        f\"but {name}=={ver} is already installed\"\n                    )\n        else:\n            metadata = await _get_pypi_json(req.name)\n            wheel, ver = self.find_wheel(metadata, req)\n            transaction[\"locked\"][req.name] = ver\n\n            recurs_reqs = metadata.get(\"info\", {}).get(\"requires_dist\") or []\n            for recurs_req in recurs_reqs:\n                await self.add_requirement(recurs_req, ctx, transaction)\n\n            transaction[\"wheels\"].append((req.name, wheel, ver))\n\n    def find_wheel(self, metadata, req):\n        releases = []\n        for ver, files in metadata.get(\"releases\", {}).items():\n            ver = self.version_scheme.suggest(ver)\n            if ver is not None:\n                releases.append((ver, files))\n\n        def version_number(release):\n            return version.NormalizedVersion(release[0])\n\n        releases = sorted(releases, key=version_number, reverse=True)\n        matcher = self.version_scheme.matcher(req.requirement)\n        for ver, meta in releases:\n            if matcher.match(ver):\n                for fileinfo in meta:\n                    if fileinfo[\"filename\"].endswith(\"py3-none-any.whl\"):\n                        return fileinfo, ver\n\n        raise ValueError(f\"Couldn't find a pure Python 3 wheel for '{req.requirement}'\")\n\n\n# Make PACKAGE_MANAGER singleton\nPACKAGE_MANAGER = _PackageManager()\ndel _PackageManager\n\n\ndef install(requirements: Union[str, List[str]]):\n    \"\"\"Install the given package and all of its dependencies.\n\n    See :ref:`loading packages <loading_packages>` for more information.\n\n    This only works for packages that are either pure Python or for packages\n    with C extensions that are built in Pyodide. If a pure Python package is not\n    found in the Pyodide repository it will be loaded from PyPi.\n\n    Parameters\n    ----------\n    requirements : ``str | List[str]``\n\n        A requirement or list of requirements to install. Each requirement is a string, which should be either\n        a package name or URL to a wheel:\n\n        - If the requirement ends in ``.whl`` it will be interpreted as a URL.\n          The file must be a wheel named in compliance with the\n          `PEP 427 naming convention <https://www.python.org/dev/peps/pep-0427/#file-format>`_.\n\n        - If the requirement does not end in ``.whl``, it will interpreted as the\n          name of a package. A package by this name must either be present in the\n          Pyodide repository at `indexURL <globalThis.loadPyodide>` or on PyPi\n\n    Returns\n    -------\n    ``Future``\n\n        A ``Future`` that resolves to ``None`` when all packages have\n        been downloaded and installed.\n    \"\"\"\n    importlib.invalidate_caches()\n    return asyncio.ensure_future(PACKAGE_MANAGER.install(requirements))\n\n\n__all__ = [\"install\"]\n\n\nif __name__ == \"__main__\":\n    install(\"snowballstemmer\")\n", "path": "packages/micropip/micropip/micropip.py"}], "after_files": [{"content": "import asyncio\nimport hashlib\nimport importlib\nimport io\nimport json\nfrom pathlib import Path\nimport zipfile\nfrom typing import Dict, Any, Union, List, Tuple\n\nfrom distlib import markers, util, version\n\n# Provide stubs for testing in native python\ntry:\n    import pyodide_js\n\n    IN_BROWSER = True\nexcept ImportError:\n    IN_BROWSER = False\n\nif IN_BROWSER:\n    # In practice, this is the `site-packages` directory.\n    WHEEL_BASE = Path(__file__).parent\nelse:\n    WHEEL_BASE = Path(\".\") / \"wheels\"\n\nif IN_BROWSER:\n    from js import fetch\n\n    async def _get_url(url):\n        resp = await fetch(url)\n        if not resp.ok:\n            raise OSError(\n                f\"Request for {url} failed with status {resp.status}: {resp.statusText}\"\n            )\n        return io.BytesIO(await resp.arrayBuffer())\n\n\nelse:\n    from urllib.request import urlopen\n\n    async def _get_url(url):\n        with urlopen(url) as fd:\n            content = fd.read()\n        return io.BytesIO(content)\n\n\nif IN_BROWSER:\n    from asyncio import gather\nelse:\n    # asyncio.gather will schedule any coroutines to run on the event loop but\n    # we want to avoid using the event loop at all. Instead just run the\n    # coroutines in sequence.\n    async def gather(*coroutines):  # type: ignore\n        result = []\n        for coroutine in coroutines:\n            result.append(await coroutine)\n        return result\n\n\nif IN_BROWSER:\n    from pyodide_js import loadedPackages\nelse:\n\n    class loadedPackages:  # type: ignore\n        pass\n\n\nasync def _get_pypi_json(pkgname):\n    url = f\"https://pypi.org/pypi/{pkgname}/json\"\n    fd = await _get_url(url)\n    return json.load(fd)\n\n\ndef _parse_wheel_url(url: str) -> Tuple[str, Dict[str, Any], str]:\n    \"\"\"Parse wheels URL and extract available metadata\n\n    See https://www.python.org/dev/peps/pep-0427/#file-name-convention\n    \"\"\"\n    file_name = Path(url).name\n    # also strip '.whl' extension.\n    wheel_name = Path(url).stem\n    tokens = wheel_name.split(\"-\")\n    # TODO: support optional build tags in the filename (cf PEP 427)\n    if len(tokens) < 5:\n        raise ValueError(f\"{file_name} is not a valid wheel file name.\")\n    version, python_tag, abi_tag, platform = tokens[-4:]\n    name = \"-\".join(tokens[:-4])\n    wheel = {\n        \"digests\": None,  # checksums not available\n        \"filename\": file_name,\n        \"packagetype\": \"bdist_wheel\",\n        \"python_version\": python_tag,\n        \"abi_tag\": abi_tag,\n        \"platform\": platform,\n        \"url\": url,\n    }\n\n    return name, wheel, version\n\n\ndef _extract_wheel(fd):\n    with zipfile.ZipFile(fd) as zf:\n        zf.extractall(WHEEL_BASE)\n\n\ndef _validate_wheel(data, fileinfo):\n    if fileinfo.get(\"digests\") is None:\n        # No checksums available, e.g. because installing\n        # from a different location than PyPi.\n        return\n    sha256 = fileinfo[\"digests\"][\"sha256\"]\n    m = hashlib.sha256()\n    m.update(data.getvalue())\n    if m.hexdigest() != sha256:\n        raise ValueError(\"Contents don't match hash\")\n\n\nasync def _install_wheel(name, fileinfo):\n    url = fileinfo[\"url\"]\n    wheel = await _get_url(url)\n    _validate_wheel(wheel, fileinfo)\n    _extract_wheel(wheel)\n    setattr(loadedPackages, name, url)\n\n\nclass _PackageManager:\n    version_scheme = version.get_scheme(\"normalized\")\n\n    def __init__(self):\n        if IN_BROWSER:\n            self.builtin_packages = pyodide_js._module.packages.dependencies.to_py()\n        else:\n            self.builtin_packages = {}\n        self.installed_packages = {}\n\n    async def install(self, requirements: Union[str, List[str]], ctx=None):\n        if ctx is None:\n            ctx = {\"extra\": None}\n\n        complete_ctx = dict(markers.DEFAULT_CONTEXT)\n        complete_ctx.update(ctx)\n\n        if isinstance(requirements, str):\n            requirements = [requirements]\n\n        transaction: Dict[str, Any] = {\n            \"wheels\": [],\n            \"pyodide_packages\": set(),\n            \"locked\": dict(self.installed_packages),\n        }\n        requirement_promises = []\n        for requirement in requirements:\n            requirement_promises.append(\n                self.add_requirement(requirement, complete_ctx, transaction)\n            )\n\n        await gather(*requirement_promises)\n\n        wheel_promises = []\n\n        # Install built-in packages\n        pyodide_packages = transaction[\"pyodide_packages\"]\n        if len(pyodide_packages):\n            # Note: branch never happens in out-of-browser testing because we\n            # report that all dependencies are empty.\n            self.installed_packages.update(dict((k, None) for k in pyodide_packages))\n            wheel_promises.append(\n                asyncio.ensure_future(pyodide_js.loadPackage(list(pyodide_packages)))\n            )\n\n        # Now install PyPI packages\n        for name, wheel, ver in transaction[\"wheels\"]:\n            wheel_promises.append(_install_wheel(name, wheel))\n            self.installed_packages[name] = ver\n        await gather(*wheel_promises)\n\n    async def add_requirement(self, requirement: str, ctx, transaction):\n        if requirement.endswith(\".whl\"):\n            # custom download location\n            name, wheel, version = _parse_wheel_url(requirement)\n            transaction[\"wheels\"].append((name, wheel, version))\n            return\n\n        req = util.parse_requirement(requirement)\n\n        # If it's a Pyodide package, use that instead of the one on PyPI\n        if req.name in self.builtin_packages:\n            transaction[\"pyodide_packages\"].add(req.name)\n            return\n\n        if req.marker:\n            if not markers.evaluator.evaluate(req.marker, ctx):\n                return\n\n        matcher = self.version_scheme.matcher(req.requirement)\n\n        # If we already have something that will work, don't\n        # fetch again\n        for name, ver in transaction[\"locked\"].items():\n            if name == req.name:\n                if matcher.match(ver):\n                    break\n                else:\n                    raise ValueError(\n                        f\"Requested '{requirement}', \"\n                        f\"but {name}=={ver} is already installed\"\n                    )\n        else:\n            metadata = await _get_pypi_json(req.name)\n            wheel, ver = self.find_wheel(metadata, req)\n            transaction[\"locked\"][req.name] = ver\n\n            recurs_reqs = metadata.get(\"info\", {}).get(\"requires_dist\") or []\n            for recurs_req in recurs_reqs:\n                await self.add_requirement(recurs_req, ctx, transaction)\n\n            transaction[\"wheels\"].append((req.name, wheel, ver))\n\n    def find_wheel(self, metadata, req):\n        releases = []\n        for ver, files in metadata.get(\"releases\", {}).items():\n            ver = self.version_scheme.suggest(ver)\n            if ver is not None:\n                releases.append((ver, files))\n\n        def version_number(release):\n            return version.NormalizedVersion(release[0])\n\n        releases = sorted(releases, key=version_number, reverse=True)\n        matcher = self.version_scheme.matcher(req.requirement)\n        for ver, meta in releases:\n            if matcher.match(ver):\n                for fileinfo in meta:\n                    if fileinfo[\"filename\"].endswith(\"py3-none-any.whl\"):\n                        return fileinfo, ver\n\n        raise ValueError(f\"Couldn't find a pure Python 3 wheel for '{req.requirement}'\")\n\n\n# Make PACKAGE_MANAGER singleton\nPACKAGE_MANAGER = _PackageManager()\ndel _PackageManager\n\n\ndef install(requirements: Union[str, List[str]]):\n    \"\"\"Install the given package and all of its dependencies.\n\n    See :ref:`loading packages <loading_packages>` for more information.\n\n    This only works for packages that are either pure Python or for packages\n    with C extensions that are built in Pyodide. If a pure Python package is not\n    found in the Pyodide repository it will be loaded from PyPi.\n\n    Parameters\n    ----------\n    requirements : ``str | List[str]``\n\n        A requirement or list of requirements to install. Each requirement is a\n        string, which should be either a package name or URL to a wheel:\n\n        - If the requirement ends in ``.whl`` it will be interpreted as a URL.\n          The file must be a wheel named in compliance with the\n          `PEP 427 naming convention <https://www.python.org/dev/peps/pep-0427/#file-format>`_.\n\n        - If the requirement does not end in ``.whl``, it will interpreted as the\n          name of a package. A package by this name must either be present in the\n          Pyodide repository at `indexURL <globalThis.loadPyodide>` or on PyPi\n\n    Returns\n    -------\n    ``Future``\n\n        A ``Future`` that resolves to ``None`` when all packages have been\n        downloaded and installed.\n    \"\"\"\n    importlib.invalidate_caches()\n    return asyncio.ensure_future(PACKAGE_MANAGER.install(requirements))\n\n\n__all__ = [\"install\"]\n\n\nif __name__ == \"__main__\":\n    install(\"snowballstemmer\")\n", "path": "packages/micropip/micropip/micropip.py"}]}