Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b537531

Browse files
committed
Merge pull request #6026 from takluyver/kernelspec-rest-launching
Kernelspecs in REST API for kernels and sessions
2 parents e8d1c5a + 91ceeb6 commit b537531

14 files changed

Lines changed: 140 additions & 81 deletions

File tree

IPython/html/notebookapp.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,9 @@ def init_configurables(self):
658658
kls = import_item(self.notebook_manager_class)
659659
self.notebook_manager = kls(parent=self, log=self.log)
660660
kls = import_item(self.session_manager_class)
661-
self.session_manager = kls(parent=self, log=self.log)
661+
self.session_manager = kls(parent=self, log=self.log,
662+
kernel_manager=self.kernel_manager,
663+
notebook_manager=self.notebook_manager)
662664
kls = import_item(self.cluster_manager_class)
663665
self.cluster_manager = kls(parent=self, log=self.log)
664666
self.cluster_manager.update_profiles()

IPython/html/services/kernels/handlers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,16 @@ def get(self):
2727
@web.authenticated
2828
@json_errors
2929
def post(self):
30+
model = self.get_json_body()
31+
if model is None:
32+
raise web.HTTPError(400, "No JSON data provided")
33+
try:
34+
name = model['name']
35+
except KeyError:
36+
raise web.HTTPError(400, "Missing field in JSON data: name")
37+
3038
km = self.kernel_manager
31-
kernel_id = km.start_kernel()
39+
kernel_id = km.start_kernel(kernel_name=name)
3240
model = km.kernel_model(kernel_id)
3341
location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
3442
self.set_header('Location', url_escape(location))

IPython/html/services/kernels/kernelmanager.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ def cwd_for_path(self, path):
7272
os_path = os.path.dirname(os_path)
7373
return os_path
7474

75-
def start_kernel(self, kernel_id=None, path=None, **kwargs):
76-
"""Start a kernel for a session an return its kernel_id.
75+
def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
76+
"""Start a kernel for a session and return its kernel_id.
7777
7878
Parameters
7979
----------
@@ -84,12 +84,16 @@ def start_kernel(self, kernel_id=None, path=None, **kwargs):
8484
path : API path
8585
The API path (unicode, '/' delimited) for the cwd.
8686
Will be transformed to an OS path relative to root_dir.
87+
kernel_name : str
88+
The name identifying which kernel spec to launch. This is ignored if
89+
an existing kernel is returned, but it may be checked in the future.
8790
"""
8891
if kernel_id is None:
8992
kwargs['extra_arguments'] = self.kernel_argv
9093
if path is not None:
9194
kwargs['cwd'] = self.cwd_for_path(path)
92-
kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
95+
kernel_id = super(MappingKernelManager, self).start_kernel(
96+
kernel_name=kernel_name, **kwargs)
9397
self.log.info("Kernel started: %s" % kernel_id)
9498
self.log.debug("Kernel args: %r" % kwargs)
9599
# register callback for failed auto-restart
@@ -111,7 +115,8 @@ def kernel_model(self, kernel_id):
111115
"""Return a dictionary of kernel information described in the
112116
JSON standard model."""
113117
self._check_kernel_id(kernel_id)
114-
model = {"id":kernel_id}
118+
model = {"id":kernel_id,
119+
"name": self._kernels[kernel_id].kernel_name}
115120
return model
116121

117122
def list_kernels(self):

IPython/html/services/kernels/tests/test_kernels_api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Test the kernels service API."""
22

3-
3+
import json
44
import requests
55

66
from IPython.html.utils import url_path_join
@@ -30,8 +30,9 @@ def list(self):
3030
def get(self, id):
3131
return self._req('GET', id)
3232

33-
def start(self):
34-
return self._req('POST', '')
33+
def start(self, name='python'):
34+
body = json.dumps({'name': name})
35+
return self._req('POST', '', body)
3536

3637
def shutdown(self, id):
3738
return self._req('DELETE', id)
@@ -69,6 +70,7 @@ def test_main_kernel_handler(self):
6970
self.assertEqual(r.status_code, 200)
7071
assert isinstance(r.json(), list)
7172
self.assertEqual(r.json()[0]['id'], kern1['id'])
73+
self.assertEqual(r.json()[0]['name'], kern1['name'])
7274

