Skip to content

Commit

Permalink
Merge pull request #240 from takluyver/jupyter-kernel-cmd
Browse files Browse the repository at this point in the history
Add 'jupyter kernel' command
  • Loading branch information
minrk authored Dec 12, 2017
2 parents 0d7d00f + 5291f94 commit 7bbb56d
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 1 deletion.
81 changes: 81 additions & 0 deletions jupyter_client/kernelapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import os
import signal
import uuid

from jupyter_core.application import JupyterApp, base_flags
from tornado.ioloop import IOLoop
from traitlets import Unicode

from . import __version__
from .kernelspec import KernelSpecManager, NATIVE_KERNEL_NAME
from .manager import KernelManager

class KernelApp(JupyterApp):
"""Launch a kernel by name in a local subprocess.
"""
version = __version__
description = "Run a kernel locally in a subprocess"

classes = [KernelManager, KernelSpecManager]

aliases = {
'kernel': 'KernelApp.kernel_name',
'ip': 'KernelManager.ip',
}
flags = {'debug': base_flags['debug']}

kernel_name = Unicode(NATIVE_KERNEL_NAME,
help = 'The name of a kernel type to start'
).tag(config=True)

def initialize(self, argv=None):
super(KernelApp, self).initialize(argv)
self.km = KernelManager(kernel_name=self.kernel_name,
config=self.config)
cf_basename = 'kernel-%s.json' % uuid.uuid4()
self.km.connection_file = os.path.join(self.runtime_dir, cf_basename)
self.loop = IOLoop.current()
self.loop.add_callback(self._record_started)

def setup_signals(self):
"""Shutdown on SIGTERM or SIGINT (Ctrl-C)"""
if os.name == 'nt':
return

def shutdown_handler(signo, frame):
self.loop.add_callback_from_signal(self.shutdown, signo)
for sig in [signal.SIGTERM, signal.SIGINT]:
signal.signal(sig, shutdown_handler)

def shutdown(self, signo):
self.log.info('Shutting down on signal %d' % signo)
self.km.shutdown_kernel()
self.loop.stop()

def log_connection_info(self):
cf = self.km.connection_file
self.log.info('Connection file: %s', cf)
self.log.info("To connect a client: --existing %s", os.path.basename(cf))

def _record_started(self):
"""For tests, create a file to indicate that we've started
Do not rely on this except in our own tests!
"""
fn = os.environ.get('JUPYTER_CLIENT_TEST_RECORD_STARTUP_PRIVATE')
if fn is not None:
with open(fn, 'wb'):
pass

def start(self):
self.log.info('Starting kernel %r', self.kernel_name)
try:
self.km.start_kernel()
self.log_connection_info()
self.setup_signals()
self.loop.start()
finally:
self.km.cleanup()


main = KernelApp.launch_instance
67 changes: 67 additions & 0 deletions jupyter_client/tests/test_kernelapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import division

import os
import shutil
from subprocess import Popen, PIPE
import sys
from tempfile import mkdtemp
import time

PY3 = sys.version_info[0] >= 3

def _launch(extra_env):
env = os.environ.copy()
env.update(extra_env)
return Popen([sys.executable, '-c',
'from jupyter_client.kernelapp import main; main()'],
env=env, stderr=(PIPE if PY3 else None))

WAIT_TIME = 10
POLL_FREQ = 10

def hacky_wait(p):
"""Python 2 subprocess doesn't have timeouts :-("""
for _ in range(WAIT_TIME * POLL_FREQ):
if p.poll() is not None:
return p.returncode
time.sleep(1 / POLL_FREQ)
else:
raise AssertionError("Process didn't exit in {} seconds"
.format(WAIT_TIME))

def test_kernelapp_lifecycle():
# Check that 'jupyter kernel' starts and terminates OK.
runtime_dir = mkdtemp()
startup_dir = mkdtemp()
started = os.path.join(startup_dir, 'started')
try:
p = _launch({'JUPYTER_RUNTIME_DIR': runtime_dir,
'JUPYTER_CLIENT_TEST_RECORD_STARTUP_PRIVATE': started,
})
# Wait for start
for _ in range(WAIT_TIME * POLL_FREQ):
if os.path.isfile(started):
break
time.sleep(1 / POLL_FREQ)
else:
raise AssertionError("No started file created in {} seconds"
.format(WAIT_TIME))

# Connection file should be there by now
files = os.listdir(runtime_dir)
assert len(files) == 1
cf = files[0]
assert cf.startswith('kernel')
assert cf.endswith('.json')

# Send SIGTERM to shut down
p.terminate()
if PY3:
_, stderr = p.communicate(timeout=WAIT_TIME)
assert cf in stderr.decode('utf-8', 'replace')
else:
hacky_wait(p)
finally:
shutil.rmtree(runtime_dir)
shutil.rmtree(startup_dir)

5 changes: 5 additions & 0 deletions scripts/jupyter-kernel
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python
from jupyter_client.kernelapp import main

if __name__ == '__main__':
main()
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ def run(self):
'entrypoints',
],
extras_require = {
'test': ['ipykernel', 'ipython', 'mock', 'pytest'],
'test': ['ipykernel', 'ipython', 'mock'],
'test:python_version == "3.3"': ['pytest<3.3.0'],
'test:python_version >= "3.4" or python_version == "2.7"': ['pytest'],
},
cmdclass = {
'bdist_egg': bdist_egg if 'bdist_egg' in sys.argv else bdist_egg_disabled,
Expand All @@ -94,6 +96,7 @@ def run(self):
'console_scripts': [
'jupyter-kernelspec = jupyter_client.kernelspecapp:KernelSpecApp.launch_instance',
'jupyter-run = jupyter_client.runapp:RunApp.launch_instance',
'jupyter-kernel = jupyter_client.kernelapp:main',
],
'jupyter_client.kernel_providers' : [
'spec = jupyter_client.discovery:KernelSpecProvider',
Expand Down

0 comments on commit 7bbb56d

Please sign in to comment.