Skip to content

Commit

Permalink
Remove PIXI_IN_SHELL from kernel env and use that for running `pi…
Browse files Browse the repository at this point in the history
…xi` verifications
  • Loading branch information
renan-r-santos committed Nov 29, 2024
1 parent d9f3418 commit 512cb85
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 28 deletions.
24 changes: 14 additions & 10 deletions src/pixi_kernel/pixi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import logging
import os
import shutil
import subprocess
from pathlib import Path
Expand Down Expand Up @@ -32,7 +31,13 @@ class Project(BaseModel):
manifest_path: str


def ensure_readiness(*, cwd: Path, required_package: str, kernel_name: str) -> Environment:
def ensure_readiness(
*,
cwd: Path,
env: dict[str, str],
required_package: str,
kernel_name: str,
) -> Environment:
"""Ensure the Pixi environment is ready to run the kernel.
This function checks the following:
Expand All @@ -52,7 +57,7 @@ def ensure_readiness(*, cwd: Path, required_package: str, kernel_name: str) -> E
raise RuntimeError(PIXI_NOT_FOUND.format(kernel_name=kernel_name))

# Ensure a supported Pixi version is installed
result = subprocess.run(["pixi", "--version"], capture_output=True, text=True)
result = subprocess.run(["pixi", "--version"], capture_output=True, env=env, text=True)
if result.returncode != 0 or not result.stdout.startswith("pixi "):
raise RuntimeError(PIXI_VERSION_ERROR.format(kernel_name=kernel_name))

Expand All @@ -67,15 +72,12 @@ def ensure_readiness(*, cwd: Path, required_package: str, kernel_name: str) -> E
PIXI_OUTDATED.format(kernel_name=kernel_name, minimum_version=MINIMUM_PIXI_VERSION)
)

# Remove PIXI_IN_SHELL for when JupyterLab was started from a Pixi shell
# https://github.com/renan-r-santos/pixi-kernel/issues/35
os.environ.pop("PIXI_IN_SHELL", None)

# Ensure there is a Pixi project in the current working directory or any of its parents
result = subprocess.run(
["pixi", "info", "--json"],
cwd=str(cwd.absolute()),
capture_output=True,
env=env,
text=True,
)
logger.info(f"pixi info stderr: {result.stderr}")
Expand All @@ -97,14 +99,15 @@ def ensure_readiness(*, cwd: Path, required_package: str, kernel_name: str) -> E
["pixi", "project", "version", "get"],
cwd=str(cwd.absolute()),
capture_output=True,
env=env,
text=True,
)
raise RuntimeError(result.stderr)

# Find the default environment and check if the required kernel package is a dependency
for env in pixi_info.environments:
if env.name == "default":
default_environment = env
for pixi_env in pixi_info.environments:
if pixi_env.name == "default":
default_environment = pixi_env
break
else:
raise RuntimeError("Default Pixi environment not found.")
Expand All @@ -124,6 +127,7 @@ def ensure_readiness(*, cwd: Path, required_package: str, kernel_name: str) -> E
["pixi", "install"],
cwd=str(cwd.absolute()),
capture_output=True,
env=env,
text=True,
)
if result.returncode != 0:
Expand Down
8 changes: 7 additions & 1 deletion src/pixi_kernel/provisioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ async def pre_launch(self, **kwargs: Any) -> dict[str, Any]:
raise ValueError("Pixi Kernel metadata is missing the 'required-package' key")

cwd = Path(kwargs.get("cwd", Path.cwd()))
logger.info(f"JupyterLab provided current working directory: {kwargs.get("cwd", None)}")
logger.info(f"JupyterLab provided this value for cwd: {kwargs.get("cwd", None)}")
logger.info(f"The current working directory is {cwd}")

# Remove PIXI_IN_SHELL for when JupyterLab is started from a Pixi shell
# https://github.com/renan-r-santos/pixi-kernel/issues/35
env: dict[str, str] = kwargs.get("env", {})
env.pop("PIXI_IN_SHELL", None)

environment = ensure_readiness(
cwd=cwd,
env=env,
required_package=required_package,
kernel_name=kernel_spec.display_name,
)
Expand Down
55 changes: 38 additions & 17 deletions tests/unit/test_pixi.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,27 @@
def test_pixi_not_installed():
message = re.escape(PIXI_NOT_FOUND.format(kernel_name="Pixi"))
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=Path.cwd(), required_package="pixi", kernel_name="Pixi")
ensure_readiness(
cwd=Path.cwd(), env=os.environ, required_package="pixi", kernel_name="Pixi"
)


@pytest.mark.usefixtures("_patch_pixi_version_exit_code")
def test_pixi_version_bad_exit_code():
message = re.escape(PIXI_VERSION_ERROR.format(kernel_name="Pixi"))
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=Path.cwd(), required_package="pixi", kernel_name="Pixi")
ensure_readiness(
cwd=Path.cwd(), env=os.environ, required_package="pixi", kernel_name="Pixi"
)


@pytest.mark.usefixtures("_patch_pixi_version_stdout")
def test_pixi_version_bad_stdout():
message = re.escape(PIXI_VERSION_ERROR.format(kernel_name="Pixi"))
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=Path.cwd(), required_package="pixi", kernel_name="Pixi")
ensure_readiness(
cwd=Path.cwd(), env=os.environ, required_package="pixi", kernel_name="Pixi"
)


@pytest.mark.usefixtures("_patch_pixi_version")
Expand All @@ -49,14 +55,18 @@ def test_outdated_pixi():
)
)
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=Path.cwd(), required_package="pixi", kernel_name="Pixi")
ensure_readiness(
cwd=Path.cwd(), env=os.environ, required_package="pixi", kernel_name="Pixi"
)


@pytest.mark.usefixtures("_patch_pixi_info_exit_code")
def test_pixi_info_bad_exit_code():
message = re.escape("Failed to run 'pixi info': error")
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=Path.cwd(), required_package="pixi", kernel_name="Pixi")
ensure_readiness(
cwd=Path.cwd(), env=os.environ, required_package="pixi", kernel_name="Pixi"
)


@pytest.mark.usefixtures("_patch_pixi_info_stdout")
Expand All @@ -65,7 +75,9 @@ def test_pixi_info_bad_stdout():
("Failed to parse 'pixi info' output: not JSON\n1 validation error for PixiInfo")
)
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=Path.cwd(), required_package="pixi", kernel_name="Pixi")
ensure_readiness(
cwd=Path.cwd(), env=os.environ, required_package="pixi", kernel_name="Pixi"
)


def test_empty_project():
Expand All @@ -77,21 +89,23 @@ def test_empty_project():
"could not find pixi.toml or pyproject.toml which is configured to use pixi"
)
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=cwd, required_package="pixi", kernel_name="Pixi")
ensure_readiness(cwd=cwd, env=os.environ, required_package="pixi", kernel_name="Pixi")


def test_bad_pixi_toml():
cwd = data_dir / "bad_pixi_toml"
message = re.escape("failed to parse project manifest")
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=cwd, required_package="pixi", kernel_name="Pixi")
ensure_readiness(cwd=cwd, env=os.environ, required_package="pixi", kernel_name="Pixi")


@pytest.mark.usefixtures("_patch_pixi_info_no_default_env")
def test_missing_default_environment():
message = re.escape("Default Pixi environment not found.")
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=Path.cwd(), required_package="pixi", kernel_name="Pixi")
ensure_readiness(
cwd=Path.cwd(), env=os.environ, required_package="pixi", kernel_name="Pixi"
)


def test_missing_ipykernel():
Expand All @@ -107,7 +121,9 @@ def test_missing_ipykernel():
)
)
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=cwd, required_package=required_package, kernel_name=kernel_name)
ensure_readiness(
cwd=cwd, env=os.environ, required_package=required_package, kernel_name=kernel_name
)


def test_non_existing_dependency():
Expand All @@ -117,7 +133,9 @@ def test_non_existing_dependency():

message = re.escape("Failed to run 'pixi install':")
with pytest.raises(RuntimeError, match=message):
ensure_readiness(cwd=cwd, required_package=required_package, kernel_name=kernel_name)
ensure_readiness(
cwd=cwd, env=os.environ, required_package=required_package, kernel_name=kernel_name
)


def test_pixi_project():
Expand All @@ -127,6 +145,7 @@ def test_pixi_project():

environment = ensure_readiness(
cwd=cwd,
env=os.environ,
required_package=required_package,
kernel_name=kernel_name,
)
Expand All @@ -140,14 +159,15 @@ def test_pyproject_project():

environment = ensure_readiness(
cwd=cwd,
env=os.environ,
required_package=required_package,
kernel_name=kernel_name,
)
assert Path(environment.prefix).parts[-2:] == ("envs", "default")


@pytest.fixture
def update_env_for_pixi_on_pixi():
def env_for_pixi_on_pixi():
result = subprocess.run(
["pixi", "run", "env"],
cwd=data_dir / "pixi_on_pixi",
Expand All @@ -157,13 +177,14 @@ def update_env_for_pixi_on_pixi():
assert result.returncode == 0, result.stderr

# Update the current environment where the tests are running to merge all the env vars returned
# by `pixi run env`
# by `pixi run env` except for PIXI_IN_SHELL
original_env = dict(os.environ)
for line in result.stdout.splitlines():
key, value = line.split("=", 1)
os.environ[key] = value
if key != "PIXI_IN_SHELL":
os.environ[key] = value

yield
yield os.environ

# Restore the original environment
os.environ.clear()
Expand All @@ -172,14 +193,14 @@ def update_env_for_pixi_on_pixi():

# https://github.com/renan-r-santos/pixi-kernel/issues/35
@pytest.mark.skipif(sys.platform == "win32", reason="No need to write Windows-specific code here")
@pytest.mark.usefixtures("update_env_for_pixi_on_pixi")
def test_pixi_on_pixi():
def test_pixi_on_pixi(env_for_pixi_on_pixi: dict[str, str]):
cwd = data_dir / "pixi_on_pixi" / "good_project"
required_package = "ipykernel"
kernel_name = "Python (Pixi)"

environment = ensure_readiness(
cwd=cwd,
env=env_for_pixi_on_pixi,
required_package=required_package,
kernel_name=kernel_name,
)
Expand Down

0 comments on commit 512cb85

Please sign in to comment.