7375
# create another kernel and check that they both are added to the
7476
# list of kernels from a GET request
@@ -89,6 +91,7 @@ def test_main_kernel_handler(self):
8991
self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
9092
rekern = r.json()
9193
self.assertEqual(rekern['id'], kern2['id'])
94+
self.assertEqual(rekern['name'], kern2['name'])
9295

9396
def test_kernel_handler(self):
9497
# GET kernel with given id

IPython/html/services/sessions/handlers.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,27 +45,28 @@ def post(self):
4545
# Creates a new session
4646
#(unless a session already exists for the named nb)
4747
sm = self.session_manager
48-
nbm = self.notebook_manager
49-
km = self.kernel_manager
48+
5049
model = self.get_json_body()
5150
if model is None:
5251
raise web.HTTPError(400, "No JSON data provided")
5352
try:
5453
name = model['notebook']['name']
5554
except KeyError:
56-
raise web.HTTPError(400, "Missing field in JSON data: name")
55+
raise web.HTTPError(400, "Missing field in JSON data: notebook.name")
5756
try:
5857
path = model['notebook']['path']
5958
except KeyError:
60-
raise web.HTTPError(400, "Missing field in JSON data: path")
59+
raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
60+
try:
61+
kernel_name = model['kernel']['name']
62+
except KeyError:
63+
raise web.HTTPError(400, "Missing field in JSON data: kernel.name")
64+
6165
# Check to see if session exists
6266
if sm.session_exists(name=name, path=path):
6367
model = sm.get_session(name=name, path=path)
6468
else:
65-
# allow nbm to specify kernels cwd
66-
kernel_path = nbm.get_kernel_path(name=name, path=path)
67-
kernel_id = km.start_kernel(path=kernel_path)
68-
model = sm.create_session(name=name, path=path, kernel_id=kernel_id)
69+
model = sm.create_session(name=name, path=path, kernel_name=kernel_name)
6970
location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
7071
self.set_header('Location', url_escape(location))
7172
self.set_status(201)
@@ -108,10 +109,7 @@ def patch(self, session_id):
108109
def delete(self, session_id):
109110
# Deletes the session with given session_id
110111
sm = self.session_manager
111-
km = self.kernel_manager
112-
session = sm.get_session(session_id=session_id)
113112
sm.delete_session(session_id)
114-
km.shutdown_kernel(session['kernel']['id'])
115113
self.set_status(204)
116114
self.finish()
117115

IPython/html/services/sessions/sessionmanager.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@
2323

2424
from IPython.config.configurable import LoggingConfigurable
2525
from IPython.utils.py3compat import unicode_type
26+
from IPython.utils.traitlets import Instance
2627

2728
#-----------------------------------------------------------------------------
2829
# Classes
2930
#-----------------------------------------------------------------------------
3031

3132
class SessionManager(LoggingConfigurable):
33+
34+
kernel_manager = Instance('IPython.html.services.kernels.kernelmanager.MappingKernelManager')
35+
notebook_manager = Instance('IPython.html.services.notebooks.nbmanager.NotebookManager', args=())
3236

3337
# Session database initialized below
3438
_cursor = None
@@ -69,10 +73,15 @@ def new_session_id(self):
6973
"Create a uuid for a new session"
7074
return unicode_type(uuid.uuid4())
7175

72-
def create_session(self, name=None, path=None, kernel_id=None):
76+
def create_session(self, name=None, path=None, kernel_name='python'):
7377
"""Creates a session and returns its model"""
7478
session_id = self.new_session_id()
75-
return self.save_session(session_id, name=name, path=path, kernel_id=kernel_id)
79+
# allow nbm to specify kernels cwd
80+
kernel_path = self.notebook_manager.get_kernel_path(name=name, path=path)
81+
kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
82+
kernel_name=kernel_name)
83+
return self.save_session(session_id, name=name, path=path,
84+
kernel_id=kernel_id)
7685

