1 """The IPython kernel spec for Jupyter""" 2 3 # Copyright (c) IPython Development Team. 4 # Distributed under the terms of the Modified BSD License. 5 6 from __future__ import annotations 7 8 import errno 9 import json 10 import os 11 import platform 12 import shutil 13 import stat 14 import sys 15 import tempfile 16 from pathlib import Path 17 from typing import Any 18 19 from jupyter_client.kernelspec import KernelSpecManager 20 from traitlets import Unicode 21 from traitlets.config import Application 22 23 pjoin = os.path.join 24 25 KERNEL_NAME = "python%i" % sys.version_info[0] 26 27 # path to kernelspec resources 28 RESOURCES = pjoin(Path(__file__).parent, "resources") 29 30 31 def make_ipkernel_cmd( 32 mod: str = "ipykernel_launcher", 33 executable: str | None = None, 34 extra_arguments: list[str] | None = None, 35 python_arguments: list[str] | None = None, 36 ) -> list[str]: 37 """Build Popen command list for launching an IPython kernel. 38 39 Parameters 40 ---------- 41 mod : str, optional (default 'ipykernel') 42 A string of an IPython module whose __main__ starts an IPython kernel 43 executable : str, optional (default sys.executable) 44 The Python executable to use for the kernel process. 45 extra_arguments : list, optional 46 A list of extra arguments to pass when executing the launch code. 47 48 Returns 49 ------- 50 A Popen command list 51 """ 52 if executable is None: 53 executable = sys.executable 54 extra_arguments = extra_arguments or [] 55 python_arguments = python_arguments or [] 56 return [executable, *python_arguments, "-m", mod, "-f", "{connection_file}", *extra_arguments] 57 58 59 def get_kernel_dict( 60 extra_arguments: list[str] | None = None, python_arguments: list[str] | None = None 61 ) -> dict[str, Any]: 62 """Construct dict for kernel.json""" 63 return { 64 "argv": make_ipkernel_cmd( 65 extra_arguments=extra_arguments, python_arguments=python_arguments 66 ), 67 "display_name": "Python %i (ipykernel)" % sys.version_info[0], 68 "language": "python", 69 "metadata": {"debugger": True}, 70 } 71 72 73 def write_kernel_spec( 74 path: Path | str | None = None, 75 overrides: dict[str, Any] | None = None, 76 extra_arguments: list[str] | None = None, 77 python_arguments: list[str] | None = None, 78 ) -> str: 79 """Write a kernel spec directory to `path` 80 81 If `path` is not specified, a temporary directory is created. 82 If `overrides` is given, the kernelspec JSON is updated before writing. 83 84 The path to the kernelspec is always returned. 85 """ 86 if path is None: 87 path = Path(tempfile.mkdtemp(suffix="_kernels")) / KERNEL_NAME 88 89 # stage resources 90 shutil.copytree(RESOURCES, path) 91 92 # ensure path is writable 93 mask = Path(path).stat().st_mode 94 if not mask & stat.S_IWUSR: 95 Path(path).chmod(mask | stat.S_IWUSR) 96 97 # write kernel.json 98 kernel_dict = get_kernel_dict(extra_arguments, python_arguments) 99 100 if overrides: 101 kernel_dict.update(overrides) 102 with open(pjoin(path, "kernel.json"), "w") as f: 103 json.dump(kernel_dict, f, indent=1) 104 105 return str(path) 106 107 108 def install( 109 kernel_spec_manager: KernelSpecManager | None = None, 110 user: bool = False, 111 kernel_name: str = KERNEL_NAME, 112 display_name: str | None = None, 113 prefix: str | None = None, 114 profile: str | None = None, 115 env: dict[str, str] | None = None, 116 frozen_modules: bool = False, 117 ) -> str: 118 """Install the IPython kernelspec for Jupyter 119 120 Parameters 121 ---------- 122 kernel_spec_manager : KernelSpecManager [optional] 123 A KernelSpecManager to use for installation. 124 If none provided, a default instance will be created. 125 user : bool [default: False] 126 Whether to do a user-only install, or system-wide. 127 kernel_name : str, optional 128 Specify a name for the kernelspec. 129 This is needed for having multiple IPython kernels for different environments. 130 display_name : str, optional 131 Specify the display name for the kernelspec 132 profile : str, optional 133 Specify a custom profile to be loaded by the kernel. 134 prefix : str, optional 135 Specify an install prefix for the kernelspec. 136 This is needed to install into a non-default location, such as a conda/virtual-env. 137 env : dict, optional 138 A dictionary of extra environment variables for the kernel. 139 These will be added to the current environment variables before the 140 kernel is started 141 frozen_modules : bool, optional 142 Whether to use frozen modules for potentially faster kernel startup. 143 Using frozen modules prevents debugging inside of some built-in 144 Python modules, such as io, abc, posixpath, ntpath, or stat. 145 The frozen modules are used in CPython for faster interpreter startup. 146 Ignored for cPython <3.11 and for other Python implementations. 147 148 Returns 149 ------- 150 The path where the kernelspec was installed. 151 """ 152 if kernel_spec_manager is None: 153 kernel_spec_manager = KernelSpecManager() 154 155 if env is None: 156 env = {} 157 158 if (kernel_name != KERNEL_NAME) and (display_name is None): 159 # kernel_name is specified and display_name is not 160 # default display_name to kernel_name 161 display_name = kernel_name 162 overrides: dict[str, Any] = {} 163 if display_name: 164 overrides["display_name"] = display_name 165 if profile: 166 extra_arguments = ["--profile", profile] 167 if not display_name: 168 # add the profile to the default display name 169 overrides["display_name"] = "Python %i [profile=%s]" % (sys.version_info[0], profile) 170 else: 171 extra_arguments = None 172 173 python_arguments = None 174 175 # addresses the debugger warning from debugpy about frozen modules 176 if sys.version_info >= (3, 11) and platform.python_implementation() == "CPython": 177 if not frozen_modules: 178 # disable frozen modules 179 python_arguments = ["-Xfrozen_modules=off"] 180 elif "PYDEVD_DISABLE_FILE_VALIDATION" not in env: 181 # user opted-in to have frozen modules, and we warned them about 182 # consequences for the - disable the debugger warning 183 env["PYDEVD_DISABLE_FILE_VALIDATION"] = "1" 184 185 if env: 186 overrides["env"] = env 187 path = write_kernel_spec( 188 overrides=overrides, extra_arguments=extra_arguments, python_arguments=python_arguments 189 ) 190 dest = kernel_spec_manager.install_kernel_spec( 191 path, kernel_name=kernel_name, user=user, prefix=prefix 192 ) 193 # cleanup afterward 194 shutil.rmtree(path) 195 return dest 196 197 198 # Entrypoint 199 200 201 class InstallIPythonKernelSpecApp(Application): 202 """Dummy app wrapping argparse""" 203 204 name = Unicode("ipython-kernel-install") 205 206 def initialize(self, argv: list[str] | None = None) -> None: 207 """Initialize the app.""" 208 if argv is None: 209 argv = sys.argv[1:] 210 self.argv = argv 211 212 def start(self) -> None: 213 """Start the app.""" 214 import argparse 215 216 parser = argparse.ArgumentParser( 217 prog=self.name, description="Install the IPython kernel spec." 218 ) 219 parser.add_argument( 220 "--user", 221 action="store_true", 222 help="Install for the current user instead of system-wide", 223 ) 224 parser.add_argument( 225 "--name", 226 type=str, 227 default=KERNEL_NAME, 228 help="Specify a name for the kernelspec." 229 " This is needed to have multiple IPython kernels at the same time.", 230 ) 231 parser.add_argument( 232 "--display-name", 233 type=str, 234 help="Specify the display name for the kernelspec." 235 " This is helpful when you have multiple IPython kernels.", 236 ) 237 parser.add_argument( 238 "--profile", 239 type=str, 240 help="Specify an IPython profile to load. " 241 "This can be used to create custom versions of the kernel.", 242 ) 243 parser.add_argument( 244 "--prefix", 245 type=str, 246 help="Specify an install prefix for the kernelspec." 247 " This is needed to install into a non-default location, such as a conda/virtual-env.", 248 ) 249 parser.add_argument( 250 "--sys-prefix", 251 action="store_const", 252 const=sys.prefix, 253 dest="prefix", 254 help="Install to Python's sys.prefix." 255 " Shorthand for --prefix='%s'. For use in conda/virtual-envs." % sys.prefix, 256 ) 257 parser.add_argument( 258 "--env", 259 action="append", 260 nargs=2, 261 metavar=("ENV", "VALUE"), 262 help="Set environment variables for the kernel.", 263 ) 264 parser.add_argument( 265 "--frozen_modules", 266 action="store_true", 267 help="Enable frozen modules for potentially faster startup." 268 " This has a downside of preventing the debugger from navigating to certain built-in modules.", 269 ) 270 opts = parser.parse_args(self.argv) 271 if opts.env: 272 opts.env = dict(opts.env) 273 try: 274 dest = install( 275 user=opts.user, 276 kernel_name=opts.name, 277 profile=opts.profile, 278 prefix=opts.prefix, 279 display_name=opts.display_name, 280 env=opts.env, 281 ) 282 except OSError as e: 283 if e.errno == errno.EACCES: 284 print(e, file=sys.stderr) 285 if opts.user: 286 print("Perhaps you want `sudo` or `--user`?", file=sys.stderr) 287 self.exit(1) 288 raise 289 print(f"Installed kernelspec {opts.name} in {dest}") 290 291 292 if __name__ == "__main__": 293 InstallIPythonKernelSpecApp.launch_instance() 294