Format: text/x-python
⬇ Download File (9.6 KB)
   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