##// END OF EJS Templates
Merge pull request #5827 from takluyver/kernelspec-rest-api...
Min RK -
r17022:410f2c0a merge
parent child Browse files
Show More
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,27 b''
1 from tornado import web
2 from ..base.handlers import IPythonHandler
3 from ..services.kernelspecs.handlers import kernel_name_regex
4
5 class KernelSpecResourceHandler(web.StaticFileHandler, IPythonHandler):
6 SUPPORTED_METHODS = ('GET', 'HEAD')
7
8 def initialize(self):
9 web.StaticFileHandler.initialize(self, path='')
10
11 @web.authenticated
12 def get(self, kernel_name, path, include_body=True):
13 ksm = self.kernel_spec_manager
14 try:
15 self.root = ksm.get_kernel_spec(kernel_name).resource_dir
16 except KeyError:
17 raise web.HTTPError(404, u'Kernel spec %s not found' % kernel_name)
18 self.log.debug("Serving kernel resource from: %s", self.root)
19 return web.StaticFileHandler.get(self, path, include_body=include_body)
20
21 @web.authenticated
22 def head(self, kernel_name, path):
23 self.get(kernel_name, path, include_body=False)
24
25 default_handlers = [
26 (r"/kernelspecs/%s/(?P<path>.*)" % kernel_name_regex, KernelSpecResourceHandler),
27 ] No newline at end of file
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,50 b''
1 """Tornado handlers for kernel specifications."""
2
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5 import json
6 from tornado import web
7
8 from ...base.handlers import IPythonHandler, json_errors
9
10
11 class MainKernelSpecHandler(IPythonHandler):
12 SUPPORTED_METHODS = ('GET',)
13
14 @web.authenticated
15 @json_errors
16 def get(self):
17 ksm = self.kernel_spec_manager
18 results = []
19 for kernel_name in ksm.find_kernel_specs():
20 d = ksm.get_kernel_spec(kernel_name).to_dict()
21 d['name'] = kernel_name
22 results.append(d)
23
24 self.set_header("Content-Type", 'application/json')
25 self.finish(json.dumps(results))
26
27
28 class KernelSpecHandler(IPythonHandler):
29 SUPPORTED_METHODS = ('GET',)
30
31 @web.authenticated
32 @json_errors
33 def get(self, kernel_name):
34 ksm = self.kernel_spec_manager
35 try:
36 kernelspec = ksm.get_kernel_spec(kernel_name)
37 except KeyError:
38 raise web.HTTPError(404, u'Kernel spec %s not found' % kernel_name)
39 self.set_header("Content-Type", 'application/json')
40 self.finish(kernelspec.to_json())
41
42
43 # URL to handler mappings
44
45 kernel_name_regex = r"(?P<kernel_name>\w+)"
46
47 default_handlers = [
48 (r"/api/kernelspecs", MainKernelSpecHandler),
49 (r"/api/kernelspecs/%s" % kernel_name_regex, KernelSpecHandler),
50 ]
@@ -0,0 +1,97 b''
1 # coding: utf-8
2 """Test the kernel specs webservice API."""
3
4 import errno
5 import io
6 import json
7 import os
8
9 pjoin = os.path.join
10
11 import requests
12
13 from IPython.html.utils import url_path_join
14 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
15
16 # Copied from IPython.kernel.tests.test_kernelspec so updating that doesn't
17 # break these tests
18 sample_kernel_json = {'argv':['cat', '{connection_file}'],
19 'display_name':'Test kernel',
20 'language':'bash',
21 }
22
23 some_resource = u"The very model of a modern major general"
24
25
26 class KernelSpecAPI(object):
27 """Wrapper for notebook API calls."""
28 def __init__(self, base_url):
29 self.base_url = base_url
30
31 def _req(self, verb, path, body=None):
32 response = requests.request(verb,
33 url_path_join(self.base_url, path),
34 data=body,
35 )
36 response.raise_for_status()
37 return response
38
39 def list(self):
40 return self._req('GET', 'api/kernelspecs')
41
42 def kernel_spec_info(self, name):
43 return self._req('GET', url_path_join('api/kernelspecs', name))
44
45 def kernel_resource(self, name, path):
46 return self._req('GET', url_path_join('kernelspecs', name, path))
47
48 class APITest(NotebookTestBase):
49 """Test the kernelspec web service API"""
50 def setUp(self):
51 ipydir = self.ipython_dir.name
52 sample_kernel_dir = pjoin(ipydir, 'kernels', 'sample')
53 try:
54 os.makedirs(sample_kernel_dir)
55 except OSError as e:
56 if e.errno != errno.EEXIST:
57 raise
58
59 with open(pjoin(sample_kernel_dir, 'kernel.json'), 'w') as f:
60 json.dump(sample_kernel_json, f)
61
62 with io.open(pjoin(sample_kernel_dir, 'resource.txt'), 'w',
63 encoding='utf-8') as f:
64 f.write(some_resource)
65
66 self.ks_api = KernelSpecAPI(self.base_url())
67
68 def test_list_kernelspecs(self):
69 specs = self.ks_api.list().json()
70 assert isinstance(specs, list)
71
72 # 2: the sample kernelspec created in setUp, and the native Python kernel
73 self.assertEqual(len(specs), 2)
74
75 def is_sample_kernelspec(s):
76 return s['name'] == 'sample' and s['display_name'] == 'Test kernel'
77
78 assert any(is_sample_kernelspec(s) for s in specs), specs
79
80 def test_get_kernelspec(self):
81 spec = self.ks_api.kernel_spec_info('Sample').json() # Case insensitive
82 self.assertEqual(spec['language'], 'bash')
83
84 def test_get_nonexistant_kernelspec(self):
85 with assert_http_error(404):
86 self.ks_api.kernel_spec_info('nonexistant')
87
88 def test_get_kernel_resource_file(self):
89 res = self.ks_api.kernel_resource('sAmple', 'resource.txt')
90 self.assertEqual(res.text, some_resource)
91
92 def test_get_nonexistant_resource(self):
93 with assert_http_error(404):
94 self.ks_api.kernel_resource('nonexistant', 'resource.txt')
95
96 with assert_http_error(404):
97 self.ks_api.kernel_resource('sample', 'nonexistant.txt')
@@ -1,386 +1,390 b''
1 """Base Tornado handlers for the notebook.
1 """Base Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19
19
20 import functools
20 import functools
21 import json
21 import json
22 import logging
22 import logging
23 import os
23 import os
24 import re
24 import re
25 import sys
25 import sys
26 import traceback
26 import traceback
27 try:
27 try:
28 # py3
28 # py3
29 from http.client import responses
29 from http.client import responses
30 except ImportError:
30 except ImportError:
31 from httplib import responses
31 from httplib import responses
32
32
33 from jinja2 import TemplateNotFound
33 from jinja2 import TemplateNotFound
34 from tornado import web
34 from tornado import web
35
35
36 try:
36 try:
37 from tornado.log import app_log
37 from tornado.log import app_log
38 except ImportError:
38 except ImportError:
39 app_log = logging.getLogger()
39 app_log = logging.getLogger()
40
40
41 from IPython.config import Application
41 from IPython.config import Application
42 from IPython.utils.path import filefind
42 from IPython.utils.path import filefind
43 from IPython.utils.py3compat import string_types
43 from IPython.utils.py3compat import string_types
44 from IPython.html.utils import is_hidden
44 from IPython.html.utils import is_hidden
45
45
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47 # Top-level handlers
47 # Top-level handlers
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 non_alphanum = re.compile(r'[^A-Za-z0-9]')
49 non_alphanum = re.compile(r'[^A-Za-z0-9]')
50
50
51 class AuthenticatedHandler(web.RequestHandler):
51 class AuthenticatedHandler(web.RequestHandler):
52 """A RequestHandler with an authenticated user."""
52 """A RequestHandler with an authenticated user."""
53
53
54 def set_default_headers(self):
54 def set_default_headers(self):
55 headers = self.settings.get('headers', {})
55 headers = self.settings.get('headers', {})
56 for header_name,value in headers.items() :
56 for header_name,value in headers.items() :
57 try:
57 try:
58 self.set_header(header_name, value)
58 self.set_header(header_name, value)
59 except Exception:
59 except Exception:
60 # tornado raise Exception (not a subclass)
60 # tornado raise Exception (not a subclass)
61 # if method is unsupported (websocket and Access-Control-Allow-Origin
61 # if method is unsupported (websocket and Access-Control-Allow-Origin
62 # for example, so just ignore)
62 # for example, so just ignore)
63 pass
63 pass
64
64
65 def clear_login_cookie(self):
65 def clear_login_cookie(self):
66 self.clear_cookie(self.cookie_name)
66 self.clear_cookie(self.cookie_name)
67
67
68 def get_current_user(self):
68 def get_current_user(self):
69 user_id = self.get_secure_cookie(self.cookie_name)
69 user_id = self.get_secure_cookie(self.cookie_name)
70 # For now the user_id should not return empty, but it could eventually
70 # For now the user_id should not return empty, but it could eventually
71 if user_id == '':
71 if user_id == '':
72 user_id = 'anonymous'
72 user_id = 'anonymous'
73 if user_id is None:
73 if user_id is None:
74 # prevent extra Invalid cookie sig warnings:
74 # prevent extra Invalid cookie sig warnings:
75 self.clear_login_cookie()
75 self.clear_login_cookie()
76 if not self.login_available:
76 if not self.login_available:
77 user_id = 'anonymous'
77 user_id = 'anonymous'
78 return user_id
78 return user_id
79
79
80 @property
80 @property
81 def cookie_name(self):
81 def cookie_name(self):
82 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
82 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
83 self.request.host
83 self.request.host
84 ))
84 ))
85 return self.settings.get('cookie_name', default_cookie_name)
85 return self.settings.get('cookie_name', default_cookie_name)
86
86
87 @property
87 @property
88 def password(self):
88 def password(self):
89 """our password"""
89 """our password"""
90 return self.settings.get('password', '')
90 return self.settings.get('password', '')
91
91
92 @property
92 @property
93 def logged_in(self):
93 def logged_in(self):
94 """Is a user currently logged in?
94 """Is a user currently logged in?
95
95
96 """
96 """
97 user = self.get_current_user()
97 user = self.get_current_user()
98 return (user and not user == 'anonymous')
98 return (user and not user == 'anonymous')
99
99
100 @property
100 @property
101 def login_available(self):
101 def login_available(self):
102 """May a user proceed to log in?
102 """May a user proceed to log in?
103
103
104 This returns True if login capability is available, irrespective of
104 This returns True if login capability is available, irrespective of
105 whether the user is already logged in or not.
105 whether the user is already logged in or not.
106
106
107 """
107 """
108 return bool(self.settings.get('password', ''))
108 return bool(self.settings.get('password', ''))
109
109
110
110
111 class IPythonHandler(AuthenticatedHandler):
111 class IPythonHandler(AuthenticatedHandler):
112 """IPython-specific extensions to authenticated handling
112 """IPython-specific extensions to authenticated handling
113
113
114 Mostly property shortcuts to IPython-specific settings.
114 Mostly property shortcuts to IPython-specific settings.
115 """
115 """
116
116
117 @property
117 @property
118 def config(self):
118 def config(self):
119 return self.settings.get('config', None)
119 return self.settings.get('config', None)
120
120
121 @property
121 @property
122 def log(self):
122 def log(self):
123 """use the IPython log by default, falling back on tornado's logger"""
123 """use the IPython log by default, falling back on tornado's logger"""
124 if Application.initialized():
124 if Application.initialized():
125 return Application.instance().log
125 return Application.instance().log
126 else:
126 else:
127 return app_log
127 return app_log
128
128
129 #---------------------------------------------------------------
129 #---------------------------------------------------------------
130 # URLs
130 # URLs
131 #---------------------------------------------------------------
131 #---------------------------------------------------------------
132
132
133 @property
133 @property
134 def mathjax_url(self):
134 def mathjax_url(self):
135 return self.settings.get('mathjax_url', '')
135 return self.settings.get('mathjax_url', '')
136
136
137 @property
137 @property
138 def base_url(self):
138 def base_url(self):
139 return self.settings.get('base_url', '/')
139 return self.settings.get('base_url', '/')
140
140
141 #---------------------------------------------------------------
141 #---------------------------------------------------------------
142 # Manager objects
142 # Manager objects
143 #---------------------------------------------------------------
143 #---------------------------------------------------------------
144
144
145 @property
145 @property
146 def kernel_manager(self):
146 def kernel_manager(self):
147 return self.settings['kernel_manager']
147 return self.settings['kernel_manager']
148
148
149 @property
149 @property
150 def notebook_manager(self):
150 def notebook_manager(self):
151 return self.settings['notebook_manager']
151 return self.settings['notebook_manager']
152
152
153 @property
153 @property
154 def cluster_manager(self):
154 def cluster_manager(self):
155 return self.settings['cluster_manager']
155 return self.settings['cluster_manager']
156
156
157 @property
157 @property
158 def session_manager(self):
158 def session_manager(self):
159 return self.settings['session_manager']
159 return self.settings['session_manager']
160
160
161 @property
161 @property
162 def kernel_spec_manager(self):
163 return self.settings['kernel_spec_manager']
164
165 @property
162 def project_dir(self):
166 def project_dir(self):
163 return self.notebook_manager.notebook_dir
167 return self.notebook_manager.notebook_dir
164
168
165 #---------------------------------------------------------------
169 #---------------------------------------------------------------
166 # template rendering
170 # template rendering
167 #---------------------------------------------------------------
171 #---------------------------------------------------------------
168
172
169 def get_template(self, name):
173 def get_template(self, name):
170 """Return the jinja template object for a given name"""
174 """Return the jinja template object for a given name"""
171 return self.settings['jinja2_env'].get_template(name)
175 return self.settings['jinja2_env'].get_template(name)
172
176
173 def render_template(self, name, **ns):
177 def render_template(self, name, **ns):
174 ns.update(self.template_namespace)
178 ns.update(self.template_namespace)
175 template = self.get_template(name)
179 template = self.get_template(name)
176 return template.render(**ns)
180 return template.render(**ns)
177
181
178 @property
182 @property
179 def template_namespace(self):
183 def template_namespace(self):
180 return dict(
184 return dict(
181 base_url=self.base_url,
185 base_url=self.base_url,
182 logged_in=self.logged_in,
186 logged_in=self.logged_in,
183 login_available=self.login_available,
187 login_available=self.login_available,
184 static_url=self.static_url,
188 static_url=self.static_url,
185 )
189 )
186
190
187 def get_json_body(self):
191 def get_json_body(self):
188 """Return the body of the request as JSON data."""
192 """Return the body of the request as JSON data."""
189 if not self.request.body:
193 if not self.request.body:
190 return None
194 return None
191 # Do we need to call body.decode('utf-8') here?
195 # Do we need to call body.decode('utf-8') here?
192 body = self.request.body.strip().decode(u'utf-8')
196 body = self.request.body.strip().decode(u'utf-8')
193 try:
197 try:
194 model = json.loads(body)
198 model = json.loads(body)
195 except Exception:
199 except Exception:
196 self.log.debug("Bad JSON: %r", body)
200 self.log.debug("Bad JSON: %r", body)
197 self.log.error("Couldn't parse JSON", exc_info=True)
201 self.log.error("Couldn't parse JSON", exc_info=True)
198 raise web.HTTPError(400, u'Invalid JSON in body of request')
202 raise web.HTTPError(400, u'Invalid JSON in body of request')
199 return model
203 return model
200
204
201 def get_error_html(self, status_code, **kwargs):
205 def get_error_html(self, status_code, **kwargs):
202 """render custom error pages"""
206 """render custom error pages"""
203 exception = kwargs.get('exception')
207 exception = kwargs.get('exception')
204 message = ''
208 message = ''
205 status_message = responses.get(status_code, 'Unknown HTTP Error')
209 status_message = responses.get(status_code, 'Unknown HTTP Error')
206 if exception:
210 if exception:
207 # get the custom message, if defined
211 # get the custom message, if defined
208 try:
212 try:
209 message = exception.log_message % exception.args
213 message = exception.log_message % exception.args
210 except Exception:
214 except Exception:
211 pass
215 pass
212
216
213 # construct the custom reason, if defined
217 # construct the custom reason, if defined
214 reason = getattr(exception, 'reason', '')
218 reason = getattr(exception, 'reason', '')
215 if reason:
219 if reason:
216 status_message = reason
220 status_message = reason
217
221
218 # build template namespace
222 # build template namespace
219 ns = dict(
223 ns = dict(
220 status_code=status_code,
224 status_code=status_code,
221 status_message=status_message,
225 status_message=status_message,
222 message=message,
226 message=message,
223 exception=exception,
227 exception=exception,
224 )
228 )
225
229
226 # render the template
230 # render the template
227 try:
231 try:
228 html = self.render_template('%s.html' % status_code, **ns)
232 html = self.render_template('%s.html' % status_code, **ns)
229 except TemplateNotFound:
233 except TemplateNotFound:
230 self.log.debug("No template for %d", status_code)
234 self.log.debug("No template for %d", status_code)
231 html = self.render_template('error.html', **ns)
235 html = self.render_template('error.html', **ns)
232 return html
236 return html
233
237
234
238
235 class Template404(IPythonHandler):
239 class Template404(IPythonHandler):
236 """Render our 404 template"""
240 """Render our 404 template"""
237 def prepare(self):
241 def prepare(self):
238 raise web.HTTPError(404)
242 raise web.HTTPError(404)
239
243
240
244
241 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
245 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
242 """static files should only be accessible when logged in"""
246 """static files should only be accessible when logged in"""
243
247
244 @web.authenticated
248 @web.authenticated
245 def get(self, path):
249 def get(self, path):
246 if os.path.splitext(path)[1] == '.ipynb':
250 if os.path.splitext(path)[1] == '.ipynb':
247 name = os.path.basename(path)
251 name = os.path.basename(path)
248 self.set_header('Content-Type', 'application/json')
252 self.set_header('Content-Type', 'application/json')
249 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
253 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
250
254
251 return web.StaticFileHandler.get(self, path)
255 return web.StaticFileHandler.get(self, path)
252
256
253 def compute_etag(self):
257 def compute_etag(self):
254 return None
258 return None
255
259
256 def validate_absolute_path(self, root, absolute_path):
260 def validate_absolute_path(self, root, absolute_path):
257 """Validate and return the absolute path.
261 """Validate and return the absolute path.
258
262
259 Requires tornado 3.1
263 Requires tornado 3.1
260
264
261 Adding to tornado's own handling, forbids the serving of hidden files.
265 Adding to tornado's own handling, forbids the serving of hidden files.
262 """
266 """
263 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
267 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
264 abs_root = os.path.abspath(root)
268 abs_root = os.path.abspath(root)
265 if is_hidden(abs_path, abs_root):
269 if is_hidden(abs_path, abs_root):
266 self.log.info("Refusing to serve hidden file, via 404 Error")
270 self.log.info("Refusing to serve hidden file, via 404 Error")
267 raise web.HTTPError(404)
271 raise web.HTTPError(404)
268 return abs_path
272 return abs_path
269
273
270
274
271 def json_errors(method):
275 def json_errors(method):
272 """Decorate methods with this to return GitHub style JSON errors.
276 """Decorate methods with this to return GitHub style JSON errors.
273
277
274 This should be used on any JSON API on any handler method that can raise HTTPErrors.
278 This should be used on any JSON API on any handler method that can raise HTTPErrors.
275
279
276 This will grab the latest HTTPError exception using sys.exc_info
280 This will grab the latest HTTPError exception using sys.exc_info
277 and then:
281 and then:
278
282
279 1. Set the HTTP status code based on the HTTPError
283 1. Set the HTTP status code based on the HTTPError
280 2. Create and return a JSON body with a message field describing
284 2. Create and return a JSON body with a message field describing
281 the error in a human readable form.
285 the error in a human readable form.
282 """
286 """
283 @functools.wraps(method)
287 @functools.wraps(method)
284 def wrapper(self, *args, **kwargs):
288 def wrapper(self, *args, **kwargs):
285 try:
289 try:
286 result = method(self, *args, **kwargs)
290 result = method(self, *args, **kwargs)
287 except web.HTTPError as e:
291 except web.HTTPError as e:
288 status = e.status_code
292 status = e.status_code
289 message = e.log_message
293 message = e.log_message
290 self.log.warn(message)
294 self.log.warn(message)
291 self.set_status(e.status_code)
295 self.set_status(e.status_code)
292 self.finish(json.dumps(dict(message=message)))
296 self.finish(json.dumps(dict(message=message)))
293 except Exception:
297 except Exception:
294 self.log.error("Unhandled error in API request", exc_info=True)
298 self.log.error("Unhandled error in API request", exc_info=True)
295 status = 500
299 status = 500
296 message = "Unknown server error"
300 message = "Unknown server error"
297 t, value, tb = sys.exc_info()
301 t, value, tb = sys.exc_info()
298 self.set_status(status)
302 self.set_status(status)
299 tb_text = ''.join(traceback.format_exception(t, value, tb))
303 tb_text = ''.join(traceback.format_exception(t, value, tb))
300 reply = dict(message=message, traceback=tb_text)
304 reply = dict(message=message, traceback=tb_text)
301 self.finish(json.dumps(reply))
305 self.finish(json.dumps(reply))
302 else:
306 else:
303 return result
307 return result
304 return wrapper
308 return wrapper
305
309
306
310
307
311
308 #-----------------------------------------------------------------------------
312 #-----------------------------------------------------------------------------
309 # File handler
313 # File handler
310 #-----------------------------------------------------------------------------
314 #-----------------------------------------------------------------------------
311
315
312 # to minimize subclass changes:
316 # to minimize subclass changes:
313 HTTPError = web.HTTPError
317 HTTPError = web.HTTPError
314
318
315 class FileFindHandler(web.StaticFileHandler):
319 class FileFindHandler(web.StaticFileHandler):
316 """subclass of StaticFileHandler for serving files from a search path"""
320 """subclass of StaticFileHandler for serving files from a search path"""
317
321
318 # cache search results, don't search for files more than once
322 # cache search results, don't search for files more than once
319 _static_paths = {}
323 _static_paths = {}
320
324
321 def initialize(self, path, default_filename=None):
325 def initialize(self, path, default_filename=None):
322 if isinstance(path, string_types):
326 if isinstance(path, string_types):
323 path = [path]
327 path = [path]
324
328
325 self.root = tuple(
329 self.root = tuple(
326 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
330 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
327 )
331 )
328 self.default_filename = default_filename
332 self.default_filename = default_filename
329
333
330 def compute_etag(self):
334 def compute_etag(self):
331 return None
335 return None
332
336
333 @classmethod
337 @classmethod
334 def get_absolute_path(cls, roots, path):
338 def get_absolute_path(cls, roots, path):
335 """locate a file to serve on our static file search path"""
339 """locate a file to serve on our static file search path"""
336 with cls._lock:
340 with cls._lock:
337 if path in cls._static_paths:
341 if path in cls._static_paths:
338 return cls._static_paths[path]
342 return cls._static_paths[path]
339 try:
343 try:
340 abspath = os.path.abspath(filefind(path, roots))
344 abspath = os.path.abspath(filefind(path, roots))
341 except IOError:
345 except IOError:
342 # IOError means not found
346 # IOError means not found
343 return ''
347 return ''
344
348
345 cls._static_paths[path] = abspath
349 cls._static_paths[path] = abspath
346 return abspath
350 return abspath
347
351
348 def validate_absolute_path(self, root, absolute_path):
352 def validate_absolute_path(self, root, absolute_path):
349 """check if the file should be served (raises 404, 403, etc.)"""
353 """check if the file should be served (raises 404, 403, etc.)"""
350 if absolute_path == '':
354 if absolute_path == '':
351 raise web.HTTPError(404)
355 raise web.HTTPError(404)
352
356
353 for root in self.root:
357 for root in self.root:
354 if (absolute_path + os.sep).startswith(root):
358 if (absolute_path + os.sep).startswith(root):
355 break
359 break
356
360
357 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
361 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
358
362
359
363
360 class TrailingSlashHandler(web.RequestHandler):
364 class TrailingSlashHandler(web.RequestHandler):
361 """Simple redirect handler that strips trailing slashes
365 """Simple redirect handler that strips trailing slashes
362
366
363 This should be the first, highest priority handler.
367 This should be the first, highest priority handler.
364 """
368 """
365
369
366 SUPPORTED_METHODS = ['GET']
370 SUPPORTED_METHODS = ['GET']
367
371
368 def get(self):
372 def get(self):
369 self.redirect(self.request.uri.rstrip('/'))
373 self.redirect(self.request.uri.rstrip('/'))
370
374
371 #-----------------------------------------------------------------------------
375 #-----------------------------------------------------------------------------
372 # URL pattern fragments for re-use
376 # URL pattern fragments for re-use
373 #-----------------------------------------------------------------------------
377 #-----------------------------------------------------------------------------
374
378
375 path_regex = r"(?P<path>(?:/.*)*)"
379 path_regex = r"(?P<path>(?:/.*)*)"
376 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
380 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
377 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
381 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
378
382
379 #-----------------------------------------------------------------------------
383 #-----------------------------------------------------------------------------
380 # URL to handler mappings
384 # URL to handler mappings
381 #-----------------------------------------------------------------------------
385 #-----------------------------------------------------------------------------
382
386
383
387
384 default_handlers = [
388 default_handlers = [
385 (r".*/", TrailingSlashHandler)
389 (r".*/", TrailingSlashHandler)
386 ]
390 ]
@@ -1,862 +1,873 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server."""
2 """A tornado based IPython notebook server."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import errno
9 import errno
10 import io
10 import io
11 import json
11 import json
12 import logging
12 import logging
13 import os
13 import os
14 import random
14 import random
15 import select
15 import select
16 import signal
16 import signal
17 import socket
17 import socket
18 import sys
18 import sys
19 import threading
19 import threading
20 import time
20 import time
21 import webbrowser
21 import webbrowser
22
22
23
23
24 # check for pyzmq 2.1.11
24 # check for pyzmq 2.1.11
25 from IPython.utils.zmqrelated import check_for_zmq
25 from IPython.utils.zmqrelated import check_for_zmq
26 check_for_zmq('2.1.11', 'IPython.html')
26 check_for_zmq('2.1.11', 'IPython.html')
27
27
28 from jinja2 import Environment, FileSystemLoader
28 from jinja2 import Environment, FileSystemLoader
29
29
30 # Install the pyzmq ioloop. This has to be done before anything else from
30 # Install the pyzmq ioloop. This has to be done before anything else from
31 # tornado is imported.
31 # tornado is imported.
32 from zmq.eventloop import ioloop
32 from zmq.eventloop import ioloop
33 ioloop.install()
33 ioloop.install()
34
34
35 # check for tornado 3.1.0
35 # check for tornado 3.1.0
36 msg = "The IPython Notebook requires tornado >= 3.1.0"
36 msg = "The IPython Notebook requires tornado >= 3.1.0"
37 try:
37 try:
38 import tornado
38 import tornado
39 except ImportError:
39 except ImportError:
40 raise ImportError(msg)
40 raise ImportError(msg)
41 try:
41 try:
42 version_info = tornado.version_info
42 version_info = tornado.version_info
43 except AttributeError:
43 except AttributeError:
44 raise ImportError(msg + ", but you have < 1.1.0")
44 raise ImportError(msg + ", but you have < 1.1.0")
45 if version_info < (3,1,0):
45 if version_info < (3,1,0):
46 raise ImportError(msg + ", but you have %s" % tornado.version)
46 raise ImportError(msg + ", but you have %s" % tornado.version)
47
47
48 from tornado import httpserver
48 from tornado import httpserver
49 from tornado import web
49 from tornado import web
50 from tornado.log import LogFormatter
50 from tornado.log import LogFormatter
51
51
52 from IPython.html import DEFAULT_STATIC_FILES_PATH
52 from IPython.html import DEFAULT_STATIC_FILES_PATH
53 from .base.handlers import Template404
53 from .base.handlers import Template404
54 from .log import log_request
54 from .log import log_request
55 from .services.kernels.kernelmanager import MappingKernelManager
55 from .services.kernels.kernelmanager import MappingKernelManager
56 from .services.notebooks.nbmanager import NotebookManager
56 from .services.notebooks.nbmanager import NotebookManager
57 from .services.notebooks.filenbmanager import FileNotebookManager
57 from .services.notebooks.filenbmanager import FileNotebookManager
58 from .services.clusters.clustermanager import ClusterManager
58 from .services.clusters.clustermanager import ClusterManager
59 from .services.sessions.sessionmanager import SessionManager
59 from .services.sessions.sessionmanager import SessionManager
60
60
61 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
61 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
62
62
63 from IPython.config import Config
63 from IPython.config import Config
64 from IPython.config.application import catch_config_error, boolean_flag
64 from IPython.config.application import catch_config_error, boolean_flag
65 from IPython.core.application import (
65 from IPython.core.application import (
66 BaseIPythonApplication, base_flags, base_aliases,
66 BaseIPythonApplication, base_flags, base_aliases,
67 )
67 )
68 from IPython.core.profiledir import ProfileDir
68 from IPython.core.profiledir import ProfileDir
69 from IPython.kernel import KernelManager
69 from IPython.kernel import KernelManager
70 from IPython.kernel.kernelspec import KernelSpecManager
70 from IPython.kernel.zmq.session import default_secure, Session
71 from IPython.kernel.zmq.session import default_secure, Session
71 from IPython.nbformat.sign import NotebookNotary
72 from IPython.nbformat.sign import NotebookNotary
72 from IPython.utils.importstring import import_item
73 from IPython.utils.importstring import import_item
73 from IPython.utils import submodule
74 from IPython.utils import submodule
74 from IPython.utils.traitlets import (
75 from IPython.utils.traitlets import (
75 Dict, Unicode, Integer, List, Bool, Bytes,
76 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
76 DottedObjectName, TraitError,
77 DottedObjectName, TraitError,
77 )
78 )
78 from IPython.utils import py3compat
79 from IPython.utils import py3compat
79 from IPython.utils.path import filefind, get_ipython_dir
80 from IPython.utils.path import filefind, get_ipython_dir
80
81
81 from .utils import url_path_join
82 from .utils import url_path_join
82
83
83 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
84 # Module globals
85 # Module globals
85 #-----------------------------------------------------------------------------
86 #-----------------------------------------------------------------------------
86
87
87 _examples = """
88 _examples = """
88 ipython notebook # start the notebook
89 ipython notebook # start the notebook
89 ipython notebook --profile=sympy # use the sympy profile
90 ipython notebook --profile=sympy # use the sympy profile
90 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
91 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
91 """
92 """
92
93
93 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
94 # Helper functions
95 # Helper functions
95 #-----------------------------------------------------------------------------
96 #-----------------------------------------------------------------------------
96
97
97 def random_ports(port, n):
98 def random_ports(port, n):
98 """Generate a list of n random ports near the given port.
99 """Generate a list of n random ports near the given port.
99
100
100 The first 5 ports will be sequential, and the remaining n-5 will be
101 The first 5 ports will be sequential, and the remaining n-5 will be
101 randomly selected in the range [port-2*n, port+2*n].
102 randomly selected in the range [port-2*n, port+2*n].
102 """
103 """
103 for i in range(min(5, n)):
104 for i in range(min(5, n)):
104 yield port + i
105 yield port + i
105 for i in range(n-5):
106 for i in range(n-5):
106 yield max(1, port + random.randint(-2*n, 2*n))
107 yield max(1, port + random.randint(-2*n, 2*n))
107
108
108 def load_handlers(name):
109 def load_handlers(name):
109 """Load the (URL pattern, handler) tuples for each component."""
110 """Load the (URL pattern, handler) tuples for each component."""
110 name = 'IPython.html.' + name
111 name = 'IPython.html.' + name
111 mod = __import__(name, fromlist=['default_handlers'])
112 mod = __import__(name, fromlist=['default_handlers'])
112 return mod.default_handlers
113 return mod.default_handlers
113
114
114 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
115 # The Tornado web application
116 # The Tornado web application
116 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
117
118
118 class NotebookWebApplication(web.Application):
119 class NotebookWebApplication(web.Application):
119
120
120 def __init__(self, ipython_app, kernel_manager, notebook_manager,
121 def __init__(self, ipython_app, kernel_manager, notebook_manager,
121 cluster_manager, session_manager, log, base_url,
122 cluster_manager, session_manager, kernel_spec_manager, log,
122 settings_overrides, jinja_env_options):
123 base_url, settings_overrides, jinja_env_options):
123
124
124 settings = self.init_settings(
125 settings = self.init_settings(
125 ipython_app, kernel_manager, notebook_manager, cluster_manager,
126 ipython_app, kernel_manager, notebook_manager, cluster_manager,
126 session_manager, log, base_url, settings_overrides, jinja_env_options)
127 session_manager, kernel_spec_manager, log, base_url,
128 settings_overrides, jinja_env_options)
127 handlers = self.init_handlers(settings)
129 handlers = self.init_handlers(settings)
128
130
129 super(NotebookWebApplication, self).__init__(handlers, **settings)
131 super(NotebookWebApplication, self).__init__(handlers, **settings)
130
132
131 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
133 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
132 cluster_manager, session_manager, log, base_url,
134 cluster_manager, session_manager, kernel_spec_manager,
133 settings_overrides, jinja_env_options=None):
135 log, base_url, settings_overrides,
136 jinja_env_options=None):
134 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
137 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
135 # base_url will always be unicode, which will in turn
138 # base_url will always be unicode, which will in turn
136 # make the patterns unicode, and ultimately result in unicode
139 # make the patterns unicode, and ultimately result in unicode
137 # keys in kwargs to handler._execute(**kwargs) in tornado.
140 # keys in kwargs to handler._execute(**kwargs) in tornado.
138 # This enforces that base_url be ascii in that situation.
141 # This enforces that base_url be ascii in that situation.
139 #
142 #
140 # Note that the URLs these patterns check against are escaped,
143 # Note that the URLs these patterns check against are escaped,
141 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
144 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
142 base_url = py3compat.unicode_to_str(base_url, 'ascii')
145 base_url = py3compat.unicode_to_str(base_url, 'ascii')
143 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
146 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
144 jenv_opt = jinja_env_options if jinja_env_options else {}
147 jenv_opt = jinja_env_options if jinja_env_options else {}
145 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
148 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
146 settings = dict(
149 settings = dict(
147 # basics
150 # basics
148 log_function=log_request,
151 log_function=log_request,
149 base_url=base_url,
152 base_url=base_url,
150 template_path=template_path,
153 template_path=template_path,
151 static_path=ipython_app.static_file_path,
154 static_path=ipython_app.static_file_path,
152 static_handler_class = FileFindHandler,
155 static_handler_class = FileFindHandler,
153 static_url_prefix = url_path_join(base_url,'/static/'),
156 static_url_prefix = url_path_join(base_url,'/static/'),
154
157
155 # authentication
158 # authentication
156 cookie_secret=ipython_app.cookie_secret,
159 cookie_secret=ipython_app.cookie_secret,
157 login_url=url_path_join(base_url,'/login'),
160 login_url=url_path_join(base_url,'/login'),
158 password=ipython_app.password,
161 password=ipython_app.password,
159
162
160 # managers
163 # managers
161 kernel_manager=kernel_manager,
164 kernel_manager=kernel_manager,
162 notebook_manager=notebook_manager,
165 notebook_manager=notebook_manager,
163 cluster_manager=cluster_manager,
166 cluster_manager=cluster_manager,
164 session_manager=session_manager,
167 session_manager=session_manager,
168 kernel_spec_manager=kernel_spec_manager,
165
169
166 # IPython stuff
170 # IPython stuff
167 nbextensions_path = ipython_app.nbextensions_path,
171 nbextensions_path = ipython_app.nbextensions_path,
168 mathjax_url=ipython_app.mathjax_url,
172 mathjax_url=ipython_app.mathjax_url,
169 config=ipython_app.config,
173 config=ipython_app.config,
170 jinja2_env=env,
174 jinja2_env=env,
171 )
175 )
172
176
173 # allow custom overrides for the tornado web app.
177 # allow custom overrides for the tornado web app.
174 settings.update(settings_overrides)
178 settings.update(settings_overrides)
175 return settings
179 return settings
176
180
177 def init_handlers(self, settings):
181 def init_handlers(self, settings):
178 # Load the (URL pattern, handler) tuples for each component.
182 # Load the (URL pattern, handler) tuples for each component.
179 handlers = []
183 handlers = []
180 handlers.extend(load_handlers('base.handlers'))
184 handlers.extend(load_handlers('base.handlers'))
181 handlers.extend(load_handlers('tree.handlers'))
185 handlers.extend(load_handlers('tree.handlers'))
182 handlers.extend(load_handlers('auth.login'))
186 handlers.extend(load_handlers('auth.login'))
183 handlers.extend(load_handlers('auth.logout'))
187 handlers.extend(load_handlers('auth.logout'))
184 handlers.extend(load_handlers('notebook.handlers'))
188 handlers.extend(load_handlers('notebook.handlers'))
185 handlers.extend(load_handlers('nbconvert.handlers'))
189 handlers.extend(load_handlers('nbconvert.handlers'))
190 handlers.extend(load_handlers('kernelspecs.handlers'))
186 handlers.extend(load_handlers('services.kernels.handlers'))
191 handlers.extend(load_handlers('services.kernels.handlers'))
187 handlers.extend(load_handlers('services.notebooks.handlers'))
192 handlers.extend(load_handlers('services.notebooks.handlers'))
188 handlers.extend(load_handlers('services.clusters.handlers'))
193 handlers.extend(load_handlers('services.clusters.handlers'))
189 handlers.extend(load_handlers('services.sessions.handlers'))
194 handlers.extend(load_handlers('services.sessions.handlers'))
190 handlers.extend(load_handlers('services.nbconvert.handlers'))
195 handlers.extend(load_handlers('services.nbconvert.handlers'))
196 handlers.extend(load_handlers('services.kernelspecs.handlers'))
191 # FIXME: /files/ should be handled by the Contents service when it exists
197 # FIXME: /files/ should be handled by the Contents service when it exists
192 nbm = settings['notebook_manager']
198 nbm = settings['notebook_manager']
193 if hasattr(nbm, 'notebook_dir'):
199 if hasattr(nbm, 'notebook_dir'):
194 handlers.extend([
200 handlers.extend([
195 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
201 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
196 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
202 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
197 ])
203 ])
198 # prepend base_url onto the patterns that we match
204 # prepend base_url onto the patterns that we match
199 new_handlers = []
205 new_handlers = []
200 for handler in handlers:
206 for handler in handlers:
201 pattern = url_path_join(settings['base_url'], handler[0])
207 pattern = url_path_join(settings['base_url'], handler[0])
202 new_handler = tuple([pattern] + list(handler[1:]))
208 new_handler = tuple([pattern] + list(handler[1:]))
203 new_handlers.append(new_handler)
209 new_handlers.append(new_handler)
204 # add 404 on the end, which will catch everything that falls through
210 # add 404 on the end, which will catch everything that falls through
205 new_handlers.append((r'(.*)', Template404))
211 new_handlers.append((r'(.*)', Template404))
206 return new_handlers
212 return new_handlers
207
213
208
214
209 class NbserverListApp(BaseIPythonApplication):
215 class NbserverListApp(BaseIPythonApplication):
210
216
211 description="List currently running notebook servers in this profile."
217 description="List currently running notebook servers in this profile."
212
218
213 flags = dict(
219 flags = dict(
214 json=({'NbserverListApp': {'json': True}},
220 json=({'NbserverListApp': {'json': True}},
215 "Produce machine-readable JSON output."),
221 "Produce machine-readable JSON output."),
216 )
222 )
217
223
218 json = Bool(False, config=True,
224 json = Bool(False, config=True,
219 help="If True, each line of output will be a JSON object with the "
225 help="If True, each line of output will be a JSON object with the "
220 "details from the server info file.")
226 "details from the server info file.")
221
227
222 def start(self):
228 def start(self):
223 if not self.json:
229 if not self.json:
224 print("Currently running servers:")
230 print("Currently running servers:")
225 for serverinfo in list_running_servers(self.profile):
231 for serverinfo in list_running_servers(self.profile):
226 if self.json:
232 if self.json:
227 print(json.dumps(serverinfo))
233 print(json.dumps(serverinfo))
228 else:
234 else:
229 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
235 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
230
236
231 #-----------------------------------------------------------------------------
237 #-----------------------------------------------------------------------------
232 # Aliases and Flags
238 # Aliases and Flags
233 #-----------------------------------------------------------------------------
239 #-----------------------------------------------------------------------------
234
240
235 flags = dict(base_flags)
241 flags = dict(base_flags)
236 flags['no-browser']=(
242 flags['no-browser']=(
237 {'NotebookApp' : {'open_browser' : False}},
243 {'NotebookApp' : {'open_browser' : False}},
238 "Don't open the notebook in a browser after startup."
244 "Don't open the notebook in a browser after startup."
239 )
245 )
240 flags['pylab']=(
246 flags['pylab']=(
241 {'NotebookApp' : {'pylab' : 'warn'}},
247 {'NotebookApp' : {'pylab' : 'warn'}},
242 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
248 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
243 )
249 )
244 flags['no-mathjax']=(
250 flags['no-mathjax']=(
245 {'NotebookApp' : {'enable_mathjax' : False}},
251 {'NotebookApp' : {'enable_mathjax' : False}},
246 """Disable MathJax
252 """Disable MathJax
247
253
248 MathJax is the javascript library IPython uses to render math/LaTeX. It is
254 MathJax is the javascript library IPython uses to render math/LaTeX. It is
249 very large, so you may want to disable it if you have a slow internet
255 very large, so you may want to disable it if you have a slow internet
250 connection, or for offline use of the notebook.
256 connection, or for offline use of the notebook.
251
257
252 When disabled, equations etc. will appear as their untransformed TeX source.
258 When disabled, equations etc. will appear as their untransformed TeX source.
253 """
259 """
254 )
260 )
255
261
256 # Add notebook manager flags
262 # Add notebook manager flags
257 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
263 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
258 'Auto-save a .py script everytime the .ipynb notebook is saved',
264 'Auto-save a .py script everytime the .ipynb notebook is saved',
259 'Do not auto-save .py scripts for every notebook'))
265 'Do not auto-save .py scripts for every notebook'))
260
266
261 aliases = dict(base_aliases)
267 aliases = dict(base_aliases)
262
268
263 aliases.update({
269 aliases.update({
264 'ip': 'NotebookApp.ip',
270 'ip': 'NotebookApp.ip',
265 'port': 'NotebookApp.port',
271 'port': 'NotebookApp.port',
266 'port-retries': 'NotebookApp.port_retries',
272 'port-retries': 'NotebookApp.port_retries',
267 'transport': 'KernelManager.transport',
273 'transport': 'KernelManager.transport',
268 'keyfile': 'NotebookApp.keyfile',
274 'keyfile': 'NotebookApp.keyfile',
269 'certfile': 'NotebookApp.certfile',
275 'certfile': 'NotebookApp.certfile',
270 'notebook-dir': 'NotebookApp.notebook_dir',
276 'notebook-dir': 'NotebookApp.notebook_dir',
271 'browser': 'NotebookApp.browser',
277 'browser': 'NotebookApp.browser',
272 'pylab': 'NotebookApp.pylab',
278 'pylab': 'NotebookApp.pylab',
273 })
279 })
274
280
275 #-----------------------------------------------------------------------------
281 #-----------------------------------------------------------------------------
276 # NotebookApp
282 # NotebookApp
277 #-----------------------------------------------------------------------------
283 #-----------------------------------------------------------------------------
278
284
279 class NotebookApp(BaseIPythonApplication):
285 class NotebookApp(BaseIPythonApplication):
280
286
281 name = 'ipython-notebook'
287 name = 'ipython-notebook'
282
288
283 description = """
289 description = """
284 The IPython HTML Notebook.
290 The IPython HTML Notebook.
285
291
286 This launches a Tornado based HTML Notebook Server that serves up an
292 This launches a Tornado based HTML Notebook Server that serves up an
287 HTML5/Javascript Notebook client.
293 HTML5/Javascript Notebook client.
288 """
294 """
289 examples = _examples
295 examples = _examples
290 aliases = aliases
296 aliases = aliases
291 flags = flags
297 flags = flags
292
298
293 classes = [
299 classes = [
294 KernelManager, ProfileDir, Session, MappingKernelManager,
300 KernelManager, ProfileDir, Session, MappingKernelManager,
295 NotebookManager, FileNotebookManager, NotebookNotary,
301 NotebookManager, FileNotebookManager, NotebookNotary,
296 ]
302 ]
297 flags = Dict(flags)
303 flags = Dict(flags)
298 aliases = Dict(aliases)
304 aliases = Dict(aliases)
299
305
300 subcommands = dict(
306 subcommands = dict(
301 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
307 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
302 )
308 )
303
309
304 kernel_argv = List(Unicode)
310 kernel_argv = List(Unicode)
305
311
306 _log_formatter_cls = LogFormatter
312 _log_formatter_cls = LogFormatter
307
313
308 def _log_level_default(self):
314 def _log_level_default(self):
309 return logging.INFO
315 return logging.INFO
310
316
311 def _log_datefmt_default(self):
317 def _log_datefmt_default(self):
312 """Exclude date from default date format"""
318 """Exclude date from default date format"""
313 return "%H:%M:%S"
319 return "%H:%M:%S"
314
320
315 def _log_format_default(self):
321 def _log_format_default(self):
316 """override default log format to include time"""
322 """override default log format to include time"""
317 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
323 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
318
324
319 # create requested profiles by default, if they don't exist:
325 # create requested profiles by default, if they don't exist:
320 auto_create = Bool(True)
326 auto_create = Bool(True)
321
327
322 # file to be opened in the notebook server
328 # file to be opened in the notebook server
323 file_to_run = Unicode('', config=True)
329 file_to_run = Unicode('', config=True)
324 def _file_to_run_changed(self, name, old, new):
330 def _file_to_run_changed(self, name, old, new):
325 path, base = os.path.split(new)
331 path, base = os.path.split(new)
326 if path:
332 if path:
327 self.file_to_run = base
333 self.file_to_run = base
328 self.notebook_dir = path
334 self.notebook_dir = path
329
335
330 # Network related information.
336 # Network related information.
331
337
332 ip = Unicode('localhost', config=True,
338 ip = Unicode('localhost', config=True,
333 help="The IP address the notebook server will listen on."
339 help="The IP address the notebook server will listen on."
334 )
340 )
335
341
336 def _ip_changed(self, name, old, new):
342 def _ip_changed(self, name, old, new):
337 if new == u'*': self.ip = u''
343 if new == u'*': self.ip = u''
338
344
339 port = Integer(8888, config=True,
345 port = Integer(8888, config=True,
340 help="The port the notebook server will listen on."
346 help="The port the notebook server will listen on."
341 )
347 )
342 port_retries = Integer(50, config=True,
348 port_retries = Integer(50, config=True,
343 help="The number of additional ports to try if the specified port is not available."
349 help="The number of additional ports to try if the specified port is not available."
344 )
350 )
345
351
346 certfile = Unicode(u'', config=True,
352 certfile = Unicode(u'', config=True,
347 help="""The full path to an SSL/TLS certificate file."""
353 help="""The full path to an SSL/TLS certificate file."""
348 )
354 )
349
355
350 keyfile = Unicode(u'', config=True,
356 keyfile = Unicode(u'', config=True,
351 help="""The full path to a private key file for usage with SSL/TLS."""
357 help="""The full path to a private key file for usage with SSL/TLS."""
352 )
358 )
353
359
354 cookie_secret = Bytes(b'', config=True,
360 cookie_secret = Bytes(b'', config=True,
355 help="""The random bytes used to secure cookies.
361 help="""The random bytes used to secure cookies.
356 By default this is a new random number every time you start the Notebook.
362 By default this is a new random number every time you start the Notebook.
357 Set it to a value in a config file to enable logins to persist across server sessions.
363 Set it to a value in a config file to enable logins to persist across server sessions.
358
364
359 Note: Cookie secrets should be kept private, do not share config files with
365 Note: Cookie secrets should be kept private, do not share config files with
360 cookie_secret stored in plaintext (you can read the value from a file).
366 cookie_secret stored in plaintext (you can read the value from a file).
361 """
367 """
362 )
368 )
363 def _cookie_secret_default(self):
369 def _cookie_secret_default(self):
364 return os.urandom(1024)
370 return os.urandom(1024)
365
371
366 password = Unicode(u'', config=True,
372 password = Unicode(u'', config=True,
367 help="""Hashed password to use for web authentication.
373 help="""Hashed password to use for web authentication.
368
374
369 To generate, type in a python/IPython shell:
375 To generate, type in a python/IPython shell:
370
376
371 from IPython.lib import passwd; passwd()
377 from IPython.lib import passwd; passwd()
372
378
373 The string should be of the form type:salt:hashed-password.
379 The string should be of the form type:salt:hashed-password.
374 """
380 """
375 )
381 )
376
382
377 open_browser = Bool(True, config=True,
383 open_browser = Bool(True, config=True,
378 help="""Whether to open in a browser after starting.
384 help="""Whether to open in a browser after starting.
379 The specific browser used is platform dependent and
385 The specific browser used is platform dependent and
380 determined by the python standard library `webbrowser`
386 determined by the python standard library `webbrowser`
381 module, unless it is overridden using the --browser
387 module, unless it is overridden using the --browser
382 (NotebookApp.browser) configuration option.
388 (NotebookApp.browser) configuration option.
383 """)
389 """)
384
390
385 browser = Unicode(u'', config=True,
391 browser = Unicode(u'', config=True,
386 help="""Specify what command to use to invoke a web
392 help="""Specify what command to use to invoke a web
387 browser when opening the notebook. If not specified, the
393 browser when opening the notebook. If not specified, the
388 default browser will be determined by the `webbrowser`
394 default browser will be determined by the `webbrowser`
389 standard library module, which allows setting of the
395 standard library module, which allows setting of the
390 BROWSER environment variable to override it.
396 BROWSER environment variable to override it.
391 """)
397 """)
392
398
393 webapp_settings = Dict(config=True,
399 webapp_settings = Dict(config=True,
394 help="Supply overrides for the tornado.web.Application that the "
400 help="Supply overrides for the tornado.web.Application that the "
395 "IPython notebook uses.")
401 "IPython notebook uses.")
396
402
397 jinja_environment_options = Dict(config=True,
403 jinja_environment_options = Dict(config=True,
398 help="Supply extra arguments that will be passed to Jinja environment.")
404 help="Supply extra arguments that will be passed to Jinja environment.")
399
405
400
406
401 enable_mathjax = Bool(True, config=True,
407 enable_mathjax = Bool(True, config=True,
402 help="""Whether to enable MathJax for typesetting math/TeX
408 help="""Whether to enable MathJax for typesetting math/TeX
403
409
404 MathJax is the javascript library IPython uses to render math/LaTeX. It is
410 MathJax is the javascript library IPython uses to render math/LaTeX. It is
405 very large, so you may want to disable it if you have a slow internet
411 very large, so you may want to disable it if you have a slow internet
406 connection, or for offline use of the notebook.
412 connection, or for offline use of the notebook.
407
413
408 When disabled, equations etc. will appear as their untransformed TeX source.
414 When disabled, equations etc. will appear as their untransformed TeX source.
409 """
415 """
410 )
416 )
411 def _enable_mathjax_changed(self, name, old, new):
417 def _enable_mathjax_changed(self, name, old, new):
412 """set mathjax url to empty if mathjax is disabled"""
418 """set mathjax url to empty if mathjax is disabled"""
413 if not new:
419 if not new:
414 self.mathjax_url = u''
420 self.mathjax_url = u''
415
421
416 base_url = Unicode('/', config=True,
422 base_url = Unicode('/', config=True,
417 help='''The base URL for the notebook server.
423 help='''The base URL for the notebook server.
418
424
419 Leading and trailing slashes can be omitted,
425 Leading and trailing slashes can be omitted,
420 and will automatically be added.
426 and will automatically be added.
421 ''')
427 ''')
422 def _base_url_changed(self, name, old, new):
428 def _base_url_changed(self, name, old, new):
423 if not new.startswith('/'):
429 if not new.startswith('/'):
424 self.base_url = '/'+new
430 self.base_url = '/'+new
425 elif not new.endswith('/'):
431 elif not new.endswith('/'):
426 self.base_url = new+'/'
432 self.base_url = new+'/'
427
433
428 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
434 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
429 def _base_project_url_changed(self, name, old, new):
435 def _base_project_url_changed(self, name, old, new):
430 self.log.warn("base_project_url is deprecated, use base_url")
436 self.log.warn("base_project_url is deprecated, use base_url")
431 self.base_url = new
437 self.base_url = new
432
438
433 extra_static_paths = List(Unicode, config=True,
439 extra_static_paths = List(Unicode, config=True,
434 help="""Extra paths to search for serving static files.
440 help="""Extra paths to search for serving static files.
435
441
436 This allows adding javascript/css to be available from the notebook server machine,
442 This allows adding javascript/css to be available from the notebook server machine,
437 or overriding individual files in the IPython"""
443 or overriding individual files in the IPython"""
438 )
444 )
439 def _extra_static_paths_default(self):
445 def _extra_static_paths_default(self):
440 return [os.path.join(self.profile_dir.location, 'static')]
446 return [os.path.join(self.profile_dir.location, 'static')]
441
447
442 @property
448 @property
443 def static_file_path(self):
449 def static_file_path(self):
444 """return extra paths + the default location"""
450 """return extra paths + the default location"""
445 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
451 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
446
452
447 nbextensions_path = List(Unicode, config=True,
453 nbextensions_path = List(Unicode, config=True,
448 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
454 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
449 )
455 )
450 def _nbextensions_path_default(self):
456 def _nbextensions_path_default(self):
451 return [os.path.join(get_ipython_dir(), 'nbextensions')]
457 return [os.path.join(get_ipython_dir(), 'nbextensions')]
452
458
453 mathjax_url = Unicode("", config=True,
459 mathjax_url = Unicode("", config=True,
454 help="""The url for MathJax.js."""
460 help="""The url for MathJax.js."""
455 )
461 )
456 def _mathjax_url_default(self):
462 def _mathjax_url_default(self):
457 if not self.enable_mathjax:
463 if not self.enable_mathjax:
458 return u''
464 return u''
459 static_url_prefix = self.webapp_settings.get("static_url_prefix",
465 static_url_prefix = self.webapp_settings.get("static_url_prefix",
460 url_path_join(self.base_url, "static")
466 url_path_join(self.base_url, "static")
461 )
467 )
462
468
463 # try local mathjax, either in nbextensions/mathjax or static/mathjax
469 # try local mathjax, either in nbextensions/mathjax or static/mathjax
464 for (url_prefix, search_path) in [
470 for (url_prefix, search_path) in [
465 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
471 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
466 (static_url_prefix, self.static_file_path),
472 (static_url_prefix, self.static_file_path),
467 ]:
473 ]:
468 self.log.debug("searching for local mathjax in %s", search_path)
474 self.log.debug("searching for local mathjax in %s", search_path)
469 try:
475 try:
470 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
476 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
471 except IOError:
477 except IOError:
472 continue
478 continue
473 else:
479 else:
474 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
480 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
475 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
481 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
476 return url
482 return url
477
483
478 # no local mathjax, serve from CDN
484 # no local mathjax, serve from CDN
479 if self.certfile:
485 if self.certfile:
480 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
486 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
481 host = u"https://c328740.ssl.cf1.rackcdn.com"
487 host = u"https://c328740.ssl.cf1.rackcdn.com"
482 else:
488 else:
483 host = u"http://cdn.mathjax.org"
489 host = u"http://cdn.mathjax.org"
484
490
485 url = host + u"/mathjax/latest/MathJax.js"
491 url = host + u"/mathjax/latest/MathJax.js"
486 self.log.info("Using MathJax from CDN: %s", url)
492 self.log.info("Using MathJax from CDN: %s", url)
487 return url
493 return url
488
494
489 def _mathjax_url_changed(self, name, old, new):
495 def _mathjax_url_changed(self, name, old, new):
490 if new and not self.enable_mathjax:
496 if new and not self.enable_mathjax:
491 # enable_mathjax=False overrides mathjax_url
497 # enable_mathjax=False overrides mathjax_url
492 self.mathjax_url = u''
498 self.mathjax_url = u''
493 else:
499 else:
494 self.log.info("Using MathJax: %s", new)
500 self.log.info("Using MathJax: %s", new)
495
501
496 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
502 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
497 config=True,
503 config=True,
498 help='The notebook manager class to use.'
504 help='The notebook manager class to use.'
499 )
505 )
500 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
506 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
501 config=True,
507 config=True,
502 help='The kernel manager class to use.'
508 help='The kernel manager class to use.'
503 )
509 )
504 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
510 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
505 config=True,
511 config=True,
506 help='The session manager class to use.'
512 help='The session manager class to use.'
507 )
513 )
508 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
514 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
509 config=True,
515 config=True,
510 help='The cluster manager class to use.'
516 help='The cluster manager class to use.'
511 )
517 )
512
518
519 kernel_spec_manager = Instance(KernelSpecManager)
520
521 def _kernel_spec_manager_default(self):
522 return KernelSpecManager(ipython_dir=self.ipython_dir)
523
513 trust_xheaders = Bool(False, config=True,
524 trust_xheaders = Bool(False, config=True,
514 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
525 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
515 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
526 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
516 )
527 )
517
528
518 info_file = Unicode()
529 info_file = Unicode()
519
530
520 def _info_file_default(self):
531 def _info_file_default(self):
521 info_file = "nbserver-%s.json"%os.getpid()
532 info_file = "nbserver-%s.json"%os.getpid()
522 return os.path.join(self.profile_dir.security_dir, info_file)
533 return os.path.join(self.profile_dir.security_dir, info_file)
523
534
524 notebook_dir = Unicode(py3compat.getcwd(), config=True,
535 notebook_dir = Unicode(py3compat.getcwd(), config=True,
525 help="The directory to use for notebooks and kernels."
536 help="The directory to use for notebooks and kernels."
526 )
537 )
527
538
528 pylab = Unicode('disabled', config=True,
539 pylab = Unicode('disabled', config=True,
529 help="""
540 help="""
530 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
541 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
531 """
542 """
532 )
543 )
533 def _pylab_changed(self, name, old, new):
544 def _pylab_changed(self, name, old, new):
534 """when --pylab is specified, display a warning and exit"""
545 """when --pylab is specified, display a warning and exit"""
535 if new != 'warn':
546 if new != 'warn':
536 backend = ' %s' % new
547 backend = ' %s' % new
537 else:
548 else:
538 backend = ''
549 backend = ''
539 self.log.error("Support for specifying --pylab on the command line has been removed.")
550 self.log.error("Support for specifying --pylab on the command line has been removed.")
540 self.log.error(
551 self.log.error(
541 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
552 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
542 )
553 )
543 self.exit(1)
554 self.exit(1)
544
555
545 def _notebook_dir_changed(self, name, old, new):
556 def _notebook_dir_changed(self, name, old, new):
546 """Do a bit of validation of the notebook dir."""
557 """Do a bit of validation of the notebook dir."""
547 if not os.path.isabs(new):
558 if not os.path.isabs(new):
548 # If we receive a non-absolute path, make it absolute.
559 # If we receive a non-absolute path, make it absolute.
549 self.notebook_dir = os.path.abspath(new)
560 self.notebook_dir = os.path.abspath(new)
550 return
561 return
551 if not os.path.isdir(new):
562 if not os.path.isdir(new):
552 raise TraitError("No such notebook dir: %r" % new)
563 raise TraitError("No such notebook dir: %r" % new)
553
564
554 # setting App.notebook_dir implies setting notebook and kernel dirs as well
565 # setting App.notebook_dir implies setting notebook and kernel dirs as well
555 self.config.FileNotebookManager.notebook_dir = new
566 self.config.FileNotebookManager.notebook_dir = new
556 self.config.MappingKernelManager.root_dir = new
567 self.config.MappingKernelManager.root_dir = new
557
568
558
569
559 def parse_command_line(self, argv=None):
570 def parse_command_line(self, argv=None):
560 super(NotebookApp, self).parse_command_line(argv)
571 super(NotebookApp, self).parse_command_line(argv)
561
572
562 if self.extra_args:
573 if self.extra_args:
563 arg0 = self.extra_args[0]
574 arg0 = self.extra_args[0]
564 f = os.path.abspath(arg0)
575 f = os.path.abspath(arg0)
565 self.argv.remove(arg0)
576 self.argv.remove(arg0)
566 if not os.path.exists(f):
577 if not os.path.exists(f):
567 self.log.critical("No such file or directory: %s", f)
578 self.log.critical("No such file or directory: %s", f)
568 self.exit(1)
579 self.exit(1)
569
580
570 # Use config here, to ensure that it takes higher priority than
581 # Use config here, to ensure that it takes higher priority than
571 # anything that comes from the profile.
582 # anything that comes from the profile.
572 c = Config()
583 c = Config()
573 if os.path.isdir(f):
584 if os.path.isdir(f):
574 c.NotebookApp.notebook_dir = f
585 c.NotebookApp.notebook_dir = f
575 elif os.path.isfile(f):
586 elif os.path.isfile(f):
576 c.NotebookApp.file_to_run = f
587 c.NotebookApp.file_to_run = f
577 self.update_config(c)
588 self.update_config(c)
578
589
579 def init_kernel_argv(self):
590 def init_kernel_argv(self):
580 """construct the kernel arguments"""
591 """construct the kernel arguments"""
581 self.kernel_argv = []
592 self.kernel_argv = []
582 # Kernel should inherit default config file from frontend
593 # Kernel should inherit default config file from frontend
583 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
594 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
584 # Kernel should get *absolute* path to profile directory
595 # Kernel should get *absolute* path to profile directory
585 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
596 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
586
597
587 def init_configurables(self):
598 def init_configurables(self):
588 # force Session default to be secure
599 # force Session default to be secure
589 default_secure(self.config)
600 default_secure(self.config)
590 kls = import_item(self.kernel_manager_class)
601 kls = import_item(self.kernel_manager_class)
591 self.kernel_manager = kls(
602 self.kernel_manager = kls(
592 parent=self, log=self.log, kernel_argv=self.kernel_argv,
603 parent=self, log=self.log, kernel_argv=self.kernel_argv,
593 connection_dir = self.profile_dir.security_dir,
604 connection_dir = self.profile_dir.security_dir,
594 )
605 )
595 kls = import_item(self.notebook_manager_class)
606 kls = import_item(self.notebook_manager_class)
596 self.notebook_manager = kls(parent=self, log=self.log)
607 self.notebook_manager = kls(parent=self, log=self.log)
597 kls = import_item(self.session_manager_class)
608 kls = import_item(self.session_manager_class)
598 self.session_manager = kls(parent=self, log=self.log)
609 self.session_manager = kls(parent=self, log=self.log)
599 kls = import_item(self.cluster_manager_class)
610 kls = import_item(self.cluster_manager_class)
600 self.cluster_manager = kls(parent=self, log=self.log)
611 self.cluster_manager = kls(parent=self, log=self.log)
601 self.cluster_manager.update_profiles()
612 self.cluster_manager.update_profiles()
602
613
603 def init_logging(self):
614 def init_logging(self):
604 # This prevents double log messages because tornado use a root logger that
615 # This prevents double log messages because tornado use a root logger that
605 # self.log is a child of. The logging module dipatches log messages to a log
616 # self.log is a child of. The logging module dipatches log messages to a log
606 # and all of its ancenstors until propagate is set to False.
617 # and all of its ancenstors until propagate is set to False.
607 self.log.propagate = False
618 self.log.propagate = False
608
619
609 # hook up tornado 3's loggers to our app handlers
620 # hook up tornado 3's loggers to our app handlers
610 logger = logging.getLogger('tornado')
621 logger = logging.getLogger('tornado')
611 logger.propagate = True
622 logger.propagate = True
612 logger.parent = self.log
623 logger.parent = self.log
613 logger.setLevel(self.log.level)
624 logger.setLevel(self.log.level)
614
625
615 def init_webapp(self):
626 def init_webapp(self):
616 """initialize tornado webapp and httpserver"""
627 """initialize tornado webapp and httpserver"""
617 self.web_app = NotebookWebApplication(
628 self.web_app = NotebookWebApplication(
618 self, self.kernel_manager, self.notebook_manager,
629 self, self.kernel_manager, self.notebook_manager,
619 self.cluster_manager, self.session_manager,
630 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
620 self.log, self.base_url, self.webapp_settings,
631 self.log, self.base_url, self.webapp_settings,
621 self.jinja_environment_options
632 self.jinja_environment_options
622 )
633 )
623 if self.certfile:
634 if self.certfile:
624 ssl_options = dict(certfile=self.certfile)
635 ssl_options = dict(certfile=self.certfile)
625 if self.keyfile:
636 if self.keyfile:
626 ssl_options['keyfile'] = self.keyfile
637 ssl_options['keyfile'] = self.keyfile
627 else:
638 else:
628 ssl_options = None
639 ssl_options = None
629 self.web_app.password = self.password
640 self.web_app.password = self.password
630 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
641 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
631 xheaders=self.trust_xheaders)
642 xheaders=self.trust_xheaders)
632 if not self.ip:
643 if not self.ip:
633 warning = "WARNING: The notebook server is listening on all IP addresses"
644 warning = "WARNING: The notebook server is listening on all IP addresses"
634 if ssl_options is None:
645 if ssl_options is None:
635 self.log.critical(warning + " and not using encryption. This "
646 self.log.critical(warning + " and not using encryption. This "
636 "is not recommended.")
647 "is not recommended.")
637 if not self.password:
648 if not self.password:
638 self.log.critical(warning + " and not using authentication. "
649 self.log.critical(warning + " and not using authentication. "
639 "This is highly insecure and not recommended.")
650 "This is highly insecure and not recommended.")
640 success = None
651 success = None
641 for port in random_ports(self.port, self.port_retries+1):
652 for port in random_ports(self.port, self.port_retries+1):
642 try:
653 try:
643 self.http_server.listen(port, self.ip)
654 self.http_server.listen(port, self.ip)
644 except socket.error as e:
655 except socket.error as e:
645 if e.errno == errno.EADDRINUSE:
656 if e.errno == errno.EADDRINUSE:
646 self.log.info('The port %i is already in use, trying another random port.' % port)
657 self.log.info('The port %i is already in use, trying another random port.' % port)
647 continue
658 continue
648 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
659 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
649 self.log.warn("Permission to listen on port %i denied" % port)
660 self.log.warn("Permission to listen on port %i denied" % port)
650 continue
661 continue
651 else:
662 else:
652 raise
663 raise
653 else:
664 else:
654 self.port = port
665 self.port = port
655 success = True
666 success = True
656 break
667 break
657 if not success:
668 if not success:
658 self.log.critical('ERROR: the notebook server could not be started because '
669 self.log.critical('ERROR: the notebook server could not be started because '
659 'no available port could be found.')
670 'no available port could be found.')
660 self.exit(1)
671 self.exit(1)
661
672
662 @property
673 @property
663 def display_url(self):
674 def display_url(self):
664 ip = self.ip if self.ip else '[all ip addresses on your system]'
675 ip = self.ip if self.ip else '[all ip addresses on your system]'
665 return self._url(ip)
676 return self._url(ip)
666
677
667 @property
678 @property
668 def connection_url(self):
679 def connection_url(self):
669 ip = self.ip if self.ip else 'localhost'
680 ip = self.ip if self.ip else 'localhost'
670 return self._url(ip)
681 return self._url(ip)
671
682
672 def _url(self, ip):
683 def _url(self, ip):
673 proto = 'https' if self.certfile else 'http'
684 proto = 'https' if self.certfile else 'http'
674 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
685 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
675
686
676 def init_signal(self):
687 def init_signal(self):
677 if not sys.platform.startswith('win'):
688 if not sys.platform.startswith('win'):
678 signal.signal(signal.SIGINT, self._handle_sigint)
689 signal.signal(signal.SIGINT, self._handle_sigint)
679 signal.signal(signal.SIGTERM, self._signal_stop)
690 signal.signal(signal.SIGTERM, self._signal_stop)
680 if hasattr(signal, 'SIGUSR1'):
691 if hasattr(signal, 'SIGUSR1'):
681 # Windows doesn't support SIGUSR1
692 # Windows doesn't support SIGUSR1
682 signal.signal(signal.SIGUSR1, self._signal_info)
693 signal.signal(signal.SIGUSR1, self._signal_info)
683 if hasattr(signal, 'SIGINFO'):
694 if hasattr(signal, 'SIGINFO'):
684 # only on BSD-based systems
695 # only on BSD-based systems
685 signal.signal(signal.SIGINFO, self._signal_info)
696 signal.signal(signal.SIGINFO, self._signal_info)
686
697
687 def _handle_sigint(self, sig, frame):
698 def _handle_sigint(self, sig, frame):
688 """SIGINT handler spawns confirmation dialog"""
699 """SIGINT handler spawns confirmation dialog"""
689 # register more forceful signal handler for ^C^C case
700 # register more forceful signal handler for ^C^C case
690 signal.signal(signal.SIGINT, self._signal_stop)
701 signal.signal(signal.SIGINT, self._signal_stop)
691 # request confirmation dialog in bg thread, to avoid
702 # request confirmation dialog in bg thread, to avoid
692 # blocking the App
703 # blocking the App
693 thread = threading.Thread(target=self._confirm_exit)
704 thread = threading.Thread(target=self._confirm_exit)
694 thread.daemon = True
705 thread.daemon = True
695 thread.start()
706 thread.start()
696
707
697 def _restore_sigint_handler(self):
708 def _restore_sigint_handler(self):
698 """callback for restoring original SIGINT handler"""
709 """callback for restoring original SIGINT handler"""
699 signal.signal(signal.SIGINT, self._handle_sigint)
710 signal.signal(signal.SIGINT, self._handle_sigint)
700
711
701 def _confirm_exit(self):
712 def _confirm_exit(self):
702 """confirm shutdown on ^C
713 """confirm shutdown on ^C
703
714
704 A second ^C, or answering 'y' within 5s will cause shutdown,
715 A second ^C, or answering 'y' within 5s will cause shutdown,
705 otherwise original SIGINT handler will be restored.
716 otherwise original SIGINT handler will be restored.
706
717
707 This doesn't work on Windows.
718 This doesn't work on Windows.
708 """
719 """
709 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
720 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
710 time.sleep(0.1)
721 time.sleep(0.1)
711 info = self.log.info
722 info = self.log.info
712 info('interrupted')
723 info('interrupted')
713 print(self.notebook_info())
724 print(self.notebook_info())
714 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
725 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
715 sys.stdout.flush()
726 sys.stdout.flush()
716 r,w,x = select.select([sys.stdin], [], [], 5)
727 r,w,x = select.select([sys.stdin], [], [], 5)
717 if r:
728 if r:
718 line = sys.stdin.readline()
729 line = sys.stdin.readline()
719 if line.lower().startswith('y') and 'n' not in line.lower():
730 if line.lower().startswith('y') and 'n' not in line.lower():
720 self.log.critical("Shutdown confirmed")
731 self.log.critical("Shutdown confirmed")
721 ioloop.IOLoop.instance().stop()
732 ioloop.IOLoop.instance().stop()
722 return
733 return
723 else:
734 else:
724 print("No answer for 5s:", end=' ')
735 print("No answer for 5s:", end=' ')
725 print("resuming operation...")
736 print("resuming operation...")
726 # no answer, or answer is no:
737 # no answer, or answer is no:
727 # set it back to original SIGINT handler
738 # set it back to original SIGINT handler
728 # use IOLoop.add_callback because signal.signal must be called
739 # use IOLoop.add_callback because signal.signal must be called
729 # from main thread
740 # from main thread
730 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
741 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
731
742
732 def _signal_stop(self, sig, frame):
743 def _signal_stop(self, sig, frame):
733 self.log.critical("received signal %s, stopping", sig)
744 self.log.critical("received signal %s, stopping", sig)
734 ioloop.IOLoop.instance().stop()
745 ioloop.IOLoop.instance().stop()
735
746
736 def _signal_info(self, sig, frame):
747 def _signal_info(self, sig, frame):
737 print(self.notebook_info())
748 print(self.notebook_info())
738
749
739 def init_components(self):
750 def init_components(self):
740 """Check the components submodule, and warn if it's unclean"""
751 """Check the components submodule, and warn if it's unclean"""
741 status = submodule.check_submodule_status()
752 status = submodule.check_submodule_status()
742 if status == 'missing':
753 if status == 'missing':
743 self.log.warn("components submodule missing, running `git submodule update`")
754 self.log.warn("components submodule missing, running `git submodule update`")
744 submodule.update_submodules(submodule.ipython_parent())
755 submodule.update_submodules(submodule.ipython_parent())
745 elif status == 'unclean':
756 elif status == 'unclean':
746 self.log.warn("components submodule unclean, you may see 404s on static/components")
757 self.log.warn("components submodule unclean, you may see 404s on static/components")
747 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
758 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
748
759
749 @catch_config_error
760 @catch_config_error
750 def initialize(self, argv=None):
761 def initialize(self, argv=None):
751 super(NotebookApp, self).initialize(argv)
762 super(NotebookApp, self).initialize(argv)
752 self.init_logging()
763 self.init_logging()
753 self.init_kernel_argv()
764 self.init_kernel_argv()
754 self.init_configurables()
765 self.init_configurables()
755 self.init_components()
766 self.init_components()
756 self.init_webapp()
767 self.init_webapp()
757 self.init_signal()
768 self.init_signal()
758
769
759 def cleanup_kernels(self):
770 def cleanup_kernels(self):
760 """Shutdown all kernels.
771 """Shutdown all kernels.
761
772
762 The kernels will shutdown themselves when this process no longer exists,
773 The kernels will shutdown themselves when this process no longer exists,
763 but explicit shutdown allows the KernelManagers to cleanup the connection files.
774 but explicit shutdown allows the KernelManagers to cleanup the connection files.
764 """
775 """
765 self.log.info('Shutting down kernels')
776 self.log.info('Shutting down kernels')
766 self.kernel_manager.shutdown_all()
777 self.kernel_manager.shutdown_all()
767
778
768 def notebook_info(self):
779 def notebook_info(self):
769 "Return the current working directory and the server url information"
780 "Return the current working directory and the server url information"
770 info = self.notebook_manager.info_string() + "\n"
781 info = self.notebook_manager.info_string() + "\n"
771 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
782 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
772 return info + "The IPython Notebook is running at: %s" % self.display_url
783 return info + "The IPython Notebook is running at: %s" % self.display_url
773
784
774 def server_info(self):
785 def server_info(self):
775 """Return a JSONable dict of information about this server."""
786 """Return a JSONable dict of information about this server."""
776 return {'url': self.connection_url,
787 return {'url': self.connection_url,
777 'hostname': self.ip if self.ip else 'localhost',
788 'hostname': self.ip if self.ip else 'localhost',
778 'port': self.port,
789 'port': self.port,
779 'secure': bool(self.certfile),
790 'secure': bool(self.certfile),
780 'base_url': self.base_url,
791 'base_url': self.base_url,
781 'notebook_dir': os.path.abspath(self.notebook_dir),
792 'notebook_dir': os.path.abspath(self.notebook_dir),
782 }
793 }
783
794
784 def write_server_info_file(self):
795 def write_server_info_file(self):
785 """Write the result of server_info() to the JSON file info_file."""
796 """Write the result of server_info() to the JSON file info_file."""
786 with open(self.info_file, 'w') as f:
797 with open(self.info_file, 'w') as f:
787 json.dump(self.server_info(), f, indent=2)
798 json.dump(self.server_info(), f, indent=2)
788
799
789 def remove_server_info_file(self):
800 def remove_server_info_file(self):
790 """Remove the nbserver-<pid>.json file created for this server.
801 """Remove the nbserver-<pid>.json file created for this server.
791
802
792 Ignores the error raised when the file has already been removed.
803 Ignores the error raised when the file has already been removed.
793 """
804 """
794 try:
805 try:
795 os.unlink(self.info_file)
806 os.unlink(self.info_file)
796 except OSError as e:
807 except OSError as e:
797 if e.errno != errno.ENOENT:
808 if e.errno != errno.ENOENT:
798 raise
809 raise
799
810
800 def start(self):
811 def start(self):
801 """ Start the IPython Notebook server app, after initialization
812 """ Start the IPython Notebook server app, after initialization
802
813
803 This method takes no arguments so all configuration and initialization
814 This method takes no arguments so all configuration and initialization
804 must be done prior to calling this method."""
815 must be done prior to calling this method."""
805 if self.subapp is not None:
816 if self.subapp is not None:
806 return self.subapp.start()
817 return self.subapp.start()
807
818
808 info = self.log.info
819 info = self.log.info
809 for line in self.notebook_info().split("\n"):
820 for line in self.notebook_info().split("\n"):
810 info(line)
821 info(line)
811 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
822 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
812
823
813 self.write_server_info_file()
824 self.write_server_info_file()
814
825
815 if self.open_browser or self.file_to_run:
826 if self.open_browser or self.file_to_run:
816 try:
827 try:
817 browser = webbrowser.get(self.browser or None)
828 browser = webbrowser.get(self.browser or None)
818 except webbrowser.Error as e:
829 except webbrowser.Error as e:
819 self.log.warn('No web browser found: %s.' % e)
830 self.log.warn('No web browser found: %s.' % e)
820 browser = None
831 browser = None
821
832
822 if self.file_to_run:
833 if self.file_to_run:
823 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
834 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
824 if not os.path.exists(fullpath):
835 if not os.path.exists(fullpath):
825 self.log.critical("%s does not exist" % fullpath)
836 self.log.critical("%s does not exist" % fullpath)
826 self.exit(1)
837 self.exit(1)
827
838
828 uri = url_path_join('notebooks', self.file_to_run)
839 uri = url_path_join('notebooks', self.file_to_run)
829 else:
840 else:
830 uri = 'tree'
841 uri = 'tree'
831 if browser:
842 if browser:
832 b = lambda : browser.open(url_path_join(self.connection_url, uri),
843 b = lambda : browser.open(url_path_join(self.connection_url, uri),
833 new=2)
844 new=2)
834 threading.Thread(target=b).start()
845 threading.Thread(target=b).start()
835 try:
846 try:
836 ioloop.IOLoop.instance().start()
847 ioloop.IOLoop.instance().start()
837 except KeyboardInterrupt:
848 except KeyboardInterrupt:
838 info("Interrupted...")
849 info("Interrupted...")
839 finally:
850 finally:
840 self.cleanup_kernels()
851 self.cleanup_kernels()
841 self.remove_server_info_file()
852 self.remove_server_info_file()
842
853
843
854
844 def list_running_servers(profile='default'):
855 def list_running_servers(profile='default'):
845 """Iterate over the server info files of running notebook servers.
856 """Iterate over the server info files of running notebook servers.
846
857
847 Given a profile name, find nbserver-* files in the security directory of
858 Given a profile name, find nbserver-* files in the security directory of
848 that profile, and yield dicts of their information, each one pertaining to
859 that profile, and yield dicts of their information, each one pertaining to
849 a currently running notebook server instance.
860 a currently running notebook server instance.
850 """
861 """
851 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
862 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
852 for file in os.listdir(pd.security_dir):
863 for file in os.listdir(pd.security_dir):
853 if file.startswith('nbserver-'):
864 if file.startswith('nbserver-'):
854 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
865 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
855 yield json.load(f)
866 yield json.load(f)
856
867
857 #-----------------------------------------------------------------------------
868 #-----------------------------------------------------------------------------
858 # Main entry point
869 # Main entry point
859 #-----------------------------------------------------------------------------
870 #-----------------------------------------------------------------------------
860
871
861 launch_new_instance = NotebookApp.launch_instance
872 launch_new_instance = NotebookApp.launch_instance
862
873
@@ -1,181 +1,192 b''
1 import io
1 import io
2 import json
2 import json
3 import os
3 import os
4 import shutil
4 import shutil
5 import sys
5 import sys
6
6
7 pjoin = os.path.join
7 pjoin = os.path.join
8
8
9 from IPython.utils.path import get_ipython_dir
9 from IPython.utils.path import get_ipython_dir
10 from IPython.utils.py3compat import PY3
10 from IPython.utils.py3compat import PY3
11 from IPython.utils.traitlets import HasTraits, List, Unicode, Dict
11 from IPython.utils.traitlets import HasTraits, List, Unicode, Dict
12
12
13 if os.name == 'nt':
13 if os.name == 'nt':
14 programdata = os.environ.get('PROGRAMDATA', None)
14 programdata = os.environ.get('PROGRAMDATA', None)
15 if programdata:
15 if programdata:
16 SYSTEM_KERNEL_DIRS = [pjoin(programdata, 'ipython', 'kernels')]
16 SYSTEM_KERNEL_DIRS = [pjoin(programdata, 'ipython', 'kernels')]
17 else: # PROGRAMDATA is not defined by default on XP.
17 else: # PROGRAMDATA is not defined by default on XP.
18 SYSTEM_KERNEL_DIRS = []
18 SYSTEM_KERNEL_DIRS = []
19 else:
19 else:
20 SYSTEM_KERNEL_DIRS = ["/usr/share/ipython/kernels",
20 SYSTEM_KERNEL_DIRS = ["/usr/share/ipython/kernels",
21 "/usr/local/share/ipython/kernels",
21 "/usr/local/share/ipython/kernels",
22 ]
22 ]
23
23
24 NATIVE_KERNEL_NAME = 'python3' if PY3 else 'python2'
24 NATIVE_KERNEL_NAME = 'python3' if PY3 else 'python2'
25
25
26 class KernelSpec(HasTraits):
26 class KernelSpec(HasTraits):
27 argv = List()
27 argv = List()
28 display_name = Unicode()
28 display_name = Unicode()
29 language = Unicode()
29 language = Unicode()
30 codemirror_mode = None
30 codemirror_mode = None
31 env = Dict()
31 env = Dict()
32
32
33 resource_dir = Unicode()
33 resource_dir = Unicode()
34
34
35 def __init__(self, resource_dir, argv, display_name, language,
35 def __init__(self, resource_dir, argv, display_name, language,
36 codemirror_mode=None):
36 codemirror_mode=None):
37 super(KernelSpec, self).__init__(resource_dir=resource_dir, argv=argv,
37 super(KernelSpec, self).__init__(resource_dir=resource_dir, argv=argv,
38 display_name=display_name, language=language,
38 display_name=display_name, language=language,
39 codemirror_mode=codemirror_mode)
39 codemirror_mode=codemirror_mode)
40 if not self.codemirror_mode:
40 if not self.codemirror_mode:
41 self.codemirror_mode = self.language
41 self.codemirror_mode = self.language
42
42
43 @classmethod
43 @classmethod
44 def from_resource_dir(cls, resource_dir):
44 def from_resource_dir(cls, resource_dir):
45 """Create a KernelSpec object by reading kernel.json
45 """Create a KernelSpec object by reading kernel.json
46
46
47 Pass the path to the *directory* containing kernel.json.
47 Pass the path to the *directory* containing kernel.json.
48 """
48 """
49 kernel_file = pjoin(resource_dir, 'kernel.json')
49 kernel_file = pjoin(resource_dir, 'kernel.json')
50 with io.open(kernel_file, 'r', encoding='utf-8') as f:
50 with io.open(kernel_file, 'r', encoding='utf-8') as f:
51 kernel_dict = json.load(f)
51 kernel_dict = json.load(f)
52 return cls(resource_dir=resource_dir, **kernel_dict)
52 return cls(resource_dir=resource_dir, **kernel_dict)
53
54 def to_dict(self):
55 return dict(argv=self.argv,
56 env=self.env,
57 display_name=self.display_name,
58 language=self.language,
59 codemirror_mode=self.codemirror_mode,
60 )
61
62 def to_json(self):
63 return json.dumps(self.to_dict())
53
64
54 def _is_kernel_dir(path):
65 def _is_kernel_dir(path):
55 """Is ``path`` a kernel directory?"""
66 """Is ``path`` a kernel directory?"""
56 return os.path.isdir(path) and os.path.isfile(pjoin(path, 'kernel.json'))
67 return os.path.isdir(path) and os.path.isfile(pjoin(path, 'kernel.json'))
57
68
58 def _list_kernels_in(dir):
69 def _list_kernels_in(dir):
59 """Return a mapping of kernel names to resource directories from dir.
70 """Return a mapping of kernel names to resource directories from dir.
60
71
61 If dir is None or does not exist, returns an empty dict.
72 If dir is None or does not exist, returns an empty dict.
62 """
73 """
63 if dir is None or not os.path.isdir(dir):
74 if dir is None or not os.path.isdir(dir):
64 return {}
75 return {}
65 return {f.lower(): pjoin(dir, f) for f in os.listdir(dir)
76 return {f.lower(): pjoin(dir, f) for f in os.listdir(dir)
66 if _is_kernel_dir(pjoin(dir, f))}
77 if _is_kernel_dir(pjoin(dir, f))}
67
78
68 class NoSuchKernel(KeyError):
79 class NoSuchKernel(KeyError):
69 def __init__(self, name):
80 def __init__(self, name):
70 self.name = name
81 self.name = name
71
82
72 class KernelSpecManager(HasTraits):
83 class KernelSpecManager(HasTraits):
73 ipython_dir = Unicode()
84 ipython_dir = Unicode()
74 def _ipython_dir_default(self):
85 def _ipython_dir_default(self):
75 return get_ipython_dir()
86 return get_ipython_dir()
76
87
77 user_kernel_dir = Unicode()
88 user_kernel_dir = Unicode()
78 def _user_kernel_dir_default(self):
89 def _user_kernel_dir_default(self):
79 return pjoin(self.ipython_dir, 'kernels')
90 return pjoin(self.ipython_dir, 'kernels')
80
91
81 kernel_dirs = List(
92 kernel_dirs = List(
82 help="List of kernel directories to search. Later ones take priority over earlier."
93 help="List of kernel directories to search. Later ones take priority over earlier."
83 )
94 )
84 def _kernel_dirs_default(self):
95 def _kernel_dirs_default(self):
85 return SYSTEM_KERNEL_DIRS + [
96 return SYSTEM_KERNEL_DIRS + [
86 self.user_kernel_dir,
97 self.user_kernel_dir,
87 ]
98 ]
88
99
89 def _make_native_kernel_dir(self):
100 def _make_native_kernel_dir(self):
90 """Makes a kernel directory for the native kernel.
101 """Makes a kernel directory for the native kernel.
91
102
92 The native kernel is the kernel using the same Python runtime as this
103 The native kernel is the kernel using the same Python runtime as this
93 process. This will put its informatino in the user kernels directory.
104 process. This will put its informatino in the user kernels directory.
94 """
105 """
95 path = pjoin(self.user_kernel_dir, NATIVE_KERNEL_NAME)
106 path = pjoin(self.user_kernel_dir, NATIVE_KERNEL_NAME)
96 os.makedirs(path, mode=0o755)
107 os.makedirs(path, mode=0o755)
97 with open(pjoin(path, 'kernel.json'), 'w') as f:
108 with open(pjoin(path, 'kernel.json'), 'w') as f:
98 json.dump({'argv':[NATIVE_KERNEL_NAME, '-c',
109 json.dump({'argv':[NATIVE_KERNEL_NAME, '-c',
99 'from IPython.kernel.zmq.kernelapp import main; main()',
110 'from IPython.kernel.zmq.kernelapp import main; main()',
100 '-f', '{connection_file}'],
111 '-f', '{connection_file}'],
101 'display_name': 'Python 3' if PY3 else 'Python 2',
112 'display_name': 'Python 3' if PY3 else 'Python 2',
102 'language': 'python',
113 'language': 'python',
103 'codemirror_mode': {'name': 'python',
114 'codemirror_mode': {'name': 'python',
104 'version': sys.version_info[0]},
115 'version': sys.version_info[0]},
105 },
116 },
106 f, indent=1)
117 f, indent=1)
107 # TODO: Copy icons into directory
118 # TODO: Copy icons into directory
108 return path
119 return path
109
120
110 def find_kernel_specs(self):
121 def find_kernel_specs(self):
111 """Returns a dict mapping kernel names to resource directories."""
122 """Returns a dict mapping kernel names to resource directories."""
112 d = {}
123 d = {}
113 for kernel_dir in self.kernel_dirs:
124 for kernel_dir in self.kernel_dirs:
114 d.update(_list_kernels_in(kernel_dir))
125 d.update(_list_kernels_in(kernel_dir))
115
126
116 if NATIVE_KERNEL_NAME not in d:
127 if NATIVE_KERNEL_NAME not in d:
117 d[NATIVE_KERNEL_NAME] = self._make_native_kernel_dir()
128 d[NATIVE_KERNEL_NAME] = self._make_native_kernel_dir()
118 return d
129 return d
119 # TODO: Caching?
130 # TODO: Caching?
120
131
121 def get_kernel_spec(self, kernel_name):
132 def get_kernel_spec(self, kernel_name):
122 """Returns a :class:`KernelSpec` instance for the given kernel_name.
133 """Returns a :class:`KernelSpec` instance for the given kernel_name.
123
134
124 Raises :exc:`NoSuchKernel` if the given kernel name is not found.
135 Raises :exc:`NoSuchKernel` if the given kernel name is not found.
125 """
136 """
126 if kernel_name == 'python':
137 if kernel_name == 'python':
127 kernel_name = NATIVE_KERNEL_NAME
138 kernel_name = NATIVE_KERNEL_NAME
128 d = self.find_kernel_specs()
139 d = self.find_kernel_specs()
129 try:
140 try:
130 resource_dir = d[kernel_name.lower()]
141 resource_dir = d[kernel_name.lower()]
131 except KeyError:
142 except KeyError:
132 raise NoSuchKernel(kernel_name)
143 raise NoSuchKernel(kernel_name)
133 return KernelSpec.from_resource_dir(resource_dir)
144 return KernelSpec.from_resource_dir(resource_dir)
134
145
135 def install_kernel_spec(self, source_dir, kernel_name=None, system=False,
146 def install_kernel_spec(self, source_dir, kernel_name=None, system=False,
136 replace=False):
147 replace=False):
137 """Install a kernel spec by copying its directory.
148 """Install a kernel spec by copying its directory.
138
149
139 If ``kernel_name`` is not given, the basename of ``source_dir`` will
150 If ``kernel_name`` is not given, the basename of ``source_dir`` will
140 be used.
151 be used.
141
152
142 If ``system`` is True, it will attempt to install into the systemwide
153 If ``system`` is True, it will attempt to install into the systemwide
143 kernel registry. If the process does not have appropriate permissions,
154 kernel registry. If the process does not have appropriate permissions,
144 an :exc:`OSError` will be raised.
155 an :exc:`OSError` will be raised.
145
156
146 If ``replace`` is True, this will replace an existing kernel of the same
157 If ``replace`` is True, this will replace an existing kernel of the same
147 name. Otherwise, if the destination already exists, an :exc:`OSError`
158 name. Otherwise, if the destination already exists, an :exc:`OSError`
148 will be raised.
159 will be raised.
149 """
160 """
150 if not kernel_name:
161 if not kernel_name:
151 kernel_name = os.path.basename(source_dir)
162 kernel_name = os.path.basename(source_dir)
152 kernel_name = kernel_name.lower()
163 kernel_name = kernel_name.lower()
153
164
154 if system:
165 if system:
155 if SYSTEM_KERNEL_DIRS:
166 if SYSTEM_KERNEL_DIRS:
156 destination = os.path.join(SYSTEM_KERNEL_DIRS[-1], kernel_name)
167 destination = os.path.join(SYSTEM_KERNEL_DIRS[-1], kernel_name)
157 else:
168 else:
158 raise EnvironmentError("No system kernel directory is available")
169 raise EnvironmentError("No system kernel directory is available")
159 else:
170 else:
160 destination = os.path.join(self.user_kernel_dir, kernel_name)
171 destination = os.path.join(self.user_kernel_dir, kernel_name)
161
172
162 if replace and os.path.isdir(destination):
173 if replace and os.path.isdir(destination):
163 shutil.rmtree(destination)
174 shutil.rmtree(destination)
164
175
165 shutil.copytree(source_dir, destination)
176 shutil.copytree(source_dir, destination)
166
177
167 def find_kernel_specs():
178 def find_kernel_specs():
168 """Returns a dict mapping kernel names to resource directories."""
179 """Returns a dict mapping kernel names to resource directories."""
169 return KernelSpecManager().find_kernel_specs()
180 return KernelSpecManager().find_kernel_specs()
170
181
171 def get_kernel_spec(kernel_name):
182 def get_kernel_spec(kernel_name):
172 """Returns a :class:`KernelSpec` instance for the given kernel_name.
183 """Returns a :class:`KernelSpec` instance for the given kernel_name.
173
184
174 Raises KeyError if the given kernel name is not found.
185 Raises KeyError if the given kernel name is not found.
175 """
186 """
176 return KernelSpecManager().get_kernel_spec(kernel_name)
187 return KernelSpecManager().get_kernel_spec(kernel_name)
177
188
178 def install_kernel_spec(source_dir, kernel_name=None, system=False):
189 def install_kernel_spec(source_dir, kernel_name=None, system=False):
179 return KernelSpecManager().install_kernel_spec(source_dir, kernel_name, system)
190 return KernelSpecManager().install_kernel_spec(source_dir, kernel_name, system)
180
191
181 install_kernel_spec.__doc__ = KernelSpecManager.install_kernel_spec.__doc__
192 install_kernel_spec.__doc__ = KernelSpecManager.install_kernel_spec.__doc__
General Comments 0
You need to be logged in to leave comments. Login now