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

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

{"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@@ -9,6 +9,7 @@\n \n from packaging.requirements import Requirement\n from packaging.version import Version\n+from packaging.markers import default_environment\n \n # Provide stubs for testing in native python\n try:\n@@ -133,7 +134,9 @@\n             self.builtin_packages = {}\n         self.installed_packages = {}\n \n-    async def install(self, requirements: Union[str, List[str]], ctx=None):\n+    async def gather_requirements(self, requirements: Union[str, List[str]], ctx=None):\n+        ctx = ctx or default_environment()\n+        ctx.setdefault(\"extra\", None)\n         if isinstance(requirements, str):\n             requirements = [requirements]\n \n@@ -149,9 +152,11 @@\n             )\n \n         await gather(*requirement_promises)\n+        return transaction\n \n+    async def install(self, requirements: Union[str, List[str]], ctx=None):\n+        transaction = await self.gather_requirements(requirements, ctx)\n         wheel_promises = []\n-\n         # Install built-in packages\n         pyodide_packages = transaction[\"pyodide_packages\"]\n         if len(pyodide_packages):\n", "issue": "Issue in using micropip to install pakcage with extra_requires\nWith 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)). \r\n\r\n`pytest` should be only activated when we do `pip install wekzeug[dev]`.\r\n\r\nYou can reproduce this with the console: https://pyodide-cdn2.iodide.io/v0.17.0a2/full/console.html\r\n```python\r\nimport micropip\r\nmicropip.install('werkzeug')\r\n```\r\n\r\nIn the console it complains `Skipping unknown package ['pytest']`.\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 packaging.requirements import Requirement\nfrom packaging.version import Version\n\n# Provide stubs for testing in native python\ntry:\n    import pyodide_js\n    from pyodide import to_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()).to_py())\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    def __init__(self):\n        if IN_BROWSER:\n            self.builtin_packages = pyodide_js._module.packages.versions.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 isinstance(requirements, str):\n            requirements = [requirements]\n\n        transaction: Dict[str, Any] = {\n            \"wheels\": [],\n            \"pyodide_packages\": [],\n            \"locked\": dict(self.installed_packages),\n        }\n        requirement_promises = []\n        for requirement in requirements:\n            requirement_promises.append(\n                self.add_requirement(requirement, 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 in\n            # that case builtin_packages is empty.\n            self.installed_packages.update(pyodide_packages)\n            wheel_promises.append(\n                asyncio.ensure_future(\n                    pyodide_js.loadPackage(\n                        to_js([name for [name, _] in pyodide_packages])\n                    )\n                )\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        \"\"\"Add a requirement to the transaction.\n\n        See PEP 508 for a description of the requirements.\n        https://www.python.org/dev/peps/pep-0508\n        \"\"\"\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 = Requirement(requirement)\n\n        # If there's a Pyodide package that matches the version constraint, use\n        # the Pyodide package instead of the one on PyPI\n        if (\n            req.name in self.builtin_packages\n            and self.builtin_packages[req.name] in req.specifier\n        ):\n            version = self.builtin_packages[req.name]\n            transaction[\"pyodide_packages\"].append((req.name, version))\n            return\n\n        if req.marker:\n            # handle environment markers\n            # https://www.python.org/dev/peps/pep-0508/#environment-markers\n            if not req.marker.evaluate(ctx):\n                return\n\n        # Is some version of this package is already installed?\n        if req.name in transaction[\"locked\"]:\n            ver = transaction[\"locked\"][req.name]\n            if ver in req.specifier:\n                # installed version matches, nothing to do\n                return\n            else:\n                raise ValueError(\n                    f\"Requested '{requirement}', \"\n                    f\"but {req.name}=={ver} is already installed\"\n                )\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: Requirement):\n        releases = metadata.get(\"releases\", {})\n        candidate_versions = sorted(\n            (Version(v) for v in req.specifier.filter(releases)),  # type: ignore\n            reverse=True,\n        )\n        for ver in candidate_versions:\n            release = releases[str(ver)]\n            for fileinfo in release:\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}'\")\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"}], "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 packaging.requirements import Requirement\nfrom packaging.version import Version\nfrom packaging.markers import default_environment\n\n# Provide stubs for testing in native python\ntry:\n    import pyodide_js\n    from pyodide import to_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()).to_py())\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    def __init__(self):\n        if IN_BROWSER:\n            self.builtin_packages = pyodide_js._module.packages.versions.to_py()\n        else:\n            self.builtin_packages = {}\n        self.installed_packages = {}\n\n    async def gather_requirements(self, requirements: Union[str, List[str]], ctx=None):\n        ctx = ctx or default_environment()\n        ctx.setdefault(\"extra\", None)\n        if isinstance(requirements, str):\n            requirements = [requirements]\n\n        transaction: Dict[str, Any] = {\n            \"wheels\": [],\n            \"pyodide_packages\": [],\n            \"locked\": dict(self.installed_packages),\n        }\n        requirement_promises = []\n        for requirement in requirements:\n            requirement_promises.append(\n                self.add_requirement(requirement, ctx, transaction)\n            )\n\n        await gather(*requirement_promises)\n        return transaction\n\n    async def install(self, requirements: Union[str, List[str]], ctx=None):\n        transaction = await self.gather_requirements(requirements, ctx)\n        wheel_promises = []\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 in\n            # that case builtin_packages is empty.\n            self.installed_packages.update(pyodide_packages)\n            wheel_promises.append(\n                asyncio.ensure_future(\n                    pyodide_js.loadPackage(\n                        to_js([name for [name, _] in pyodide_packages])\n                    )\n                )\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        \"\"\"Add a requirement to the transaction.\n\n        See PEP 508 for a description of the requirements.\n        https://www.python.org/dev/peps/pep-0508\n        \"\"\"\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 = Requirement(requirement)\n\n        # If there's a Pyodide package that matches the version constraint, use\n        # the Pyodide package instead of the one on PyPI\n        if (\n            req.name in self.builtin_packages\n            and self.builtin_packages[req.name] in req.specifier\n        ):\n            version = self.builtin_packages[req.name]\n            transaction[\"pyodide_packages\"].append((req.name, version))\n            return\n\n        if req.marker:\n            # handle environment markers\n            # https://www.python.org/dev/peps/pep-0508/#environment-markers\n            if not req.marker.evaluate(ctx):\n                return\n\n        # Is some version of this package is already installed?\n        if req.name in transaction[\"locked\"]:\n            ver = transaction[\"locked\"][req.name]\n            if ver in req.specifier:\n                # installed version matches, nothing to do\n                return\n            else:\n                raise ValueError(\n                    f\"Requested '{requirement}', \"\n                    f\"but {req.name}=={ver} is already installed\"\n                )\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: Requirement):\n        releases = metadata.get(\"releases\", {})\n        candidate_versions = sorted(\n            (Version(v) for v in req.specifier.filter(releases)),  # type: ignore\n            reverse=True,\n        )\n        for ver in candidate_versions:\n            release = releases[str(ver)]\n            for fileinfo in release:\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}'\")\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"}]}