Skip to content

Commit 5d4a869

Browse files
authored
Port install into PackageManager (#119)
* feat: initialize repodata * feat: port install * fix: relative imports
1 parent f9ec77e commit 5d4a869

File tree

4 files changed

+265
-185
lines changed

4 files changed

+265
-185
lines changed

micropip/_commands/install.py

+4-168
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
import asyncio
2-
import importlib
3-
from pathlib import Path
1+
from micropip import package_index
42

5-
from packaging.markers import default_environment
6-
7-
from .. import package_index
8-
from .._compat import loadPackage, to_js
9-
from ..constants import FAQ_URLS
10-
from ..logging import setup_logging
11-
from ..transaction import Transaction
3+
from ..install import install as _install
124

135

146
async def install(
@@ -21,165 +13,9 @@ async def install(
2113
*,
2214
verbose: bool | int = False,
2315
) -> None:
24-
"""Install the given package and all of its dependencies.
25-
26-
If a package is not found in the Pyodide repository it will be loaded from
27-
PyPI. Micropip can only load pure Python wheels or wasm32/emscripten wheels
28-
built by Pyodide.
29-
30-
When used in web browsers, downloads from PyPI will be cached. When run in
31-
Node.js, packages are currently not cached, and will be re-downloaded each
32-
time ``micropip.install`` is run.
33-
34-
Parameters
35-
----------
36-
requirements :
37-
38-
A requirement or list of requirements to install. Each requirement is a
39-
string, which should be either a package name or a wheel URI:
40-
41-
- If the requirement does not end in ``.whl``, it will be interpreted as
42-
a package name. A package with this name must either be present
43-
in the Pyodide lock file or on PyPI.
44-
45-
- If the requirement ends in ``.whl``, it is a wheel URI. The part of
46-
the requirement after the last ``/`` must be a valid wheel name in
47-
compliance with the `PEP 427 naming convention
48-
<https://www.python.org/dev/peps/pep-0427/#file-format>`_.
49-
50-
- If a wheel URI starts with ``emfs:``, it will be interpreted as a path
51-
in the Emscripten file system (Pyodide's file system). E.g.,
52-
``emfs:../relative/path/wheel.whl`` or ``emfs:/absolute/path/wheel.whl``.
53-
In this case, only .whl files are supported.
54-
55-
- If a wheel URI requirement starts with ``http:`` or ``https:`` it will
56-
be interpreted as a URL.
57-
58-
- In node, you can access the native file system using a URI that starts
59-
with ``file:``. In the browser this will not work.
60-
61-
keep_going :
62-
63-
This parameter decides the behavior of the micropip when it encounters a
64-
Python package without a pure Python wheel while doing dependency
65-
resolution:
66-
67-
- If ``False``, an error will be raised on first package with a missing
68-
wheel.
69-
70-
- If ``True``, the micropip will keep going after the first error, and
71-
report a list of errors at the end.
72-
73-
deps :
74-
75-
If ``True``, install dependencies specified in METADATA file for each
76-
package. Otherwise do not install dependencies.
77-
78-
credentials :
79-
80-
This parameter specifies the value of ``credentials`` when calling the
81-
`fetch() <https://developer.mozilla.org/en-US/docs/Web/API/fetch>`__
82-
function which is used to download the package.
83-
84-
When not specified, ``fetch()`` is called without ``credentials``.
85-
86-
pre :
87-
88-
If ``True``, include pre-release and development versions. By default,
89-
micropip only finds stable versions.
90-
91-
index_urls :
92-
93-
A list of URLs or a single URL to use as the package index when looking
94-
up packages. If None, *https://pypi.org/pypi/{package_name}/json* is used.
95-
96-
- The index URL should support the
97-
`JSON API <https://warehouse.pypa.io/api-reference/json/>`__ .
98-
99-
- The index URL may contain the placeholder {package_name} which will be
100-
replaced with the package name when looking up a package. If it does not
101-
contain the placeholder, the package name will be appended to the URL.
102-
103-
- If a list of URLs is provided, micropip will try each URL in order until
104-
it finds a package. If no package is found, an error will be raised.
105-
106-
verbose :
107-
Print more information about the process.
108-
By default, micropip is silent. Setting ``verbose=True`` will print
109-
similar information as pip.
110-
"""
111-
logger = setup_logging(verbose)
112-
113-
ctx = default_environment()
114-
if isinstance(requirements, str):
115-
requirements = [requirements]
116-
117-
fetch_kwargs = dict()
118-
119-
if credentials:
120-
fetch_kwargs["credentials"] = credentials
121-
122-
# Note: getsitepackages is not available in a virtual environment...
123-
# See https://github.com/pypa/virtualenv/issues/228 (issue is closed but
124-
# problem is not fixed)
125-
from site import getsitepackages
126-
127-
wheel_base = Path(getsitepackages()[0])
128-
12916
if index_urls is None:
13017
index_urls = package_index.INDEX_URLS[:]
13118

132-
transaction = Transaction(
133-
ctx=ctx, # type: ignore[arg-type]
134-
ctx_extras=[],
135-
keep_going=keep_going,
136-
deps=deps,
137-
pre=pre,
138-
fetch_kwargs=fetch_kwargs,
139-
verbose=verbose,
140-
index_urls=index_urls,
19+
return await _install(
20+
requirements, keep_going, deps, credentials, pre, index_urls, verbose=verbose
14121
)
142-
await transaction.gather_requirements(requirements)
143-
144-
if transaction.failed:
145-
failed_requirements = ", ".join([f"'{req}'" for req in transaction.failed])
146-
raise ValueError(
147-
f"Can't find a pure Python 3 wheel for: {failed_requirements}\n"
148-
f"See: {FAQ_URLS['cant_find_wheel']}\n"
149-
)
150-
151-
package_names = [pkg.name for pkg in transaction.pyodide_packages] + [
152-
pkg.name for pkg in transaction.wheels
153-
]
154-
155-
if package_names:
156-
logger.info("Installing collected packages: " + ", ".join(package_names))
157-
158-
wheel_promises = []
159-
# Install built-in packages
160-
pyodide_packages = transaction.pyodide_packages
161-
if len(pyodide_packages):
162-
# Note: branch never happens in out-of-browser testing because in
163-
# that case REPODATA_PACKAGES is empty.
164-
wheel_promises.append(
165-
asyncio.ensure_future(
166-
loadPackage(to_js([name for [name, _, _] in pyodide_packages]))
167-
)
168-
)
169-
170-
# Now install PyPI packages
171-
for wheel in transaction.wheels:
172-
# detect whether the wheel metadata is from PyPI or from custom location
173-
# wheel metadata from PyPI has SHA256 checksum digest.
174-
wheel_promises.append(wheel.install(wheel_base))
175-
176-
await asyncio.gather(*wheel_promises)
177-
178-
packages = [f"{pkg.name}-{pkg.version}" for pkg in transaction.pyodide_packages] + [
179-
f"{pkg.name}-{pkg.version}" for pkg in transaction.wheels
180-
]
181-
182-
if packages:
183-
logger.info("Successfully installed " + ", ".join(packages))
184-
185-
importlib.invalidate_caches()

micropip/install.py

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import asyncio
2+
import importlib
3+
from pathlib import Path
4+
5+
from packaging.markers import default_environment
6+
7+
from . import package_index
8+
from ._compat import loadPackage, to_js
9+
from .constants import FAQ_URLS
10+
from .logging import setup_logging
11+
from .transaction import Transaction
12+
13+
14+
async def install(
15+
requirements: str | list[str],
16+
keep_going: bool = False,
17+
deps: bool = True,
18+
credentials: str | None = None,
19+
pre: bool = False,
20+
index_urls: list[str] | str | None = None,
21+
*,
22+
verbose: bool | int = False,
23+
) -> None:
24+
"""Install the given package and all of its dependencies.
25+
26+
If a package is not found in the Pyodide repository it will be loaded from
27+
PyPI. Micropip can only load pure Python wheels or wasm32/emscripten wheels
28+
built by Pyodide.
29+
30+
When used in web browsers, downloads from PyPI will be cached. When run in
31+
Node.js, packages are currently not cached, and will be re-downloaded each
32+
time ``micropip.install`` is run.
33+
34+
Parameters
35+
----------
36+
requirements :
37+
38+
A requirement or list of requirements to install. Each requirement is a
39+
string, which should be either a package name or a wheel URI:
40+
41+
- If the requirement does not end in ``.whl``, it will be interpreted as
42+
a package name. A package with this name must either be present
43+
in the Pyodide lock file or on PyPI.
44+
45+
- If the requirement ends in ``.whl``, it is a wheel URI. The part of
46+
the requirement after the last ``/`` must be a valid wheel name in
47+
compliance with the `PEP 427 naming convention
48+
<https://www.python.org/dev/peps/pep-0427/#file-format>`_.
49+
50+
- If a wheel URI starts with ``emfs:``, it will be interpreted as a path
51+
in the Emscripten file system (Pyodide's file system). E.g.,
52+
``emfs:../relative/path/wheel.whl`` or ``emfs:/absolute/path/wheel.whl``.
53+
In this case, only .whl files are supported.
54+
55+
- If a wheel URI requirement starts with ``http:`` or ``https:`` it will
56+
be interpreted as a URL.
57+
58+
- In node, you can access the native file system using a URI that starts
59+
with ``file:``. In the browser this will not work.
60+
61+
keep_going :
62+
63+
This parameter decides the behavior of the micropip when it encounters a
64+
Python package without a pure Python wheel while doing dependency
65+
resolution:
66+
67+
- If ``False``, an error will be raised on first package with a missing
68+
wheel.
69+
70+
- If ``True``, the micropip will keep going after the first error, and
71+
report a list of errors at the end.
72+
73+
deps :
74+
75+
If ``True``, install dependencies specified in METADATA file for each
76+
package. Otherwise do not install dependencies.
77+
78+
credentials :
79+
80+
This parameter specifies the value of ``credentials`` when calling the
81+
`fetch() <https://developer.mozilla.org/en-US/docs/Web/API/fetch>`__
82+
function which is used to download the package.
83+
84+
When not specified, ``fetch()`` is called without ``credentials``.
85+
86+
pre :
87+
88+
If ``True``, include pre-release and development versions. By default,
89+
micropip only finds stable versions.
90+
91+
index_urls :
92+
93+
A list of URLs or a single URL to use as the package index when looking
94+
up packages. If None, *https://pypi.org/pypi/{package_name}/json* is used.
95+
96+
- The index URL should support the
97+
`JSON API <https://warehouse.pypa.io/api-reference/json/>`__ .
98+
99+
- The index URL may contain the placeholder {package_name} which will be
100+
replaced with the package name when looking up a package. If it does not
101+
contain the placeholder, the package name will be appended to the URL.
102+
103+
- If a list of URLs is provided, micropip will try each URL in order until
104+
it finds a package. If no package is found, an error will be raised.
105+
106+
verbose :
107+
Print more information about the process.
108+
By default, micropip is silent. Setting ``verbose=True`` will print
109+
similar information as pip.
110+
"""
111+
logger = setup_logging(verbose)
112+
113+
ctx = default_environment()
114+
if isinstance(requirements, str):
115+
requirements = [requirements]
116+
117+
fetch_kwargs = dict()
118+
119+
if credentials:
120+
fetch_kwargs["credentials"] = credentials
121+
122+
# Note: getsitepackages is not available in a virtual environment...
123+
# See https://github.com/pypa/virtualenv/issues/228 (issue is closed but
124+
# problem is not fixed)
125+
from site import getsitepackages
126+
127+
wheel_base = Path(getsitepackages()[0])
128+
129+
if index_urls is None:
130+
index_urls = package_index.INDEX_URLS[:]
131+
132+
transaction = Transaction(
133+
ctx=ctx, # type: ignore[arg-type]
134+
ctx_extras=[],
135+
keep_going=keep_going,
136+
deps=deps,
137+
pre=pre,
138+
fetch_kwargs=fetch_kwargs,
139+
verbose=verbose,
140+
index_urls=index_urls,
141+
)
142+
await transaction.gather_requirements(requirements)
143+
144+
if transaction.failed:
145+
failed_requirements = ", ".join([f"'{req}'" for req in transaction.failed])
146+
raise ValueError(
147+
f"Can't find a pure Python 3 wheel for: {failed_requirements}\n"
148+
f"See: {FAQ_URLS['cant_find_wheel']}\n"
149+
)
150+
151+
package_names = [pkg.name for pkg in transaction.pyodide_packages] + [
152+
pkg.name for pkg in transaction.wheels
153+
]
154+
155+
if package_names:
156+
logger.info("Installing collected packages: " + ", ".join(package_names))
157+
158+
wheel_promises = []
159+
# Install built-in packages
160+
pyodide_packages = transaction.pyodide_packages
161+
if len(pyodide_packages):
162+
# Note: branch never happens in out-of-browser testing because in
163+
# that case REPODATA_PACKAGES is empty.
164+
wheel_promises.append(
165+
asyncio.ensure_future(
166+
loadPackage(to_js([name for [name, _, _] in pyodide_packages]))
167+
)
168+
)
169+
170+
# Now install PyPI packages
171+
for wheel in transaction.wheels:
172+
# detect whether the wheel metadata is from PyPI or from custom location
173+
# wheel metadata from PyPI has SHA256 checksum digest.
174+
wheel_promises.append(wheel.install(wheel_base))
175+
176+
await asyncio.gather(*wheel_promises)
177+
178+
packages = [f"{pkg.name}-{pkg.version}" for pkg in transaction.pyodide_packages] + [
179+
f"{pkg.name}-{pkg.version}" for pkg in transaction.wheels
180+
]
181+
182+
if packages:
183+
logger.info("Successfully installed " + ", ".join(packages))
184+
185+
importlib.invalidate_caches()

0 commit comments

Comments
 (0)