Skip to content
This repository was archived by the owner on Feb 14, 2024. It is now read-only.

Commit c456fbe

Browse files
martinRenoujtpio
andauthored
Add support for pip dependencies (#102)
* Allow pre-installing pip packages * Run blabk * Iterate * Iterate * Iterate * Fix RECORD * Run black * Update comment Co-authored-by: Jeremy Tuloup <[email protected]> * Install docs build dependencies from conda-forge * Update empack dependency * Use latest jupyterlite-sphinx in docs * Add some docs about the pip packages support --------- Co-authored-by: Jeremy Tuloup <[email protected]>
1 parent 7d7fda7 commit c456fbe

File tree

9 files changed

+168
-45
lines changed

9 files changed

+168
-45
lines changed

README.md

-4
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ You can also pick another name for that environment file (*e.g.* `custom.yml`),
6666
jupyter lite build --XeusPythonEnv.environment_file=custom.yml
6767
```
6868

69-
#### About pip dependencies
70-
71-
It is common to provide `pip` dependencies in a conda environment file, this is currently **not supported** by xeus-python.
72-
7369
## Contributing
7470

7571
### Development install

docs/build-environment.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies:
1414
- yarn
1515
- jupyterlab >=3.5.3,<3.6
1616
- jupyterlite-core >=0.1.0,<0.2.0
17-
- empack >=3,<4
17+
- empack >=3.1.0
1818
- pip:
19-
- jupyterlite-sphinx
19+
- jupyterlite-sphinx >=0.9.1
2020
- ..

docs/conf.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
# -*- coding: utf-8 -*-
22

33
extensions = [
4-
'jupyterlite_sphinx',
5-
'myst_parser',
4+
"jupyterlite_sphinx",
5+
"myst_parser",
66
]
77

88
myst_enable_extensions = [
99
"linkify",
1010
]
1111

12-
master_doc = 'index'
13-
source_suffix = '.rst'
12+
master_doc = "index"
13+
source_suffix = ".rst"
1414

15-
project = 'jupyterlite-xeus-python'
16-
copyright = 'JupyterLite Team'
17-
author = 'JupyterLite Team'
15+
project = "jupyterlite-xeus-python"
16+
copyright = "JupyterLite Team"
17+
author = "JupyterLite Team"
1818

1919
exclude_patterns = []
2020

@@ -23,8 +23,8 @@
2323
jupyterlite_dir = "."
2424

2525
html_theme_options = {
26-
"logo": {
27-
"image_light": "xeus-python.svg",
28-
"image_dark": "xeus-python.svg",
29-
}
26+
"logo": {
27+
"image_light": "xeus-python.svg",
28+
"image_dark": "xeus-python.svg",
29+
}
3030
}

docs/configuration.md

+32-7
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ Say you want to install `NumPy`, `Matplotlib` and `ipycanvas`, it can be done by
1313
```
1414
name: xeus-python-kernel
1515
channels:
16-
- https://repo.mamba.pm/emscripten-forge
17-
- https://repo.mamba.pm/conda-forge
16+
- https://repo.mamba.pm/emscripten-forge
17+
- https://repo.mamba.pm/conda-forge
1818
dependencies:
19-
- numpy
20-
- matplotlib
21-
- ipycanvas
19+
- numpy
20+
- matplotlib
21+
- ipycanvas
2222
```
2323

2424
Then you only need to build JupyterLite:
@@ -33,8 +33,8 @@ You can also pick another name for that environment file (*e.g.* `custom.yml`),
3333
jupyter lite build --XeusPythonEnv.environment_file=custom.yml
3434
```
3535

36-
```{note}
37-
It is common to provide `pip` dependencies in a conda environment file. This is currently **not supported** by xeus-python, but there is a [work-in-progress](https://github.com/jupyterlite/xeus-python-kernel/pull/102) to support it.
36+
```{warning}
37+
It is common to provide `pip` dependencies in a conda environment file. This is currently **partially supported** by xeus-python. See "pip packages" section.
3838
```
3939

4040
Then those packages are usable directly:
@@ -55,6 +55,31 @@ Then those packages are usable directly:
5555
plt.show();
5656
```
5757

58+
### pip packages
59+
60+
⚠ This feature is experimental. You won't have the same user-experience as when using conda/mamba in a "normal" setup ⚠
61+
62+
`xeus-python` provides a way to install packages with pip.
63+
64+
There are a couple of limitations that you should be aware of:
65+
- it can only install pure Python packages (Python code + data files)
66+
- it does not install the package dependencies, you should make sure to install them yourself using conda-forge/emscripten-forge.
67+
68+
For example, if you were to install `ipycanvas` from PyPI, you would need to install the ipycanvas dependencies for it to work (`pillow`, `numpy` and `ipywidgets`):
69+
70+
```
71+
name: xeus-python-kernel
72+
channels:
73+
- https://repo.mamba.pm/emscripten-forge
74+
- https://repo.mamba.pm/conda-forge
75+
dependencies:
76+
- numpy
77+
- pillow
78+
- ipywidgets
79+
- pip:
80+
- ipycanvas
81+
```
82+
5883
## Advanced Configuration
5984

6085
```{warning}

docs/environment.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ channels:
55
dependencies:
66
- numpy
77
- matplotlib
8-
- ipycanvas
8+
- pillow
9+
- ipywidgets
10+
- pip:
11+
- ipycanvas

jupyterlite_xeus_python/build.py

+104-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import csv
12
import os
23
from copy import copy
34
from pathlib import Path
45
import requests
56
import shutil
67
from subprocess import check_call, run, DEVNULL
8+
from tempfile import TemporaryDirectory
79
from typing import List
810
from urllib.parse import urlparse
11+
import sys
912

1013
import yaml
1114

@@ -25,7 +28,9 @@
2528
MICROMAMBA_COMMAND = shutil.which("micromamba")
2629
CONDA_COMMAND = shutil.which("conda")
2730

28-
PYTHON_VERSION = "3.10"
31+
PYTHON_MAJOR = 3
32+
PYTHON_MINOR = 10
33+
PYTHON_VERSION = f"{PYTHON_MAJOR}.{PYTHON_MINOR}"
2934

3035
XEUS_PYTHON_VERSION = "0.15.9"
3136

@@ -119,6 +124,90 @@ def _create_config(prefix_path):
119124
os.environ["CONDARC"] = str(prefix_path / ".condarc")
120125

121126

127+
def _install_pip_dependencies(prefix_path, dependencies, log=None):
128+
# Why is this so damn complicated?
129+
# Isn't it easier to download the .whl ourselves? pip is hell
130+
131+
if log is not None:
132+
log.warning(
133+
"""
134+
Installing pip dependencies. This is very much experimental so use this feature at your own risks.
135+
Note that you can only install pure-python packages.
136+
pip is being run with the --no-deps option to not pull undesired system-specific dependencies, so please
137+
install your package dependencies from emscripten-forge or conda-forge.
138+
"""
139+
)
140+
141+
# Installing with pip in another prefix that has a different Python version IS NOT POSSIBLE
142+
# So we need to do this whole mess "manually"
143+
pkg_dir = TemporaryDirectory()
144+
145+
run(
146+
[
147+
"pip",
148+
"install",
149+
*dependencies,
150+
# Install in a tmp directory while we process it
151+
"--target",
152+
pkg_dir.name,
153+
# Specify the right Python version
154+
"--python-version",
155+
PYTHON_VERSION,
156+
# No dependency installed
157+
"--no-deps",
158+
"--no-input",
159+
"--verbose",
160+
],
161+
check=True,
162+
)
163+
164+
# We need to read the RECORD and try to be smart about what goes
165+
# under site-packages and what goes where
166+
packages_dist_info = Path(pkg_dir.name).glob("*.dist-info")
167+
168+
for package_dist_info in packages_dist_info:
169+
with open(package_dist_info / "RECORD", "r") as record:
170+
record_content = record.read()
171+
record_csv = csv.reader(record_content.splitlines())
172+
all_files = [_file[0] for _file in record_csv]
173+
174+
non_supported_files = [".so", ".a", ".dylib", ".lib", ".exe" ".dll"]
175+
176+
# List of tuples: (path: str, inside_site_packages: bool)
177+
files = [(_file, not _file.startswith("../../")) for _file in all_files]
178+
179+
# Why?
180+
fixed_record_data = record_content.replace("../../", "../../../")
181+
182+
# OVERWRITE RECORD file
183+
with open(package_dist_info / "RECORD", "w") as record:
184+
record.write(fixed_record_data)
185+
186+
# COPY files under `prefix_path`
187+
for _file, inside_site_packages in files:
188+
path = Path(_file)
189+
190+
# FAIL if .so / .a / .dylib / .lib / .exe / .dll
191+
if path.suffix in non_supported_files:
192+
raise RuntimeError(
193+
"Cannot install binary PyPI package, only pure Python packages are supported"
194+
)
195+
196+
file_path = _file[6:] if not inside_site_packages else _file
197+
install_path = (
198+
prefix_path
199+
if not inside_site_packages
200+
else prefix_path / "lib" / f"python{PYTHON_VERSION}" / "site-packages"
201+
)
202+
203+
src_path = Path(pkg_dir.name) / file_path
204+
dest_path = install_path / file_path
205+
206+
os.makedirs(dest_path.parent, exist_ok=True)
207+
208+
shutil.copy(src_path, dest_path)
209+
210+
122211
def build_and_pack_emscripten_env(
123212
python_version: str = PYTHON_VERSION,
124213
xeus_python_version: str = XEUS_PYTHON_VERSION,
@@ -130,6 +219,7 @@ def build_and_pack_emscripten_env(
130219
output_path: str = ".",
131220
build_worker: bool = False,
132221
force: bool = False,
222+
log=None,
133223
):
134224
"""Build a conda environment for the emscripten platform and pack it with empack."""
135225
channels = copy(CHANNELS)
@@ -146,6 +236,8 @@ def build_and_pack_emscripten_env(
146236
if packages or xeus_python_version or environment_file:
147237
bail_early = False
148238

239+
pip_dependencies = []
240+
149241
# Process environment.yml file
150242
if environment_file and Path(environment_file).exists():
151243
bail_early = False
@@ -168,10 +260,7 @@ def build_and_pack_emscripten_env(
168260
if isinstance(dependency, str) and dependency not in specs:
169261
specs.append(dependency)
170262
elif isinstance(dependency, dict) and dependency.get("pip") is not None:
171-
raise RuntimeError(
172-
"""Cannot install pip dependencies in the xeus-python Emscripten environment (yet?).
173-
"""
174-
)
263+
pip_dependencies = dependency["pip"]
175264

176265
# Bail early if there is nothing to do
177266
if bail_early and not force:
@@ -192,6 +281,10 @@ def build_and_pack_emscripten_env(
192281
# Create emscripten env with the given packages
193282
create_env(env_name, root_prefix, specs, channels)
194283

284+
# Install pip dependencies
285+
if pip_dependencies:
286+
_install_pip_dependencies(prefix_path, pip_dependencies, log=log)
287+
195288
pack_kwargs = {}
196289

197290
# Download env filter config
@@ -203,9 +296,7 @@ def build_and_pack_emscripten_env(
203296
yaml.safe_load(empack_config_content)
204297
)
205298
else:
206-
pack_kwargs["file_filters"] = pkg_file_filter_from_yaml(
207-
empack_config
208-
)
299+
pack_kwargs["file_filters"] = pkg_file_filter_from_yaml(empack_config)
209300
else:
210301
pack_kwargs["file_filters"] = pkg_file_filter_from_yaml(DEFAULT_CONFIG_PATH)
211302

@@ -235,13 +326,16 @@ def build_and_pack_emscripten_env(
235326

236327
worker = worker.replace("XEUS_KERNEL_FILE", "'xpython_wasm.js'")
237328
worker = worker.replace("LANGUAGE_DATA_FILE", "'python_data.js'")
238-
worker = worker.replace("importScripts(DATA_FILE);", """
329+
worker = worker.replace(
330+
"importScripts(DATA_FILE);",
331+
"""
239332
await globalThis.Module.bootstrap_from_empack_packed_environment(
240333
`./empack_env_meta.json`, /* packages_json_url */
241334
".", /* package_tarballs_root_url */
242335
false /* verbose */
243336
);
244-
""")
337+
""",
338+
)
245339
with open(Path(output_path) / "worker.ts", "w") as fobj:
246340
fobj.write(worker)
247341

jupyterlite_xeus_python/env_build_addon.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ def from_string(self, s):
3838

3939

4040
class XeusPythonEnv(FederatedExtensionAddon):
41-
4241
__all__ = ["post_build"]
4342

4443
xeus_python_version = Unicode(XEUS_PYTHON_VERSION).tag(
@@ -86,6 +85,7 @@ def post_build(self, manager):
8685
environment_file=Path(self.manager.lite_dir) / self.environment_file,
8786
empack_config=self.empack_config,
8887
output_path=self.cwd.name,
88+
log=self.log,
8989
)
9090

9191
# Find the federated extensions in the emscripten-env and install them
@@ -99,17 +99,14 @@ def post_build(self, manager):
9999
dest = self.output_extensions / "@jupyterlite" / "xeus-python-kernel" / "static"
100100

101101
# copy *.tar.gz for all side packages
102-
for item in Path(self.cwd.name) .iterdir():
102+
for item in Path(self.cwd.name).iterdir():
103103
if item.suffix == ".gz":
104-
105104
file = item.name
106105
yield dict(
107106
name=f"xeus:copy:{file}",
108107
actions=[(self.copy_one, [item, dest / file])],
109108
)
110109

111-
112-
113110
for file in [
114111
"empack_env_meta.json",
115112
"xpython_wasm.js",

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"traitlets",
6262
"jupyterlite-core>=0.1.0",
6363
"requests",
64-
"empack>=3,<4",
64+
"empack>=3.1,<4",
6565
"typer",
6666
],
6767
zip_safe=False,

tests/test_xeus_python_env.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ def test_python_env():
2323
# Check env
2424
assert os.path.isdir("/tmp/xeus-python-kernel/envs/xeus-python-kernel")
2525

26-
assert os.path.isfile("/tmp/xeus-python-kernel/envs/xeus-python-kernel/bin/xpython_wasm.js")
27-
assert os.path.isfile("/tmp/xeus-python-kernel/envs/xeus-python-kernel/bin/xpython_wasm.wasm")
26+
assert os.path.isfile(
27+
"/tmp/xeus-python-kernel/envs/xeus-python-kernel/bin/xpython_wasm.js"
28+
)
29+
assert os.path.isfile(
30+
"/tmp/xeus-python-kernel/envs/xeus-python-kernel/bin/xpython_wasm.wasm"
31+
)
2832

2933
# Check empack output
3034
assert os.path.isfile(Path(addon.cwd.name) / "empack_env_meta.json")
@@ -46,8 +50,12 @@ def test_python_env_from_file_1():
4650
# Check env
4751
assert os.path.isdir("/tmp/xeus-python-kernel/envs/xeus-python-kernel-1")
4852

49-
assert os.path.isfile("/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/bin/xpython_wasm.js")
50-
assert os.path.isfile("/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/bin/xpython_wasm.wasm")
53+
assert os.path.isfile(
54+
"/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/bin/xpython_wasm.js"
55+
)
56+
assert os.path.isfile(
57+
"/tmp/xeus-python-kernel/envs/xeus-python-kernel-1/bin/xpython_wasm.wasm"
58+
)
5159

5260
# Check empack output
5361
assert os.path.isfile(Path(addon.cwd.name) / "empack_env_meta.json")

0 commit comments

Comments
 (0)