|
"""The IPython kernel spec for Jupyter""" |
|
|
|
|
|
|
|
|
|
from __future__ import annotations |
|
|
|
import errno |
|
import json |
|
import os |
|
import platform |
|
import shutil |
|
import stat |
|
import sys |
|
import tempfile |
|
from pathlib import Path |
|
from typing import Any |
|
|
|
from jupyter_client.kernelspec import KernelSpecManager |
|
from traitlets import Unicode |
|
from traitlets.config import Application |
|
|
|
pjoin = os.path.join |
|
|
|
KERNEL_NAME = "python%i" % sys.version_info[0] |
|
|
|
|
|
RESOURCES = pjoin(Path(__file__).parent, "resources") |
|
|
|
|
|
def make_ipkernel_cmd( |
|
mod: str = "ipykernel_launcher", |
|
executable: str | None = None, |
|
extra_arguments: list[str] | None = None, |
|
python_arguments: list[str] | None = None, |
|
) -> list[str]: |
|
"""Build Popen command list for launching an IPython kernel. |
|
|
|
Parameters |
|
---------- |
|
mod : str, optional (default 'ipykernel') |
|
A string of an IPython module whose __main__ starts an IPython kernel |
|
executable : str, optional (default sys.executable) |
|
The Python executable to use for the kernel process. |
|
extra_arguments : list, optional |
|
A list of extra arguments to pass when executing the launch code. |
|
|
|
Returns |
|
------- |
|
A Popen command list |
|
""" |
|
if executable is None: |
|
executable = sys.executable |
|
extra_arguments = extra_arguments or [] |
|
python_arguments = python_arguments or [] |
|
return [executable, *python_arguments, "-m", mod, "-f", "{connection_file}", *extra_arguments] |
|
|
|
|
|
def get_kernel_dict( |
|
extra_arguments: list[str] | None = None, python_arguments: list[str] | None = None |
|
) -> dict[str, Any]: |
|
"""Construct dict for kernel.json""" |
|
return { |
|
"argv": make_ipkernel_cmd( |
|
extra_arguments=extra_arguments, python_arguments=python_arguments |
|
), |
|
"display_name": "Python %i (ipykernel)" % sys.version_info[0], |
|
"language": "python", |
|
"metadata": {"debugger": True}, |
|
} |
|
|
|
|
|
def write_kernel_spec( |
|
path: Path | str | None = None, |
|
overrides: dict[str, Any] | None = None, |
|
extra_arguments: list[str] | None = None, |
|
python_arguments: list[str] | None = None, |
|
) -> str: |
|
"""Write a kernel spec directory to `path` |
|
|
|
If `path` is not specified, a temporary directory is created. |
|
If `overrides` is given, the kernelspec JSON is updated before writing. |
|
|
|
The path to the kernelspec is always returned. |
|
""" |
|
if path is None: |
|
path = Path(tempfile.mkdtemp(suffix="_kernels")) / KERNEL_NAME |
|
|
|
|
|
shutil.copytree(RESOURCES, path) |
|
|
|
|
|
mask = Path(path).stat().st_mode |
|
if not mask & stat.S_IWUSR: |
|
Path(path).chmod(mask | stat.S_IWUSR) |
|
|
|
|
|
kernel_dict = get_kernel_dict(extra_arguments, python_arguments) |
|
|
|
if overrides: |
|
kernel_dict.update(overrides) |
|
with open(pjoin(path, "kernel.json"), "w") as f: |
|
json.dump(kernel_dict, f, indent=1) |
|
|
|
return str(path) |
|
|
|
|
|
def install( |
|
kernel_spec_manager: KernelSpecManager | None = None, |
|
user: bool = False, |
|
kernel_name: str = KERNEL_NAME, |
|
display_name: str | None = None, |
|
prefix: str | None = None, |
|
profile: str | None = None, |
|
env: dict[str, str] | None = None, |
|
frozen_modules: bool = False, |
|
) -> str: |
|
"""Install the IPython kernelspec for Jupyter |
|
|
|
Parameters |
|
---------- |
|
kernel_spec_manager : KernelSpecManager [optional] |
|
A KernelSpecManager to use for installation. |
|
If none provided, a default instance will be created. |
|
user : bool [default: False] |
|
Whether to do a user-only install, or system-wide. |
|
kernel_name : str, optional |
|
Specify a name for the kernelspec. |
|
This is needed for having multiple IPython kernels for different environments. |
|
display_name : str, optional |
|
Specify the display name for the kernelspec |
|
profile : str, optional |
|
Specify a custom profile to be loaded by the kernel. |
|
prefix : str, optional |
|
Specify an install prefix for the kernelspec. |
|
This is needed to install into a non-default location, such as a conda/virtual-env. |
|
env : dict, optional |
|
A dictionary of extra environment variables for the kernel. |
|
These will be added to the current environment variables before the |
|
kernel is started |
|
frozen_modules : bool, optional |
|
Whether to use frozen modules for potentially faster kernel startup. |
|
Using frozen modules prevents debugging inside of some built-in |
|
Python modules, such as io, abc, posixpath, ntpath, or stat. |
|
The frozen modules are used in CPython for faster interpreter startup. |
|
Ignored for cPython <3.11 and for other Python implementations. |
|
|
|
Returns |
|
------- |
|
The path where the kernelspec was installed. |
|
""" |
|
if kernel_spec_manager is None: |
|
kernel_spec_manager = KernelSpecManager() |
|
|
|
if env is None: |
|
env = {} |
|
|
|
if (kernel_name != KERNEL_NAME) and (display_name is None): |
|
|
|
|
|
display_name = kernel_name |
|
overrides: dict[str, Any] = {} |
|
if display_name: |
|
overrides["display_name"] = display_name |
|
if profile: |
|
extra_arguments = ["--profile", profile] |
|
if not display_name: |
|
|
|
overrides["display_name"] = "Python %i [profile=%s]" % (sys.version_info[0], profile) |
|
else: |
|
extra_arguments = None |
|
|
|
python_arguments = None |
|
|
|
|
|
if sys.version_info >= (3, 11) and platform.python_implementation() == "CPython": |
|
if not frozen_modules: |
|
|
|
python_arguments = ["-Xfrozen_modules=off"] |
|
elif "PYDEVD_DISABLE_FILE_VALIDATION" not in env: |
|
|
|
|
|
env["PYDEVD_DISABLE_FILE_VALIDATION"] = "1" |
|
|
|
if env: |
|
overrides["env"] = env |
|
path = write_kernel_spec( |
|
overrides=overrides, extra_arguments=extra_arguments, python_arguments=python_arguments |
|
) |
|
dest = kernel_spec_manager.install_kernel_spec( |
|
path, kernel_name=kernel_name, user=user, prefix=prefix |
|
) |
|
|
|
shutil.rmtree(path) |
|
return dest |
|
|
|
|
|
|
|
|
|
|
|
class InstallIPythonKernelSpecApp(Application): |
|
"""Dummy app wrapping argparse""" |
|
|
|
name = Unicode("ipython-kernel-install") |
|
|
|
def initialize(self, argv: list[str] | None = None) -> None: |
|
"""Initialize the app.""" |
|
if argv is None: |
|
argv = sys.argv[1:] |
|
self.argv = argv |
|
|
|
def start(self) -> None: |
|
"""Start the app.""" |
|
import argparse |
|
|
|
parser = argparse.ArgumentParser( |
|
prog=self.name, description="Install the IPython kernel spec." |
|
) |
|
parser.add_argument( |
|
"--user", |
|
action="store_true", |
|
help="Install for the current user instead of system-wide", |
|
) |
|
parser.add_argument( |
|
"--name", |
|
type=str, |
|
default=KERNEL_NAME, |
|
help="Specify a name for the kernelspec." |
|
" This is needed to have multiple IPython kernels at the same time.", |
|
) |
|
parser.add_argument( |
|
"--display-name", |
|
type=str, |
|
help="Specify the display name for the kernelspec." |
|
" This is helpful when you have multiple IPython kernels.", |
|
) |
|
parser.add_argument( |
|
"--profile", |
|
type=str, |
|
help="Specify an IPython profile to load. " |
|
"This can be used to create custom versions of the kernel.", |
|
) |
|
parser.add_argument( |
|
"--prefix", |
|
type=str, |
|
help="Specify an install prefix for the kernelspec." |
|
" This is needed to install into a non-default location, such as a conda/virtual-env.", |
|
) |
|
parser.add_argument( |
|
"--sys-prefix", |
|
action="store_const", |
|
const=sys.prefix, |
|
dest="prefix", |
|
help="Install to Python's sys.prefix." |
|
" Shorthand for --prefix='%s'. For use in conda/virtual-envs." % sys.prefix, |
|
) |
|
parser.add_argument( |
|
"--env", |
|
action="append", |
|
nargs=2, |
|
metavar=("ENV", "VALUE"), |
|
help="Set environment variables for the kernel.", |
|
) |
|
parser.add_argument( |
|
"--frozen_modules", |
|
action="store_true", |
|
help="Enable frozen modules for potentially faster startup." |
|
" This has a downside of preventing the debugger from navigating to certain built-in modules.", |
|
) |
|
opts = parser.parse_args(self.argv) |
|
if opts.env: |
|
opts.env = dict(opts.env) |
|
try: |
|
dest = install( |
|
user=opts.user, |
|
kernel_name=opts.name, |
|
profile=opts.profile, |
|
prefix=opts.prefix, |
|
display_name=opts.display_name, |
|
env=opts.env, |
|
) |
|
except OSError as e: |
|
if e.errno == errno.EACCES: |
|
print(e, file=sys.stderr) |
|
if opts.user: |
|
print("Perhaps you want `sudo` or `--user`?", file=sys.stderr) |
|
self.exit(1) |
|
raise |
|
print(f"Installed kernelspec {opts.name} in {dest}") |
|
|
|
|
|
if __name__ == "__main__": |
|
InstallIPythonKernelSpecApp.launch_instance() |
|
|