##// END OF EJS Templates
Add tests and fix some issues...
Thomas Kluyver -
Show More
@@ -1,222 +1,227 b''
1 1 """A base class session manager.
2 2
3 3 Authors:
4 4
5 5 * Zach Sailer
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import uuid
20 20 import sqlite3
21 21
22 22 from tornado import web
23 23
24 24 from IPython.config.configurable import LoggingConfigurable
25 25 from IPython.utils.py3compat import unicode_type
26 26 from IPython.utils.traitlets import Instance
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Classes
30 30 #-----------------------------------------------------------------------------
31 31
32 32 class SessionManager(LoggingConfigurable):
33 33
34 34 kernel_manager = Instance('IPython.html.services.kernels.kernelmanager.MappingKernelManager')
35 35 contents_manager = Instance('IPython.html.services.contents.manager.ContentsManager', args=())
36 36
37 37 # Session database initialized below
38 38 _cursor = None
39 39 _connection = None
40 40 _columns = {'session_id', 'name', 'path', 'kernel_id'}
41 41
42 42 @property
43 43 def cursor(self):
44 44 """Start a cursor and create a database called 'session'"""
45 45 if self._cursor is None:
46 46 self._cursor = self.connection.cursor()
47 47 self._cursor.execute("""CREATE TABLE session
48 48 (session_id, name, path, kernel_id)""")
49 49 return self._cursor
50 50
51 51 @property
52 52 def connection(self):
53 53 """Start a database connection"""
54 54 if self._connection is None:
55 55 self._connection = sqlite3.connect(':memory:')
56 56 self._connection.row_factory = sqlite3.Row
57 57 return self._connection
58 58
59 59 def __del__(self):
60 60 """Close connection once SessionManager closes"""
61 61 self.cursor.close()
62 62
63 63 def session_exists(self, name, path):
64 64 """Check to see if the session for a given notebook exists"""
65 65 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
66 66 reply = self.cursor.fetchone()
67 67 if reply is None:
68 68 return False
69 69 else:
70 70 return True
71 71
72 72 def new_session_id(self):
73 73 "Create a uuid for a new session"
74 74 return unicode_type(uuid.uuid4())
75 75
76 76 def create_session(self, name=None, path=None, kernel_name='python'):
77 77 """Creates a session and returns its model"""
78 78 session_id = self.new_session_id()
79 79 # allow nbm to specify kernels cwd
80 80 kernel_path = self.contents_manager.get_kernel_path(name=name, path=path)
81 81 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
82 82 kernel_name=kernel_name)
83 83 return self.save_session(session_id, name=name, path=path,
84 84 kernel_id=kernel_id)
85 85
86 86 def save_session(self, session_id, name=None, path=None, kernel_id=None):
87 87 """Saves the items for the session with the given session_id
88 88
89 89 Given a session_id (and any other of the arguments), this method
90 90 creates a row in the sqlite session database that holds the information
91 91 for a session.
92 92
93 93 Parameters
94 94 ----------
95 95 session_id : str
96 96 uuid for the session; this method must be given a session_id
97 97 name : str
98 98 the .ipynb notebook name that started the session
99 99 path : str
100 100 the path to the named notebook
101 101 kernel_id : str
102 102 a uuid for the kernel associated with this session
103 103
104 104 Returns
105 105 -------
106 106 model : dict
107 107 a dictionary of the session model
108 108 """
109 109 self.cursor.execute("INSERT INTO session VALUES (?,?,?,?)",
110 110 (session_id, name, path, kernel_id)
111 111 )
112 112 return self.get_session(session_id=session_id)
113 113
114 114 def get_session(self, **kwargs):
115 115 """Returns the model for a particular session.
116 116
117 117 Takes a keyword argument and searches for the value in the session
118 118 database, then returns the rest of the session's info.
119 119
120 120 Parameters
121 121 ----------
122 122 **kwargs : keyword argument
123 123 must be given one of the keywords and values from the session database
124 124 (i.e. session_id, name, path, kernel_id)
125 125
126 126 Returns
127 127 -------
128 128 model : dict
129 129 returns a dictionary that includes all the information from the
130 130 session described by the kwarg.
131 131 """
132 132 if not kwargs:
133 133 raise TypeError("must specify a column to query")
134 134
135 135 conditions = []
136 136 for column in kwargs.keys():
137 137 if column not in self._columns:
138 138 raise TypeError("No such column: %r", column)
139 139 conditions.append("%s=?" % column)
140 140
141 141 query = "SELECT * FROM session WHERE %s" % (' AND '.join(conditions))
142 142
143 143 self.cursor.execute(query, list(kwargs.values()))
144 144 try:
145 145 row = self.cursor.fetchone()
146 146 except KeyError:
147 147 # The kernel is missing, so the session just got deleted.
148 148 row = None
149 149
150 150 if row is None:
151 151 q = []
152 152 for key, value in kwargs.items():
153 153 q.append("%s=%r" % (key, value))
154 154
155 155 raise web.HTTPError(404, u'Session not found: %s' % (', '.join(q)))
156 156
157 157 return self.row_to_model(row)
158 158
159 159 def update_session(self, session_id, **kwargs):
160 160 """Updates the values in the session database.
161 161
162 162 Changes the values of the session with the given session_id
163 163 with the values from the keyword arguments.
164 164
165 165 Parameters
166 166 ----------
167 167 session_id : str
168 168 a uuid that identifies a session in the sqlite3 database
169 169 **kwargs : str
170 170 the key must correspond to a column title in session database,
171 171 and the value replaces the current value in the session
172 172 with session_id.
173 173 """
174 174 self.get_session(session_id=session_id)
175 175
176 176 if not kwargs:
177 177 # no changes
178 178 return
179 179
180 180 sets = []
181 181 for column in kwargs.keys():
182 182 if column not in self._columns:
183 183 raise TypeError("No such column: %r" % column)
184 184 sets.append("%s=?" % column)
185 185 query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
186 186 self.cursor.execute(query, list(kwargs.values()) + [session_id])
187 187
188 188 def row_to_model(self, row):
189 189 """Takes sqlite database session row and turns it into a dictionary"""
190 190 if row['kernel_id'] not in self.kernel_manager:
191 191 # The kernel was killed or died without deleting the session.
192 self.delete_session(row['session_id'])
192 # We can't use delete_session here because that tries to find
193 # and shut down the kernel.
194 self.cursor.execute("DELETE FROM session WHERE session_id=?",
195 (row['session_id'],))
193 196 raise KeyError
194 197
195 198 model = {
196 199 'id': row['session_id'],
197 200 'notebook': {
198 201 'name': row['name'],
199 202 'path': row['path']
200 203 },
201 204 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
202 205 }
203 206 return model
204 207
205 208 def list_sessions(self):
206 209 """Returns a list of dictionaries containing all the information from
207 210 the session database"""
208 211 c = self.cursor.execute("SELECT * FROM session")
209 212 result = []
210 for row in c:
213 # We need to use fetchall() here, because row_to_model can delete rows,
214 # which messes up the cursor if we're iterating over rows.
215 for row in c.fetchall():
211 216 try:
212 217 result.append(self.row_to_model(row))
213 218 except KeyError:
214 219 pass
215 220 return result
216 221
217 222 def delete_session(self, session_id):
218 223 """Deletes the row in the session database with given session_id"""
219 224 # Check that session exists before deleting
220 225 session = self.get_session(session_id=session_id)
221 226 self.kernel_manager.shutdown_kernel(session['kernel']['id'])
222 227 self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))
@@ -1,105 +1,140 b''
1 1 """Tests for the session manager."""
2 2
3 3 from unittest import TestCase
4 4
5 5 from tornado import web
6 6
7 7 from ..sessionmanager import SessionManager
8 8 from IPython.html.services.kernels.kernelmanager import MappingKernelManager
9 9
10 10 class DummyKernel(object):
11 11 def __init__(self, kernel_name='python'):
12 12 self.kernel_name = kernel_name
13 13
14 14 class DummyMKM(MappingKernelManager):
15 15 """MappingKernelManager interface that doesn't start kernels, for testing"""
16 16 def __init__(self, *args, **kwargs):
17 17 super(DummyMKM, self).__init__(*args, **kwargs)
18 18 self.id_letters = iter(u'ABCDEFGHIJK')
19 19
20 20 def _new_id(self):
21 21 return next(self.id_letters)
22 22
23 23 def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
24 24 kernel_id = kernel_id or self._new_id()
25 25 self._kernels[kernel_id] = DummyKernel(kernel_name=kernel_name)
26 26 return kernel_id
27 27
28 28 def shutdown_kernel(self, kernel_id, now=False):
29 29 del self._kernels[kernel_id]
30 30
31 31 class TestSessionManager(TestCase):
32 32
33 33 def test_get_session(self):
34 34 sm = SessionManager(kernel_manager=DummyMKM())
35 35 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
36 36 kernel_name='bar')['id']
37 37 model = sm.get_session(session_id=session_id)
38 38 expected = {'id':session_id,
39 39 'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'},
40 40 'kernel': {'id':u'A', 'name': 'bar'}}
41 41 self.assertEqual(model, expected)
42 42
43 43 def test_bad_get_session(self):
44 44 # Should raise error if a bad key is passed to the database.
45 45 sm = SessionManager(kernel_manager=DummyMKM())
46 46 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
47 47 kernel_name='foo')['id']
48 48 self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
49 49
50 def test_get_session_dead_kernel(self):
51 sm = SessionManager(kernel_manager=DummyMKM())
52 session = sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python')
53 # kill the kernel
54 sm.kernel_manager.shutdown_kernel(session['kernel']['id'])
55 with self.assertRaises(KeyError):
56 sm.get_session(session_id=session['id'])
57 # no sessions left
58 listed = sm.list_sessions()
59 self.assertEqual(listed, [])
60
50 61 def test_list_sessions(self):
51 62 sm = SessionManager(kernel_manager=DummyMKM())
52 63 sessions = [
53 64 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
54 65 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
55 66 sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
56 67 ]
57 68 sessions = sm.list_sessions()
58 69 expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
59 70 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
60 71 {'id':sessions[1]['id'], 'notebook': {'name':u'test2.ipynb',
61 72 'path': u'/path/to/2/'}, 'kernel':{'id':u'B', 'name':'python'}},
62 73 {'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
63 74 'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
64 75 self.assertEqual(sessions, expected)
65 76
77 def test_list_sessions_dead_kernel(self):
78 sm = SessionManager(kernel_manager=DummyMKM())
79 sessions = [
80 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
81 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
82 ]
83 # kill one of the kernels
84 sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
85 listed = sm.list_sessions()
86 expected = [
87 {
88 'id': sessions[1]['id'],
89 'notebook': {
90 'name': u'test2.ipynb',
91 'path': u'/path/to/2/',
92 },
93 'kernel': {
94 'id': u'B',
95 'name':'python',
96 }
97 }
98 ]
99 self.assertEqual(listed, expected)
100
66 101 def test_update_session(self):
67 102 sm = SessionManager(kernel_manager=DummyMKM())
68 103 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
69 104 kernel_name='julia')['id']
70 105 sm.update_session(session_id, name='new_name.ipynb')
71 106 model = sm.get_session(session_id=session_id)
72 107 expected = {'id':session_id,
73 108 'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'},
74 109 'kernel':{'id':u'A', 'name':'julia'}}
75 110 self.assertEqual(model, expected)
76 111
77 112 def test_bad_update_session(self):
78 113 # try to update a session with a bad keyword ~ raise error
79 114 sm = SessionManager(kernel_manager=DummyMKM())
80 115 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
81 116 kernel_name='ir')['id']
82 117 self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
83 118
84 119 def test_delete_session(self):
85 120 sm = SessionManager(kernel_manager=DummyMKM())
86 121 sessions = [
87 122 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
88 123 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
89 124 sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
90 125 ]
91 126 sm.delete_session(sessions[1]['id'])
92 127 new_sessions = sm.list_sessions()
93 128 expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
94 129 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
95 130 {'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
96 131 'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
97 132 self.assertEqual(new_sessions, expected)
98 133
99 134 def test_bad_delete_session(self):
100 135 # try to delete a session that doesn't exist ~ raise error
101 136 sm = SessionManager(kernel_manager=DummyMKM())
102 137 sm.create_session(name='test.ipynb', path='/path/to/', kernel_name='python')
103 138 self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
104 139 self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
105 140
General Comments 0
You need to be logged in to leave comments. Login now