Repository: pyodide/pyodide

Issue:
Issue in using micropip to install pakcage with extra_requires
With the pyodide v0.17.0a2, I have some issues in using loading `werkzeug`. It's wired that it tries to load `pytest` which is not the default runtime dependency (see [setup.py](https://github.com/pallets/werkzeug/blob/master/setup.py)). 

`pytest` should be only activated when we do `pip install wekzeug[dev]`.

You can reproduce this with the console: https://pyodide-cdn2.iodide.io/v0.17.0a2/full/console.html
```python
import micropip
micropip.install('werkzeug')
```

In the console it complains `Skipping unknown package ['pytest']`.


Discussion:
Thanks for the report! I think this bug was fixed in #1457 or #1463. Indeed the problem doesn't occur in `dev/full/console.html`.
Great! I tried with dev console and indeed it works now. Thanks you @hoodmane !
Hi, I noticed that I cannot get the actual package to work if, see below:

With the dev console at https://pyodide-cdn2.iodide.io/dev/full/console.html

If I start to add `await` before `micropip.install` I got an error:
```python
>>> import micropip
>>> await micropip.install(["werkzeug", "imjoy-rpc"])
Traceback (most recent call last):
​  File "/lib/python3.8/site-packages/pyodide/console.py", line 233, i
n load_packages_and_run
    result = await eval_code_async(source, self.locals)
​  File "/lib/python3.8/site-packages/pyodide/_base.py", line 419, in 
eval_code_async
    return await CodeRunner(
​  File "/lib/python3.8/site-packages/pyodide/_base.py", line 278, in 
run_async
    res = await res
​  File "<exec>", line 1, in <module>
​  File "/lib/python3.8/asyncio/futures.py", line 260, in __await__
    yield self  # This tells Task to wait for completion.
​  File "/lib/python3.8/asyncio/tasks.py", line 349, in __wakeup
    future.result()
​  File "/lib/python3.8/asyncio/futures.py", line 178, in result
    raise self._exception
​  File "/lib/python3.8/asyncio/tasks.py", line 282, in __step
    result = coro.throw(exc)
​  File "/lib/python3.8/site-packages/micropip.py", line 151, in insta
ll
    await gather(*requirement_promises)
​  File "/lib/python3.8/asyncio/futures.py", line 260, in __await__
    yield self  # This tells Task to wait for completion.
​  File "/lib/python3.8/asyncio/tasks.py", line 349, in __wakeup
    future.result()
​  File "/lib/python3.8/asyncio/futures.py", line 178, in result
    raise self._exception
​  File "/lib/python3.8/asyncio/tasks.py", line 280, in __step
    result = coro.send(None)
​  File "/lib/python3.8/site-packages/micropip.py", line 222, in add_r
equirement
    await self.add_requirement(recurs_req, ctx, transaction)
​  File "/lib/python3.8/site-packages/micropip.py", line 202, in add_r
equirement
    if not req.marker.evaluate(ctx):
​  File "/lib/python3.8/site-packages/packaging/markers.py", line 328,
 in evaluate
    return _evaluate_markers(self._markers, current_environment)
​  File "/lib/python3.8/site-packages/packaging/markers.py", line 244,
 in _evaluate_markers
    lhs_value = _get_env(environment, lhs.value)
​  File "/lib/python3.8/site-packages/packaging/markers.py", line 224,
 in _get_env
    raise UndefinedEnvironmentName(
​packaging.markers.UndefinedEnvironmentName: 'extra' does not exist in
 evaluation environment.
```


@hoodmane Could you please take a look?
I think this is a quick fix. I believe this is a regression introduced in #1463.

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
@@ -9,6 +9,7 @@
 
 from packaging.requirements import Requirement
 from packaging.version import Version
+from packaging.markers import default_environment
 
 # Provide stubs for testing in native python
 try:
@@ -133,7 +134,9 @@ def __init__(self):
             self.builtin_packages = {}
         self.installed_packages = {}
 
-    async def install(self, requirements: Union[str, List[str]], ctx=None):
+    async def gather_requirements(self, requirements: Union[str, List[str]], ctx=None):
+        ctx = ctx or default_environment()
+        ctx.setdefault("extra", None)
         if isinstance(requirements, str):
             requirements = [requirements]
 
@@ -149,9 +152,11 @@ async def install(self, requirements: Union[str, List[str]], ctx=None):
             )
 
         await gather(*requirement_promises)
+        return transaction
 
+    async def install(self, requirements: Union[str, List[str]], ctx=None):
+        transaction = await self.gather_requirements(requirements, ctx)
         wheel_promises = []
-
         # Install built-in packages
         pyodide_packages = transaction["pyodide_packages"]
         if len(pyodide_packages):


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
@@ -87,24 +87,29 @@ def test_install_custom_url(selenium_standalone_micropip, web_server_tst_data):
     )
 
 
-def test_add_requirement_relative_url():
-    pytest.importorskip("packaging")
-    import micropip
-
-    transaction = {"wheels": []}
-    coroutine = micropip.PACKAGE_MANAGER.add_requirement(
-        "./snowballstemmer-2.0.0-py2.py3-none-any.whl", {}, transaction
-    )
+def run_sync(coroutine):
     # The following is a way to synchronously run a coroutine that does only
-    # synchronous operations (and assert that it indeed only did synch
+    # synchronous operations (and assert that it indeed only did sync
     # operations)
     try:
         coroutine.send(None)
-    except StopIteration as _result:
-        pass
+    except StopIteration as result:
+        return result.args and result.args[0]
     else:
         raise Exception("Coroutine didn't finish in one pass")
 
+
+def test_add_requirement_relative_url():
+    pytest.importorskip("packaging")
+    import micropip
+
+    transaction = {"wheels": []}
+    run_sync(
+        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"
@@ -116,6 +121,28 @@ def test_add_requirement_relative_url():
     assert req["url"] == "./snowballstemmer-2.0.0-py2.py3-none-any.whl"
 
 
+def test_add_requirement_marker():
+    pytest.importorskip("packaging")
+    import micropip
+
+    transaction = run_sync(
+        micropip.PACKAGE_MANAGER.gather_requirements(
+            [
+                "werkzeug",
+                'contextvars ; python_version < "3.7"',
+                'aiocontextvars ; python_version < "3.7"',
+                "numpy ; extra == 'full'",
+                "zarr ; extra == 'full'",
+                "numpy ; extra == 'jupyter'",
+                "ipykernel ; extra == 'jupyter'",
+                "numpy ; extra == 'socketio'",
+                "python-socketio[client] ; extra == 'socketio'",
+            ]
+        )
+    )
+    assert len(transaction["wheels"]) == 1
+
+
 def test_install_custom_relative_url(selenium_standalone_micropip):
     selenium = selenium_standalone_micropip
     root = Path(__file__).resolve().parents[2]