Skip to content

Commit a476b0d

Browse files
committed
Merge pull request #81 from minrk/kernelspec-remove
add `jupyter kernelspec remove`
2 parents 3d3ef19 + e9fd704 commit a476b0d

File tree

3 files changed

+108
-4
lines changed

3 files changed

+108
-4
lines changed

jupyter_client/kernelspec.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
pjoin = os.path.join
1313

1414
from ipython_genutils.py3compat import PY3
15-
from traitlets import HasTraits, List, Unicode, Dict, Set, Type
15+
from traitlets import HasTraits, List, Unicode, Dict, Set, Bool, Type
1616
from traitlets.config import LoggingConfigurable
1717

1818
from jupyter_core.paths import jupyter_data_dir, jupyter_path, SYSTEM_JUPYTER_PATH
@@ -80,6 +80,12 @@ class KernelSpecManager(LoggingConfigurable):
8080
"""
8181
)
8282

83+
ensure_native_kernel = Bool(True, config=True,
84+
help="""If there is no Python kernelspec registered and the IPython
85+
kernel is available, ensure it is added to the spec list.
86+
"""
87+
)
88+
8389
data_dir = Unicode()
8490
def _data_dir_default(self):
8591
return jupyter_data_dir()
@@ -123,7 +129,7 @@ def find_kernel_specs(self):
123129
self.log.debug("Found kernel %s in %s", kname, kernel_dir)
124130
d[kname] = spec
125131

126-
if NATIVE_KERNEL_NAME not in d:
132+
if self.ensure_native_kernel and NATIVE_KERNEL_NAME not in d:
127133
try:
128134
from ipykernel.kernelspec import RESOURCES
129135
self.log.debug("Native kernel (%s) available from %s",
@@ -176,6 +182,25 @@ def get_all_specs(self):
176182
"spec": self._get_kernel_spec_by_name(kname, d[kname]).to_dict()
177183
} for kname in d}
178184

185+
def remove_kernel_spec(self, name):
186+
"""Remove a kernel spec directory by name.
187+
188+
Returns the path that was deleted.
189+
"""
190+
save_native = self.ensure_native_kernel
191+
try:
192+
self.ensure_native_kernel = False
193+
specs = self.find_kernel_specs()
194+
finally:
195+
self.ensure_native_kernel = save_native
196+
spec_dir = specs[name]
197+
self.log.debug("Removing %s", spec_dir)
198+
if os.path.islink(spec_dir):
199+
os.remove(spec_dir)
200+
else:
201+
shutil.rmtree(spec_dir)
202+
return spec_dir
203+
179204
def _get_destination_dir(self, kernel_name, user=False, prefix=None):
180205
if user:
181206
return os.path.join(self.user_kernel_dir, kernel_name)

jupyter_client/kernelspecapp.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@
1313
from jupyter_core.application import (
1414
JupyterApp, base_flags, base_aliases
1515
)
16-
from traitlets import Instance, Dict, Unicode, Bool
16+
from traitlets import Instance, Dict, Unicode, Bool, List
1717

1818
from . import __version__
1919
from .kernelspec import KernelSpecManager
2020

21+
try:
22+
raw_input
23+
except NameError:
24+
# py3
25+
raw_input = input
26+
2127
class ListKernelSpecs(JupyterApp):
2228
version = __version__
2329
description = """List installed kernel specifications."""
@@ -145,6 +151,61 @@ def start(self):
145151
self.exit(1)
146152
raise
147153

154+
class RemoveKernelSpec(JupyterApp):
155+
version = __version__
156+
description = """Remove one or more Jupyter kernelspecs by name."""
157+
examples = """jupyter kernelspec remove python2 [my_kernel ...]"""
158+
159+
force = Bool(False, config=True,
160+
help="""Force removal, don't prompt for confirmation."""
161+
)
162+
spec_names = List(Unicode())
163+
164+
kernel_spec_manager = Instance(KernelSpecManager)
165+
def _kernel_spec_manager_default(self):
166+
return KernelSpecManager(data_dir=self.data_dir, parent=self)
167+
168+
flags = {
169+
'f': ({'RemoveKernelSpec': {'force': True}}, force.get_metadata('help')),
170+
}
171+
flags.update(JupyterApp.flags)
172+
173+
def parse_command_line(self, argv):
174+
super(RemoveKernelSpec, self).parse_command_line(argv)
175+
# accept positional arg as profile name
176+
if self.extra_args:
177+
self.spec_names = sorted(set(self.extra_args)) # remove duplicates
178+
else:
179+
self.exit("No kernelspec specified.")
180+
181+
def start(self):
182+
self.kernel_spec_manager.ensure_native_kernel = False
183+
spec_paths = self.kernel_spec_manager.find_kernel_specs()
184+
missing = set(self.spec_names).difference(set(spec_paths))
185+
if missing:
186+
self.exit("Couldn't find kernel spec(s): %s" % ', '.join(missing))
187+
188+
if not self.force:
189+
print("Kernel specs to remove:")
190+
for name in self.spec_names:
191+
print(" %s\t%s" % (name.ljust(20), spec_paths[name]))
192+
answer = raw_input("Remove %i kernel specs [y/N]: " % len(self.spec_names))
193+
if not answer.lower().startswith('y'):
194+
return
195+
196+
for kernel_name in self.spec_names:
197+
try:
198+
path = self.kernel_spec_manager.remove_kernel_spec(kernel_name)
199+
except OSError as e:
200+
if e.errno == errno.EACCES:
201+
print(e, file=sys.stderr)
202+
print("Perhaps you want sudo?", file=sys.stderr)
203+
self.exit(1)
204+
else:
205+
raise
206+
self.log.info("Removed %s", path)
207+
208+
148209
class InstallNativeKernelSpec(JupyterApp):
149210
version = __version__
150211
description = """[DEPRECATED] Install the IPython kernel spec directory for this Python."""
@@ -189,6 +250,8 @@ class KernelSpecApp(Application):
189250
subcommands = Dict({
190251
'list': (ListKernelSpecs, ListKernelSpecs.description.splitlines()[0]),
191252
'install': (InstallKernelSpec, InstallKernelSpec.description.splitlines()[0]),
253+
'uninstall': (RemoveKernelSpec, "Alias for remove"),
254+
'remove': (RemoveKernelSpec, RemoveKernelSpec.description.splitlines()[0]),
192255
'install-self': (InstallNativeKernelSpec, InstallNativeKernelSpec.description.splitlines()[0]),
193256
})
194257

