Skip to content

Commit f6c6198

Browse files
minrktakluyver
authored andcommitted
Backport PR #240: Add 'jupyter kernel' command
A simple lead in to the 'kernel nanny' work, this adds a command so you can do: ``` jupyter kernel --kernel python ``` I intend this for post-5.0. Signed-off-by: Thomas Kluyver <[email protected]>
1 parent f40dcd3 commit f6c6198

File tree

4 files changed

+158
-2
lines changed

4 files changed

+158
-2
lines changed

jupyter_client/kernelapp.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import os
2+
import signal
3+
import uuid
4+
5+
from jupyter_core.application import JupyterApp, base_flags
6+
from tornado.ioloop import IOLoop
7+
from traitlets import Unicode
8+
9+
from . import __version__
10+
from .kernelspec import KernelSpecManager, NATIVE_KERNEL_NAME
11+
from .manager import KernelManager
12+
13+
class KernelApp(JupyterApp):
14+
"""Launch a kernel by name in a local subprocess.
15+
"""
16+
version = __version__
17+
description = "Run a kernel locally in a subprocess"
18+
19+
classes = [KernelManager, KernelSpecManager]
20+
21+
aliases = {
22+
'kernel': 'KernelApp.kernel_name',
23+
'ip': 'KernelManager.ip',
24+
}
25+
flags = {'debug': base_flags['debug']}
26+
27+
kernel_name = Unicode(NATIVE_KERNEL_NAME,
28+
help = 'The name of a kernel type to start'
29+
).tag(config=True)
30+
31+
def initialize(self, argv=None):
32+
super(KernelApp, self).initialize(argv)
33+
self.km = KernelManager(kernel_name=self.kernel_name,
34+
config=self.config)
35+
cf_basename = 'kernel-%s.json' % uuid.uuid4()
36+
self.km.connection_file = os.path.join(self.runtime_dir, cf_basename)
37+
self.loop = IOLoop.current()
38+
self.loop.add_callback(self._record_started)
39+
40+
def setup_signals(self):
41+
"""Shutdown on SIGTERM or SIGINT (Ctrl-C)"""
42+
if os.name == 'nt':
43+
return
44+
45+
def shutdown_handler(signo, frame):
46+
self.loop.add_callback_from_signal(self.shutdown, signo)
47+
for sig in [signal.SIGTERM, signal.SIGINT]:
48+
signal.signal(sig, shutdown_handler)
49+
50+
def shutdown(self, signo):
51+
self.log.info('Shutting down on signal %d' % signo)
52+
self.km.shutdown_kernel()
53+
self.loop.stop()
54+
55+
def log_connection_info(self):
56+
cf = self.km.connection_file
57+
self.log.info('Connection file: %s', cf)
58+
self.log.info("To connect a client: --existing %s", os.path.basename(cf))
59+
60+
def _record_started(self):
61+
"""For tests, create a file to indicate that we've started
62+
63+
Do not rely on this except in our own tests!
64+
"""
65+
fn = os.environ.get('JUPYTER_CLIENT_TEST_RECORD_STARTUP_PRIVATE')
66+
if fn is not None:
67+
with open(fn, 'wb'):
68+
pass
69+
70+
def start(self):
71+
self.log.info('Starting kernel %r', self.kernel_name)
72+
try:
73+
self.km.start_kernel()
74+
self.log_connection_info()
75+
self.setup_signals()
76+
self.loop.start()
77+
finally:
78+
self.km.cleanup()
79+
80+
81+
main = KernelApp.launch_instance
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import division
2+
3+
import os
4+
import shutil
5+
from subprocess import Popen, PIPE
6+
import sys
7+
from tempfile import mkdtemp
8+
import time
9+
10+
PY3 = sys.version_info[0] >= 3
11+
12+
def _launch(extra_env):
13+
env = os.environ.copy()
14+
env.update(extra_env)
15+
return Popen([sys.executable, '-c',
16+
'from jupyter_client.kernelapp import main; main()'],
17+
env=env, stderr=(PIPE if PY3 else None))
18+
19+
WAIT_TIME = 10
20+
POLL_FREQ = 10
21+
22+
def hacky_wait(p):
23+
"""Python 2 subprocess doesn't have timeouts :-("""
24+
for _ in range(WAIT_TIME * POLL_FREQ):
25+
if p.poll() is not None:
26+
return p.returncode
27+
time.sleep(1 / POLL_FREQ)
28+
else:
29+
raise AssertionError("Process didn't exit in {} seconds"
30+
.format(WAIT_TIME))
31+
32+
def test_kernelapp_lifecycle():
33+
# Check that 'jupyter kernel' starts and terminates OK.
34+
runtime_dir = mkdtemp()
35+
startup_dir = mkdtemp()
36+
started = os.path.join(startup_dir, 'started')
37+
try:
38+
p = _launch({'JUPYTER_RUNTIME_DIR': runtime_dir,
39+
'JUPYTER_CLIENT_TEST_RECORD_STARTUP_PRIVATE': started,
40+
})
41+
# Wait for start
42+
for _ in range(WAIT_TIME * POLL_FREQ):
43+
if os.path.isfile(started):
44+
break
45+
time.sleep(1 / POLL_FREQ)
46+
else:
47+
raise AssertionError("No started file created in {} seconds"
48+
.format(WAIT_TIME))
49+
50+
# Connection file should be there by now
51+
files = os.listdir(runtime_dir)
52+
assert len(files) == 1
53+
cf = files[0]
54+
assert cf.startswith('kernel')
55+
assert cf.endswith('.json')
56+
57+
# Send SIGTERM to shut down
58+
p.terminate()
59+
if PY3:
60+
_, stderr = p.communicate(timeout=WAIT_TIME)
61+
assert cf in stderr.decode('utf-8', 'replace')
62+
else:
63+
hacky_wait(p)
64+
finally:
65+
shutil.rmtree(runtime_dir)
66+
shutil.rmtree(startup_dir)
67+

scripts/jupyter-kernel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env python
2+
from jupyter_client.kernelapp import main
3+
4+
if __name__ == '__main__':
5+
main()

setup.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ def run(self):
8484
'python-dateutil>=2.1',
8585
],
8686
extras_require = {
87-
'test': ['ipykernel', 'ipython', 'mock', 'pytest'],
87+
'test': ['ipykernel', 'ipython', 'mock'],
88+
'test:python_version == "3.3"': ['pytest<3.3.0'],
89+
'test:python_version >= "3.4" or python_version == "2.7"': ['pytest'],
8890
},
8991
cmdclass = {
9092
'bdist_egg': bdist_egg if 'bdist_egg' in sys.argv else bdist_egg_disabled,
@@ -93,7 +95,8 @@ def run(self):
9395
'console_scripts': [
9496
'jupyter-kernelspec = jupyter_client.kernelspecapp:KernelSpecApp.launch_instance',
9597
'jupyter-run = jupyter_client.runapp:RunApp.launch_instance',
96-
]
98+
'jupyter-kernel = jupyter_client.kernelapp:main',
99+
],
97100
},
98101
)
99102

0 commit comments

Comments
 (0)