kernelspecapp.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. """Apps for managing kernel specs."""
  2. # Copyright (c) Jupyter 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.path
  8. import sys
  9. import typing as t
  10. from pathlib import Path
  11. from jupyter_core.application import JupyterApp, base_aliases, base_flags
  12. from traitlets import Bool, Dict, Instance, List, Unicode
  13. from traitlets.config.application import Application
  14. from . import __version__
  15. from .kernelspec import KernelSpecManager
  16. from .provisioning.factory import KernelProvisionerFactory
  17. class ListKernelSpecs(JupyterApp):
  18. """An app to list kernel specs."""
  19. version = __version__
  20. description = """List installed kernel specifications."""
  21. kernel_spec_manager = Instance(KernelSpecManager)
  22. json_output = Bool(
  23. False,
  24. help="output spec name and location as machine-readable json.",
  25. config=True,
  26. )
  27. missing_kernels = Bool(
  28. False,
  29. help="List only specs with missing interpreters.",
  30. config=True,
  31. )
  32. flags = {
  33. "json": (
  34. {"ListKernelSpecs": {"json_output": True}},
  35. "output spec name and location as machine-readable json.",
  36. ),
  37. "missing": (
  38. {"ListKernelSpecs": {"missing_kernels": True}},
  39. "output only missing kernels",
  40. ),
  41. "debug": base_flags["debug"],
  42. }
  43. def _kernel_spec_manager_default(self) -> KernelSpecManager:
  44. return KernelSpecManager(parent=self, data_dir=self.data_dir)
  45. def start(self) -> dict[str, t.Any] | None: # type:ignore[override]
  46. """Start the application."""
  47. paths = self.kernel_spec_manager.find_kernel_specs()
  48. specs = self.kernel_spec_manager.get_all_specs()
  49. if self.missing_kernels:
  50. paths, specs = _limit_to_missing(paths, specs)
  51. if not self.json_output:
  52. if not specs:
  53. print("No kernels available")
  54. return None
  55. # pad to width of longest kernel name
  56. name_len = len(sorted(paths, key=lambda name: len(name))[-1])
  57. def path_key(item: t.Any) -> t.Any:
  58. """sort key function for Jupyter path priority"""
  59. path = item[1]
  60. for idx, prefix in enumerate(self.jupyter_path):
  61. if path.startswith(prefix):
  62. return (idx, path)
  63. # not in jupyter path, artificially added to the front
  64. return (-1, path)
  65. print("Available kernels:")
  66. for kernelname, path in sorted(paths.items(), key=path_key):
  67. print(f" {kernelname.ljust(name_len)} {path}")
  68. else:
  69. print(json.dumps({"kernelspecs": specs}, indent=2))
  70. return specs
  71. class InstallKernelSpec(JupyterApp):
  72. """An app to install a kernel spec."""
  73. version = __version__
  74. description = """Install a kernel specification directory.
  75. Given a SOURCE DIRECTORY containing a kernel spec,
  76. jupyter will copy that directory into one of the Jupyter kernel directories.
  77. The default is to install kernelspecs for all users.
  78. `--user` can be specified to install a kernel only for the current user.
  79. """
  80. examples = """
  81. jupyter kernelspec install /path/to/my_kernel --user
  82. """
  83. usage = "jupyter kernelspec install SOURCE_DIR [--options]"
  84. kernel_spec_manager = Instance(KernelSpecManager)
  85. def _kernel_spec_manager_default(self) -> KernelSpecManager:
  86. return KernelSpecManager(data_dir=self.data_dir)
  87. sourcedir = Unicode()
  88. kernel_name = Unicode("", config=True, help="Install the kernel spec with this name")
  89. def _kernel_name_default(self) -> str:
  90. return os.path.basename(self.sourcedir)
  91. user = Bool(
  92. False,
  93. config=True,
  94. help="""
  95. Try to install the kernel spec to the per-user directory instead of
  96. the system or environment directory.
  97. """,
  98. )
  99. prefix = Unicode(
  100. "",
  101. config=True,
  102. help="""Specify a prefix to install to, e.g. an env.
  103. The kernelspec will be installed in PREFIX/share/jupyter/kernels/
  104. """,
  105. )
  106. replace = Bool(False, config=True, help="Replace any existing kernel spec with this name.")
  107. aliases = {
  108. "name": "InstallKernelSpec.kernel_name",
  109. "prefix": "InstallKernelSpec.prefix",
  110. }
  111. aliases.update(base_aliases)
  112. flags = {
  113. "user": (
  114. {"InstallKernelSpec": {"user": True}},
  115. "Install to the per-user kernel registry",
  116. ),
  117. "replace": (
  118. {"InstallKernelSpec": {"replace": True}},
  119. "Replace any existing kernel spec with this name.",
  120. ),
  121. "sys-prefix": (
  122. {"InstallKernelSpec": {"prefix": sys.prefix}},
  123. "Install to Python's sys.prefix. Useful in conda/virtual environments.",
  124. ),
  125. "debug": base_flags["debug"],
  126. }
  127. def parse_command_line(self, argv: None | list[str]) -> None: # type:ignore[override]
  128. """Parse the command line args."""
  129. super().parse_command_line(argv)
  130. # accept positional arg as profile name
  131. if self.extra_args:
  132. self.sourcedir = self.extra_args[0]
  133. else:
  134. print("No source directory specified.", file=sys.stderr)
  135. self.exit(1)
  136. def start(self) -> None:
  137. """Start the application."""
  138. if self.user and self.prefix:
  139. self.exit("Can't specify both user and prefix. Please choose one or the other.")
  140. try:
  141. self.kernel_spec_manager.install_kernel_spec(
  142. self.sourcedir,
  143. kernel_name=self.kernel_name,
  144. user=self.user,
  145. prefix=self.prefix,
  146. replace=self.replace,
  147. )
  148. except OSError as e:
  149. if e.errno == errno.EACCES:
  150. print(e, file=sys.stderr)
  151. if not self.user:
  152. print("Perhaps you want to install with `sudo` or `--user`?", file=sys.stderr)
  153. self.exit(1)
  154. elif e.errno == errno.EEXIST:
  155. print(f"A kernel spec is already present at {e.filename}", file=sys.stderr)
  156. self.exit(1)
  157. raise
  158. class RemoveKernelSpec(JupyterApp):
  159. """An app to remove a kernel spec."""
  160. version = __version__
  161. description = """Remove one or more Jupyter kernelspecs by name."""
  162. examples = """jupyter kernelspec remove python2 [my_kernel ...]"""
  163. force = Bool(False, config=True, help="""Force removal, don't prompt for confirmation.""")
  164. spec_names = List(Unicode())
  165. missing_kernels = Bool(
  166. False,
  167. help="Remove missing specs.",
  168. config=True,
  169. )
  170. kernel_spec_manager = Instance(KernelSpecManager)
  171. def _kernel_spec_manager_default(self) -> KernelSpecManager:
  172. return KernelSpecManager(data_dir=self.data_dir, parent=self)
  173. flags = {
  174. "f": ({"RemoveKernelSpec": {"force": True}}, force.help),
  175. "missing": (
  176. {"RemoveKernelSpec": {"missing_kernels": True}},
  177. "remove missing kernels",
  178. ),
  179. }
  180. flags.update(JupyterApp.flags)
  181. def parse_command_line(self, argv: list[str] | None) -> None: # type:ignore[override]
  182. """Parse the command line args."""
  183. super().parse_command_line(argv)
  184. # accept positional arg as profile name
  185. if self.extra_args:
  186. self.spec_names = sorted(set(self.extra_args)) # remove duplicates
  187. else:
  188. self.spec_names = []
  189. def start(self) -> None:
  190. """Start the application."""
  191. self.kernel_spec_manager.ensure_native_kernel = False
  192. spec_paths = self.kernel_spec_manager.find_kernel_specs()
  193. if self.missing_kernels:
  194. _, spec = _limit_to_missing(
  195. spec_paths,
  196. self.kernel_spec_manager.get_all_specs(),
  197. )
  198. # append missing kernels
  199. self.spec_names = sorted(set(self.spec_names + list(spec)))
  200. missing = set(self.spec_names).difference(set(spec_paths))
  201. if missing:
  202. self.exit("Couldn't find kernel spec(s): %s" % ", ".join(missing))
  203. if not (self.force or self.answer_yes):
  204. print("Kernel specs to remove:")
  205. for name in self.spec_names:
  206. path = spec_paths.get(name, name)
  207. print(f" {name.ljust(20)}\t{path.ljust(20)}")
  208. answer = input("Remove %i kernel specs [y/N]: " % len(self.spec_names))
  209. if not answer.lower().startswith("y"):
  210. return
  211. for kernel_name in self.spec_names:
  212. try:
  213. path = self.kernel_spec_manager.remove_kernel_spec(kernel_name)
  214. except OSError as e:
  215. if e.errno == errno.EACCES:
  216. print(e, file=sys.stderr)
  217. print("Perhaps you want sudo?", file=sys.stderr)
  218. self.exit(1)
  219. else:
  220. raise
  221. print(f"Removed {path}")
  222. class InstallNativeKernelSpec(JupyterApp):
  223. """An app to install the native kernel spec."""
  224. version = __version__
  225. description = """[DEPRECATED] Install the IPython kernel spec directory for this Python."""
  226. kernel_spec_manager = Instance(KernelSpecManager)
  227. def _kernel_spec_manager_default(self) -> KernelSpecManager: # pragma: no cover
  228. return KernelSpecManager(data_dir=self.data_dir)
  229. user = Bool(
  230. False,
  231. config=True,
  232. help="""
  233. Try to install the kernel spec to the per-user directory instead of
  234. the system or environment directory.
  235. """,
  236. )
  237. flags = {
  238. "user": (
  239. {"InstallNativeKernelSpec": {"user": True}},
  240. "Install to the per-user kernel registry",
  241. ),
  242. "debug": base_flags["debug"],
  243. }
  244. def start(self) -> None: # pragma: no cover
  245. """Start the application."""
  246. self.log.warning(
  247. "`jupyter kernelspec install-self` is DEPRECATED as of 4.0."
  248. " You probably want `ipython kernel install` to install the IPython kernelspec."
  249. )
  250. try:
  251. from ipykernel import kernelspec
  252. except ModuleNotFoundError:
  253. print("ipykernel not available, can't install its spec.", file=sys.stderr)
  254. self.exit(1)
  255. try:
  256. kernelspec.install(self.kernel_spec_manager, user=self.user)
  257. except OSError as e:
  258. if e.errno == errno.EACCES:
  259. print(e, file=sys.stderr)
  260. if not self.user:
  261. print(
  262. "Perhaps you want to install with `sudo` or `--user`?",
  263. file=sys.stderr,
  264. )
  265. self.exit(1)
  266. self.exit(e) # type:ignore[arg-type]
  267. class ListProvisioners(JupyterApp):
  268. """An app to list provisioners."""
  269. version = __version__
  270. description = """List available provisioners for use in kernel specifications."""
  271. def start(self) -> None:
  272. """Start the application."""
  273. kfp = KernelProvisionerFactory.instance(parent=self)
  274. print("Available kernel provisioners:")
  275. provisioners = kfp.get_provisioner_entries()
  276. # pad to width of longest kernel name
  277. name_len = len(sorted(provisioners, key=lambda name: len(name))[-1])
  278. for name in sorted(provisioners):
  279. print(f" {name.ljust(name_len)} {provisioners[name]}")
  280. class KernelSpecApp(Application):
  281. """An app to manage kernel specs."""
  282. version = __version__
  283. name = "jupyter kernelspec"
  284. description = """Manage Jupyter kernel specifications."""
  285. subcommands = Dict(
  286. {
  287. "list": (ListKernelSpecs, ListKernelSpecs.description.splitlines()[0]),
  288. "install": (
  289. InstallKernelSpec,
  290. InstallKernelSpec.description.splitlines()[0],
  291. ),
  292. "uninstall": (RemoveKernelSpec, "Alias for remove"),
  293. "remove": (RemoveKernelSpec, RemoveKernelSpec.description.splitlines()[0]),
  294. "install-self": (
  295. InstallNativeKernelSpec,
  296. InstallNativeKernelSpec.description.splitlines()[0],
  297. ),
  298. "provisioners": (ListProvisioners, ListProvisioners.description.splitlines()[0]),
  299. }
  300. )
  301. aliases = {}
  302. flags = {}
  303. def start(self) -> None:
  304. """Start the application."""
  305. if self.subapp is None:
  306. print("No subcommand specified. Must specify one of: %s" % list(self.subcommands))
  307. print()
  308. self.print_description()
  309. self.print_subcommands()
  310. self.exit(1)
  311. else:
  312. return self.subapp.start()
  313. def _limit_to_missing(
  314. paths: dict[str, str], specs: dict[str, t.Any]
  315. ) -> tuple[dict[str, str], dict[str, t.Any]]:
  316. from shutil import which
  317. missing: dict[str, t.Any] = {}
  318. for name, data in specs.items():
  319. exe = data["spec"]["argv"][0]
  320. # if exe exists or is on the path, keep it
  321. if Path(exe).exists() or which(exe):
  322. continue
  323. missing[name] = data
  324. paths_: dict[str, str] = {k: v for k, v in paths.items() if k in missing}
  325. return paths_, missing
  326. if __name__ == "__main__":
  327. KernelSpecApp.launch_instance()