From 4538894162011b3eeb43e4a86e6f15bc076067a1 Mon Sep 17 00:00:00 2001 From: wpk-nist-gov Date: Mon, 24 Feb 2025 16:37:34 -0500 Subject: [PATCH] feat: added `--missing` flag to list and remove subcommands --- jupyter_client/kernelspecapp.py | 51 +++++++++++++++++++++++++++++++-- tests/test_kernelspecapp.py | 5 ++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/jupyter_client/kernelspecapp.py b/jupyter_client/kernelspecapp.py index 186aa9cf..37acfa55 100644 --- a/jupyter_client/kernelspecapp.py +++ b/jupyter_client/kernelspecapp.py @@ -8,6 +8,7 @@ import os.path import sys import typing as t +from pathlib import Path from jupyter_core.application import JupyterApp, base_aliases, base_flags from traitlets import Bool, Dict, Instance, List, Unicode @@ -29,12 +30,20 @@ class ListKernelSpecs(JupyterApp): help="output spec name and location as machine-readable json.", config=True, ) - + missing_kernels = Bool( + False, + help="List only specs with missing interpreters.", + config=True, + ) flags = { "json": ( {"ListKernelSpecs": {"json_output": True}}, "output spec name and location as machine-readable json.", ), + "missing": ( + {"ListKernelSpecs": {"missing_kernels": True}}, + "output only missing kernels", + ), "debug": base_flags["debug"], } @@ -45,6 +54,10 @@ def start(self) -> dict[str, t.Any] | None: # type:ignore[override] """Start the application.""" paths = self.kernel_spec_manager.find_kernel_specs() specs = self.kernel_spec_manager.get_all_specs() + + if self.missing_kernels: + paths, specs = _limit_to_missing(paths, specs) + if not self.json_output: if not specs: print("No kernels available") @@ -177,6 +190,11 @@ class RemoveKernelSpec(JupyterApp): force = Bool(False, config=True, help="""Force removal, don't prompt for confirmation.""") spec_names = List(Unicode()) + missing_kernels = Bool( + False, + help="Remove missing specs.", + config=True, + ) kernel_spec_manager = Instance(KernelSpecManager) @@ -185,6 +203,10 @@ def _kernel_spec_manager_default(self) -> KernelSpecManager: flags = { "f": ({"RemoveKernelSpec": {"force": True}}, force.help), + "missing": ( + {"RemoveKernelSpec": {"missing_kernels": True}}, + "remove missing kernels", + ), } flags.update(JupyterApp.flags) @@ -195,12 +217,22 @@ def parse_command_line(self, argv: list[str] | None) -> None: # type:ignore[ove if self.extra_args: self.spec_names = sorted(set(self.extra_args)) # remove duplicates else: - self.exit("No kernelspec specified.") + self.spec_names = [] def start(self) -> None: """Start the application.""" self.kernel_spec_manager.ensure_native_kernel = False spec_paths = self.kernel_spec_manager.find_kernel_specs() + + if self.missing_kernels: + _, spec = _limit_to_missing( + spec_paths, + self.kernel_spec_manager.get_all_specs(), + ) + + # append missing kernels + self.spec_names = sorted(set(self.spec_names + list(spec))) + missing = set(self.spec_names).difference(set(spec_paths)) if missing: self.exit("Couldn't find kernel spec(s): %s" % ", ".join(missing)) @@ -337,5 +369,20 @@ def start(self) -> None: return self.subapp.start() +def _limit_to_missing( + paths: dict[str, str], specs: dict[str, t.Any] +) -> tuple[dict[str, str], dict[str, t.Any]]: + specs_: dict[str, t.Any] = { + k: v + for k, v in specs.items() + # If have kernel installed from current environment + # Probably not the best way to do this, but it works in edge cases I've run into. + if (prog := v["spec"]["argv"][0]) != "python" and not Path(prog).exists() + } + + paths_: dict[str, str] = {k: v for k, v in paths.items() if k in specs_} + return paths_, specs_ + + if __name__ == "__main__": KernelSpecApp.launch_instance() diff --git a/tests/test_kernelspecapp.py b/tests/test_kernelspecapp.py index 119b6fb6..85cb7a8e 100644 --- a/tests/test_kernelspecapp.py +++ b/tests/test_kernelspecapp.py @@ -38,6 +38,11 @@ def test_kernelspec_sub_apps(jp_kernel_dir): specs = app3.start() assert specs and "echo" not in specs + app4 = ListKernelSpecs(missing_kernels=True) + app4.kernel_spec_manager.kernel_dirs.append(kernel_dir) + specs = app4.start() + assert specs is None + def test_kernelspec_app(): app = KernelSpecApp()