88import os .path
99import sys
1010import typing as t
11+ from pathlib import Path
1112
1213from jupyter_core .application import JupyterApp , base_aliases , base_flags
1314from traitlets import Bool , Dict , Instance , List , Unicode
@@ -29,12 +30,20 @@ class ListKernelSpecs(JupyterApp):
2930 help = "output spec name and location as machine-readable json." ,
3031 config = True ,
3132 )
32-
33+ missing_kernels = Bool (
34+ False ,
35+ help = "List only specs with missing interpreters." ,
36+ config = True ,
37+ )
3338 flags = {
3439 "json" : (
3540 {"ListKernelSpecs" : {"json_output" : True }},
3641 "output spec name and location as machine-readable json." ,
3742 ),
43+ "missing" : (
44+ {"ListKernelSpecs" : {"missing_kernels" : True }},
45+ "output only missing kernels" ,
46+ ),
3847 "debug" : base_flags ["debug" ],
3948 }
4049
@@ -45,6 +54,10 @@ def start(self) -> dict[str, t.Any] | None: # type:ignore[override]
4554 """Start the application."""
4655 paths = self .kernel_spec_manager .find_kernel_specs ()
4756 specs = self .kernel_spec_manager .get_all_specs ()
57+
58+ if self .missing_kernels :
59+ paths , specs = _limit_to_missing (paths , specs )
60+
4861 if not self .json_output :
4962 if not specs :
5063 print ("No kernels available" )
@@ -177,6 +190,11 @@ class RemoveKernelSpec(JupyterApp):
177190
178191 force = Bool (False , config = True , help = """Force removal, don't prompt for confirmation.""" )
179192 spec_names = List (Unicode ())
193+ missing_kernels = Bool (
194+ False ,
195+ help = "Remove missing specs." ,
196+ config = True ,
197+ )
180198
181199 kernel_spec_manager = Instance (KernelSpecManager )
182200
@@ -185,6 +203,10 @@ def _kernel_spec_manager_default(self) -> KernelSpecManager:
185203
186204 flags = {
187205 "f" : ({"RemoveKernelSpec" : {"force" : True }}, force .help ),
206+ "missing" : (
207+ {"RemoveKernelSpec" : {"missing_kernels" : True }},
208+ "remove missing kernels" ,
209+ ),
188210 }
189211 flags .update (JupyterApp .flags )
190212
@@ -195,12 +217,22 @@ def parse_command_line(self, argv: list[str] | None) -> None: # type:ignore[ove
195217 if self .extra_args :
196218 self .spec_names = sorted (set (self .extra_args )) # remove duplicates
197219 else :
198- self .exit ( "No kernelspec specified." )
220+ self .spec_names = []
199221
200222 def start (self ) -> None :
201223 """Start the application."""
202224 self .kernel_spec_manager .ensure_native_kernel = False
203225 spec_paths = self .kernel_spec_manager .find_kernel_specs ()
226+
227+ if self .missing_kernels :
228+ _ , spec = _limit_to_missing (
229+ spec_paths ,
230+ self .kernel_spec_manager .get_all_specs (),
231+ )
232+
233+ # append missing kernels
234+ self .spec_names = sorted (set (self .spec_names + list (spec )))
235+
204236 missing = set (self .spec_names ).difference (set (spec_paths ))
205237 if missing :
206238 self .exit ("Couldn't find kernel spec(s): %s" % ", " .join (missing ))
@@ -337,5 +369,22 @@ def start(self) -> None:
337369 return self .subapp .start ()
338370
339371
372+ def _limit_to_missing (
373+ paths : dict [str , str ], specs : dict [str , t .Any ]
374+ ) -> tuple [dict [str , str ], dict [str , t .Any ]]:
375+ from shutil import which
376+
377+ missing : dict [str , t .Any ] = {}
378+ for name , data in specs .items ():
379+ exe = data ["spec" ]["argv" ][0 ]
380+ # if exe exists or is on the path, keep it
381+ if Path (exe ).exists () or which (exe ):
382+ continue
383+ missing [name ] = data
384+
385+ paths_ : dict [str , str ] = {k : v for k , v in paths .items () if k in missing }
386+ return paths_ , missing
387+
388+
340389if __name__ == "__main__" :
341390 KernelSpecApp .launch_instance ()
0 commit comments