Repository: pyodide/pyodide

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.


Discussion:
> change the check here: [...] to `if requirement.endswith(".whl")`

+1 for this. Would you like to make a PR (and improve documentation) ? :)

There are currently two places for micropip documentation,
 - the [user manual](https://pyodide.readthedocs.io/en/latest/loading_packages.html#micropip)
 - the [docstring](https://pyodide.readthedocs.io/en/latest/micropip-api/micropip.install.html#micropip.install)

and it would be good to cross link them better. With typically a short description in the docstring (but still more extensive than what we have now) linking to the user manual for more details.
> Would you like to make a PR (and improve documentation) ?

Yeah I'll do that. I've been planning to make some PRs to improve the docs but I keep being struck by laziness.


Resolving patch:
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 @@ def do_resolve(*args):
             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 version_number(release):
 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):


Test patch:
diff --git a/packages/micropip/test_micropip.py b/packages/micropip/test_micropip.py
--- a/packages/micropip/test_micropip.py
+++ b/packages/micropip/test_micropip.py
@@ -71,6 +71,43 @@ def test_install_custom_url(selenium_standalone, web_server_tst_data):
     selenium_standalone.run("import snowballstemmer")
 
 
+def test_add_requirement_relative_url():
+    pytest.importorskip("distlib")
+    import micropip
+
+    transaction = {"wheels": []}
+    micropip.PACKAGE_MANAGER.add_requirement(
+        "./snowballstemmer-2.0.0-py2.py3-none-any.whl", {}, transaction
+    )
+    [name, req, version] = transaction["wheels"][0]
+    assert name == "snowballstemmer"
+    assert version == "2.0.0"
+    assert req["filename"] == "snowballstemmer-2.0.0-py2.py3-none-any.whl"
+    assert req["packagetype"] == "bdist_wheel"
+    assert req["python_version"] == "py2.py3"
+    assert req["abi_tag"] == "none"
+    assert req["platform"] == "any"
+    assert req["url"] == "./snowballstemmer-2.0.0-py2.py3-none-any.whl"
+
+
+def test_install_custom_relative_url(selenium_standalone):
+    selenium_standalone.load_package("micropip")
+    selenium_standalone.run("import micropip")
+
+    root = Path(__file__).resolve().parents[2]
+    src = root / "src" / "tests" / "data"
+    target = root / "build" / "test_data"
+    target.symlink_to(src, True)
+    try:
+        url = "./test_data/snowballstemmer-2.0.0-py2.py3-none-any.whl"
+        selenium_standalone.run(f"micropip.install('{url}')")
+        # wait untill micropip is loaded
+        time.sleep(1)
+        selenium_standalone.run("import snowballstemmer")
+    finally:
+        target.unlink()
+
+
 def test_last_version_from_pypi():
     pytest.importorskip("distlib")
     import micropip