We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Allow Micropip to download from relative urls
I think to allow relative urls, we'd only need to change the check here:

https://github.com/iodide-project/pyodide/blob/3a06f5dfcb9b536e9ece1f68f6963717acf82486/packages/micropip/micropip/micropip.py#L201

to `if "/" in requirement` or `if requirement.endswith(".whl")`.

Also, the documentation of `micropip.install` is a bit lacking. It'd be good to add an explanation of how `micropip` decides what a url is.
But for instance, it could be helpful to indicate the case where `url` is `"some_package-vers-py3-none-any.whl"`: does this expect a file `some_package-vers-py3-none-any.whl` to be in the current directory?

Would be good to mention that all wheels need to be named according to pep 427 too.
https://www.python.org/dev/peps/pep-0427/#file-name-convention
Redundancies / differences between pyodide.loadPackage and micropip.install?
It seems to me that `pyodide.loadPackage` and `micropip.install` have significant redundancies in their functionality. Is there any difference in their purpose? Which one is better? Could they be merged? If not, it would be good to add a very explicit explanation of their differences to the docs.

--- 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 try:
2     from js import Promise, XMLHttpRequest
3 except ImportError:
4     XMLHttpRequest = None
5 
6 try:
7     from js import pyodide as js_pyodide
8 except ImportError:
9 
10     class js_pyodide:  # type: ignore
11         """A mock object to allow import of this package outside pyodide"""
12 
13         class _module:
14             class packages:
15                 dependencies = []  # type: ignore
16 
17 
18 import hashlib
19 import importlib
20 import io
21 import json
22 from pathlib import Path
23 import zipfile
24 from typing import Dict, Any, Union, List, Tuple
25 
26 from distlib import markers, util, version
27 
28 
29 def _nullop(*args):
30     return
31 
32 
33 # Provide implementations of HTTP fetching for in-browser and out-of-browser to
34 # make testing easier
35 if XMLHttpRequest is not None:
36     import pyodide  # noqa
37 
38     def _get_url(url):
39         req = XMLHttpRequest.new()
40         req.open("GET", url, False)
41         req.send(None)
42         return io.StringIO(req.response)
43 
44     def _get_url_async(url, cb):
45         req = XMLHttpRequest.new()
46         req.open("GET", url, True)
47         req.responseType = "arraybuffer"
48 
49         def callback(e):
50             if req.readyState == 4:
51                 cb(io.BytesIO(req.response))
52 
53         req.onreadystatechange = callback
54         req.send(None)
55 
56     # In practice, this is the `site-packages` directory.
57     WHEEL_BASE = Path(__file__).parent
58 else:
59     # Outside the browser
60     from urllib.request import urlopen
61 
62     def _get_url(url):
63         with urlopen(url) as fd:
64             content = fd.read()
65         return io.BytesIO(content)
66 
67     def _get_url_async(url, cb):
68         cb(_get_url(url))
69 
70     WHEEL_BASE = Path(".") / "wheels"
71 
72 
73 def _get_pypi_json(pkgname):
74     url = f"https://pypi.org/pypi/{pkgname}/json"
75     fd = _get_url(url)
76     return json.load(fd)
77 
78 
79 def _parse_wheel_url(url: str) -> Tuple[str, Dict[str, Any], str]:
80     """Parse wheels url and extract available metadata
81 
82     See https://www.python.org/dev/peps/pep-0427/#file-name-convention
83     """
84     file_name = Path(url).name
85     # also strip '.whl' extension.
86     wheel_name = Path(url).stem
87     tokens = wheel_name.split("-")
88     # TODO: support optional build tags in the filename (cf PEP 427)
89     if len(tokens) < 5:
90         raise ValueError(f"{file_name} is not a valid wheel file name.")
91     version, python_tag, abi_tag, platform = tokens[-4:]
92     name = "-".join(tokens[:-4])
93     wheel = {
94         "digests": None,  # checksums not available
95         "filename": file_name,
96         "packagetype": "bdist_wheel",
97         "python_version": python_tag,
98         "abi_tag": abi_tag,
99         "platform": platform,
100         "url": url,
101     }
102 
103     return name, wheel, version
104 
105 
106 class _WheelInstaller:
107     def extract_wheel(self, fd):
108         with zipfile.ZipFile(fd) as zf:
109             zf.extractall(WHEEL_BASE)
110 
111     def validate_wheel(self, data, fileinfo):
112         if fileinfo.get("digests") is None:
113             # No checksums available, e.g. because installing
114             # from a different location than PyPi.
115             return
116         sha256 = fileinfo["digests"]["sha256"]
117         m = hashlib.sha256()
118         m.update(data.getvalue())
119         if m.hexdigest() != sha256:
120             raise ValueError("Contents don't match hash")
121 
122     def __call__(self, name, fileinfo, resolve, reject):
123         url = self.fetch_wheel(name, fileinfo)
124 
125         def callback(wheel):
126             try:
127                 self.validate_wheel(wheel, fileinfo)
128                 self.extract_wheel(wheel)
129             except Exception as e:
130                 reject(str(e))
131             else:
132                 resolve()
133 
134         _get_url_async(url, callback)
135 
136 
137 class _RawWheelInstaller(_WheelInstaller):
138     def fetch_wheel(self, name, fileinfo):
139         return fileinfo["url"]
140 
141 
142 class _PackageManager:
143     version_scheme = version.get_scheme("normalized")
144 
145     def __init__(self):
146         self.builtin_packages = {}
147         self.builtin_packages.update(js_pyodide._module.packages.dependencies)
148         self.installed_packages = {}
149 
150     def install(
151         self,
152         requirements: Union[str, List[str]],
153         ctx=None,
154         wheel_installer=None,
155         resolve=_nullop,
156         reject=_nullop,
157     ):
158         try:
159             if ctx is None:
160                 ctx = {"extra": None}
161 
162             if wheel_installer is None:
163                 wheel_installer = _RawWheelInstaller()
164 
165             complete_ctx = dict(markers.DEFAULT_CONTEXT)
166             complete_ctx.update(ctx)
167 
168             if isinstance(requirements, str):
169                 requirements = [requirements]
170 
171             transaction: Dict[str, Any] = {
172                 "wheels": [],
173                 "pyodide_packages": set(),
174                 "locked": dict(self.installed_packages),
175             }
176             for requirement in requirements:
177                 self.add_requirement(requirement, complete_ctx, transaction)
178         except Exception as e:
179             reject(str(e))
180 
181         resolve_count = [len(transaction["wheels"])]
182 
183         def do_resolve(*args):
184             resolve_count[0] -= 1
185             if resolve_count[0] == 0:
186                 resolve(f'Installed {", ".join(self.installed_packages.keys())}')
187 
188         # Install built-in packages
189         pyodide_packages = transaction["pyodide_packages"]
190         if len(pyodide_packages):
191             resolve_count[0] += 1
192             self.installed_packages.update(dict((k, None) for k in pyodide_packages))
193             js_pyodide.loadPackage(list(pyodide_packages)).then(do_resolve)
194 
195         # Now install PyPI packages
196         for name, wheel, ver in transaction["wheels"]:
197             wheel_installer(name, wheel, do_resolve, reject)
198             self.installed_packages[name] = ver
199 
200     def add_requirement(self, requirement: str, ctx, transaction):
201         if requirement.startswith(("http://", "https://")):
202             # custom download location
203             name, wheel, version = _parse_wheel_url(requirement)
204             transaction["wheels"].append((name, wheel, version))
205             return
206 
207         req = util.parse_requirement(requirement)
208 
209         # If it's a Pyodide package, use that instead of the one on PyPI
210         if req.name in self.builtin_packages:
211             transaction["pyodide_packages"].add(req.name)
212             return
213 
214         if req.marker:
215             if not markers.evaluator.evaluate(req.marker, ctx):
216                 return
217 
218         matcher = self.version_scheme.matcher(req.requirement)
219 
220         # If we already have something that will work, don't
221         # fetch again
222         for name, ver in transaction["locked"].items():
223             if name == req.name:
224                 if matcher.match(ver):
225                     break
226                 else:
227                     raise ValueError(
228                         f"Requested '{requirement}', "
229                         f"but {name}=={ver} is already installed"
230                     )
231         else:
232             metadata = _get_pypi_json(req.name)
233             wheel, ver = self.find_wheel(metadata, req)
234             transaction["locked"][req.name] = ver
235 
236             recurs_reqs = metadata.get("info", {}).get("requires_dist") or []
237             for recurs_req in recurs_reqs:
238                 self.add_requirement(recurs_req, ctx, transaction)
239 
240             transaction["wheels"].append((req.name, wheel, ver))
241 
242     def find_wheel(self, metadata, req):
243         releases = []
244         for ver, files in metadata.get("releases", {}).items():
245             ver = self.version_scheme.suggest(ver)
246             if ver is not None:
247                 releases.append((ver, files))
248 
249         def version_number(release):
250             return version.NormalizedVersion(release[0])
251 
252         releases = sorted(releases, key=version_number, reverse=True)
253         matcher = self.version_scheme.matcher(req.requirement)
254         for ver, meta in releases:
255             if matcher.match(ver):
256                 for fileinfo in meta:
257                     if fileinfo["filename"].endswith("py3-none-any.whl"):
258                         return fileinfo, ver
259 
260         raise ValueError(f"Couldn't find a pure Python 3 wheel for '{req.requirement}'")
261 
262 
263 # Make PACKAGE_MANAGER singleton
264 PACKAGE_MANAGER = _PackageManager()
265 del _PackageManager
266 
267 
268 def install(requirements: Union[str, List[str]]):
269     """Install the given package and all of its dependencies.
270 
271     This only works for pure Python wheels or for packages built
272     in pyodide. If a package is not found in the pyodide repository
273     it will be loaded from PyPi.
274 
275     Parameters
276     ----------
277     requirements
278        a requirements or a list of requirements to install.
279        Can be composed either of
280 
281          - package names, as defined in pyodide repository or on PyPi
282          - URLs pointing to pure Python wheels. The file name of such wheels
283            end with ``none-any.whl``.
284 
285     Returns
286     -------
287     a Promise that resolves when all packages have downloaded and installed.
288     """
289 
290     def do_install(resolve, reject):
291         PACKAGE_MANAGER.install(requirements, resolve=resolve, reject=reject)
292         importlib.invalidate_caches()
293 
294     return Promise.new(do_install)
295 
296 
297 __all__ = ["install"]
298 
299 
300 if __name__ == "__main__":
301     install("snowballstemmer")
302
```
--- 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
@@ -198,7 +198,7 @@
             self.installed_packages[name] = ver
 
     def add_requirement(self, requirement: str, ctx, transaction):
-        if requirement.startswith(("http://", "https://")):
+        if requirement.endswith(".whl"):
             # custom download location
             name, wheel, version = _parse_wheel_url(requirement)
             transaction["wheels"].append((name, wheel, version))
@@ -268,23 +268,28 @@
 def install(requirements: Union[str, List[str]]):
     """Install the given package and all of its dependencies.
 
-    This only works for pure Python wheels or for packages built
-    in pyodide. If a package is not found in the pyodide repository
-    it will be loaded from PyPi.
+    See :ref:`loading packages <loading_packages>` for more information.
+
+    This only works for packages that are either pure Python or for packages with
+    C extensions that are built in pyodide. If a pure Python package is not found
+    in the pyodide repository it will be loaded from PyPi.
 
     Parameters
     ----------
     requirements
-       a requirements or a list of requirements to install.
-       Can be composed either of
+       A requirement or list of requirements to install.
+       Each requirement is a string.
+
+         - If the requirement ends in ".whl", the file will be interpreted as a url.
+           The file must be a wheel named in compliance with the
+           [PEP 427 naming convention](https://www.python.org/dev/peps/pep-0427/#file-format)
 
-         - package names, as defined in pyodide repository or on PyPi
-         - URLs pointing to pure Python wheels. The file name of such wheels
-           end with ``none-any.whl``.
+         - A package name. A package by this name must either be present in the pyodide
+           repository at `languagePluginUrl` or on PyPi.
 
     Returns
     -------
-    a Promise that resolves when all packages have downloaded and installed.
+    A Promise that resolves when all packages have been downloaded and installed.
     """
 
     def do_install(resolve, reject):

{"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@@ -198,7 +198,7 @@\n             self.installed_packages[name] = ver\n \n     def add_requirement(self, requirement: str, ctx, transaction):\n-        if requirement.startswith((\"http://\", \"https://\")):\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@@ -268,23 +268,28 @@\n def install(requirements: Union[str, List[str]]):\n     \"\"\"Install the given package and all of its dependencies.\n \n-    This only works for pure Python wheels or for packages built\n-    in pyodide. If a package is not found in the pyodide repository\n-    it will be loaded from PyPi.\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 with\n+    C extensions that are built in pyodide. If a pure Python package is not found\n+    in the pyodide repository it will be loaded from PyPi.\n \n     Parameters\n     ----------\n     requirements\n-       a requirements or a list of requirements to install.\n-       Can be composed either of\n+       A requirement or list of requirements to install.\n+       Each requirement is a string.\n+\n+         - If the requirement ends in \".whl\", the file 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-         - package names, as defined in pyodide repository or on PyPi\n-         - URLs pointing to pure Python wheels. The file name of such wheels\n-           end with ``none-any.whl``.\n+         - A package name. A package by this name must either be present in the pyodide\n+           repository at `languagePluginUrl` or on PyPi.\n \n     Returns\n     -------\n-    a Promise that resolves when all packages have downloaded and installed.\n+    A Promise that resolves when all packages have been downloaded and installed.\n     \"\"\"\n \n     def do_install(resolve, reject):\n", "issue": "Allow Micropip to download from relative urls\nI think to allow relative urls, we'd only need to change the check here:\r\n\r\nhttps://github.com/iodide-project/pyodide/blob/3a06f5dfcb9b536e9ece1f68f6963717acf82486/packages/micropip/micropip/micropip.py#L201\r\n\r\nto `if \"/\" in requirement` or `if requirement.endswith(\".whl\")`.\r\n\r\nAlso, the documentation of `micropip.install` is a bit lacking. It'd be good to add an explanation of how `micropip` decides what a url is.\r\nBut for instance, it could be helpful to indicate the case where `url` is `\"some_package-vers-py3-none-any.whl\"`: does this expect a file `some_package-vers-py3-none-any.whl` to be in the current directory?\r\n\r\nWould be good to mention that all wheels need to be named according to pep 427 too.\r\nhttps://www.python.org/dev/peps/pep-0427/#file-name-convention\nRedundancies / differences between pyodide.loadPackage and micropip.install?\nIt seems to me that `pyodide.loadPackage` and `micropip.install` have significant redundancies in their functionality. Is there any difference in their purpose? Which one is better? Could they be merged? If not, it would be good to add a very explicit explanation of their differences to the docs.\n", "before_files": [{"content": "try:\n    from js import Promise, XMLHttpRequest\nexcept ImportError:\n    XMLHttpRequest = None\n\ntry:\n    from js import pyodide as js_pyodide\nexcept ImportError:\n\n    class js_pyodide:  # type: ignore\n        \"\"\"A mock object to allow import of this package outside pyodide\"\"\"\n\n        class _module:\n            class packages:\n                dependencies = []  # type: ignore\n\n\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\ndef _nullop(*args):\n    return\n\n\n# Provide implementations of HTTP fetching for in-browser and out-of-browser to\n# make testing easier\nif XMLHttpRequest is not None:\n    import pyodide  # noqa\n\n    def _get_url(url):\n        req = XMLHttpRequest.new()\n        req.open(\"GET\", url, False)\n        req.send(None)\n        return io.StringIO(req.response)\n\n    def _get_url_async(url, cb):\n        req = XMLHttpRequest.new()\n        req.open(\"GET\", url, True)\n        req.responseType = \"arraybuffer\"\n\n        def callback(e):\n            if req.readyState == 4:\n                cb(io.BytesIO(req.response))\n\n        req.onreadystatechange = callback\n        req.send(None)\n\n    # In practice, this is the `site-packages` directory.\n    WHEEL_BASE = Path(__file__).parent\nelse:\n    # Outside the browser\n    from urllib.request import urlopen\n\n    def _get_url(url):\n        with urlopen(url) as fd:\n            content = fd.read()\n        return io.BytesIO(content)\n\n    def _get_url_async(url, cb):\n        cb(_get_url(url))\n\n    WHEEL_BASE = Path(\".\") / \"wheels\"\n\n\ndef _get_pypi_json(pkgname):\n    url = f\"https://pypi.org/pypi/{pkgname}/json\"\n    fd = _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\nclass _WheelInstaller:\n    def extract_wheel(self, fd):\n        with zipfile.ZipFile(fd) as zf:\n            zf.extractall(WHEEL_BASE)\n\n    def validate_wheel(self, 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    def __call__(self, name, fileinfo, resolve, reject):\n        url = self.fetch_wheel(name, fileinfo)\n\n        def callback(wheel):\n            try:\n                self.validate_wheel(wheel, fileinfo)\n                self.extract_wheel(wheel)\n            except Exception as e:\n                reject(str(e))\n            else:\n                resolve()\n\n        _get_url_async(url, callback)\n\n\nclass _RawWheelInstaller(_WheelInstaller):\n    def fetch_wheel(self, name, fileinfo):\n        return fileinfo[\"url\"]\n\n\nclass _PackageManager:\n    version_scheme = version.get_scheme(\"normalized\")\n\n    def __init__(self):\n        self.builtin_packages = {}\n        self.builtin_packages.update(js_pyodide._module.packages.dependencies)\n        self.installed_packages = {}\n\n    def install(\n        self,\n        requirements: Union[str, List[str]],\n        ctx=None,\n        wheel_installer=None,\n        resolve=_nullop,\n        reject=_nullop,\n    ):\n        try:\n            if ctx is None:\n                ctx = {\"extra\": None}\n\n            if wheel_installer is None:\n                wheel_installer = _RawWheelInstaller()\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            for requirement in requirements:\n                self.add_requirement(requirement, complete_ctx, transaction)\n        except Exception as e:\n            reject(str(e))\n\n        resolve_count = [len(transaction[\"wheels\"])]\n\n        def do_resolve(*args):\n            resolve_count[0] -= 1\n            if resolve_count[0] == 0:\n                resolve(f'Installed {\", \".join(self.installed_packages.keys())}')\n\n        # Install built-in packages\n        pyodide_packages = transaction[\"pyodide_packages\"]\n        if len(pyodide_packages):\n            resolve_count[0] += 1\n            self.installed_packages.update(dict((k, None) for k in pyodide_packages))\n            js_pyodide.loadPackage(list(pyodide_packages)).then(do_resolve)\n\n        # Now install PyPI packages\n        for name, wheel, ver in transaction[\"wheels\"]:\n            wheel_installer(name, wheel, do_resolve, reject)\n            self.installed_packages[name] = ver\n\n    def add_requirement(self, requirement: str, ctx, transaction):\n        if requirement.startswith((\"http://\", \"https://\")):\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 = _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                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    This only works for pure Python wheels or for packages built\n    in pyodide. If a package is not found in the pyodide repository\n    it will be loaded from PyPi.\n\n    Parameters\n    ----------\n    requirements\n       a requirements or a list of requirements to install.\n       Can be composed either of\n\n         - package names, as defined in pyodide repository or on PyPi\n         - URLs pointing to pure Python wheels. The file name of such wheels\n           end with ``none-any.whl``.\n\n    Returns\n    -------\n    a Promise that resolves when all packages have downloaded and installed.\n    \"\"\"\n\n    def do_install(resolve, reject):\n        PACKAGE_MANAGER.install(requirements, resolve=resolve, reject=reject)\n        importlib.invalidate_caches()\n\n    return Promise.new(do_install)\n\n\n__all__ = [\"install\"]\n\n\nif __name__ == \"__main__\":\n    install(\"snowballstemmer\")\n", "path": "packages/micropip/micropip/micropip.py"}], "after_files": [{"content": "try:\n    from js import Promise, XMLHttpRequest\nexcept ImportError:\n    XMLHttpRequest = None\n\ntry:\n    from js import pyodide as js_pyodide\nexcept ImportError:\n\n    class js_pyodide:  # type: ignore\n        \"\"\"A mock object to allow import of this package outside pyodide\"\"\"\n\n        class _module:\n            class packages:\n                dependencies = []  # type: ignore\n\n\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\ndef _nullop(*args):\n    return\n\n\n# Provide implementations of HTTP fetching for in-browser and out-of-browser to\n# make testing easier\nif XMLHttpRequest is not None:\n    import pyodide  # noqa\n\n    def _get_url(url):\n        req = XMLHttpRequest.new()\n        req.open(\"GET\", url, False)\n        req.send(None)\n        return io.StringIO(req.response)\n\n    def _get_url_async(url, cb):\n        req = XMLHttpRequest.new()\n        req.open(\"GET\", url, True)\n        req.responseType = \"arraybuffer\"\n\n        def callback(e):\n            if req.readyState == 4:\n                cb(io.BytesIO(req.response))\n\n        req.onreadystatechange = callback\n        req.send(None)\n\n    # In practice, this is the `site-packages` directory.\n    WHEEL_BASE = Path(__file__).parent\nelse:\n    # Outside the browser\n    from urllib.request import urlopen\n\n    def _get_url(url):\n        with urlopen(url) as fd:\n            content = fd.read()\n        return io.BytesIO(content)\n\n    def _get_url_async(url, cb):\n        cb(_get_url(url))\n\n    WHEEL_BASE = Path(\".\") / \"wheels\"\n\n\ndef _get_pypi_json(pkgname):\n    url = f\"https://pypi.org/pypi/{pkgname}/json\"\n    fd = _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\nclass _WheelInstaller:\n    def extract_wheel(self, fd):\n        with zipfile.ZipFile(fd) as zf:\n            zf.extractall(WHEEL_BASE)\n\n    def validate_wheel(self, 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    def __call__(self, name, fileinfo, resolve, reject):\n        url = self.fetch_wheel(name, fileinfo)\n\n        def callback(wheel):\n            try:\n                self.validate_wheel(wheel, fileinfo)\n                self.extract_wheel(wheel)\n            except Exception as e:\n                reject(str(e))\n            else:\n                resolve()\n\n        _get_url_async(url, callback)\n\n\nclass _RawWheelInstaller(_WheelInstaller):\n    def fetch_wheel(self, name, fileinfo):\n        return fileinfo[\"url\"]\n\n\nclass _PackageManager:\n    version_scheme = version.get_scheme(\"normalized\")\n\n    def __init__(self):\n        self.builtin_packages = {}\n        self.builtin_packages.update(js_pyodide._module.packages.dependencies)\n        self.installed_packages = {}\n\n    def install(\n        self,\n        requirements: Union[str, List[str]],\n        ctx=None,\n        wheel_installer=None,\n        resolve=_nullop,\n        reject=_nullop,\n    ):\n        try:\n            if ctx is None:\n                ctx = {\"extra\": None}\n\n            if wheel_installer is None:\n                wheel_installer = _RawWheelInstaller()\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            for requirement in requirements:\n                self.add_requirement(requirement, complete_ctx, transaction)\n        except Exception as e:\n            reject(str(e))\n\n        resolve_count = [len(transaction[\"wheels\"])]\n\n        def do_resolve(*args):\n            resolve_count[0] -= 1\n            if resolve_count[0] == 0:\n                resolve(f'Installed {\", \".join(self.installed_packages.keys())}')\n\n        # Install built-in packages\n        pyodide_packages = transaction[\"pyodide_packages\"]\n        if len(pyodide_packages):\n            resolve_count[0] += 1\n            self.installed_packages.update(dict((k, None) for k in pyodide_packages))\n            js_pyodide.loadPackage(list(pyodide_packages)).then(do_resolve)\n\n        # Now install PyPI packages\n        for name, wheel, ver in transaction[\"wheels\"]:\n            wheel_installer(name, wheel, do_resolve, reject)\n            self.installed_packages[name] = ver\n\n    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 = _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                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 with\n    C extensions that are built in pyodide. If a pure Python package is not found\n    in the pyodide repository it will be loaded from PyPi.\n\n    Parameters\n    ----------\n    requirements\n       A requirement or list of requirements to install.\n       Each requirement is a string.\n\n         - If the requirement ends in \".whl\", the file 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         - A package name. A package by this name must either be present in the pyodide\n           repository at `languagePluginUrl` or on PyPi.\n\n    Returns\n    -------\n    A Promise that resolves when all packages have been downloaded and installed.\n    \"\"\"\n\n    def do_install(resolve, reject):\n        PACKAGE_MANAGER.install(requirements, resolve=resolve, reject=reject)\n        importlib.invalidate_caches()\n\n    return Promise.new(do_install)\n\n\n__all__ = [\"install\"]\n\n\nif __name__ == \"__main__\":\n    install(\"snowballstemmer\")\n", "path": "packages/micropip/micropip/micropip.py"}]}