7786
def save_session(self, session_id, name=None, path=None, kernel_id=None):
7887
"""Saves the items for the session with the given session_id
@@ -170,8 +179,7 @@ def update_session(self, session_id, **kwargs):
170179
query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
171180
self.cursor.execute(query, list(kwargs.values()) + [session_id])
172181

173-
@staticmethod
174-
def row_factory(cursor, row):
182+
def row_factory(self, cursor, row):
175183
"""Takes sqlite database session row and turns it into a dictionary"""
176184
row = sqlite3.Row(cursor, row)
177185
model = {
@@ -180,9 +188,7 @@ def row_factory(cursor, row):
180188
'name': row['name'],
181189
'path': row['path']
182190
},
183-
'kernel': {
184-
'id': row['kernel_id'],
185-
}
191+
'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
186192
}
187193
return model
188194

@@ -195,5 +201,6 @@ def list_sessions(self):
195201
def delete_session(self, session_id):
196202
"""Deletes the row in the session database with given session_id"""
197203
# Check that session exists before deleting
198-
self.get_session(session_id=session_id)
204+
session = self.get_session(session_id=session_id)
205+
self.kernel_manager.shutdown_kernel(session['kernel']['id'])
199206
self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))

IPython/html/services/sessions/tests/test_sessionmanager.py

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,79 +5,101 @@
55
from tornado import web
66

77
from ..sessionmanager import SessionManager
8+
from IPython.html.services.kernels.kernelmanager import MappingKernelManager
9+
10+
class DummyKernel(object):
11+
def __init__(self, kernel_name='python'):
12+
self.kernel_name = kernel_name
13+
14+
class DummyMKM(MappingKernelManager):
15+
"""MappingKernelManager interface that doesn't start kernels, for testing"""
16+
def __init__(self, *args, **kwargs):
17+
super(DummyMKM, self).__init__(*args, **kwargs)
18+
self.id_letters = iter(u'ABCDEFGHIJK')
19+
20+
def _new_id(self):
21+
return next(self.id_letters)
22+
23+
def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
24+
kernel_id = kernel_id or self._new_id()
25+
self._kernels[kernel_id] = DummyKernel(kernel_name=kernel_name)
26+
return kernel_id
27+
28+
def shutdown_kernel(self, kernel_id, now=False):
29+
del self._kernels[kernel_id]
830

931
class TestSessionManager(TestCase):
1032

1133
def test_get_session(self):
12-
sm = SessionManager()
13-
session_id = sm.new_session_id()
14-
sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678')
34+
sm = SessionManager(kernel_manager=DummyMKM())
35+
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
36+
kernel_name='bar')['id']
1537
model = sm.get_session(session_id=session_id)
16-
expected = {'id':session_id, 'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678'}}
38+
expected = {'id':session_id,
39+
'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'},
40+
'kernel': {'id':u'A', 'name': 'bar'}}
1741
self.assertEqual(model, expected)
1842

1943
def test_bad_get_session(self):
2044
# Should raise error if a bad key is passed to the database.
21-
sm = SessionManager()
22-
session_id = sm.new_session_id()
23-
sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678')
45+
sm = SessionManager(kernel_manager=DummyMKM())
46+
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
47+
kernel_name='foo')['id']
2448
self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
2549

2650
def test_list_sessions(self):
27-
sm = SessionManager()
28-
session_id1 = sm.new_session_id()
29-
session_id2 = sm.new_session_id()
30-
session_id3 = sm.new_session_id()
31-
sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678')
32-
sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678')
33-
sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678')
51+
sm = SessionManager(kernel_manager=DummyMKM())
52+
sessions = [
53+
sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
54+
sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
55+
sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
56+
]
3457
sessions = sm.list_sessions()
35-
expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb',
36-
'path': u'/path/to/1/'}, 'kernel':{'id':u'5678'}},
37-
{'id':session_id2, 'notebook': {'name':u'test2.ipynb',
38-
'path': u'/path/to/2/'}, 'kernel':{'id':u'5678'}},
39-
{'id':session_id3, 'notebook':{'name':u'test3.ipynb',
40-
'path': u'/path/to/3/'}, 'kernel':{'id':u'5678'}}]
58+
expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
59+
'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
60+
{'id':sessions[1]['id'], 'notebook': {'name':u'test2.ipynb',
61+
'path': u'/path/to/2/'}, 'kernel':{'id':u'B', 'name':'python'}},
62+
{'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
63+
'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
4164
self.assertEqual(sessions, expected)
4265

4366
def test_update_session(self):
44-
sm = SessionManager()
45-
session_id = sm.new_session_id()
46-
sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id=None)
47-
sm.update_session(session_id, kernel_id='5678')
67+
sm = SessionManager(kernel_manager=DummyMKM())
68+
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
69+
kernel_name='julia')['id']
4870
sm.update_session(session_id, name='new_name.ipynb')
4971
model = sm.get_session(session_id=session_id)
50-
expected = {'id':session_id, 'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678'}}
72+
expected = {'id':session_id,
73+
'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'},
74+
'kernel':{'id':u'A', 'name':'julia'}}
5175
self.assertEqual(model, expected)
5276

