kernelspec.py 9.6 KB

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