jupyter_client/tests/test_kernelspec.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from logging import StreamHandler
44
import os
55
from os.path import join as pjoin
6+
from subprocess import Popen, PIPE, STDOUT
7+
import sys
68
import unittest
79

810
try:
@@ -27,7 +29,7 @@ class KernelSpecTests(unittest.TestCase):
2729

2830
def _install_sample_kernel(self, kernels_dir):
2931
"""install a sample kernel in a kernels directory"""
30-
sample_kernel_dir = pjoin(kernels_dir, 'Sample')
32+
sample_kernel_dir = pjoin(kernels_dir, 'sample')
3133
os.makedirs(sample_kernel_dir)
3234
json_file = pjoin(sample_kernel_dir, 'kernel.json')
3335
with open(json_file, 'w') as f:
@@ -40,6 +42,7 @@ def setUp(self):
4042
'JUPYTER_CONFIG_DIR': pjoin(td.name, 'jupyter'),
4143
'JUPYTER_DATA_DIR': pjoin(td.name, 'jupyter_data'),
4244
'JUPYTER_RUNTIME_DIR': pjoin(td.name, 'jupyter_runtime'),
45+
'IPYTHONDIR': pjoin(td.name, 'ipython'),
4346
})
4447
self.env_patch.start()
4548
self.addCleanup(td.cleanup)
@@ -130,3 +133,16 @@ def test_cant_install_kernel_spec(self):
130133
self.ksm.install_kernel_spec(self.installable_kernel,
131134
kernel_name='tstinstalled',
132135
user=False)
136+
137+
def test_remove_kernel_spec(self):
138+
path = self.ksm.remove_kernel_spec('sample')
139+
self.assertEqual(path, self.sample_kernel_dir)
140+
141+
def test_remove_kernel_spec_app(self):
142+
p = Popen(
143+
[sys.executable, '-m', 'jupyter_client.kernelspecapp', 'remove', 'sample', '-f'],
144+
stdout=PIPE, stderr=STDOUT,
145+
env=os.environ,
146+
)
147+
out, _ = p.communicate()
148+
self.assertEqual(p.returncode, 0, out.decode('utf8', 'replace'))

0 commit comments

Comments
 (0)