5377
def test_bad_update_session(self):
5478
# try to update a session with a bad keyword ~ raise error
55-
sm = SessionManager()
56-
session_id = sm.new_session_id()
57-
sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678')
79+
sm = SessionManager(kernel_manager=DummyMKM())
80+
session_id = sm.create_session(name='test.ipynb', path='/path/to/',
81+
kernel_name='ir')['id']
5882
self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
5983

6084
def test_delete_session(self):
61-
sm = SessionManager()
62-
session_id1 = sm.new_session_id()
63-
session_id2 = sm.new_session_id()
64-
session_id3 = sm.new_session_id()
65-
sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678')
66-
sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678')
67-
sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678')
68-
sm.delete_session(session_id2)
69-
sessions = sm.list_sessions()
70-
expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb',
71-
'path': u'/path/to/1/'}, 'kernel':{'id':u'5678'}},
72-
{'id':session_id3, 'notebook':{'name':u'test3.ipynb',
73-
'path': u'/path/to/3/'}, 'kernel':{'id':u'5678'}}]
74-
self.assertEqual(sessions, expected)
85+
sm = SessionManager(kernel_manager=DummyMKM())
86+
sessions = [
87+
sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
88+
sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
89+
sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
90+
]
91+
sm.delete_session(sessions[1]['id'])
92+
new_sessions = sm.list_sessions()
93+
expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
94+
'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
95+
{'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
96+
'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
97+
self.assertEqual(new_sessions, expected)
7598

7699
def test_bad_delete_session(self):
77100
# try to delete a session that doesn't exist ~ raise error
78-
sm = SessionManager()
79-
session_id = sm.new_session_id()
80-
sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678')
101+
sm = SessionManager(kernel_manager=DummyMKM())
102+
sm.create_session(name='test.ipynb', path='/path/to/', kernel_name='python')
81103
self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
82104
self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
83105

IPython/html/services/sessions/tests/test_sessions_api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ def list(self):
3737
def get(self, id):
3838
return self._req('GET', id)
3939

40-
def create(self, name, path):
41-
body = json.dumps({'notebook': {'name':name, 'path':path}})
40+
def create(self, name, path, kernel_name='python'):
41+
body = json.dumps({'notebook': {'name':name, 'path':path},
42+
'kernel': {'name': kernel_name}})
4243
return self._req('POST', '', body)
4344

4445
def modify(self, id, name, path):

IPython/html/static/notebook/js/notebook.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ define([
5959
this.keyboard_manager = options.keyboard_manager;
6060
this.save_widget = options.save_widget;
6161
this.tooltip = new tooltip.Tooltip(this.events);
62+
// default_kernel_name is a temporary measure while we implement proper
63+
// kernel selection and delayed start. Do not rely on it.
64+
this.default_kernel_name = 'python';
6265
// TODO: This code smells (and the other `= this` line a couple lines down)
6366
// We need a better way to deal with circular instance references.
6467
this.keyboard_manager.notebook = this;
@@ -1495,7 +1498,12 @@ define([
14951498
base_url: this.base_url,
14961499
notebook_path: this.notebook_path,
14971500
notebook_name: this.notebook_name,
1501+
// For now, create all sessions with the 'python' kernel, which is the
1502+
// default. Later, the user will be able to select kernels. This is
1503+
// overridden if KernelManager.kernel_cmd is specified for the server.
1504+
kernel_name: this.default_kernel_name,
14981505
notebook: this});
1506+
14991507
this.session.start($.proxy(this._session_started, this));
15001508
};
15011509

0 commit comments

Comments
 (0)