Skip to content

Commit 34a469f

Browse files
Improve logic for locating the Pixi binary (#63)
1 parent 32f849e commit 34a469f

File tree

22 files changed

+6901
-6279
lines changed

22 files changed

+6901
-6279
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
fail-fast: false
1313
matrix:
1414
os: [ubuntu-latest, macos-latest, windows-latest]
15-
pixi-version: ['0.39.0', '0.48.2']
15+
pixi-version: ['0.39.0', '0.50.2']
1616
runs-on: ${{ matrix.os }}
1717
steps:
1818
- name: Checkout repo

README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,31 @@ support](#kernel-support) table and replace `ipykernel` with the desired kernel
2727
1. Install Pixi and `pixi-kernel` alongside JupyterLab using your favorite package manager.
2828
2. Restart JupyterLab.
2929
3. Create a new directory and initialize a Pixi project with `pixi init` and `pixi add ipykernel`.
30-
4. Restart the kernel and you are good to go.
30+
4. Select the Python Pixi kernel and you are good to go.
3131

3232
See the [Pixi docs](https://pixi.sh/latest/) for more information on how to use Pixi.
3333

34+
## Configuration
35+
36+
### Custom Pixi binary location
37+
38+
By default, `pixi-kernel` will try to find the Pixi binary in this order:
39+
40+
1. Use `shutil.which("pixi")` to find Pixi in your PATH
41+
2. Check for a configuration file stored at:
42+
- Linux/macOS: `$HOME/.config/pixi-kernel/config.toml`
43+
- Windows: `$Env:USERPROFILE\.config\pixi-kernel\config.toml`
44+
3. Check the default Pixi installation location:
45+
- Linux/macOS: `$HOME/.pixi/bin/pixi`
46+
- Windows: `$Env:USERPROFILE\.pixi\bin\pixi.exe`
47+
48+
If you have Pixi installed in a non-standard location, you can create a configuration file to
49+
specify its path:
50+
51+
```toml
52+
pixi-path = "/path/to/your/pixi"
53+
```
54+
3455
## Kernel support
3556

3657
Pixi kernel supports the following kernels:
@@ -60,9 +81,11 @@ If you're using pixi-kernel on JupyterHub and cannot access the environment wher
6081
installed, you can use the following workaround:
6182

6283
1. Install `pixi-kernel` locally: `pip install pixi-kernel --user`
63-
2. Restart your JupyterLab server
84+
2. Install Pixi
85+
3. Restart your JupyterLab server
6486

65-
See https://github.com/renan-r-santos/pixi-kernel/issues/51 for more information.
87+
See https://github.com/renan-r-santos/pixi-kernel/issues/62 and
88+
https://github.com/renan-r-santos/pixi-kernel/issues/51 for more information.
6689

6790
## Limitations
6891

package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pixi-kernel",
3-
"version": "0.6.4",
3+
"version": "0.6.5",
44
"description": "Jupyter kernels using Pixi for reproducible notebooks.",
55
"keywords": [
66
"kernel",
@@ -54,27 +54,27 @@
5454
"watch:labextension": "jupyter labextension watch ."
5555
},
5656
"dependencies": {
57-
"@jupyterlab/application": "^4.4.3",
58-
"@jupyterlab/coreutils": "^6.4.3",
59-
"@jupyterlab/notebook": "^4.4.3",
60-
"@jupyterlab/services": "^7.4.3",
57+
"@jupyterlab/application": "^4.4.5",
58+
"@jupyterlab/coreutils": "^6.4.5",
59+
"@jupyterlab/notebook": "^4.4.5",
60+
"@jupyterlab/services": "^7.4.5",
6161
"@rjsf/utils": "^5.24.12",
6262
"react": "^18.3.1"
6363
},
6464
"devDependencies": {
6565
"@eslint/eslintrc": "^3.3.1",
66-
"@eslint/js": "^9.29.0",
67-
"@jupyterlab/builder": "^4.4.3",
66+
"@eslint/js": "^9.32.0",
67+
"@jupyterlab/builder": "^4.4.5",
6868
"@types/json-schema": "^7.0.15",
6969
"@types/react": "^18.3.12",
7070
"@types/react-addons-linked-state-mixin": "^0.14.27",
71-
"@typescript-eslint/eslint-plugin": "^8.34.1",
72-
"@typescript-eslint/parser": "^8.34.1",
73-
"eslint": "^9.29.0",
74-
"eslint-config-prettier": "^10.1.5",
75-
"eslint-plugin-prettier": "^5.5.0",
71+
"@typescript-eslint/eslint-plugin": "^8.38.0",
72+
"@typescript-eslint/parser": "^8.38.0",
73+
"eslint": "^9.32.0",
74+
"eslint-config-prettier": "^10.1.8",
75+
"eslint-plugin-prettier": "^5.5.3",
7676
"npm-run-all2": "^8.0.4",
77-
"prettier": "^3.5.3",
77+
"prettier": "^3.6.2",
7878
"rimraf": "^6.0.1",
7979
"source-map-loader": "^5.0.0",
8080
"typescript": "5.5.4"

pixi_kernel/compatibility.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import shutil
2+
import sys
3+
from pathlib import Path
4+
from typing import Any
25

36
from returns.result import Failure, Result, Success
47

58
from .async_subprocess import subprocess_exec
69

7-
MINIMUM_PIXI_VERSION = (0, 30, 0)
10+
if sys.version_info >= (3, 11):
11+
import tomllib
12+
else:
13+
import tomli as tomllib
14+
15+
16+
MINIMUM_PIXI_VERSION = (0, 39, 0)
817

918

1019
PIXI_NOT_FOUND = """Pixi was not detected in your system.
@@ -34,11 +43,53 @@
3443
"""
3544

3645

46+
def get_config_file() -> Path:
47+
return Path.home() / ".config" / "pixi-kernel" / "config.toml"
48+
49+
50+
def get_default_pixi_path() -> Path:
51+
if sys.platform == "win32":
52+
return Path.home() / ".pixi" / "bin" / "pixi.exe"
53+
else:
54+
return Path.home() / ".pixi" / "bin" / "pixi"
55+
56+
57+
def find_pixi_binary() -> Result[str, None]:
58+
# 1. Check if the Pixi binary is available in the system PATH
59+
pixi_path = shutil.which("pixi")
60+
if pixi_path is not None:
61+
return Success(pixi_path)
62+
63+
# 2. Check if a config file exists and read the Pixi path from it
64+
config_file = get_config_file()
65+
if config_file.is_file():
66+
try:
67+
content = Path(config_file).read_text()
68+
config = tomllib.loads(content)
69+
pixi_path = config.get("pixi-path")
70+
if pixi_path is not None and Path(pixi_path).is_file():
71+
return Success(pixi_path)
72+
except (OSError, tomllib.TOMLDecodeError):
73+
pass
74+
75+
# 3. Check if the default installation path exists
76+
# https://pixi.sh/latest/installation/#installer-script-options
77+
default_pixi_path = get_default_pixi_path()
78+
if default_pixi_path.is_file():
79+
return Success(str(default_pixi_path))
80+
81+
return Failure(None)
82+
83+
3784
async def has_compatible_pixi() -> Result[None, str]:
38-
if shutil.which("pixi") is None:
85+
result = find_pixi_binary()
86+
87+
if isinstance(result, Failure):
3988
return Failure(PIXI_NOT_FOUND)
89+
else:
90+
pixi_path = result.unwrap()
4091

41-
returncode, stdout, stderr = await subprocess_exec("pixi", "--version")
92+
returncode, stdout, stderr = await subprocess_exec(pixi_path, "--version")
4293
if returncode != 0 or not stdout.startswith("pixi "):
4394
return Failure(PIXI_VERSION_ERROR)
4495

@@ -50,3 +101,11 @@ async def has_compatible_pixi() -> Result[None, str]:
50101
return Failure(PIXI_OUTDATED.format(minimum_version=minimum_version))
51102

52103
return Success(None)
104+
105+
106+
async def run_pixi(*args: str, **kwargs: Any) -> tuple[int, str, str]:
107+
# It is safe to unwrap as `has_compatible_pixi()` would already have checked for a compatible
108+
# Pixi binary.
109+
pixi = find_pixi_binary().unwrap()
110+
111+
return await subprocess_exec(pixi, *args, **kwargs)

pixi_kernel/env.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from pydantic import ValidationError
55

6-
from .async_subprocess import subprocess_exec
6+
from .compatibility import run_pixi
77
from .types import PixiInfo
88

99
DEFAULT_ENVIRONMENT = "default"
@@ -15,7 +15,7 @@ async def envs_from_path(path: Path) -> list[str]:
1515
env = os.environ.copy()
1616
env.pop("PIXI_IN_SHELL", None)
1717

18-
returncode, stdout, stderr = await subprocess_exec("pixi", "info", "--json", cwd=path, env=env)
18+
returncode, stdout, stderr = await run_pixi("info", "--json", cwd=path, env=env)
1919
if returncode != 0:
2020
return [DEFAULT_ENVIRONMENT]
2121

pixi_kernel/readiness.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
from pydantic import ValidationError
66
from returns.result import Failure, Result, Success
77

8-
from .async_subprocess import subprocess_exec
9-
from .compatibility import has_compatible_pixi
8+
from .compatibility import has_compatible_pixi, run_pixi
109
from .types import Environment, PixiInfo
1110

1211
PIXI_KERNEL_NOT_FOUND = """To run the {kernel_name} kernel, you need to add the {required_package}
@@ -41,7 +40,7 @@ async def verify_env_readiness(
4140
return result
4241

4342
# Ensure there is a Pixi project in the current working directory or any of its parents
44-
returncode, stdout, stderr = await subprocess_exec("pixi", "info", "--json", cwd=cwd, env=env)
43+
returncode, stdout, stderr = await run_pixi("info", "--json", cwd=cwd, env=env)
4544

4645
logger.info(f"pixi info stderr: {stderr}")
4746
logger.info(f"pixi info stdout: {stdout}")
@@ -56,14 +55,7 @@ async def verify_env_readiness(
5655
if pixi_info.project is None:
5756
# Attempt to get a good error message by running `pixi project version get`. Maybe there's
5857
# a typo in the toml file (parsing error) or there is no project at all.
59-
returncode, stdout, stderr = await subprocess_exec(
60-
"pixi",
61-
"project",
62-
"version",
63-
"get",
64-
cwd=cwd,
65-
env=env,
66-
)
58+
returncode, stdout, stderr = await run_pixi("project", "version", "get", cwd=cwd, env=env)
6759
return Failure(stderr)
6860

6961
# Find the Pixi environment and check if the required kernel package is a dependency
@@ -77,9 +69,7 @@ async def verify_env_readiness(
7769
dependencies = pixi_environment.dependencies + pixi_environment.pypi_dependencies
7870
if required_package not in dependencies:
7971
# Check transitive dependencies
80-
returncode, stdout, stderr = await subprocess_exec(
81-
"pixi", "list", "--json", cwd=cwd, env=env
82-
)
72+
returncode, stdout, stderr = await run_pixi("list", "--json", cwd=cwd, env=env)
8373

8474
logger.info(f"pixi list stderr: {stderr}")
8575
logger.info(f"pixi list stdout: {stdout}")
@@ -99,13 +89,8 @@ async def verify_env_readiness(
9989
return Failure(f"Failed to parse 'pixi list' output: {stdout}\n{exception}")
10090

10191
# Make sure the environment can be solved and is up-to-date
102-
returncode, stdout, stderr = await subprocess_exec(
103-
"pixi",
104-
"install",
105-
"--environment",
106-
environment_name,
107-
cwd=cwd,
108-
env=env,
92+
returncode, stdout, stderr = await run_pixi(
93+
"install", "--environment", environment_name, cwd=cwd, env=env
10994
)
11095
if returncode != 0:
11196
return Failure(f"Failed to run 'pixi install --environment {environment_name}': {stderr}")

pyproject.toml

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pixi-kernel"
3-
version = "0.6.4"
3+
version = "0.6.5"
44
description = "Jupyter kernels using Pixi for reproducible notebooks"
55
license = { text = "MIT" }
66
authors = [
@@ -35,9 +35,10 @@ requires-python = ">=3.9,<4.0"
3535
dependencies = [
3636
"ipykernel>=6",
3737
"jupyter-client>=7",
38-
"jupyter_server>=2.4.0,<3",
39-
"pydantic>=2,<3",
40-
"returns>=0.23,<0.24", # 0.24 drops Python 3.9 support
38+
"jupyter_server>=2.4.0",
39+
"pydantic>=2",
40+
"returns>=0.23",
41+
"tomli>=2; python_version<'3.11'",
4142
]
4243

4344
[project.urls]
@@ -47,14 +48,15 @@ Repository = "https://github.com/renan-r-santos/pixi-kernel"
4748

4849
[tool.uv]
4950
dev-dependencies = [
50-
"jupyter-kernel-test>=0.7,<0.8",
51-
"jupyterlab>=4,<5",
52-
"mypy>=1,<2",
53-
"pytest>=8,<9",
54-
"pytest-asyncio>=1,<2",
55-
"pytest-cov>=6,<7",
56-
"ruff>=0.12,<0.13",
57-
"tox-uv>=1,<2",
51+
"jupyter-kernel-test>=0.7",
52+
"jupyterlab>=4",
53+
"msgspec[toml]>=0.19.0",
54+
"mypy>=1",
55+
"pytest>=8",
56+
"pytest-asyncio>=1",
57+
"pytest-cov>=6",
58+
"ruff>=0.12",
59+
"tox-uv>=1",
5860
]
5961

6062
[project.entry-points."jupyter_client.kernel_provisioners"]

0 commit comments

Comments
 (0)