##// END OF EJS Templates
jupyter_notebook imports
Min RK -
Show More
@@ -1,3 +1,3
1 1 if __name__ == '__main__':
2 from IPython.html import notebookapp as app
2 from jupyter_notebook import notebookapp as app
3 3 app.launch_new_instance()
@@ -1,528 +1,528
1 1 """Base Tornado handlers for the notebook server."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import functools
7 7 import json
8 8 import logging
9 9 import os
10 10 import re
11 11 import sys
12 12 import traceback
13 13 try:
14 14 # py3
15 15 from http.client import responses
16 16 except ImportError:
17 17 from httplib import responses
18 18
19 19 from jinja2 import TemplateNotFound
20 20 from tornado import web
21 21
22 22 from tornado import gen
23 23 from tornado.log import app_log
24 24
25 25
26 26 import IPython
27 27 from IPython.utils.sysinfo import get_sys_info
28 28
29 29 from IPython.config import Application
30 30 from IPython.utils.path import filefind
31 31 from IPython.utils.py3compat import string_types
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
32 from jupyter_notebook.utils import is_hidden, url_path_join, url_escape
33 33
34 from IPython.html.services.security import csp_report_uri
34 from jupyter_notebook.services.security import csp_report_uri
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Top-level handlers
38 38 #-----------------------------------------------------------------------------
39 39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
40 40
41 41 sys_info = json.dumps(get_sys_info())
42 42
43 43 class AuthenticatedHandler(web.RequestHandler):
44 44 """A RequestHandler with an authenticated user."""
45 45
46 46 def set_default_headers(self):
47 47 headers = self.settings.get('headers', {})
48 48
49 49 if "Content-Security-Policy" not in headers:
50 50 headers["Content-Security-Policy"] = (
51 51 "frame-ancestors 'self'; "
52 52 # Make sure the report-uri is relative to the base_url
53 53 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
54 54 )
55 55
56 56 # Allow for overriding headers
57 57 for header_name,value in headers.items() :
58 58 try:
59 59 self.set_header(header_name, value)
60 60 except Exception as e:
61 61 # tornado raise Exception (not a subclass)
62 62 # if method is unsupported (websocket and Access-Control-Allow-Origin
63 63 # for example, so just ignore)
64 64 self.log.debug(e)
65 65
66 66 def clear_login_cookie(self):
67 67 self.clear_cookie(self.cookie_name)
68 68
69 69 def get_current_user(self):
70 70 if self.login_handler is None:
71 71 return 'anonymous'
72 72 return self.login_handler.get_user(self)
73 73
74 74 @property
75 75 def cookie_name(self):
76 76 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
77 77 self.request.host
78 78 ))
79 79 return self.settings.get('cookie_name', default_cookie_name)
80 80
81 81 @property
82 82 def logged_in(self):
83 83 """Is a user currently logged in?"""
84 84 user = self.get_current_user()
85 85 return (user and not user == 'anonymous')
86 86
87 87 @property
88 88 def login_handler(self):
89 89 """Return the login handler for this application, if any."""
90 90 return self.settings.get('login_handler_class', None)
91 91
92 92 @property
93 93 def login_available(self):
94 94 """May a user proceed to log in?
95 95
96 96 This returns True if login capability is available, irrespective of
97 97 whether the user is already logged in or not.
98 98
99 99 """
100 100 if self.login_handler is None:
101 101 return False
102 102 return bool(self.login_handler.login_available(self.settings))
103 103
104 104
105 105 class IPythonHandler(AuthenticatedHandler):
106 106 """IPython-specific extensions to authenticated handling
107 107
108 108 Mostly property shortcuts to IPython-specific settings.
109 109 """
110 110
111 111 @property
112 112 def config(self):
113 113 return self.settings.get('config', None)
114 114
115 115 @property
116 116 def log(self):
117 117 """use the IPython log by default, falling back on tornado's logger"""
118 118 if Application.initialized():
119 119 return Application.instance().log
120 120 else:
121 121 return app_log
122 122
123 123 @property
124 124 def jinja_template_vars(self):
125 125 """User-supplied values to supply to jinja templates."""
126 126 return self.settings.get('jinja_template_vars', {})
127 127
128 128 #---------------------------------------------------------------
129 129 # URLs
130 130 #---------------------------------------------------------------
131 131
132 132 @property
133 133 def version_hash(self):
134 134 """The version hash to use for cache hints for static files"""
135 135 return self.settings.get('version_hash', '')
136 136
137 137 @property
138 138 def mathjax_url(self):
139 139 return self.settings.get('mathjax_url', '')
140 140
141 141 @property
142 142 def base_url(self):
143 143 return self.settings.get('base_url', '/')
144 144
145 145 @property
146 146 def default_url(self):
147 147 return self.settings.get('default_url', '')
148 148
149 149 @property
150 150 def ws_url(self):
151 151 return self.settings.get('websocket_url', '')
152 152
153 153 @property
154 154 def contents_js_source(self):
155 155 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
156 156 'services/contents'))
157 157 return self.settings.get('contents_js_source', 'services/contents')
158 158
159 159 #---------------------------------------------------------------
160 160 # Manager objects
161 161 #---------------------------------------------------------------
162 162
163 163 @property
164 164 def kernel_manager(self):
165 165 return self.settings['kernel_manager']
166 166
167 167 @property
168 168 def contents_manager(self):
169 169 return self.settings['contents_manager']
170 170
171 171 @property
172 172 def cluster_manager(self):
173 173 return self.settings['cluster_manager']
174 174
175 175 @property
176 176 def session_manager(self):
177 177 return self.settings['session_manager']
178 178
179 179 @property
180 180 def terminal_manager(self):
181 181 return self.settings['terminal_manager']
182 182
183 183 @property
184 184 def kernel_spec_manager(self):
185 185 return self.settings['kernel_spec_manager']
186 186
187 187 @property
188 188 def config_manager(self):
189 189 return self.settings['config_manager']
190 190
191 191 #---------------------------------------------------------------
192 192 # CORS
193 193 #---------------------------------------------------------------
194 194
195 195 @property
196 196 def allow_origin(self):
197 197 """Normal Access-Control-Allow-Origin"""
198 198 return self.settings.get('allow_origin', '')
199 199
200 200 @property
201 201 def allow_origin_pat(self):
202 202 """Regular expression version of allow_origin"""
203 203 return self.settings.get('allow_origin_pat', None)
204 204
205 205 @property
206 206 def allow_credentials(self):
207 207 """Whether to set Access-Control-Allow-Credentials"""
208 208 return self.settings.get('allow_credentials', False)
209 209
210 210 def set_default_headers(self):
211 211 """Add CORS headers, if defined"""
212 212 super(IPythonHandler, self).set_default_headers()
213 213 if self.allow_origin:
214 214 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
215 215 elif self.allow_origin_pat:
216 216 origin = self.get_origin()
217 217 if origin and self.allow_origin_pat.match(origin):
218 218 self.set_header("Access-Control-Allow-Origin", origin)
219 219 if self.allow_credentials:
220 220 self.set_header("Access-Control-Allow-Credentials", 'true')
221 221
222 222 def get_origin(self):
223 223 # Handle WebSocket Origin naming convention differences
224 224 # The difference between version 8 and 13 is that in 8 the
225 225 # client sends a "Sec-Websocket-Origin" header and in 13 it's
226 226 # simply "Origin".
227 227 if "Origin" in self.request.headers:
228 228 origin = self.request.headers.get("Origin")
229 229 else:
230 230 origin = self.request.headers.get("Sec-Websocket-Origin", None)
231 231 return origin
232 232
233 233 #---------------------------------------------------------------
234 234 # template rendering
235 235 #---------------------------------------------------------------
236 236
237 237 def get_template(self, name):
238 238 """Return the jinja template object for a given name"""
239 239 return self.settings['jinja2_env'].get_template(name)
240 240
241 241 def render_template(self, name, **ns):
242 242 ns.update(self.template_namespace)
243 243 template = self.get_template(name)
244 244 return template.render(**ns)
245 245
246 246 @property
247 247 def template_namespace(self):
248 248 return dict(
249 249 base_url=self.base_url,
250 250 default_url=self.default_url,
251 251 ws_url=self.ws_url,
252 252 logged_in=self.logged_in,
253 253 login_available=self.login_available,
254 254 static_url=self.static_url,
255 255 sys_info=sys_info,
256 256 contents_js_source=self.contents_js_source,
257 257 version_hash=self.version_hash,
258 258 **self.jinja_template_vars
259 259 )
260 260
261 261 def get_json_body(self):
262 262 """Return the body of the request as JSON data."""
263 263 if not self.request.body:
264 264 return None
265 265 # Do we need to call body.decode('utf-8') here?
266 266 body = self.request.body.strip().decode(u'utf-8')
267 267 try:
268 268 model = json.loads(body)
269 269 except Exception:
270 270 self.log.debug("Bad JSON: %r", body)
271 271 self.log.error("Couldn't parse JSON", exc_info=True)
272 272 raise web.HTTPError(400, u'Invalid JSON in body of request')
273 273 return model
274 274
275 275 def write_error(self, status_code, **kwargs):
276 276 """render custom error pages"""
277 277 exc_info = kwargs.get('exc_info')
278 278 message = ''
279 279 status_message = responses.get(status_code, 'Unknown HTTP Error')
280 280 if exc_info:
281 281 exception = exc_info[1]
282 282 # get the custom message, if defined
283 283 try:
284 284 message = exception.log_message % exception.args
285 285 except Exception:
286 286 pass
287 287
288 288 # construct the custom reason, if defined
289 289 reason = getattr(exception, 'reason', '')
290 290 if reason:
291 291 status_message = reason
292 292
293 293 # build template namespace
294 294 ns = dict(
295 295 status_code=status_code,
296 296 status_message=status_message,
297 297 message=message,
298 298 exception=exception,
299 299 )
300 300
301 301 self.set_header('Content-Type', 'text/html')
302 302 # render the template
303 303 try:
304 304 html = self.render_template('%s.html' % status_code, **ns)
305 305 except TemplateNotFound:
306 306 self.log.debug("No template for %d", status_code)
307 307 html = self.render_template('error.html', **ns)
308 308
309 309 self.write(html)
310 310
311 311
312 312
313 313 class Template404(IPythonHandler):
314 314 """Render our 404 template"""
315 315 def prepare(self):
316 316 raise web.HTTPError(404)
317 317
318 318
319 319 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
320 320 """static files should only be accessible when logged in"""
321 321
322 322 @web.authenticated
323 323 def get(self, path):
324 324 if os.path.splitext(path)[1] == '.ipynb':
325 325 name = path.rsplit('/', 1)[-1]
326 326 self.set_header('Content-Type', 'application/json')
327 327 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
328 328
329 329 return web.StaticFileHandler.get(self, path)
330 330
331 331 def set_headers(self):
332 332 super(AuthenticatedFileHandler, self).set_headers()
333 333 # disable browser caching, rely on 304 replies for savings
334 334 if "v" not in self.request.arguments:
335 335 self.add_header("Cache-Control", "no-cache")
336 336
337 337 def compute_etag(self):
338 338 return None
339 339
340 340 def validate_absolute_path(self, root, absolute_path):
341 341 """Validate and return the absolute path.
342 342
343 343 Requires tornado 3.1
344 344
345 345 Adding to tornado's own handling, forbids the serving of hidden files.
346 346 """
347 347 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
348 348 abs_root = os.path.abspath(root)
349 349 if is_hidden(abs_path, abs_root):
350 350 self.log.info("Refusing to serve hidden file, via 404 Error")
351 351 raise web.HTTPError(404)
352 352 return abs_path
353 353
354 354
355 355 def json_errors(method):
356 356 """Decorate methods with this to return GitHub style JSON errors.
357 357
358 358 This should be used on any JSON API on any handler method that can raise HTTPErrors.
359 359
360 360 This will grab the latest HTTPError exception using sys.exc_info
361 361 and then:
362 362
363 363 1. Set the HTTP status code based on the HTTPError
364 364 2. Create and return a JSON body with a message field describing
365 365 the error in a human readable form.
366 366 """
367 367 @functools.wraps(method)
368 368 @gen.coroutine
369 369 def wrapper(self, *args, **kwargs):
370 370 try:
371 371 result = yield gen.maybe_future(method(self, *args, **kwargs))
372 372 except web.HTTPError as e:
373 373 status = e.status_code
374 374 message = e.log_message
375 375 self.log.warn(message)
376 376 self.set_status(e.status_code)
377 377 reply = dict(message=message, reason=e.reason)
378 378 self.finish(json.dumps(reply))
379 379 except Exception:
380 380 self.log.error("Unhandled error in API request", exc_info=True)
381 381 status = 500
382 382 message = "Unknown server error"
383 383 t, value, tb = sys.exc_info()
384 384 self.set_status(status)
385 385 tb_text = ''.join(traceback.format_exception(t, value, tb))
386 386 reply = dict(message=message, reason=None, traceback=tb_text)
387 387 self.finish(json.dumps(reply))
388 388 else:
389 389 # FIXME: can use regular return in generators in py3
390 390 raise gen.Return(result)
391 391 return wrapper
392 392
393 393
394 394
395 395 #-----------------------------------------------------------------------------
396 396 # File handler
397 397 #-----------------------------------------------------------------------------
398 398
399 399 # to minimize subclass changes:
400 400 HTTPError = web.HTTPError
401 401
402 402 class FileFindHandler(web.StaticFileHandler):
403 403 """subclass of StaticFileHandler for serving files from a search path"""
404 404
405 405 # cache search results, don't search for files more than once
406 406 _static_paths = {}
407 407
408 408 def set_headers(self):
409 409 super(FileFindHandler, self).set_headers()
410 410 # disable browser caching, rely on 304 replies for savings
411 411 if "v" not in self.request.arguments or \
412 412 any(self.request.path.startswith(path) for path in self.no_cache_paths):
413 413 self.set_header("Cache-Control", "no-cache")
414 414
415 415 def initialize(self, path, default_filename=None, no_cache_paths=None):
416 416 self.no_cache_paths = no_cache_paths or []
417 417
418 418 if isinstance(path, string_types):
419 419 path = [path]
420 420
421 421 self.root = tuple(
422 422 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
423 423 )
424 424 self.default_filename = default_filename
425 425
426 426 def compute_etag(self):
427 427 return None
428 428
429 429 @classmethod
430 430 def get_absolute_path(cls, roots, path):
431 431 """locate a file to serve on our static file search path"""
432 432 with cls._lock:
433 433 if path in cls._static_paths:
434 434 return cls._static_paths[path]
435 435 try:
436 436 abspath = os.path.abspath(filefind(path, roots))
437 437 except IOError:
438 438 # IOError means not found
439 439 return ''
440 440
441 441 cls._static_paths[path] = abspath
442 442 return abspath
443 443
444 444 def validate_absolute_path(self, root, absolute_path):
445 445 """check if the file should be served (raises 404, 403, etc.)"""
446 446 if absolute_path == '':
447 447 raise web.HTTPError(404)
448 448
449 449 for root in self.root:
450 450 if (absolute_path + os.sep).startswith(root):
451 451 break
452 452
453 453 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
454 454
455 455
456 456 class ApiVersionHandler(IPythonHandler):
457 457
458 458 @json_errors
459 459 def get(self):
460 460 # not authenticated, so give as few info as possible
461 461 self.finish(json.dumps({"version":IPython.__version__}))
462 462
463 463
464 464 class TrailingSlashHandler(web.RequestHandler):
465 465 """Simple redirect handler that strips trailing slashes
466 466
467 467 This should be the first, highest priority handler.
468 468 """
469 469
470 470 def get(self):
471 471 self.redirect(self.request.uri.rstrip('/'))
472 472
473 473 post = put = get
474 474
475 475
476 476 class FilesRedirectHandler(IPythonHandler):
477 477 """Handler for redirecting relative URLs to the /files/ handler"""
478 478
479 479 @staticmethod
480 480 def redirect_to_files(self, path):
481 481 """make redirect logic a reusable static method
482 482
483 483 so it can be called from other handlers.
484 484 """
485 485 cm = self.contents_manager
486 486 if cm.dir_exists(path):
487 487 # it's a *directory*, redirect to /tree
488 488 url = url_path_join(self.base_url, 'tree', path)
489 489 else:
490 490 orig_path = path
491 491 # otherwise, redirect to /files
492 492 parts = path.split('/')
493 493
494 494 if not cm.file_exists(path=path) and 'files' in parts:
495 495 # redirect without files/ iff it would 404
496 496 # this preserves pre-2.0-style 'files/' links
497 497 self.log.warn("Deprecated files/ URL: %s", orig_path)
498 498 parts.remove('files')
499 499 path = '/'.join(parts)
500 500
501 501 if not cm.file_exists(path=path):
502 502 raise web.HTTPError(404)
503 503
504 504 url = url_path_join(self.base_url, 'files', path)
505 505 url = url_escape(url)
506 506 self.log.debug("Redirecting %s to %s", self.request.path, url)
507 507 self.redirect(url)
508 508
509 509 def get(self, path=''):
510 510 return self.redirect_to_files(self, path)
511 511
512 512
513 513 #-----------------------------------------------------------------------------
514 514 # URL pattern fragments for re-use
515 515 #-----------------------------------------------------------------------------
516 516
517 517 # path matches any number of `/foo[/bar...]` or just `/` or ''
518 518 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
519 519
520 520 #-----------------------------------------------------------------------------
521 521 # URL to handler mappings
522 522 #-----------------------------------------------------------------------------
523 523
524 524
525 525 default_handlers = [
526 526 (r".*/", TrailingSlashHandler),
527 527 (r"api", ApiVersionHandler)
528 528 ]
@@ -1,281 +1,281
1 1 # coding: utf-8
2 2 """Tornado handlers for WebSocket <-> ZMQ sockets."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import os
8 8 import json
9 9 import struct
10 10 import warnings
11 11 import sys
12 12
13 13 try:
14 14 from urllib.parse import urlparse # Py 3
15 15 except ImportError:
16 16 from urlparse import urlparse # Py 2
17 17
18 18 import tornado
19 19 from tornado import gen, ioloop, web
20 20 from tornado.websocket import WebSocketHandler
21 21
22 22 from IPython.kernel.zmq.session import Session
23 23 from jupyter_client.jsonutil import date_default, extract_dates
24 24 from IPython.utils.py3compat import cast_unicode
25 25
26 26 from .handlers import IPythonHandler
27 27
28 28 def serialize_binary_message(msg):
29 29 """serialize a message as a binary blob
30 30
31 31 Header:
32 32
33 33 4 bytes: number of msg parts (nbufs) as 32b int
34 34 4 * nbufs bytes: offset for each buffer as integer as 32b int
35 35
36 36 Offsets are from the start of the buffer, including the header.
37 37
38 38 Returns
39 39 -------
40 40
41 41 The message serialized to bytes.
42 42
43 43 """
44 44 # don't modify msg or buffer list in-place
45 45 msg = msg.copy()
46 46 buffers = list(msg.pop('buffers'))
47 47 if sys.version_info < (3, 4):
48 48 buffers = [x.tobytes() for x in buffers]
49 49 bmsg = json.dumps(msg, default=date_default).encode('utf8')
50 50 buffers.insert(0, bmsg)
51 51 nbufs = len(buffers)
52 52 offsets = [4 * (nbufs + 1)]
53 53 for buf in buffers[:-1]:
54 54 offsets.append(offsets[-1] + len(buf))
55 55 offsets_buf = struct.pack('!' + 'I' * (nbufs + 1), nbufs, *offsets)
56 56 buffers.insert(0, offsets_buf)
57 57 return b''.join(buffers)
58 58
59 59
60 60 def deserialize_binary_message(bmsg):
61 61 """deserialize a message from a binary blog
62 62
63 63 Header:
64 64
65 65 4 bytes: number of msg parts (nbufs) as 32b int
66 66 4 * nbufs bytes: offset for each buffer as integer as 32b int
67 67
68 68 Offsets are from the start of the buffer, including the header.
69 69
70 70 Returns
71 71 -------
72 72
73 73 message dictionary
74 74 """
75 75 nbufs = struct.unpack('!i', bmsg[:4])[0]
76 76 offsets = list(struct.unpack('!' + 'I' * nbufs, bmsg[4:4*(nbufs+1)]))
77 77 offsets.append(None)
78 78 bufs = []
79 79 for start, stop in zip(offsets[:-1], offsets[1:]):
80 80 bufs.append(bmsg[start:stop])
81 81 msg = json.loads(bufs[0].decode('utf8'))
82 82 msg['header'] = extract_dates(msg['header'])
83 83 msg['parent_header'] = extract_dates(msg['parent_header'])
84 84 msg['buffers'] = bufs[1:]
85 85 return msg
86 86
87 87 # ping interval for keeping websockets alive (30 seconds)
88 88 WS_PING_INTERVAL = 30000
89 89
90 90 if os.environ.get('IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS', False):
91 91 warnings.warn("""Allowing draft76 websocket connections!
92 92 This should only be done for testing with phantomjs!""")
93 from IPython.html import allow76
93 from jupyter_notebook import allow76
94 94 WebSocketHandler = allow76.AllowDraftWebSocketHandler
95 95 # draft 76 doesn't support ping
96 96 WS_PING_INTERVAL = 0
97 97
98 98 class ZMQStreamHandler(WebSocketHandler):
99 99
100 100 if tornado.version_info < (4,1):
101 101 """Backport send_error from tornado 4.1 to 4.0"""
102 102 def send_error(self, *args, **kwargs):
103 103 if self.stream is None:
104 104 super(WebSocketHandler, self).send_error(*args, **kwargs)
105 105 else:
106 106 # If we get an uncaught exception during the handshake,
107 107 # we have no choice but to abruptly close the connection.
108 108 # TODO: for uncaught exceptions after the handshake,
109 109 # we can close the connection more gracefully.
110 110 self.stream.close()
111 111
112 112
113 113 def check_origin(self, origin):
114 114 """Check Origin == Host or Access-Control-Allow-Origin.
115 115
116 116 Tornado >= 4 calls this method automatically, raising 403 if it returns False.
117 117 """
118 118 if self.allow_origin == '*':
119 119 return True
120 120
121 121 host = self.request.headers.get("Host")
122 122
123 123 # If no header is provided, assume we can't verify origin
124 124 if origin is None:
125 125 self.log.warn("Missing Origin header, rejecting WebSocket connection.")
126 126 return False
127 127 if host is None:
128 128 self.log.warn("Missing Host header, rejecting WebSocket connection.")
129 129 return False
130 130
131 131 origin = origin.lower()
132 132 origin_host = urlparse(origin).netloc
133 133
134 134 # OK if origin matches host
135 135 if origin_host == host:
136 136 return True
137 137
138 138 # Check CORS headers
139 139 if self.allow_origin:
140 140 allow = self.allow_origin == origin
141 141 elif self.allow_origin_pat:
142 142 allow = bool(self.allow_origin_pat.match(origin))
143 143 else:
144 144 # No CORS headers deny the request
145 145 allow = False
146 146 if not allow:
147 147 self.log.warn("Blocking Cross Origin WebSocket Attempt. Origin: %s, Host: %s",
148 148 origin, host,
149 149 )
150 150 return allow
151 151
152 152 def clear_cookie(self, *args, **kwargs):
153 153 """meaningless for websockets"""
154 154 pass
155 155
156 156 def _reserialize_reply(self, msg_list, channel=None):
157 157 """Reserialize a reply message using JSON.
158 158
159 159 This takes the msg list from the ZMQ socket, deserializes it using
160 160 self.session and then serializes the result using JSON. This method
161 161 should be used by self._on_zmq_reply to build messages that can
162 162 be sent back to the browser.
163 163 """
164 164 idents, msg_list = self.session.feed_identities(msg_list)
165 165 msg = self.session.deserialize(msg_list)
166 166 if channel:
167 167 msg['channel'] = channel
168 168 if msg['buffers']:
169 169 buf = serialize_binary_message(msg)
170 170 return buf
171 171 else:
172 172 smsg = json.dumps(msg, default=date_default)
173 173 return cast_unicode(smsg)
174 174
175 175 def _on_zmq_reply(self, stream, msg_list):
176 176 # Sometimes this gets triggered when the on_close method is scheduled in the
177 177 # eventloop but hasn't been called.
178 178 if self.stream.closed() or stream.closed():
179 179 self.log.warn("zmq message arrived on closed channel")
180 180 self.close()
181 181 return
182 182 channel = getattr(stream, 'channel', None)
183 183 try:
184 184 msg = self._reserialize_reply(msg_list, channel=channel)
185 185 except Exception:
186 186 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
187 187 else:
188 188 self.write_message(msg, binary=isinstance(msg, bytes))
189 189
190 190 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
191 191 ping_callback = None
192 192 last_ping = 0
193 193 last_pong = 0
194 194
195 195 @property
196 196 def ping_interval(self):
197 197 """The interval for websocket keep-alive pings.
198 198
199 199 Set ws_ping_interval = 0 to disable pings.
200 200 """
201 201 return self.settings.get('ws_ping_interval', WS_PING_INTERVAL)
202 202
203 203 @property
204 204 def ping_timeout(self):
205 205 """If no ping is received in this many milliseconds,
206 206 close the websocket connection (VPNs, etc. can fail to cleanly close ws connections).
207 207 Default is max of 3 pings or 30 seconds.
208 208 """
209 209 return self.settings.get('ws_ping_timeout',
210 210 max(3 * self.ping_interval, WS_PING_INTERVAL)
211 211 )
212 212
213 213 def set_default_headers(self):
214 214 """Undo the set_default_headers in IPythonHandler
215 215
216 216 which doesn't make sense for websockets
217 217 """
218 218 pass
219 219
220 220 def pre_get(self):
221 221 """Run before finishing the GET request
222 222
223 223 Extend this method to add logic that should fire before
224 224 the websocket finishes completing.
225 225 """
226 226 # authenticate the request before opening the websocket
227 227 if self.get_current_user() is None:
228 228 self.log.warn("Couldn't authenticate WebSocket connection")
229 229 raise web.HTTPError(403)
230 230
231 231 if self.get_argument('session_id', False):
232 232 self.session.session = cast_unicode(self.get_argument('session_id'))
233 233 else:
234 234 self.log.warn("No session ID specified")
235 235
236 236 @gen.coroutine
237 237 def get(self, *args, **kwargs):
238 238 # pre_get can be a coroutine in subclasses
239 239 # assign and yield in two step to avoid tornado 3 issues
240 240 res = self.pre_get()
241 241 yield gen.maybe_future(res)
242 242 super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
243 243
244 244 def initialize(self):
245 245 self.log.debug("Initializing websocket connection %s", self.request.path)
246 246 self.session = Session(config=self.config)
247 247
248 248 def open(self, *args, **kwargs):
249 249 self.log.debug("Opening websocket %s", self.request.path)
250 250
251 251 # start the pinging
252 252 if self.ping_interval > 0:
253 253 loop = ioloop.IOLoop.current()
254 254 self.last_ping = loop.time() # Remember time of last ping
255 255 self.last_pong = self.last_ping
256 256 self.ping_callback = ioloop.PeriodicCallback(
257 257 self.send_ping, self.ping_interval, io_loop=loop,
258 258 )
259 259 self.ping_callback.start()
260 260
261 261 def send_ping(self):
262 262 """send a ping to keep the websocket alive"""
263 263 if self.stream.closed() and self.ping_callback is not None:
264 264 self.ping_callback.stop()
265 265 return
266 266
267 267 # check for timeout on pong. Make sure that we really have sent a recent ping in
268 268 # case the machine with both server and client has been suspended since the last ping.
269 269 now = ioloop.IOLoop.current().time()
270 270 since_last_pong = 1e3 * (now - self.last_pong)
271 271 since_last_ping = 1e3 * (now - self.last_ping)
272 272 if since_last_ping < 2*self.ping_interval and since_last_pong > self.ping_timeout:
273 273 self.log.warn("WebSocket ping timeout after %i ms.", since_last_pong)
274 274 self.close()
275 275 return
276 276
277 277 self.ping(b'')
278 278 self.last_ping = now
279 279
280 280 def on_pong(self, data):
281 281 self.last_pong = ioloop.IOLoop.current().time()
@@ -1,55 +1,55
1 1 """Serve files directly from the ContentsManager."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import os
7 7 import mimetypes
8 8 import json
9 9 import base64
10 10
11 11 from tornado import web
12 12
13 from IPython.html.base.handlers import IPythonHandler
13 from jupyter_notebook.base.handlers import IPythonHandler
14 14
15 15 class FilesHandler(IPythonHandler):
16 16 """serve files via ContentsManager"""
17 17
18 18 @web.authenticated
19 19 def get(self, path):
20 20 cm = self.contents_manager
21 21 if cm.is_hidden(path):
22 22 self.log.info("Refusing to serve hidden file, via 404 Error")
23 23 raise web.HTTPError(404)
24 24
25 25 path = path.strip('/')
26 26 if '/' in path:
27 27 _, name = path.rsplit('/', 1)
28 28 else:
29 29 name = path
30 30
31 31 model = cm.get(path, type='file')
32 32
33 33 if self.get_argument("download", False):
34 34 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
35 35
36 36 # get mimetype from filename
37 37 if name.endswith('.ipynb'):
38 38 self.set_header('Content-Type', 'application/json')
39 39 else:
40 40 cur_mime = mimetypes.guess_type(name)[0]
41 41 if cur_mime is not None:
42 42 self.set_header('Content-Type', cur_mime)
43 43
44 44 if model['format'] == 'base64':
45 45 b64_bytes = model['content'].encode('ascii')
46 46 self.write(base64.decodestring(b64_bytes))
47 47 elif model['format'] == 'json':
48 48 self.write(json.dumps(model['content']))
49 49 else:
50 50 self.write(model['content'])
51 51 self.flush()
52 52
53 53 default_handlers = [
54 54 (r"/files/(.*)", FilesHandler),
55 55 ] No newline at end of file
@@ -1,132 +1,132
1 1 # coding: utf-8
2 2 import base64
3 3 import io
4 4 import json
5 5 import os
6 6 from os.path import join as pjoin
7 7 import shutil
8 8
9 9 import requests
10 10
11 from IPython.html.utils import url_path_join
12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
11 from jupyter_notebook.utils import url_path_join
12 from jupyter_notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
13 13 from IPython.nbformat import write
14 14 from IPython.nbformat.v4 import (
15 15 new_notebook, new_markdown_cell, new_code_cell, new_output,
16 16 )
17 17
18 18 from IPython.testing.decorators import onlyif_cmds_exist
19 19
20 20
21 21 class NbconvertAPI(object):
22 22 """Wrapper for nbconvert API calls."""
23 23 def __init__(self, base_url):
24 24 self.base_url = base_url
25 25
26 26 def _req(self, verb, path, body=None, params=None):
27 27 response = requests.request(verb,
28 28 url_path_join(self.base_url, 'nbconvert', path),
29 29 data=body, params=params,
30 30 )
31 31 response.raise_for_status()
32 32 return response
33 33
34 34 def from_file(self, format, path, name, download=False):
35 35 return self._req('GET', url_path_join(format, path, name),
36 36 params={'download':download})
37 37
38 38 def from_post(self, format, nbmodel):
39 39 body = json.dumps(nbmodel)
40 40 return self._req('POST', format, body)
41 41
42 42 def list_formats(self):
43 43 return self._req('GET', '')
44 44
45 45 png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
46 46 b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
47 47 b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82'
48 48 ).decode('ascii')
49 49
50 50 class APITest(NotebookTestBase):
51 51 def setUp(self):
52 52 nbdir = self.notebook_dir.name
53 53
54 54 if not os.path.isdir(pjoin(nbdir, 'foo')):
55 55 os.mkdir(pjoin(nbdir, 'foo'))
56 56
57 57 nb = new_notebook()
58 58
59 59 nb.cells.append(new_markdown_cell(u'Created by test ³'))
60 60 cc1 = new_code_cell(source=u'print(2*6)')
61 61 cc1.outputs.append(new_output(output_type="stream", text=u'12'))
62 62 cc1.outputs.append(new_output(output_type="execute_result",
63 63 data={'image/png' : png_green_pixel},
64 64 execution_count=1,
65 65 ))
66 66 nb.cells.append(cc1)
67 67
68 68 with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
69 69 encoding='utf-8') as f:
70 70 write(nb, f, version=4)
71 71
72 72 self.nbconvert_api = NbconvertAPI(self.base_url())
73 73
74 74 def tearDown(self):
75 75 nbdir = self.notebook_dir.name
76 76
77 77 for dname in ['foo']:
78 78 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
79 79
80 80 @onlyif_cmds_exist('pandoc')
81 81 def test_from_file(self):
82 82 r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')
83 83 self.assertEqual(r.status_code, 200)
84 84 self.assertIn(u'text/html', r.headers['Content-Type'])
85 85 self.assertIn(u'Created by test', r.text)
86 86 self.assertIn(u'print', r.text)
87 87
88 88 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb')
89 89 self.assertIn(u'text/x-python', r.headers['Content-Type'])
90 90 self.assertIn(u'print(2*6)', r.text)
91 91
92 92 @onlyif_cmds_exist('pandoc')
93 93 def test_from_file_404(self):
94 94 with assert_http_error(404):
95 95 self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb')
96 96
97 97 @onlyif_cmds_exist('pandoc')
98 98 def test_from_file_download(self):
99 99 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True)
100 100 content_disposition = r.headers['Content-Disposition']
101 101 self.assertIn('attachment', content_disposition)
102 102 self.assertIn('testnb.py', content_disposition)
103 103
104 104 @onlyif_cmds_exist('pandoc')
105 105 def test_from_file_zip(self):
106 106 r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True)
107 107 self.assertIn(u'application/zip', r.headers['Content-Type'])
108 108 self.assertIn(u'.zip', r.headers['Content-Disposition'])
109 109
110 110 @onlyif_cmds_exist('pandoc')
111 111 def test_from_post(self):
112 112 nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
113 113 nbmodel = requests.get(nbmodel_url).json()
114 114
115 115 r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
116 116 self.assertEqual(r.status_code, 200)
117 117 self.assertIn(u'text/html', r.headers['Content-Type'])
118 118 self.assertIn(u'Created by test', r.text)
119 119 self.assertIn(u'print', r.text)
120 120
121 121 r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel)
122 122 self.assertIn(u'text/x-python', r.headers['Content-Type'])
123 123 self.assertIn(u'print(2*6)', r.text)
124 124
125 125 @onlyif_cmds_exist('pandoc')
126 126 def test_from_post_zip(self):
127 127 nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
128 128 nbmodel = requests.get(nbmodel_url).json()
129 129
130 130 r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
131 131 self.assertIn(u'application/zip', r.headers['Content-Type'])
132 132 self.assertIn(u'.zip', r.headers['Content-Disposition'])
@@ -1,1148 +1,1148
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import datetime
11 11 import errno
12 12 import importlib
13 13 import io
14 14 import json
15 15 import logging
16 16 import os
17 17 import random
18 18 import re
19 19 import select
20 20 import signal
21 21 import socket
22 22 import ssl
23 23 import sys
24 24 import threading
25 25 import webbrowser
26 26
27 27
28 28 from jinja2 import Environment, FileSystemLoader
29 29
30 30 # Install the pyzmq ioloop. This has to be done before anything else from
31 31 # tornado is imported.
32 32 from zmq.eventloop import ioloop
33 33 ioloop.install()
34 34
35 35 # check for tornado 3.1.0
36 36 msg = "The IPython Notebook requires tornado >= 4.0"
37 37 try:
38 38 import tornado
39 39 except ImportError:
40 40 raise ImportError(msg)
41 41 try:
42 42 version_info = tornado.version_info
43 43 except AttributeError:
44 44 raise ImportError(msg + ", but you have < 1.1.0")
45 45 if version_info < (4,0):
46 46 raise ImportError(msg + ", but you have %s" % tornado.version)
47 47
48 48 from tornado import httpserver
49 49 from tornado import web
50 50 from tornado.log import LogFormatter, app_log, access_log, gen_log
51 51
52 from IPython.html import (
52 from jupyter_notebook import (
53 53 DEFAULT_STATIC_FILES_PATH,
54 54 DEFAULT_TEMPLATE_PATH_LIST,
55 55 )
56 56 from .base.handlers import Template404
57 57 from .log import log_request
58 58 from .services.kernels.kernelmanager import MappingKernelManager
59 59 from .services.config import ConfigManager
60 60 from .services.contents.manager import ContentsManager
61 61 from .services.contents.filemanager import FileContentsManager
62 62 from .services.clusters.clustermanager import ClusterManager
63 63 from .services.sessions.sessionmanager import SessionManager
64 64
65 65 from .auth.login import LoginHandler
66 66 from .auth.logout import LogoutHandler
67 67 from .base.handlers import IPythonHandler, FileFindHandler
68 68
69 69 from IPython.config import Config
70 70 from IPython.config.application import catch_config_error, boolean_flag
71 71 from IPython.core.application import (
72 72 BaseIPythonApplication, base_flags, base_aliases,
73 73 )
74 74 from IPython.core.profiledir import ProfileDir
75 75 from IPython.kernel import KernelManager
76 76 from IPython.kernel.kernelspec import KernelSpecManager, NoSuchKernel, NATIVE_KERNEL_NAME
77 77 from IPython.kernel.zmq.session import Session
78 78 from IPython.nbformat.sign import NotebookNotary
79 79 from IPython.utils.importstring import import_item
80 80 from IPython.utils import submodule
81 81 from IPython.utils.traitlets import (
82 82 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
83 83 TraitError, Type,
84 84 )
85 85 from IPython.utils import py3compat
86 86 from IPython.utils.path import filefind, get_ipython_dir
87 87 from IPython.utils.sysinfo import get_sys_info
88 88
89 89 from .nbextensions import SYSTEM_NBEXTENSIONS_DIRS
90 90 from .utils import url_path_join, check_pid
91 91
92 92 #-----------------------------------------------------------------------------
93 93 # Module globals
94 94 #-----------------------------------------------------------------------------
95 95
96 96 _examples = """
97 97 ipython notebook # start the notebook
98 98 ipython notebook --profile=sympy # use the sympy profile
99 99 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
100 100 """
101 101
102 102 #-----------------------------------------------------------------------------
103 103 # Helper functions
104 104 #-----------------------------------------------------------------------------
105 105
106 106 def random_ports(port, n):
107 107 """Generate a list of n random ports near the given port.
108 108
109 109 The first 5 ports will be sequential, and the remaining n-5 will be
110 110 randomly selected in the range [port-2*n, port+2*n].
111 111 """
112 112 for i in range(min(5, n)):
113 113 yield port + i
114 114 for i in range(n-5):
115 115 yield max(1, port + random.randint(-2*n, 2*n))
116 116
117 117 def load_handlers(name):
118 118 """Load the (URL pattern, handler) tuples for each component."""
119 name = 'IPython.html.' + name
119 name = 'jupyter_notebook.' + name
120 120 mod = __import__(name, fromlist=['default_handlers'])
121 121 return mod.default_handlers
122 122
123 123 #-----------------------------------------------------------------------------
124 124 # The Tornado web application
125 125 #-----------------------------------------------------------------------------
126 126
127 127 class NotebookWebApplication(web.Application):
128 128
129 129 def __init__(self, ipython_app, kernel_manager, contents_manager,
130 130 cluster_manager, session_manager, kernel_spec_manager,
131 131 config_manager, log,
132 132 base_url, default_url, settings_overrides, jinja_env_options):
133 133
134 134 settings = self.init_settings(
135 135 ipython_app, kernel_manager, contents_manager, cluster_manager,
136 136 session_manager, kernel_spec_manager, config_manager, log, base_url,
137 137 default_url, settings_overrides, jinja_env_options)
138 138 handlers = self.init_handlers(settings)
139 139
140 140 super(NotebookWebApplication, self).__init__(handlers, **settings)
141 141
142 142 def init_settings(self, ipython_app, kernel_manager, contents_manager,
143 143 cluster_manager, session_manager, kernel_spec_manager,
144 144 config_manager,
145 145 log, base_url, default_url, settings_overrides,
146 146 jinja_env_options=None):
147 147
148 148 _template_path = settings_overrides.get(
149 149 "template_path",
150 150 ipython_app.template_file_path,
151 151 )
152 152 if isinstance(_template_path, str):
153 153 _template_path = (_template_path,)
154 154 template_path = [os.path.expanduser(path) for path in _template_path]
155 155
156 156 jenv_opt = jinja_env_options if jinja_env_options else {}
157 157 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
158 158
159 159 sys_info = get_sys_info()
160 160 if sys_info['commit_source'] == 'repository':
161 161 # don't cache (rely on 304) when working from master
162 162 version_hash = ''
163 163 else:
164 164 # reset the cache on server restart
165 165 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
166 166
167 167 settings = dict(
168 168 # basics
169 169 log_function=log_request,
170 170 base_url=base_url,
171 171 default_url=default_url,
172 172 template_path=template_path,
173 173 static_path=ipython_app.static_file_path,
174 174 static_handler_class = FileFindHandler,
175 175 static_url_prefix = url_path_join(base_url,'/static/'),
176 176 static_handler_args = {
177 177 # don't cache custom.js
178 178 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
179 179 },
180 180 version_hash=version_hash,
181 181
182 182 # authentication
183 183 cookie_secret=ipython_app.cookie_secret,
184 184 login_url=url_path_join(base_url,'/login'),
185 185 login_handler_class=ipython_app.login_handler_class,
186 186 logout_handler_class=ipython_app.logout_handler_class,
187 187 password=ipython_app.password,
188 188
189 189 # managers
190 190 kernel_manager=kernel_manager,
191 191 contents_manager=contents_manager,
192 192 cluster_manager=cluster_manager,
193 193 session_manager=session_manager,
194 194 kernel_spec_manager=kernel_spec_manager,
195 195 config_manager=config_manager,
196 196
197 197 # IPython stuff
198 198 jinja_template_vars=ipython_app.jinja_template_vars,
199 199 nbextensions_path=ipython_app.nbextensions_path,
200 200 websocket_url=ipython_app.websocket_url,
201 201 mathjax_url=ipython_app.mathjax_url,
202 202 config=ipython_app.config,
203 203 jinja2_env=env,
204 204 terminals_available=False, # Set later if terminals are available
205 205 )
206 206
207 207 # allow custom overrides for the tornado web app.
208 208 settings.update(settings_overrides)
209 209 return settings
210 210
211 211 def init_handlers(self, settings):
212 212 """Load the (URL pattern, handler) tuples for each component."""
213 213
214 214 # Order matters. The first handler to match the URL will handle the request.
215 215 handlers = []
216 216 handlers.extend(load_handlers('tree.handlers'))
217 217 handlers.extend([(r"/login", settings['login_handler_class'])])
218 218 handlers.extend([(r"/logout", settings['logout_handler_class'])])
219 219 handlers.extend(load_handlers('files.handlers'))
220 220 handlers.extend(load_handlers('notebook.handlers'))
221 221 handlers.extend(load_handlers('nbconvert.handlers'))
222 222 handlers.extend(load_handlers('kernelspecs.handlers'))
223 223 handlers.extend(load_handlers('edit.handlers'))
224 224 handlers.extend(load_handlers('services.config.handlers'))
225 225 handlers.extend(load_handlers('services.kernels.handlers'))
226 226 handlers.extend(load_handlers('services.contents.handlers'))
227 227 handlers.extend(load_handlers('services.clusters.handlers'))
228 228 handlers.extend(load_handlers('services.sessions.handlers'))
229 229 handlers.extend(load_handlers('services.nbconvert.handlers'))
230 230 handlers.extend(load_handlers('services.kernelspecs.handlers'))
231 231 handlers.extend(load_handlers('services.security.handlers'))
232 232 handlers.append(
233 233 (r"/nbextensions/(.*)", FileFindHandler, {
234 234 'path': settings['nbextensions_path'],
235 235 'no_cache_paths': ['/'], # don't cache anything in nbextensions
236 236 }),
237 237 )
238 238 # register base handlers last
239 239 handlers.extend(load_handlers('base.handlers'))
240 240 # set the URL that will be redirected from `/`
241 241 handlers.append(
242 242 (r'/?', web.RedirectHandler, {
243 243 'url' : settings['default_url'],
244 244 'permanent': False, # want 302, not 301
245 245 })
246 246 )
247 247 # prepend base_url onto the patterns that we match
248 248 new_handlers = []
249 249 for handler in handlers:
250 250 pattern = url_path_join(settings['base_url'], handler[0])
251 251 new_handler = tuple([pattern] + list(handler[1:]))
252 252 new_handlers.append(new_handler)
253 253 # add 404 on the end, which will catch everything that falls through
254 254 new_handlers.append((r'(.*)', Template404))
255 255 return new_handlers
256 256
257 257
258 258 class NbserverListApp(BaseIPythonApplication):
259 259
260 260 description="List currently running notebook servers in this profile."
261 261
262 262 flags = dict(
263 263 json=({'NbserverListApp': {'json': True}},
264 264 "Produce machine-readable JSON output."),
265 265 )
266 266
267 267 json = Bool(False, config=True,
268 268 help="If True, each line of output will be a JSON object with the "
269 269 "details from the server info file.")
270 270
271 271 def start(self):
272 272 if not self.json:
273 273 print("Currently running servers:")
274 274 for serverinfo in list_running_servers(self.profile):
275 275 if self.json:
276 276 print(json.dumps(serverinfo))
277 277 else:
278 278 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
279 279
280 280 #-----------------------------------------------------------------------------
281 281 # Aliases and Flags
282 282 #-----------------------------------------------------------------------------
283 283
284 284 flags = dict(base_flags)
285 285 flags['no-browser']=(
286 286 {'NotebookApp' : {'open_browser' : False}},
287 287 "Don't open the notebook in a browser after startup."
288 288 )
289 289 flags['pylab']=(
290 290 {'NotebookApp' : {'pylab' : 'warn'}},
291 291 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
292 292 )
293 293 flags['no-mathjax']=(
294 294 {'NotebookApp' : {'enable_mathjax' : False}},
295 295 """Disable MathJax
296 296
297 297 MathJax is the javascript library IPython uses to render math/LaTeX. It is
298 298 very large, so you may want to disable it if you have a slow internet
299 299 connection, or for offline use of the notebook.
300 300
301 301 When disabled, equations etc. will appear as their untransformed TeX source.
302 302 """
303 303 )
304 304
305 305 # Add notebook manager flags
306 306 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
307 307 'DEPRECATED, IGNORED',
308 308 'DEPRECATED, IGNORED'))
309 309
310 310 aliases = dict(base_aliases)
311 311
312 312 aliases.update({
313 313 'ip': 'NotebookApp.ip',
314 314 'port': 'NotebookApp.port',
315 315 'port-retries': 'NotebookApp.port_retries',
316 316 'transport': 'KernelManager.transport',
317 317 'keyfile': 'NotebookApp.keyfile',
318 318 'certfile': 'NotebookApp.certfile',
319 319 'notebook-dir': 'NotebookApp.notebook_dir',
320 320 'browser': 'NotebookApp.browser',
321 321 'pylab': 'NotebookApp.pylab',
322 322 })
323 323
324 324 #-----------------------------------------------------------------------------
325 325 # NotebookApp
326 326 #-----------------------------------------------------------------------------
327 327
328 328 class NotebookApp(BaseIPythonApplication):
329 329
330 330 name = 'ipython-notebook'
331 331
332 332 description = """
333 333 The IPython HTML Notebook.
334 334
335 335 This launches a Tornado based HTML Notebook Server that serves up an
336 336 HTML5/Javascript Notebook client.
337 337 """
338 338 examples = _examples
339 339 aliases = aliases
340 340 flags = flags
341 341
342 342 classes = [
343 343 KernelManager, ProfileDir, Session, MappingKernelManager,
344 344 ContentsManager, FileContentsManager, NotebookNotary,
345 345 KernelSpecManager,
346 346 ]
347 347 flags = Dict(flags)
348 348 aliases = Dict(aliases)
349 349
350 350 subcommands = dict(
351 351 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
352 352 )
353 353
354 354 _log_formatter_cls = LogFormatter
355 355
356 356 def _log_level_default(self):
357 357 return logging.INFO
358 358
359 359 def _log_datefmt_default(self):
360 360 """Exclude date from default date format"""
361 361 return "%H:%M:%S"
362 362
363 363 def _log_format_default(self):
364 364 """override default log format to include time"""
365 365 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
366 366
367 367 # create requested profiles by default, if they don't exist:
368 368 auto_create = Bool(True)
369 369
370 370 # file to be opened in the notebook server
371 371 file_to_run = Unicode('', config=True)
372 372
373 373 # Network related information
374 374
375 375 allow_origin = Unicode('', config=True,
376 376 help="""Set the Access-Control-Allow-Origin header
377 377
378 378 Use '*' to allow any origin to access your server.
379 379
380 380 Takes precedence over allow_origin_pat.
381 381 """
382 382 )
383 383
384 384 allow_origin_pat = Unicode('', config=True,
385 385 help="""Use a regular expression for the Access-Control-Allow-Origin header
386 386
387 387 Requests from an origin matching the expression will get replies with:
388 388
389 389 Access-Control-Allow-Origin: origin
390 390
391 391 where `origin` is the origin of the request.
392 392
393 393 Ignored if allow_origin is set.
394 394 """
395 395 )
396 396
397 397 allow_credentials = Bool(False, config=True,
398 398 help="Set the Access-Control-Allow-Credentials: true header"
399 399 )
400 400
401 401 default_url = Unicode('/tree', config=True,
402 402 help="The default URL to redirect to from `/`"
403 403 )
404 404
405 405 ip = Unicode('localhost', config=True,
406 406 help="The IP address the notebook server will listen on."
407 407 )
408 408 def _ip_default(self):
409 409 """Return localhost if available, 127.0.0.1 otherwise.
410 410
411 411 On some (horribly broken) systems, localhost cannot be bound.
412 412 """
413 413 s = socket.socket()
414 414 try:
415 415 s.bind(('localhost', 0))
416 416 except socket.error as e:
417 417 self.log.warn("Cannot bind to localhost, using 127.0.0.1 as default ip\n%s", e)
418 418 return '127.0.0.1'
419 419 else:
420 420 s.close()
421 421 return 'localhost'
422 422
423 423 def _ip_changed(self, name, old, new):
424 424 if new == u'*': self.ip = u''
425 425
426 426 port = Integer(8888, config=True,
427 427 help="The port the notebook server will listen on."
428 428 )
429 429 port_retries = Integer(50, config=True,
430 430 help="The number of additional ports to try if the specified port is not available."
431 431 )
432 432
433 433 certfile = Unicode(u'', config=True,
434 434 help="""The full path to an SSL/TLS certificate file."""
435 435 )
436 436
437 437 keyfile = Unicode(u'', config=True,
438 438 help="""The full path to a private key file for usage with SSL/TLS."""
439 439 )
440 440
441 441 cookie_secret_file = Unicode(config=True,
442 442 help="""The file where the cookie secret is stored."""
443 443 )
444 444 def _cookie_secret_file_default(self):
445 445 if self.profile_dir is None:
446 446 return ''
447 447 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
448 448
449 449 cookie_secret = Bytes(b'', config=True,
450 450 help="""The random bytes used to secure cookies.
451 451 By default this is a new random number every time you start the Notebook.
452 452 Set it to a value in a config file to enable logins to persist across server sessions.
453 453
454 454 Note: Cookie secrets should be kept private, do not share config files with
455 455 cookie_secret stored in plaintext (you can read the value from a file).
456 456 """
457 457 )
458 458 def _cookie_secret_default(self):
459 459 if os.path.exists(self.cookie_secret_file):
460 460 with io.open(self.cookie_secret_file, 'rb') as f:
461 461 return f.read()
462 462 else:
463 463 secret = base64.encodestring(os.urandom(1024))
464 464 self._write_cookie_secret_file(secret)
465 465 return secret
466 466
467 467 def _write_cookie_secret_file(self, secret):
468 468 """write my secret to my secret_file"""
469 469 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
470 470 with io.open(self.cookie_secret_file, 'wb') as f:
471 471 f.write(secret)
472 472 try:
473 473 os.chmod(self.cookie_secret_file, 0o600)
474 474 except OSError:
475 475 self.log.warn(
476 476 "Could not set permissions on %s",
477 477 self.cookie_secret_file
478 478 )
479 479
480 480 password = Unicode(u'', config=True,
481 481 help="""Hashed password to use for web authentication.
482 482
483 483 To generate, type in a python/IPython shell:
484 484
485 485 from IPython.lib import passwd; passwd()
486 486
487 487 The string should be of the form type:salt:hashed-password.
488 488 """
489 489 )
490 490
491 491 open_browser = Bool(True, config=True,
492 492 help="""Whether to open in a browser after starting.
493 493 The specific browser used is platform dependent and
494 494 determined by the python standard library `webbrowser`
495 495 module, unless it is overridden using the --browser
496 496 (NotebookApp.browser) configuration option.
497 497 """)
498 498
499 499 browser = Unicode(u'', config=True,
500 500 help="""Specify what command to use to invoke a web
501 501 browser when opening the notebook. If not specified, the
502 502 default browser will be determined by the `webbrowser`
503 503 standard library module, which allows setting of the
504 504 BROWSER environment variable to override it.
505 505 """)
506 506
507 507 webapp_settings = Dict(config=True,
508 508 help="DEPRECATED, use tornado_settings"
509 509 )
510 510 def _webapp_settings_changed(self, name, old, new):
511 511 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
512 512 self.tornado_settings = new
513 513
514 514 tornado_settings = Dict(config=True,
515 515 help="Supply overrides for the tornado.web.Application that the "
516 516 "IPython notebook uses.")
517 517
518 518 ssl_options = Dict(config=True,
519 519 help="""Supply SSL options for the tornado HTTPServer.
520 520 See the tornado docs for details.""")
521 521
522 522 jinja_environment_options = Dict(config=True,
523 523 help="Supply extra arguments that will be passed to Jinja environment.")
524 524
525 525 jinja_template_vars = Dict(
526 526 config=True,
527 527 help="Extra variables to supply to jinja templates when rendering.",
528 528 )
529 529
530 530 enable_mathjax = Bool(True, config=True,
531 531 help="""Whether to enable MathJax for typesetting math/TeX
532 532
533 533 MathJax is the javascript library IPython uses to render math/LaTeX. It is
534 534 very large, so you may want to disable it if you have a slow internet
535 535 connection, or for offline use of the notebook.
536 536
537 537 When disabled, equations etc. will appear as their untransformed TeX source.
538 538 """
539 539 )
540 540 def _enable_mathjax_changed(self, name, old, new):
541 541 """set mathjax url to empty if mathjax is disabled"""
542 542 if not new:
543 543 self.mathjax_url = u''
544 544
545 545 base_url = Unicode('/', config=True,
546 546 help='''The base URL for the notebook server.
547 547
548 548 Leading and trailing slashes can be omitted,
549 549 and will automatically be added.
550 550 ''')
551 551 def _base_url_changed(self, name, old, new):
552 552 if not new.startswith('/'):
553 553 self.base_url = '/'+new
554 554 elif not new.endswith('/'):
555 555 self.base_url = new+'/'
556 556
557 557 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
558 558 def _base_project_url_changed(self, name, old, new):
559 559 self.log.warn("base_project_url is deprecated, use base_url")
560 560 self.base_url = new
561 561
562 562 extra_static_paths = List(Unicode, config=True,
563 563 help="""Extra paths to search for serving static files.
564 564
565 565 This allows adding javascript/css to be available from the notebook server machine,
566 566 or overriding individual files in the IPython"""
567 567 )
568 568 def _extra_static_paths_default(self):
569 569 return [os.path.join(self.profile_dir.location, 'static')]
570 570
571 571 @property
572 572 def static_file_path(self):
573 573 """return extra paths + the default location"""
574 574 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
575 575
576 576 extra_template_paths = List(Unicode, config=True,
577 577 help="""Extra paths to search for serving jinja templates.
578 578
579 Can be used to override templates from IPython.html.templates."""
579 Can be used to override templates from jupyter_notebook.templates."""
580 580 )
581 581 def _extra_template_paths_default(self):
582 582 return []
583 583
584 584 @property
585 585 def template_file_path(self):
586 586 """return extra paths + the default locations"""
587 587 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
588 588
589 589 extra_nbextensions_path = List(Unicode, config=True,
590 590 help="""extra paths to look for Javascript notebook extensions"""
591 591 )
592 592
593 593 @property
594 594 def nbextensions_path(self):
595 595 """The path to look for Javascript notebook extensions"""
596 596 return self.extra_nbextensions_path + [os.path.join(get_ipython_dir(), 'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS
597 597
598 598 websocket_url = Unicode("", config=True,
599 599 help="""The base URL for websockets,
600 600 if it differs from the HTTP server (hint: it almost certainly doesn't).
601 601
602 602 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
603 603 """
604 604 )
605 605 mathjax_url = Unicode("", config=True,
606 606 help="""The url for MathJax.js."""
607 607 )
608 608 def _mathjax_url_default(self):
609 609 if not self.enable_mathjax:
610 610 return u''
611 611 static_url_prefix = self.tornado_settings.get("static_url_prefix",
612 612 url_path_join(self.base_url, "static")
613 613 )
614 614
615 615 # try local mathjax, either in nbextensions/mathjax or static/mathjax
616 616 for (url_prefix, search_path) in [
617 617 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
618 618 (static_url_prefix, self.static_file_path),
619 619 ]:
620 620 self.log.debug("searching for local mathjax in %s", search_path)
621 621 try:
622 622 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
623 623 except IOError:
624 624 continue
625 625 else:
626 626 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
627 627 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
628 628 return url
629 629
630 630 # no local mathjax, serve from CDN
631 631 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
632 632 self.log.info("Using MathJax from CDN: %s", url)
633 633 return url
634 634
635 635 def _mathjax_url_changed(self, name, old, new):
636 636 if new and not self.enable_mathjax:
637 637 # enable_mathjax=False overrides mathjax_url
638 638 self.mathjax_url = u''
639 639 else:
640 640 self.log.info("Using MathJax: %s", new)
641 641
642 642 contents_manager_class = Type(
643 643 default_value=FileContentsManager,
644 644 klass=ContentsManager,
645 645 config=True,
646 646 help='The notebook manager class to use.'
647 647 )
648 648 kernel_manager_class = Type(
649 649 default_value=MappingKernelManager,
650 650 config=True,
651 651 help='The kernel manager class to use.'
652 652 )
653 653 session_manager_class = Type(
654 654 default_value=SessionManager,
655 655 config=True,
656 656 help='The session manager class to use.'
657 657 )
658 658 cluster_manager_class = Type(
659 659 default_value=ClusterManager,
660 660 config=True,
661 661 help='The cluster manager class to use.'
662 662 )
663 663
664 664 config_manager_class = Type(
665 665 default_value=ConfigManager,
666 666 config = True,
667 667 help='The config manager class to use'
668 668 )
669 669
670 670 kernel_spec_manager = Instance(KernelSpecManager, allow_none=True)
671 671
672 672 kernel_spec_manager_class = Type(
673 673 default_value=KernelSpecManager,
674 674 config=True,
675 675 help="""
676 676 The kernel spec manager class to use. Should be a subclass
677 677 of `IPython.kernel.kernelspec.KernelSpecManager`.
678 678
679 679 The Api of KernelSpecManager is provisional and might change
680 680 without warning between this version of IPython and the next stable one.
681 681 """
682 682 )
683 683
684 684 login_handler_class = Type(
685 685 default_value=LoginHandler,
686 686 klass=web.RequestHandler,
687 687 config=True,
688 688 help='The login handler class to use.',
689 689 )
690 690
691 691 logout_handler_class = Type(
692 692 default_value=LogoutHandler,
693 693 klass=web.RequestHandler,
694 694 config=True,
695 695 help='The logout handler class to use.',
696 696 )
697 697
698 698 trust_xheaders = Bool(False, config=True,
699 699 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
700 700 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
701 701 )
702 702
703 703 info_file = Unicode()
704 704
705 705 def _info_file_default(self):
706 706 info_file = "nbserver-%s.json"%os.getpid()
707 707 return os.path.join(self.profile_dir.security_dir, info_file)
708 708
709 709 pylab = Unicode('disabled', config=True,
710 710 help="""
711 711 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
712 712 """
713 713 )
714 714 def _pylab_changed(self, name, old, new):
715 715 """when --pylab is specified, display a warning and exit"""
716 716 if new != 'warn':
717 717 backend = ' %s' % new
718 718 else:
719 719 backend = ''
720 720 self.log.error("Support for specifying --pylab on the command line has been removed.")
721 721 self.log.error(
722 722 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
723 723 )
724 724 self.exit(1)
725 725
726 726 notebook_dir = Unicode(config=True,
727 727 help="The directory to use for notebooks and kernels."
728 728 )
729 729
730 730 def _notebook_dir_default(self):
731 731 if self.file_to_run:
732 732 return os.path.dirname(os.path.abspath(self.file_to_run))
733 733 else:
734 734 return py3compat.getcwd()
735 735
736 736 def _notebook_dir_changed(self, name, old, new):
737 737 """Do a bit of validation of the notebook dir."""
738 738 if not os.path.isabs(new):
739 739 # If we receive a non-absolute path, make it absolute.
740 740 self.notebook_dir = os.path.abspath(new)
741 741 return
742 742 if not os.path.isdir(new):
743 743 raise TraitError("No such notebook dir: %r" % new)
744 744
745 745 # setting App.notebook_dir implies setting notebook and kernel dirs as well
746 746 self.config.FileContentsManager.root_dir = new
747 747 self.config.MappingKernelManager.root_dir = new
748 748
749 749 server_extensions = List(Unicode(), config=True,
750 750 help=("Python modules to load as notebook server extensions. "
751 751 "This is an experimental API, and may change in future releases.")
752 752 )
753 753
754 754 reraise_server_extension_failures = Bool(
755 755 False,
756 756 config=True,
757 757 help="Reraise exceptions encountered loading server extensions?",
758 758 )
759 759
760 760 def parse_command_line(self, argv=None):
761 761 super(NotebookApp, self).parse_command_line(argv)
762 762
763 763 if self.extra_args:
764 764 arg0 = self.extra_args[0]
765 765 f = os.path.abspath(arg0)
766 766 self.argv.remove(arg0)
767 767 if not os.path.exists(f):
768 768 self.log.critical("No such file or directory: %s", f)
769 769 self.exit(1)
770 770
771 771 # Use config here, to ensure that it takes higher priority than
772 772 # anything that comes from the profile.
773 773 c = Config()
774 774 if os.path.isdir(f):
775 775 c.NotebookApp.notebook_dir = f
776 776 elif os.path.isfile(f):
777 777 c.NotebookApp.file_to_run = f
778 778 self.update_config(c)
779 779
780 780 def init_configurables(self):
781 781 self.kernel_spec_manager = self.kernel_spec_manager_class(
782 782 parent=self,
783 783 ipython_dir=self.ipython_dir,
784 784 )
785 785 self.kernel_manager = self.kernel_manager_class(
786 786 parent=self,
787 787 log=self.log,
788 788 connection_dir=self.profile_dir.security_dir,
789 789 kernel_spec_manager=self.kernel_spec_manager,
790 790 )
791 791 self.contents_manager = self.contents_manager_class(
792 792 parent=self,
793 793 log=self.log,
794 794 )
795 795 self.session_manager = self.session_manager_class(
796 796 parent=self,
797 797 log=self.log,
798 798 kernel_manager=self.kernel_manager,
799 799 contents_manager=self.contents_manager,
800 800 )
801 801 self.cluster_manager = self.cluster_manager_class(
802 802 parent=self,
803 803 log=self.log,
804 804 )
805 805
806 806 self.config_manager = self.config_manager_class(
807 807 parent=self,
808 808 log=self.log,
809 809 profile_dir=self.profile_dir.location,
810 810 )
811 811
812 812 def init_logging(self):
813 813 # This prevents double log messages because tornado use a root logger that
814 814 # self.log is a child of. The logging module dipatches log messages to a log
815 815 # and all of its ancenstors until propagate is set to False.
816 816 self.log.propagate = False
817 817
818 818 for log in app_log, access_log, gen_log:
819 819 # consistent log output name (NotebookApp instead of tornado.access, etc.)
820 820 log.name = self.log.name
821 821 # hook up tornado 3's loggers to our app handlers
822 822 logger = logging.getLogger('tornado')
823 823 logger.propagate = True
824 824 logger.parent = self.log
825 825 logger.setLevel(self.log.level)
826 826
827 827 def init_webapp(self):
828 828 """initialize tornado webapp and httpserver"""
829 829 self.tornado_settings['allow_origin'] = self.allow_origin
830 830 if self.allow_origin_pat:
831 831 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
832 832 self.tornado_settings['allow_credentials'] = self.allow_credentials
833 833 # ensure default_url starts with base_url
834 834 if not self.default_url.startswith(self.base_url):
835 835 self.default_url = url_path_join(self.base_url, self.default_url)
836 836
837 837 self.web_app = NotebookWebApplication(
838 838 self, self.kernel_manager, self.contents_manager,
839 839 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
840 840 self.config_manager,
841 841 self.log, self.base_url, self.default_url, self.tornado_settings,
842 842 self.jinja_environment_options
843 843 )
844 844 ssl_options = self.ssl_options
845 845 if self.certfile:
846 846 ssl_options['certfile'] = self.certfile
847 847 if self.keyfile:
848 848 ssl_options['keyfile'] = self.keyfile
849 849 if not ssl_options:
850 850 # None indicates no SSL config
851 851 ssl_options = None
852 852 else:
853 853 # Disable SSLv3, since its use is discouraged.
854 854 ssl_options['ssl_version']=ssl.PROTOCOL_TLSv1
855 855 self.login_handler_class.validate_security(self, ssl_options=ssl_options)
856 856 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
857 857 xheaders=self.trust_xheaders)
858 858
859 859 success = None
860 860 for port in random_ports(self.port, self.port_retries+1):
861 861 try:
862 862 self.http_server.listen(port, self.ip)
863 863 except socket.error as e:
864 864 if e.errno == errno.EADDRINUSE:
865 865 self.log.info('The port %i is already in use, trying another random port.' % port)
866 866 continue
867 867 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
868 868 self.log.warn("Permission to listen on port %i denied" % port)
869 869 continue
870 870 else:
871 871 raise
872 872 else:
873 873 self.port = port
874 874 success = True
875 875 break
876 876 if not success:
877 877 self.log.critical('ERROR: the notebook server could not be started because '
878 878 'no available port could be found.')
879 879 self.exit(1)
880 880
881 881 @property
882 882 def display_url(self):
883 883 ip = self.ip if self.ip else '[all ip addresses on your system]'
884 884 return self._url(ip)
885 885
886 886 @property
887 887 def connection_url(self):
888 888 ip = self.ip if self.ip else 'localhost'
889 889 return self._url(ip)
890 890
891 891 def _url(self, ip):
892 892 proto = 'https' if self.certfile else 'http'
893 893 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
894 894
895 895 def init_terminals(self):
896 896 try:
897 897 from .terminal import initialize
898 898 initialize(self.web_app)
899 899 self.web_app.settings['terminals_available'] = True
900 900 except ImportError as e:
901 901 log = self.log.debug if sys.platform == 'win32' else self.log.warn
902 902 log("Terminals not available (error was %s)", e)
903 903
904 904 def init_signal(self):
905 905 if not sys.platform.startswith('win'):
906 906 signal.signal(signal.SIGINT, self._handle_sigint)
907 907 signal.signal(signal.SIGTERM, self._signal_stop)
908 908 if hasattr(signal, 'SIGUSR1'):
909 909 # Windows doesn't support SIGUSR1
910 910 signal.signal(signal.SIGUSR1, self._signal_info)
911 911 if hasattr(signal, 'SIGINFO'):
912 912 # only on BSD-based systems
913 913 signal.signal(signal.SIGINFO, self._signal_info)
914 914
915 915 def _handle_sigint(self, sig, frame):
916 916 """SIGINT handler spawns confirmation dialog"""
917 917 # register more forceful signal handler for ^C^C case
918 918 signal.signal(signal.SIGINT, self._signal_stop)
919 919 # request confirmation dialog in bg thread, to avoid
920 920 # blocking the App
921 921 thread = threading.Thread(target=self._confirm_exit)
922 922 thread.daemon = True
923 923 thread.start()
924 924
925 925 def _restore_sigint_handler(self):
926 926 """callback for restoring original SIGINT handler"""
927 927 signal.signal(signal.SIGINT, self._handle_sigint)
928 928
929 929 def _confirm_exit(self):
930 930 """confirm shutdown on ^C
931 931
932 932 A second ^C, or answering 'y' within 5s will cause shutdown,
933 933 otherwise original SIGINT handler will be restored.
934 934
935 935 This doesn't work on Windows.
936 936 """
937 937 info = self.log.info
938 938 info('interrupted')
939 939 print(self.notebook_info())
940 940 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
941 941 sys.stdout.flush()
942 942 r,w,x = select.select([sys.stdin], [], [], 5)
943 943 if r:
944 944 line = sys.stdin.readline()
945 945 if line.lower().startswith('y') and 'n' not in line.lower():
946 946 self.log.critical("Shutdown confirmed")
947 947 ioloop.IOLoop.current().stop()
948 948 return
949 949 else:
950 950 print("No answer for 5s:", end=' ')
951 951 print("resuming operation...")
952 952 # no answer, or answer is no:
953 953 # set it back to original SIGINT handler
954 954 # use IOLoop.add_callback because signal.signal must be called
955 955 # from main thread
956 956 ioloop.IOLoop.current().add_callback(self._restore_sigint_handler)
957 957
958 958 def _signal_stop(self, sig, frame):
959 959 self.log.critical("received signal %s, stopping", sig)
960 960 ioloop.IOLoop.current().stop()
961 961
962 962 def _signal_info(self, sig, frame):
963 963 print(self.notebook_info())
964 964
965 965 def init_components(self):
966 966 """Check the components submodule, and warn if it's unclean"""
967 967 status = submodule.check_submodule_status()
968 968 if status == 'missing':
969 969 self.log.warn("components submodule missing, running `git submodule update`")
970 970 submodule.update_submodules(submodule.ipython_parent())
971 971 elif status == 'unclean':
972 972 self.log.warn("components submodule unclean, you may see 404s on static/components")
973 973 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
974 974
975 975 def init_kernel_specs(self):
976 976 """Check that the IPython kernel is present, if available"""
977 977 try:
978 978 self.kernel_spec_manager.get_kernel_spec(NATIVE_KERNEL_NAME)
979 979 except NoSuchKernel:
980 980 try:
981 981 import ipython_kernel
982 982 except ImportError:
983 983 self.log.warn("IPython kernel not available")
984 984 else:
985 985 self.log.warn("Installing IPython kernel spec")
986 986 self.kernel_spec_manager.install_native_kernel_spec(user=True)
987 987
988 988
989 989 def init_server_extensions(self):
990 990 """Load any extensions specified by config.
991 991
992 992 Import the module, then call the load_jupyter_server_extension function,
993 993 if one exists.
994 994
995 995 The extension API is experimental, and may change in future releases.
996 996 """
997 997 for modulename in self.server_extensions:
998 998 try:
999 999 mod = importlib.import_module(modulename)
1000 1000 func = getattr(mod, 'load_jupyter_server_extension', None)
1001 1001 if func is not None:
1002 1002 func(self)
1003 1003 except Exception:
1004 1004 if self.reraise_server_extension_failures:
1005 1005 raise
1006 1006 self.log.warn("Error loading server extension %s", modulename,
1007 1007 exc_info=True)
1008 1008
1009 1009 @catch_config_error
1010 1010 def initialize(self, argv=None):
1011 1011 super(NotebookApp, self).initialize(argv)
1012 1012 self.init_logging()
1013 1013 self.init_configurables()
1014 1014 self.init_components()
1015 1015 self.init_webapp()
1016 1016 self.init_kernel_specs()
1017 1017 self.init_terminals()
1018 1018 self.init_signal()
1019 1019 self.init_server_extensions()
1020 1020
1021 1021 def cleanup_kernels(self):
1022 1022 """Shutdown all kernels.
1023 1023
1024 1024 The kernels will shutdown themselves when this process no longer exists,
1025 1025 but explicit shutdown allows the KernelManagers to cleanup the connection files.
1026 1026 """
1027 1027 self.log.info('Shutting down kernels')
1028 1028 self.kernel_manager.shutdown_all()
1029 1029
1030 1030 def notebook_info(self):
1031 1031 "Return the current working directory and the server url information"
1032 1032 info = self.contents_manager.info_string() + "\n"
1033 1033 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
1034 1034 return info + "The IPython Notebook is running at: %s" % self.display_url
1035 1035
1036 1036 def server_info(self):
1037 1037 """Return a JSONable dict of information about this server."""
1038 1038 return {'url': self.connection_url,
1039 1039 'hostname': self.ip if self.ip else 'localhost',
1040 1040 'port': self.port,
1041 1041 'secure': bool(self.certfile),
1042 1042 'base_url': self.base_url,
1043 1043 'notebook_dir': os.path.abspath(self.notebook_dir),
1044 1044 'pid': os.getpid()
1045 1045 }
1046 1046
1047 1047 def write_server_info_file(self):
1048 1048 """Write the result of server_info() to the JSON file info_file."""
1049 1049 with open(self.info_file, 'w') as f:
1050 1050 json.dump(self.server_info(), f, indent=2)
1051 1051
1052 1052 def remove_server_info_file(self):
1053 1053 """Remove the nbserver-<pid>.json file created for this server.
1054 1054
1055 1055 Ignores the error raised when the file has already been removed.
1056 1056 """
1057 1057 try:
1058 1058 os.unlink(self.info_file)
1059 1059 except OSError as e:
1060 1060 if e.errno != errno.ENOENT:
1061 1061 raise
1062 1062
1063 1063 def start(self):
1064 1064 """ Start the IPython Notebook server app, after initialization
1065 1065
1066 1066 This method takes no arguments so all configuration and initialization
1067 1067 must be done prior to calling this method."""
1068 1068 if self.subapp is not None:
1069 1069 return self.subapp.start()
1070 1070
1071 1071 info = self.log.info
1072 1072 for line in self.notebook_info().split("\n"):
1073 1073 info(line)
1074 1074 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
1075 1075
1076 1076 self.write_server_info_file()
1077 1077
1078 1078 if self.open_browser or self.file_to_run:
1079 1079 try:
1080 1080 browser = webbrowser.get(self.browser or None)
1081 1081 except webbrowser.Error as e:
1082 1082 self.log.warn('No web browser found: %s.' % e)
1083 1083 browser = None
1084 1084
1085 1085 if self.file_to_run:
1086 1086 if not os.path.exists(self.file_to_run):
1087 1087 self.log.critical("%s does not exist" % self.file_to_run)
1088 1088 self.exit(1)
1089 1089
1090 1090 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1091 1091 uri = url_path_join('notebooks', *relpath.split(os.sep))
1092 1092 else:
1093 1093 uri = 'tree'
1094 1094 if browser:
1095 1095 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1096 1096 new=2)
1097 1097 threading.Thread(target=b).start()
1098 1098
1099 1099 self.io_loop = ioloop.IOLoop.current()
1100 1100 if sys.platform.startswith('win'):
1101 1101 # add no-op to wake every 5s
1102 1102 # to handle signals that may be ignored by the inner loop
1103 1103 pc = ioloop.PeriodicCallback(lambda : None, 5000)
1104 1104 pc.start()
1105 1105 try:
1106 1106 self.io_loop.start()
1107 1107 except KeyboardInterrupt:
1108 1108 info("Interrupted...")
1109 1109 finally:
1110 1110 self.cleanup_kernels()
1111 1111 self.remove_server_info_file()
1112 1112
1113 1113 def stop(self):
1114 1114 def _stop():
1115 1115 self.http_server.stop()
1116 1116 self.io_loop.stop()
1117 1117 self.io_loop.add_callback(_stop)
1118 1118
1119 1119
1120 1120 def list_running_servers(profile='default'):
1121 1121 """Iterate over the server info files of running notebook servers.
1122 1122
1123 1123 Given a profile name, find nbserver-* files in the security directory of
1124 1124 that profile, and yield dicts of their information, each one pertaining to
1125 1125 a currently running notebook server instance.
1126 1126 """
1127 1127 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1128 1128 for file in os.listdir(pd.security_dir):
1129 1129 if file.startswith('nbserver-'):
1130 1130 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1131 1131 info = json.load(f)
1132 1132
1133 1133 # Simple check whether that process is really still running
1134 1134 # Also remove leftover files from IPython 2.x without a pid field
1135 1135 if ('pid' in info) and check_pid(info['pid']):
1136 1136 yield info
1137 1137 else:
1138 1138 # If the process has died, try to delete its info file
1139 1139 try:
1140 1140 os.unlink(file)
1141 1141 except OSError:
1142 1142 pass # TODO: This should warn or log or something
1143 1143 #-----------------------------------------------------------------------------
1144 1144 # Main entry point
1145 1145 #-----------------------------------------------------------------------------
1146 1146
1147 1147 launch_new_instance = NotebookApp.launch_instance
1148 1148
@@ -1,68 +1,68
1 1 # coding: utf-8
2 2 """Test the config webservice API."""
3 3
4 4 import json
5 5
6 6 import requests
7 7
8 from IPython.html.utils import url_path_join
9 from IPython.html.tests.launchnotebook import NotebookTestBase
8 from jupyter_notebook.utils import url_path_join
9 from jupyter_notebook.tests.launchnotebook import NotebookTestBase
10 10
11 11
12 12 class ConfigAPI(object):
13 13 """Wrapper for notebook API calls."""
14 14 def __init__(self, base_url):
15 15 self.base_url = base_url
16 16
17 17 def _req(self, verb, section, body=None):
18 18 response = requests.request(verb,
19 19 url_path_join(self.base_url, 'api/config', section),
20 20 data=body,
21 21 )
22 22 response.raise_for_status()
23 23 return response
24 24
25 25 def get(self, section):
26 26 return self._req('GET', section)
27 27
28 28 def set(self, section, values):
29 29 return self._req('PUT', section, json.dumps(values))
30 30
31 31 def modify(self, section, values):
32 32 return self._req('PATCH', section, json.dumps(values))
33 33
34 34 class APITest(NotebookTestBase):
35 35 """Test the config web service API"""
36 36 def setUp(self):
37 37 self.config_api = ConfigAPI(self.base_url())
38 38
39 39 def test_create_retrieve_config(self):
40 40 sample = {'foo': 'bar', 'baz': 73}
41 41 r = self.config_api.set('example', sample)
42 42 self.assertEqual(r.status_code, 204)
43 43
44 44 r = self.config_api.get('example')
45 45 self.assertEqual(r.status_code, 200)
46 46 self.assertEqual(r.json(), sample)
47 47
48 48 def test_modify(self):
49 49 sample = {'foo': 'bar', 'baz': 73,
50 50 'sub': {'a': 6, 'b': 7}, 'sub2': {'c': 8}}
51 51 self.config_api.set('example', sample)
52 52
53 53 r = self.config_api.modify('example', {'foo': None, # should delete foo
54 54 'baz': 75,
55 55 'wib': [1,2,3],
56 56 'sub': {'a': 8, 'b': None, 'd': 9},
57 57 'sub2': {'c': None} # should delete sub2
58 58 })
59 59 self.assertEqual(r.status_code, 200)
60 60 self.assertEqual(r.json(), {'baz': 75, 'wib': [1,2,3],
61 61 'sub': {'a': 8, 'd': 9}})
62 62
63 63 def test_get_unknown(self):
64 64 # We should get an empty config dictionary instead of a 404
65 65 r = self.config_api.get('nonexistant')
66 66 self.assertEqual(r.status_code, 200)
67 67 self.assertEqual(r.json(), {})
68 68
@@ -1,256 +1,256
1 1 """
2 2 Utilities for file-based Contents/Checkpoints managers.
3 3 """
4 4
5 5 # Copyright (c) IPython Development Team.
6 6 # Distributed under the terms of the Modified BSD License.
7 7
8 8 import base64
9 9 from contextlib import contextmanager
10 10 import errno
11 11 import io
12 12 import os
13 13 import shutil
14 14 import tempfile
15 15
16 16 from tornado.web import HTTPError
17 17
18 from IPython.html.utils import (
18 from jupyter_notebook.utils import (
19 19 to_api_path,
20 20 to_os_path,
21 21 )
22 22 from IPython import nbformat
23 23 from IPython.utils.py3compat import str_to_unicode
24 24
25 25
26 26 def _copy_metadata(src, dst):
27 27 """Copy the set of metadata we want for atomic_writing.
28 28
29 29 Permission bits and flags. We'd like to copy file ownership as well, but we
30 30 can't do that.
31 31 """
32 32 shutil.copymode(src, dst)
33 33 st = os.stat(src)
34 34 if hasattr(os, 'chflags') and hasattr(st, 'st_flags'):
35 35 os.chflags(dst, st.st_flags)
36 36
37 37 @contextmanager
38 38 def atomic_writing(path, text=True, encoding='utf-8', **kwargs):
39 39 """Context manager to write to a file only if the entire write is successful.
40 40
41 41 This works by creating a temporary file in the same directory, and renaming
42 42 it over the old file if the context is exited without an error. If other
43 43 file names are hard linked to the target file, this relationship will not be
44 44 preserved.
45 45
46 46 On Windows, there is a small chink in the atomicity: the target file is
47 47 deleted before renaming the temporary file over it. This appears to be
48 48 unavoidable.
49 49
50 50 Parameters
51 51 ----------
52 52 path : str
53 53 The target file to write to.
54 54
55 55 text : bool, optional
56 56 Whether to open the file in text mode (i.e. to write unicode). Default is
57 57 True.
58 58
59 59 encoding : str, optional
60 60 The encoding to use for files opened in text mode. Default is UTF-8.
61 61
62 62 **kwargs
63 63 Passed to :func:`io.open`.
64 64 """
65 65 # realpath doesn't work on Windows: http://bugs.python.org/issue9949
66 66 # Luckily, we only need to resolve the file itself being a symlink, not
67 67 # any of its directories, so this will suffice:
68 68 if os.path.islink(path):
69 69 path = os.path.join(os.path.dirname(path), os.readlink(path))
70 70
71 71 dirname, basename = os.path.split(path)
72 72 tmp_dir = tempfile.mkdtemp(prefix=basename, dir=dirname)
73 73 tmp_path = os.path.join(tmp_dir, basename)
74 74 if text:
75 75 fileobj = io.open(tmp_path, 'w', encoding=encoding, **kwargs)
76 76 else:
77 77 fileobj = io.open(tmp_path, 'wb', **kwargs)
78 78
79 79 try:
80 80 yield fileobj
81 81 except:
82 82 fileobj.close()
83 83 shutil.rmtree(tmp_dir)
84 84 raise
85 85
86 86 # Flush to disk
87 87 fileobj.flush()
88 88 os.fsync(fileobj.fileno())
89 89
90 90 # Written successfully, now rename it
91 91 fileobj.close()
92 92
93 93 # Copy permission bits, access time, etc.
94 94 try:
95 95 _copy_metadata(path, tmp_path)
96 96 except OSError:
97 97 # e.g. the file didn't already exist. Ignore any failure to copy metadata
98 98 pass
99 99
100 100 if os.name == 'nt' and os.path.exists(path):
101 101 # Rename over existing file doesn't work on Windows
102 102 os.remove(path)
103 103
104 104 os.rename(tmp_path, path)
105 105 shutil.rmtree(tmp_dir)
106 106
107 107
108 108 class FileManagerMixin(object):
109 109 """
110 110 Mixin for ContentsAPI classes that interact with the filesystem.
111 111
112 112 Provides facilities for reading, writing, and copying both notebooks and
113 113 generic files.
114 114
115 115 Shared by FileContentsManager and FileCheckpoints.
116 116
117 117 Note
118 118 ----
119 119 Classes using this mixin must provide the following attributes:
120 120
121 121 root_dir : unicode
122 122 A directory against against which API-style paths are to be resolved.
123 123
124 124 log : logging.Logger
125 125 """
126 126
127 127 @contextmanager
128 128 def open(self, os_path, *args, **kwargs):
129 129 """wrapper around io.open that turns permission errors into 403"""
130 130 with self.perm_to_403(os_path):
131 131 with io.open(os_path, *args, **kwargs) as f:
132 132 yield f
133 133
134 134 @contextmanager
135 135 def atomic_writing(self, os_path, *args, **kwargs):
136 136 """wrapper around atomic_writing that turns permission errors to 403"""
137 137 with self.perm_to_403(os_path):
138 138 with atomic_writing(os_path, *args, **kwargs) as f:
139 139 yield f
140 140
141 141 @contextmanager
142 142 def perm_to_403(self, os_path=''):
143 143 """context manager for turning permission errors into 403."""
144 144 try:
145 145 yield
146 146 except (OSError, IOError) as e:
147 147 if e.errno in {errno.EPERM, errno.EACCES}:
148 148 # make 403 error message without root prefix
149 149 # this may not work perfectly on unicode paths on Python 2,
150 150 # but nobody should be doing that anyway.
151 151 if not os_path:
152 152 os_path = str_to_unicode(e.filename or 'unknown file')
153 153 path = to_api_path(os_path, root=self.root_dir)
154 154 raise HTTPError(403, u'Permission denied: %s' % path)
155 155 else:
156 156 raise
157 157
158 158 def _copy(self, src, dest):
159 159 """copy src to dest
160 160
161 161 like shutil.copy2, but log errors in copystat
162 162 """
163 163 shutil.copyfile(src, dest)
164 164 try:
165 165 shutil.copystat(src, dest)
166 166 except OSError:
167 167 self.log.debug("copystat on %s failed", dest, exc_info=True)
168 168
169 169 def _get_os_path(self, path):
170 170 """Given an API path, return its file system path.
171 171
172 172 Parameters
173 173 ----------
174 174 path : string
175 175 The relative API path to the named file.
176 176
177 177 Returns
178 178 -------
179 179 path : string
180 180 Native, absolute OS path to for a file.
181 181
182 182 Raises
183 183 ------
184 184 404: if path is outside root
185 185 """
186 186 root = os.path.abspath(self.root_dir)
187 187 os_path = to_os_path(path, root)
188 188 if not (os.path.abspath(os_path) + os.path.sep).startswith(root):
189 189 raise HTTPError(404, "%s is outside root contents directory" % path)
190 190 return os_path
191 191
192 192 def _read_notebook(self, os_path, as_version=4):
193 193 """Read a notebook from an os path."""
194 194 with self.open(os_path, 'r', encoding='utf-8') as f:
195 195 try:
196 196 return nbformat.read(f, as_version=as_version)
197 197 except Exception as e:
198 198 raise HTTPError(
199 199 400,
200 200 u"Unreadable Notebook: %s %r" % (os_path, e),
201 201 )
202 202
203 203 def _save_notebook(self, os_path, nb):
204 204 """Save a notebook to an os_path."""
205 205 with self.atomic_writing(os_path, encoding='utf-8') as f:
206 206 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
207 207
208 208 def _read_file(self, os_path, format):
209 209 """Read a non-notebook file.
210 210
211 211 os_path: The path to be read.
212 212 format:
213 213 If 'text', the contents will be decoded as UTF-8.
214 214 If 'base64', the raw bytes contents will be encoded as base64.
215 215 If not specified, try to decode as UTF-8, and fall back to base64
216 216 """
217 217 if not os.path.isfile(os_path):
218 218 raise HTTPError(400, "Cannot read non-file %s" % os_path)
219 219
220 220 with self.open(os_path, 'rb') as f:
221 221 bcontent = f.read()
222 222
223 223 if format is None or format == 'text':
224 224 # Try to interpret as unicode if format is unknown or if unicode
225 225 # was explicitly requested.
226 226 try:
227 227 return bcontent.decode('utf8'), 'text'
228 228 except UnicodeError:
229 229 if format == 'text':
230 230 raise HTTPError(
231 231 400,
232 232 "%s is not UTF-8 encoded" % os_path,
233 233 reason='bad format',
234 234 )
235 235 return base64.encodestring(bcontent).decode('ascii'), 'base64'
236 236
237 237 def _save_file(self, os_path, content, format):
238 238 """Save content of a generic file."""
239 239 if format not in {'text', 'base64'}:
240 240 raise HTTPError(
241 241 400,
242 242 "Must specify format of file contents as 'text' or 'base64'",
243 243 )
244 244 try:
245 245 if format == 'text':
246 246 bcontent = content.encode('utf8')
247 247 else:
248 248 b64_bytes = content.encode('ascii')
249 249 bcontent = base64.decodestring(b64_bytes)
250 250 except Exception as e:
251 251 raise HTTPError(
252 252 400, u'Encoding error saving %s: %s' % (os_path, e)
253 253 )
254 254
255 255 with self.atomic_writing(os_path, text=False) as f:
256 256 f.write(bcontent)
@@ -1,473 +1,473
1 1 """A contents manager that uses the local file system for storage."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6
7 7 import io
8 8 import os
9 9 import shutil
10 10 import mimetypes
11 11
12 12 from tornado import web
13 13
14 14 from .filecheckpoints import FileCheckpoints
15 15 from .fileio import FileManagerMixin
16 16 from .manager import ContentsManager
17 17
18 18 from IPython import nbformat
19 19 from IPython.utils.importstring import import_item
20 20 from IPython.utils.traitlets import Any, Unicode, Bool, TraitError
21 21 from IPython.utils.py3compat import getcwd, string_types
22 22 from IPython.utils import tz
23 from IPython.html.utils import (
23 from jupyter_notebook.utils import (
24 24 is_hidden,
25 25 to_api_path,
26 26 )
27 27
28 28 _script_exporter = None
29 29
30 30
31 31 def _post_save_script(model, os_path, contents_manager, **kwargs):
32 32 """convert notebooks to Python script after save with nbconvert
33 33
34 34 replaces `ipython notebook --script`
35 35 """
36 36 from IPython.nbconvert.exporters.script import ScriptExporter
37 37
38 38 if model['type'] != 'notebook':
39 39 return
40 40
41 41 global _script_exporter
42 42 if _script_exporter is None:
43 43 _script_exporter = ScriptExporter(parent=contents_manager)
44 44 log = contents_manager.log
45 45
46 46 base, ext = os.path.splitext(os_path)
47 47 py_fname = base + '.py'
48 48 script, resources = _script_exporter.from_filename(os_path)
49 49 script_fname = base + resources.get('output_extension', '.txt')
50 50 log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir))
51 51 with io.open(script_fname, 'w', encoding='utf-8') as f:
52 52 f.write(script)
53 53
54 54
55 55 class FileContentsManager(FileManagerMixin, ContentsManager):
56 56
57 57 root_dir = Unicode(config=True)
58 58
59 59 def _root_dir_default(self):
60 60 try:
61 61 return self.parent.notebook_dir
62 62 except AttributeError:
63 63 return getcwd()
64 64
65 65 save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook')
66 66 def _save_script_changed(self):
67 67 self.log.warn("""
68 68 `--script` is deprecated. You can trigger nbconvert via pre- or post-save hooks:
69 69
70 70 ContentsManager.pre_save_hook
71 71 FileContentsManager.post_save_hook
72 72
73 73 A post-save hook has been registered that calls:
74 74
75 75 ipython nbconvert --to script [notebook]
76 76
77 77 which behaves similarly to `--script`.
78 78 """)
79 79
80 80 self.post_save_hook = _post_save_script
81 81
82 82 post_save_hook = Any(None, config=True,
83 83 help="""Python callable or importstring thereof
84 84
85 85 to be called on the path of a file just saved.
86 86
87 87 This can be used to process the file on disk,
88 88 such as converting the notebook to a script or HTML via nbconvert.
89 89
90 90 It will be called as (all arguments passed by keyword)::
91 91
92 92 hook(os_path=os_path, model=model, contents_manager=instance)
93 93
94 94 - path: the filesystem path to the file just written
95 95 - model: the model representing the file
96 96 - contents_manager: this ContentsManager instance
97 97 """
98 98 )
99 99 def _post_save_hook_changed(self, name, old, new):
100 100 if new and isinstance(new, string_types):
101 101 self.post_save_hook = import_item(self.post_save_hook)
102 102 elif new:
103 103 if not callable(new):
104 104 raise TraitError("post_save_hook must be callable")
105 105
106 106 def run_post_save_hook(self, model, os_path):
107 107 """Run the post-save hook if defined, and log errors"""
108 108 if self.post_save_hook:
109 109 try:
110 110 self.log.debug("Running post-save hook on %s", os_path)
111 111 self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
112 112 except Exception:
113 113 self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
114 114
115 115 def _root_dir_changed(self, name, old, new):
116 116 """Do a bit of validation of the root_dir."""
117 117 if not os.path.isabs(new):
118 118 # If we receive a non-absolute path, make it absolute.
119 119 self.root_dir = os.path.abspath(new)
120 120 return
121 121 if not os.path.isdir(new):
122 122 raise TraitError("%r is not a directory" % new)
123 123
124 124 def _checkpoints_class_default(self):
125 125 return FileCheckpoints
126 126
127 127 def is_hidden(self, path):
128 128 """Does the API style path correspond to a hidden directory or file?
129 129
130 130 Parameters
131 131 ----------
132 132 path : string
133 133 The path to check. This is an API path (`/` separated,
134 134 relative to root_dir).
135 135
136 136 Returns
137 137 -------
138 138 hidden : bool
139 139 Whether the path exists and is hidden.
140 140 """
141 141 path = path.strip('/')
142 142 os_path = self._get_os_path(path=path)
143 143 return is_hidden(os_path, self.root_dir)
144 144
145 145 def file_exists(self, path):
146 146 """Returns True if the file exists, else returns False.
147 147
148 148 API-style wrapper for os.path.isfile
149 149
150 150 Parameters
151 151 ----------
152 152 path : string
153 153 The relative path to the file (with '/' as separator)
154 154
155 155 Returns
156 156 -------
157 157 exists : bool
158 158 Whether the file exists.
159 159 """
160 160 path = path.strip('/')
161 161 os_path = self._get_os_path(path)
162 162 return os.path.isfile(os_path)
163 163
164 164 def dir_exists(self, path):
165 165 """Does the API-style path refer to an extant directory?
166 166
167 167 API-style wrapper for os.path.isdir
168 168
169 169 Parameters
170 170 ----------
171 171 path : string
172 172 The path to check. This is an API path (`/` separated,
173 173 relative to root_dir).
174 174
175 175 Returns
176 176 -------
177 177 exists : bool
178 178 Whether the path is indeed a directory.
179 179 """
180 180 path = path.strip('/')
181 181 os_path = self._get_os_path(path=path)
182 182 return os.path.isdir(os_path)
183 183
184 184 def exists(self, path):
185 185 """Returns True if the path exists, else returns False.
186 186
187 187 API-style wrapper for os.path.exists
188 188
189 189 Parameters
190 190 ----------
191 191 path : string
192 192 The API path to the file (with '/' as separator)
193 193
194 194 Returns
195 195 -------
196 196 exists : bool
197 197 Whether the target exists.
198 198 """
199 199 path = path.strip('/')
200 200 os_path = self._get_os_path(path=path)
201 201 return os.path.exists(os_path)
202 202
203 203 def _base_model(self, path):
204 204 """Build the common base of a contents model"""
205 205 os_path = self._get_os_path(path)
206 206 info = os.stat(os_path)
207 207 last_modified = tz.utcfromtimestamp(info.st_mtime)
208 208 created = tz.utcfromtimestamp(info.st_ctime)
209 209 # Create the base model.
210 210 model = {}
211 211 model['name'] = path.rsplit('/', 1)[-1]
212 212 model['path'] = path
213 213 model['last_modified'] = last_modified
214 214 model['created'] = created
215 215 model['content'] = None
216 216 model['format'] = None
217 217 model['mimetype'] = None
218 218 try:
219 219 model['writable'] = os.access(os_path, os.W_OK)
220 220 except OSError:
221 221 self.log.error("Failed to check write permissions on %s", os_path)
222 222 model['writable'] = False
223 223 return model
224 224
225 225 def _dir_model(self, path, content=True):
226 226 """Build a model for a directory
227 227
228 228 if content is requested, will include a listing of the directory
229 229 """
230 230 os_path = self._get_os_path(path)
231 231
232 232 four_o_four = u'directory does not exist: %r' % path
233 233
234 234 if not os.path.isdir(os_path):
235 235 raise web.HTTPError(404, four_o_four)
236 236 elif is_hidden(os_path, self.root_dir):
237 237 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
238 238 os_path
239 239 )
240 240 raise web.HTTPError(404, four_o_four)
241 241
242 242 model = self._base_model(path)
243 243 model['type'] = 'directory'
244 244 if content:
245 245 model['content'] = contents = []
246 246 os_dir = self._get_os_path(path)
247 247 for name in os.listdir(os_dir):
248 248 os_path = os.path.join(os_dir, name)
249 249 # skip over broken symlinks in listing
250 250 if not os.path.exists(os_path):
251 251 self.log.warn("%s doesn't exist", os_path)
252 252 continue
253 253 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
254 254 self.log.debug("%s not a regular file", os_path)
255 255 continue
256 256 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
257 257 contents.append(self.get(
258 258 path='%s/%s' % (path, name),
259 259 content=False)
260 260 )
261 261
262 262 model['format'] = 'json'
263 263
264 264 return model
265 265
266 266 def _file_model(self, path, content=True, format=None):
267 267 """Build a model for a file
268 268
269 269 if content is requested, include the file contents.
270 270
271 271 format:
272 272 If 'text', the contents will be decoded as UTF-8.
273 273 If 'base64', the raw bytes contents will be encoded as base64.
274 274 If not specified, try to decode as UTF-8, and fall back to base64
275 275 """
276 276 model = self._base_model(path)
277 277 model['type'] = 'file'
278 278
279 279 os_path = self._get_os_path(path)
280 280
281 281 if content:
282 282 content, format = self._read_file(os_path, format)
283 283 default_mime = {
284 284 'text': 'text/plain',
285 285 'base64': 'application/octet-stream'
286 286 }[format]
287 287
288 288 model.update(
289 289 content=content,
290 290 format=format,
291 291 mimetype=mimetypes.guess_type(os_path)[0] or default_mime,
292 292 )
293 293
294 294 return model
295 295
296 296 def _notebook_model(self, path, content=True):
297 297 """Build a notebook model
298 298
299 299 if content is requested, the notebook content will be populated
300 300 as a JSON structure (not double-serialized)
301 301 """
302 302 model = self._base_model(path)
303 303 model['type'] = 'notebook'
304 304 if content:
305 305 os_path = self._get_os_path(path)
306 306 nb = self._read_notebook(os_path, as_version=4)
307 307 self.mark_trusted_cells(nb, path)
308 308 model['content'] = nb
309 309 model['format'] = 'json'
310 310 self.validate_notebook_model(model)
311 311 return model
312 312
313 313 def get(self, path, content=True, type=None, format=None):
314 314 """ Takes a path for an entity and returns its model
315 315
316 316 Parameters
317 317 ----------
318 318 path : str
319 319 the API path that describes the relative path for the target
320 320 content : bool
321 321 Whether to include the contents in the reply
322 322 type : str, optional
323 323 The requested type - 'file', 'notebook', or 'directory'.
324 324 Will raise HTTPError 400 if the content doesn't match.
325 325 format : str, optional
326 326 The requested format for file contents. 'text' or 'base64'.
327 327 Ignored if this returns a notebook or directory model.
328 328
329 329 Returns
330 330 -------
331 331 model : dict
332 332 the contents model. If content=True, returns the contents
333 333 of the file or directory as well.
334 334 """
335 335 path = path.strip('/')
336 336
337 337 if not self.exists(path):
338 338 raise web.HTTPError(404, u'No such file or directory: %s' % path)
339 339
340 340 os_path = self._get_os_path(path)
341 341 if os.path.isdir(os_path):
342 342 if type not in (None, 'directory'):
343 343 raise web.HTTPError(400,
344 344 u'%s is a directory, not a %s' % (path, type), reason='bad type')
345 345 model = self._dir_model(path, content=content)
346 346 elif type == 'notebook' or (type is None and path.endswith('.ipynb')):
347 347 model = self._notebook_model(path, content=content)
348 348 else:
349 349 if type == 'directory':
350 350 raise web.HTTPError(400,
351 351 u'%s is not a directory' % path, reason='bad type')
352 352 model = self._file_model(path, content=content, format=format)
353 353 return model
354 354
355 355 def _save_directory(self, os_path, model, path=''):
356 356 """create a directory"""
357 357 if is_hidden(os_path, self.root_dir):
358 358 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
359 359 if not os.path.exists(os_path):
360 360 with self.perm_to_403():
361 361 os.mkdir(os_path)
362 362 elif not os.path.isdir(os_path):
363 363 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
364 364 else:
365 365 self.log.debug("Directory %r already exists", os_path)
366 366
367 367 def save(self, model, path=''):
368 368 """Save the file model and return the model with no content."""
369 369 path = path.strip('/')
370 370
371 371 if 'type' not in model:
372 372 raise web.HTTPError(400, u'No file type provided')
373 373 if 'content' not in model and model['type'] != 'directory':
374 374 raise web.HTTPError(400, u'No file content provided')
375 375
376 376 os_path = self._get_os_path(path)
377 377 self.log.debug("Saving %s", os_path)
378 378
379 379 self.run_pre_save_hook(model=model, path=path)
380 380
381 381 try:
382 382 if model['type'] == 'notebook':
383 383 nb = nbformat.from_dict(model['content'])
384 384 self.check_and_sign(nb, path)
385 385 self._save_notebook(os_path, nb)
386 386 # One checkpoint should always exist for notebooks.
387 387 if not self.checkpoints.list_checkpoints(path):
388 388 self.create_checkpoint(path)
389 389 elif model['type'] == 'file':
390 390 # Missing format will be handled internally by _save_file.
391 391 self._save_file(os_path, model['content'], model.get('format'))
392 392 elif model['type'] == 'directory':
393 393 self._save_directory(os_path, model, path)
394 394 else:
395 395 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
396 396 except web.HTTPError:
397 397 raise
398 398 except Exception as e:
399 399 self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
400 400 raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
401 401
402 402 validation_message = None
403 403 if model['type'] == 'notebook':
404 404 self.validate_notebook_model(model)
405 405 validation_message = model.get('message', None)
406 406
407 407 model = self.get(path, content=False)
408 408 if validation_message:
409 409 model['message'] = validation_message
410 410
411 411 self.run_post_save_hook(model=model, os_path=os_path)
412 412
413 413 return model
414 414
415 415 def delete_file(self, path):
416 416 """Delete file at path."""
417 417 path = path.strip('/')
418 418 os_path = self._get_os_path(path)
419 419 rm = os.unlink
420 420 if os.path.isdir(os_path):
421 421 listing = os.listdir(os_path)
422 422 # Don't delete non-empty directories.
423 423 # A directory containing only leftover checkpoints is
424 424 # considered empty.
425 425 cp_dir = getattr(self.checkpoints, 'checkpoint_dir', None)
426 426 for entry in listing:
427 427 if entry != cp_dir:
428 428 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
429 429 elif not os.path.isfile(os_path):
430 430 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
431 431
432 432 if os.path.isdir(os_path):
433 433 self.log.debug("Removing directory %s", os_path)
434 434 with self.perm_to_403():
435 435 shutil.rmtree(os_path)
436 436 else:
437 437 self.log.debug("Unlinking file %s", os_path)
438 438 with self.perm_to_403():
439 439 rm(os_path)
440 440
441 441 def rename_file(self, old_path, new_path):
442 442 """Rename a file."""
443 443 old_path = old_path.strip('/')
444 444 new_path = new_path.strip('/')
445 445 if new_path == old_path:
446 446 return
447 447
448 448 new_os_path = self._get_os_path(new_path)
449 449 old_os_path = self._get_os_path(old_path)
450 450
451 451 # Should we proceed with the move?
452 452 if os.path.exists(new_os_path):
453 453 raise web.HTTPError(409, u'File already exists: %s' % new_path)
454 454
455 455 # Move the file
456 456 try:
457 457 with self.perm_to_403():
458 458 shutil.move(old_os_path, new_os_path)
459 459 except web.HTTPError:
460 460 raise
461 461 except Exception as e:
462 462 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
463 463
464 464 def info_string(self):
465 465 return "Serving notebooks from local directory: %s" % self.root_dir
466 466
467 467 def get_kernel_path(self, path, model=None):
468 468 """Return the initial API path of a kernel associated with a given notebook"""
469 469 if '/' in path:
470 470 parent_dir = path.rsplit('/', 1)[0]
471 471 else:
472 472 parent_dir = ''
473 473 return parent_dir
@@ -1,342 +1,342
1 1 """Tornado handlers for the contents web service."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7
8 8 from tornado import gen, web
9 9
10 from IPython.html.utils import url_path_join, url_escape
10 from jupyter_notebook.utils import url_path_join, url_escape
11 11 from jupyter_client.jsonutil import date_default
12 12
13 from IPython.html.base.handlers import (
13 from jupyter_notebook.base.handlers import (
14 14 IPythonHandler, json_errors, path_regex,
15 15 )
16 16
17 17
18 18 def sort_key(model):
19 19 """key function for case-insensitive sort by name and type"""
20 20 iname = model['name'].lower()
21 21 type_key = {
22 22 'directory' : '0',
23 23 'notebook' : '1',
24 24 'file' : '2',
25 25 }.get(model['type'], '9')
26 26 return u'%s%s' % (type_key, iname)
27 27
28 28
29 29 def validate_model(model, expect_content):
30 30 """
31 31 Validate a model returned by a ContentsManager method.
32 32
33 33 If expect_content is True, then we expect non-null entries for 'content'
34 34 and 'format'.
35 35 """
36 36 required_keys = {
37 37 "name",
38 38 "path",
39 39 "type",
40 40 "writable",
41 41 "created",
42 42 "last_modified",
43 43 "mimetype",
44 44 "content",
45 45 "format",
46 46 }
47 47 missing = required_keys - set(model.keys())
48 48 if missing:
49 49 raise web.HTTPError(
50 50 500,
51 51 u"Missing Model Keys: {missing}".format(missing=missing),
52 52 )
53 53
54 54 maybe_none_keys = ['content', 'format']
55 55 if model['type'] == 'file':
56 56 # mimetype should be populated only for file models
57 57 maybe_none_keys.append('mimetype')
58 58 if expect_content:
59 59 errors = [key for key in maybe_none_keys if model[key] is None]
60 60 if errors:
61 61 raise web.HTTPError(
62 62 500,
63 63 u"Keys unexpectedly None: {keys}".format(keys=errors),
64 64 )
65 65 else:
66 66 errors = {
67 67 key: model[key]
68 68 for key in maybe_none_keys
69 69 if model[key] is not None
70 70 }
71 71 if errors:
72 72 raise web.HTTPError(
73 73 500,
74 74 u"Keys unexpectedly not None: {keys}".format(keys=errors),
75 75 )
76 76
77 77
78 78 class ContentsHandler(IPythonHandler):
79 79
80 80 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
81 81
82 82 def location_url(self, path):
83 83 """Return the full URL location of a file.
84 84
85 85 Parameters
86 86 ----------
87 87 path : unicode
88 88 The API path of the file, such as "foo/bar.txt".
89 89 """
90 90 return url_escape(url_path_join(
91 91 self.base_url, 'api', 'contents', path
92 92 ))
93 93
94 94 def _finish_model(self, model, location=True):
95 95 """Finish a JSON request with a model, setting relevant headers, etc."""
96 96 if location:
97 97 location = self.location_url(model['path'])
98 98 self.set_header('Location', location)
99 99 self.set_header('Last-Modified', model['last_modified'])
100 100 self.set_header('Content-Type', 'application/json')
101 101 self.finish(json.dumps(model, default=date_default))
102 102
103 103 @web.authenticated
104 104 @json_errors
105 105 @gen.coroutine
106 106 def get(self, path=''):
107 107 """Return a model for a file or directory.
108 108
109 109 A directory model contains a list of models (without content)
110 110 of the files and directories it contains.
111 111 """
112 112 path = path or ''
113 113 type = self.get_query_argument('type', default=None)
114 114 if type not in {None, 'directory', 'file', 'notebook'}:
115 115 raise web.HTTPError(400, u'Type %r is invalid' % type)
116 116
117 117 format = self.get_query_argument('format', default=None)
118 118 if format not in {None, 'text', 'base64'}:
119 119 raise web.HTTPError(400, u'Format %r is invalid' % format)
120 120 content = self.get_query_argument('content', default='1')
121 121 if content not in {'0', '1'}:
122 122 raise web.HTTPError(400, u'Content %r is invalid' % content)
123 123 content = int(content)
124 124
125 125 model = yield gen.maybe_future(self.contents_manager.get(
126 126 path=path, type=type, format=format, content=content,
127 127 ))
128 128 if model['type'] == 'directory' and content:
129 129 # group listing by type, then by name (case-insensitive)
130 130 # FIXME: sorting should be done in the frontends
131 131 model['content'].sort(key=sort_key)
132 132 validate_model(model, expect_content=content)
133 133 self._finish_model(model, location=False)
134 134
135 135 @web.authenticated
136 136 @json_errors
137 137 @gen.coroutine
138 138 def patch(self, path=''):
139 139 """PATCH renames a file or directory without re-uploading content."""
140 140 cm = self.contents_manager
141 141 model = self.get_json_body()
142 142 if model is None:
143 143 raise web.HTTPError(400, u'JSON body missing')
144 144 model = yield gen.maybe_future(cm.update(model, path))
145 145 validate_model(model, expect_content=False)
146 146 self._finish_model(model)
147 147
148 148 @gen.coroutine
149 149 def _copy(self, copy_from, copy_to=None):
150 150 """Copy a file, optionally specifying a target directory."""
151 151 self.log.info(u"Copying {copy_from} to {copy_to}".format(
152 152 copy_from=copy_from,
153 153 copy_to=copy_to or '',
154 154 ))
155 155 model = yield gen.maybe_future(self.contents_manager.copy(copy_from, copy_to))
156 156 self.set_status(201)
157 157 validate_model(model, expect_content=False)
158 158 self._finish_model(model)
159 159
160 160 @gen.coroutine
161 161 def _upload(self, model, path):
162 162 """Handle upload of a new file to path"""
163 163 self.log.info(u"Uploading file to %s", path)
164 164 model = yield gen.maybe_future(self.contents_manager.new(model, path))
165 165 self.set_status(201)
166 166 validate_model(model, expect_content=False)
167 167 self._finish_model(model)
168 168
169 169 @gen.coroutine
170 170 def _new_untitled(self, path, type='', ext=''):
171 171 """Create a new, empty untitled entity"""
172 172 self.log.info(u"Creating new %s in %s", type or 'file', path)
173 173 model = yield gen.maybe_future(self.contents_manager.new_untitled(path=path, type=type, ext=ext))
174 174 self.set_status(201)
175 175 validate_model(model, expect_content=False)
176 176 self._finish_model(model)
177 177
178 178 @gen.coroutine
179 179 def _save(self, model, path):
180 180 """Save an existing file."""
181 181 self.log.info(u"Saving file at %s", path)
182 182 model = yield gen.maybe_future(self.contents_manager.save(model, path))
183 183 validate_model(model, expect_content=False)
184 184 self._finish_model(model)
185 185
186 186 @web.authenticated
187 187 @json_errors
188 188 @gen.coroutine
189 189 def post(self, path=''):
190 190 """Create a new file in the specified path.
191 191
192 192 POST creates new files. The server always decides on the name.
193 193
194 194 POST /api/contents/path
195 195 New untitled, empty file or directory.
196 196 POST /api/contents/path
197 197 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
198 198 New copy of OtherNotebook in path
199 199 """
200 200
201 201 cm = self.contents_manager
202 202
203 203 if cm.file_exists(path):
204 204 raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
205 205
206 206 if not cm.dir_exists(path):
207 207 raise web.HTTPError(404, "No such directory: %s" % path)
208 208
209 209 model = self.get_json_body()
210 210
211 211 if model is not None:
212 212 copy_from = model.get('copy_from')
213 213 ext = model.get('ext', '')
214 214 type = model.get('type', '')
215 215 if copy_from:
216 216 yield self._copy(copy_from, path)
217 217 else:
218 218 yield self._new_untitled(path, type=type, ext=ext)
219 219 else:
220 220 yield self._new_untitled(path)
221 221
222 222 @web.authenticated
223 223 @json_errors
224 224 @gen.coroutine
225 225 def put(self, path=''):
226 226 """Saves the file in the location specified by name and path.
227 227
228 228 PUT is very similar to POST, but the requester specifies the name,
229 229 whereas with POST, the server picks the name.
230 230
231 231 PUT /api/contents/path/Name.ipynb
232 232 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
233 233 in `content` key of JSON request body. If content is not specified,
234 234 create a new empty notebook.
235 235 """
236 236 model = self.get_json_body()
237 237 if model:
238 238 if model.get('copy_from'):
239 239 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
240 240 exists = yield gen.maybe_future(self.contents_manager.file_exists(path))
241 241 if exists:
242 242 yield gen.maybe_future(self._save(model, path))
243 243 else:
244 244 yield gen.maybe_future(self._upload(model, path))
245 245 else:
246 246 yield gen.maybe_future(self._new_untitled(path))
247 247
248 248 @web.authenticated
249 249 @json_errors
250 250 @gen.coroutine
251 251 def delete(self, path=''):
252 252 """delete a file in the given path"""
253 253 cm = self.contents_manager
254 254 self.log.warn('delete %s', path)
255 255 yield gen.maybe_future(cm.delete(path))
256 256 self.set_status(204)
257 257 self.finish()
258 258
259 259
260 260 class CheckpointsHandler(IPythonHandler):
261 261
262 262 SUPPORTED_METHODS = ('GET', 'POST')
263 263
264 264 @web.authenticated
265 265 @json_errors
266 266 @gen.coroutine
267 267 def get(self, path=''):
268 268 """get lists checkpoints for a file"""
269 269 cm = self.contents_manager
270 270 checkpoints = yield gen.maybe_future(cm.list_checkpoints(path))
271 271 data = json.dumps(checkpoints, default=date_default)
272 272 self.finish(data)
273 273
274 274 @web.authenticated
275 275 @json_errors
276 276 @gen.coroutine
277 277 def post(self, path=''):
278 278 """post creates a new checkpoint"""
279 279 cm = self.contents_manager
280 280 checkpoint = yield gen.maybe_future(cm.create_checkpoint(path))
281 281 data = json.dumps(checkpoint, default=date_default)
282 282 location = url_path_join(self.base_url, 'api/contents',
283 283 path, 'checkpoints', checkpoint['id'])
284 284 self.set_header('Location', url_escape(location))
285 285 self.set_status(201)
286 286 self.finish(data)
287 287
288 288
289 289 class ModifyCheckpointsHandler(IPythonHandler):
290 290
291 291 SUPPORTED_METHODS = ('POST', 'DELETE')
292 292
293 293 @web.authenticated
294 294 @json_errors
295 295 @gen.coroutine
296 296 def post(self, path, checkpoint_id):
297 297 """post restores a file from a checkpoint"""
298 298 cm = self.contents_manager
299 299 yield gen.maybe_future(cm.restore_checkpoint(checkpoint_id, path))
300 300 self.set_status(204)
301 301 self.finish()
302 302
303 303 @web.authenticated
304 304 @json_errors
305 305 @gen.coroutine
306 306 def delete(self, path, checkpoint_id):
307 307 """delete clears a checkpoint for a given file"""
308 308 cm = self.contents_manager
309 309 yield gen.maybe_future(cm.delete_checkpoint(checkpoint_id, path))
310 310 self.set_status(204)
311 311 self.finish()
312 312
313 313
314 314 class NotebooksRedirectHandler(IPythonHandler):
315 315 """Redirect /api/notebooks to /api/contents"""
316 316 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
317 317
318 318 def get(self, path):
319 319 self.log.warn("/api/notebooks is deprecated, use /api/contents")
320 320 self.redirect(url_path_join(
321 321 self.base_url,
322 322 'api/contents',
323 323 path
324 324 ))
325 325
326 326 put = patch = post = delete = get
327 327
328 328
329 329 #-----------------------------------------------------------------------------
330 330 # URL to handler mappings
331 331 #-----------------------------------------------------------------------------
332 332
333 333
334 334 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
335 335
336 336 default_handlers = [
337 337 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
338 338 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
339 339 ModifyCheckpointsHandler),
340 340 (r"/api/contents%s" % path_regex, ContentsHandler),
341 341 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
342 342 ]
@@ -1,694 +1,694
1 1 # coding: utf-8
2 2 """Test the contents webservice API."""
3 3
4 4 import base64
5 5 from contextlib import contextmanager
6 6 import io
7 7 import json
8 8 import os
9 9 import shutil
10 10 from unicodedata import normalize
11 11
12 12 pjoin = os.path.join
13 13
14 14 import requests
15 15
16 16 from ..filecheckpoints import GenericFileCheckpoints
17 17
18 18 from IPython.config import Config
19 from IPython.html.utils import url_path_join, url_escape, to_os_path
20 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
19 from jupyter_notebook.utils import url_path_join, url_escape, to_os_path
20 from jupyter_notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
21 21 from IPython.nbformat import read, write, from_dict
22 22 from IPython.nbformat.v4 import (
23 23 new_notebook, new_markdown_cell,
24 24 )
25 25 from IPython.nbformat import v2
26 26 from IPython.utils import py3compat
27 27 from IPython.utils.tempdir import TemporaryDirectory
28 28
29 29 def uniq_stable(elems):
30 30 """uniq_stable(elems) -> list
31 31
32 32 Return from an iterable, a list of all the unique elements in the input,
33 33 maintaining the order in which they first appear.
34 34 """
35 35 seen = set()
36 36 return [x for x in elems if x not in seen and not seen.add(x)]
37 37
38 38 def notebooks_only(dir_model):
39 39 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
40 40
41 41 def dirs_only(dir_model):
42 42 return [x for x in dir_model['content'] if x['type']=='directory']
43 43
44 44
45 45 class API(object):
46 46 """Wrapper for contents API calls."""
47 47 def __init__(self, base_url):
48 48 self.base_url = base_url
49 49
50 50 def _req(self, verb, path, body=None, params=None):
51 51 response = requests.request(verb,
52 52 url_path_join(self.base_url, 'api/contents', path),
53 53 data=body, params=params,
54 54 )
55 55 response.raise_for_status()
56 56 return response
57 57
58 58 def list(self, path='/'):
59 59 return self._req('GET', path)
60 60
61 61 def read(self, path, type=None, format=None, content=None):
62 62 params = {}
63 63 if type is not None:
64 64 params['type'] = type
65 65 if format is not None:
66 66 params['format'] = format
67 67 if content == False:
68 68 params['content'] = '0'
69 69 return self._req('GET', path, params=params)
70 70
71 71 def create_untitled(self, path='/', ext='.ipynb'):
72 72 body = None
73 73 if ext:
74 74 body = json.dumps({'ext': ext})
75 75 return self._req('POST', path, body)
76 76
77 77 def mkdir_untitled(self, path='/'):
78 78 return self._req('POST', path, json.dumps({'type': 'directory'}))
79 79
80 80 def copy(self, copy_from, path='/'):
81 81 body = json.dumps({'copy_from':copy_from})
82 82 return self._req('POST', path, body)
83 83
84 84 def create(self, path='/'):
85 85 return self._req('PUT', path)
86 86
87 87 def upload(self, path, body):
88 88 return self._req('PUT', path, body)
89 89
90 90 def mkdir(self, path='/'):
91 91 return self._req('PUT', path, json.dumps({'type': 'directory'}))
92 92
93 93 def copy_put(self, copy_from, path='/'):
94 94 body = json.dumps({'copy_from':copy_from})
95 95 return self._req('PUT', path, body)
96 96
97 97 def save(self, path, body):
98 98 return self._req('PUT', path, body)
99 99
100 100 def delete(self, path='/'):
101 101 return self._req('DELETE', path)
102 102
103 103 def rename(self, path, new_path):
104 104 body = json.dumps({'path': new_path})
105 105 return self._req('PATCH', path, body)
106 106
107 107 def get_checkpoints(self, path):
108 108 return self._req('GET', url_path_join(path, 'checkpoints'))
109 109
110 110 def new_checkpoint(self, path):
111 111 return self._req('POST', url_path_join(path, 'checkpoints'))
112 112
113 113 def restore_checkpoint(self, path, checkpoint_id):
114 114 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
115 115
116 116 def delete_checkpoint(self, path, checkpoint_id):
117 117 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
118 118
119 119 class APITest(NotebookTestBase):
120 120 """Test the kernels web service API"""
121 121 dirs_nbs = [('', 'inroot'),
122 122 ('Directory with spaces in', 'inspace'),
123 123 (u'unicodé', 'innonascii'),
124 124 ('foo', 'a'),
125 125 ('foo', 'b'),
126 126 ('foo', 'name with spaces'),
127 127 ('foo', u'unicodé'),
128 128 ('foo/bar', 'baz'),
129 129 ('ordering', 'A'),
130 130 ('ordering', 'b'),
131 131 ('ordering', 'C'),
132 132 (u'å b', u'ç d'),
133 133 ]
134 134 hidden_dirs = ['.hidden', '__pycache__']
135 135
136 136 # Don't include root dir.
137 137 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs[1:]])
138 138 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
139 139
140 140 @staticmethod
141 141 def _blob_for_name(name):
142 142 return name.encode('utf-8') + b'\xFF'
143 143
144 144 @staticmethod
145 145 def _txt_for_name(name):
146 146 return u'%s text file' % name
147 147
148 148 def to_os_path(self, api_path):
149 149 return to_os_path(api_path, root=self.notebook_dir.name)
150 150
151 151 def make_dir(self, api_path):
152 152 """Create a directory at api_path"""
153 153 os_path = self.to_os_path(api_path)
154 154 try:
155 155 os.makedirs(os_path)
156 156 except OSError:
157 157 print("Directory already exists: %r" % os_path)
158 158
159 159 def make_txt(self, api_path, txt):
160 160 """Make a text file at a given api_path"""
161 161 os_path = self.to_os_path(api_path)
162 162 with io.open(os_path, 'w', encoding='utf-8') as f:
163 163 f.write(txt)
164 164
165 165 def make_blob(self, api_path, blob):
166 166 """Make a binary file at a given api_path"""
167 167 os_path = self.to_os_path(api_path)
168 168 with io.open(os_path, 'wb') as f:
169 169 f.write(blob)
170 170
171 171 def make_nb(self, api_path, nb):
172 172 """Make a notebook file at a given api_path"""
173 173 os_path = self.to_os_path(api_path)
174 174
175 175 with io.open(os_path, 'w', encoding='utf-8') as f:
176 176 write(nb, f, version=4)
177 177
178 178 def delete_dir(self, api_path):
179 179 """Delete a directory at api_path, removing any contents."""
180 180 os_path = self.to_os_path(api_path)
181 181 shutil.rmtree(os_path, ignore_errors=True)
182 182
183 183 def delete_file(self, api_path):
184 184 """Delete a file at the given path if it exists."""
185 185 if self.isfile(api_path):
186 186 os.unlink(self.to_os_path(api_path))
187 187
188 188 def isfile(self, api_path):
189 189 return os.path.isfile(self.to_os_path(api_path))
190 190
191 191 def isdir(self, api_path):
192 192 return os.path.isdir(self.to_os_path(api_path))
193 193
194 194 def setUp(self):
195 195
196 196 for d in (self.dirs + self.hidden_dirs):
197 197 self.make_dir(d)
198 198
199 199 for d, name in self.dirs_nbs:
200 200 # create a notebook
201 201 nb = new_notebook()
202 202 self.make_nb(u'{}/{}.ipynb'.format(d, name), nb)
203 203
204 204 # create a text file
205 205 txt = self._txt_for_name(name)
206 206 self.make_txt(u'{}/{}.txt'.format(d, name), txt)
207 207
208 208 # create a binary file
209 209 blob = self._blob_for_name(name)
210 210 self.make_blob(u'{}/{}.blob'.format(d, name), blob)
211 211
212 212 self.api = API(self.base_url())
213 213
214 214 def tearDown(self):
215 215 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
216 216 self.delete_dir(dname)
217 217 self.delete_file('inroot.ipynb')
218 218
219 219 def test_list_notebooks(self):
220 220 nbs = notebooks_only(self.api.list().json())
221 221 self.assertEqual(len(nbs), 1)
222 222 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
223 223
224 224 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
225 225 self.assertEqual(len(nbs), 1)
226 226 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
227 227
228 228 nbs = notebooks_only(self.api.list(u'/unicodé/').json())
229 229 self.assertEqual(len(nbs), 1)
230 230 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
231 231 self.assertEqual(nbs[0]['path'], u'unicodé/innonascii.ipynb')
232 232
233 233 nbs = notebooks_only(self.api.list('/foo/bar/').json())
234 234 self.assertEqual(len(nbs), 1)
235 235 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
236 236 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
237 237
238 238 nbs = notebooks_only(self.api.list('foo').json())
239 239 self.assertEqual(len(nbs), 4)
240 240 nbnames = { normalize('NFC', n['name']) for n in nbs }
241 241 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb']
242 242 expected = { normalize('NFC', name) for name in expected }
243 243 self.assertEqual(nbnames, expected)
244 244
245 245 nbs = notebooks_only(self.api.list('ordering').json())
246 246 nbnames = [n['name'] for n in nbs]
247 247 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
248 248 self.assertEqual(nbnames, expected)
249 249
250 250 def test_list_dirs(self):
251 251 dirs = dirs_only(self.api.list().json())
252 252 dir_names = {normalize('NFC', d['name']) for d in dirs}
253 253 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
254 254
255 255 def test_get_dir_no_content(self):
256 256 for d in self.dirs:
257 257 model = self.api.read(d, content=False).json()
258 258 self.assertEqual(model['path'], d)
259 259 self.assertEqual(model['type'], 'directory')
260 260 self.assertIn('content', model)
261 261 self.assertEqual(model['content'], None)
262 262
263 263 def test_list_nonexistant_dir(self):
264 264 with assert_http_error(404):
265 265 self.api.list('nonexistant')
266 266
267 267 def test_get_nb_contents(self):
268 268 for d, name in self.dirs_nbs:
269 269 path = url_path_join(d, name + '.ipynb')
270 270 nb = self.api.read(path).json()
271 271 self.assertEqual(nb['name'], u'%s.ipynb' % name)
272 272 self.assertEqual(nb['path'], path)
273 273 self.assertEqual(nb['type'], 'notebook')
274 274 self.assertIn('content', nb)
275 275 self.assertEqual(nb['format'], 'json')
276 276 self.assertIn('metadata', nb['content'])
277 277 self.assertIsInstance(nb['content']['metadata'], dict)
278 278
279 279 def test_get_nb_no_content(self):
280 280 for d, name in self.dirs_nbs:
281 281 path = url_path_join(d, name + '.ipynb')
282 282 nb = self.api.read(path, content=False).json()
283 283 self.assertEqual(nb['name'], u'%s.ipynb' % name)
284 284 self.assertEqual(nb['path'], path)
285 285 self.assertEqual(nb['type'], 'notebook')
286 286 self.assertIn('content', nb)
287 287 self.assertEqual(nb['content'], None)
288 288
289 289 def test_get_contents_no_such_file(self):
290 290 # Name that doesn't exist - should be a 404
291 291 with assert_http_error(404):
292 292 self.api.read('foo/q.ipynb')
293 293
294 294 def test_get_text_file_contents(self):
295 295 for d, name in self.dirs_nbs:
296 296 path = url_path_join(d, name + '.txt')
297 297 model = self.api.read(path).json()
298 298 self.assertEqual(model['name'], u'%s.txt' % name)
299 299 self.assertEqual(model['path'], path)
300 300 self.assertIn('content', model)
301 301 self.assertEqual(model['format'], 'text')
302 302 self.assertEqual(model['type'], 'file')
303 303 self.assertEqual(model['content'], self._txt_for_name(name))
304 304
305 305 # Name that doesn't exist - should be a 404
306 306 with assert_http_error(404):
307 307 self.api.read('foo/q.txt')
308 308
309 309 # Specifying format=text should fail on a non-UTF-8 file
310 310 with assert_http_error(400):
311 311 self.api.read('foo/bar/baz.blob', type='file', format='text')
312 312
313 313 def test_get_binary_file_contents(self):
314 314 for d, name in self.dirs_nbs:
315 315 path = url_path_join(d, name + '.blob')
316 316 model = self.api.read(path).json()
317 317 self.assertEqual(model['name'], u'%s.blob' % name)
318 318 self.assertEqual(model['path'], path)
319 319 self.assertIn('content', model)
320 320 self.assertEqual(model['format'], 'base64')
321 321 self.assertEqual(model['type'], 'file')
322 322 self.assertEqual(
323 323 base64.decodestring(model['content'].encode('ascii')),
324 324 self._blob_for_name(name),
325 325 )
326 326
327 327 # Name that doesn't exist - should be a 404
328 328 with assert_http_error(404):
329 329 self.api.read('foo/q.txt')
330 330
331 331 def test_get_bad_type(self):
332 332 with assert_http_error(400):
333 333 self.api.read(u'unicodé', type='file') # this is a directory
334 334
335 335 with assert_http_error(400):
336 336 self.api.read(u'unicodé/innonascii.ipynb', type='directory')
337 337
338 338 def _check_created(self, resp, path, type='notebook'):
339 339 self.assertEqual(resp.status_code, 201)
340 340 location_header = py3compat.str_to_unicode(resp.headers['Location'])
341 341 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
342 342 rjson = resp.json()
343 343 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
344 344 self.assertEqual(rjson['path'], path)
345 345 self.assertEqual(rjson['type'], type)
346 346 isright = self.isdir if type == 'directory' else self.isfile
347 347 assert isright(path)
348 348
349 349 def test_create_untitled(self):
350 350 resp = self.api.create_untitled(path=u'Ã¥ b')
351 351 self._check_created(resp, u'Ã¥ b/Untitled.ipynb')
352 352
353 353 # Second time
354 354 resp = self.api.create_untitled(path=u'Ã¥ b')
355 355 self._check_created(resp, u'Ã¥ b/Untitled1.ipynb')
356 356
357 357 # And two directories down
358 358 resp = self.api.create_untitled(path='foo/bar')
359 359 self._check_created(resp, 'foo/bar/Untitled.ipynb')
360 360
361 361 def test_create_untitled_txt(self):
362 362 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
363 363 self._check_created(resp, 'foo/bar/untitled.txt', type='file')
364 364
365 365 resp = self.api.read(path='foo/bar/untitled.txt')
366 366 model = resp.json()
367 367 self.assertEqual(model['type'], 'file')
368 368 self.assertEqual(model['format'], 'text')
369 369 self.assertEqual(model['content'], '')
370 370
371 371 def test_upload(self):
372 372 nb = new_notebook()
373 373 nbmodel = {'content': nb, 'type': 'notebook'}
374 374 path = u'å b/Upload tést.ipynb'
375 375 resp = self.api.upload(path, body=json.dumps(nbmodel))
376 376 self._check_created(resp, path)
377 377
378 378 def test_mkdir_untitled(self):
379 379 resp = self.api.mkdir_untitled(path=u'Ã¥ b')
380 380 self._check_created(resp, u'Ã¥ b/Untitled Folder', type='directory')
381 381
382 382 # Second time
383 383 resp = self.api.mkdir_untitled(path=u'Ã¥ b')
384 384 self._check_created(resp, u'Ã¥ b/Untitled Folder 1', type='directory')
385 385
386 386 # And two directories down
387 387 resp = self.api.mkdir_untitled(path='foo/bar')
388 388 self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
389 389
390 390 def test_mkdir(self):
391 391 path = u'å b/New ∂ir'
392 392 resp = self.api.mkdir(path)
393 393 self._check_created(resp, path, type='directory')
394 394
395 395 def test_mkdir_hidden_400(self):
396 396 with assert_http_error(400):
397 397 resp = self.api.mkdir(u'Ã¥ b/.hidden')
398 398
399 399 def test_upload_txt(self):
400 400 body = u'ünicode téxt'
401 401 model = {
402 402 'content' : body,
403 403 'format' : 'text',
404 404 'type' : 'file',
405 405 }
406 406 path = u'å b/Upload tést.txt'
407 407 resp = self.api.upload(path, body=json.dumps(model))
408 408
409 409 # check roundtrip
410 410 resp = self.api.read(path)
411 411 model = resp.json()
412 412 self.assertEqual(model['type'], 'file')
413 413 self.assertEqual(model['format'], 'text')
414 414 self.assertEqual(model['content'], body)
415 415
416 416 def test_upload_b64(self):
417 417 body = b'\xFFblob'
418 418 b64body = base64.encodestring(body).decode('ascii')
419 419 model = {
420 420 'content' : b64body,
421 421 'format' : 'base64',
422 422 'type' : 'file',
423 423 }
424 424 path = u'å b/Upload tést.blob'
425 425 resp = self.api.upload(path, body=json.dumps(model))
426 426
427 427 # check roundtrip
428 428 resp = self.api.read(path)
429 429 model = resp.json()
430 430 self.assertEqual(model['type'], 'file')
431 431 self.assertEqual(model['path'], path)
432 432 self.assertEqual(model['format'], 'base64')
433 433 decoded = base64.decodestring(model['content'].encode('ascii'))
434 434 self.assertEqual(decoded, body)
435 435
436 436 def test_upload_v2(self):
437 437 nb = v2.new_notebook()
438 438 ws = v2.new_worksheet()
439 439 nb.worksheets.append(ws)
440 440 ws.cells.append(v2.new_code_cell(input='print("hi")'))
441 441 nbmodel = {'content': nb, 'type': 'notebook'}
442 442 path = u'å b/Upload tést.ipynb'
443 443 resp = self.api.upload(path, body=json.dumps(nbmodel))
444 444 self._check_created(resp, path)
445 445 resp = self.api.read(path)
446 446 data = resp.json()
447 447 self.assertEqual(data['content']['nbformat'], 4)
448 448
449 449 def test_copy(self):
450 450 resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
451 451 self._check_created(resp, u'å b/ç d-Copy1.ipynb')
452 452
453 453 resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
454 454 self._check_created(resp, u'å b/ç d-Copy2.ipynb')
455 455
456 456 def test_copy_copy(self):
457 457 resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
458 458 self._check_created(resp, u'å b/ç d-Copy1.ipynb')
459 459
460 460 resp = self.api.copy(u'å b/ç d-Copy1.ipynb', u'å b')
461 461 self._check_created(resp, u'å b/ç d-Copy2.ipynb')
462 462
463 463 def test_copy_path(self):
464 464 resp = self.api.copy(u'foo/a.ipynb', u'Ã¥ b')
465 465 self._check_created(resp, u'Ã¥ b/a.ipynb')
466 466
467 467 resp = self.api.copy(u'foo/a.ipynb', u'Ã¥ b')
468 468 self._check_created(resp, u'Ã¥ b/a-Copy1.ipynb')
469 469
470 470 def test_copy_put_400(self):
471 471 with assert_http_error(400):
472 472 resp = self.api.copy_put(u'å b/ç d.ipynb', u'å b/cøpy.ipynb')
473 473
474 474 def test_copy_dir_400(self):
475 475 # can't copy directories
476 476 with assert_http_error(400):
477 477 resp = self.api.copy(u'Ã¥ b', u'foo')
478 478
479 479 def test_delete(self):
480 480 for d, name in self.dirs_nbs:
481 481 print('%r, %r' % (d, name))
482 482 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
483 483 self.assertEqual(resp.status_code, 204)
484 484
485 485 for d in self.dirs + ['/']:
486 486 nbs = notebooks_only(self.api.list(d).json())
487 487 print('------')
488 488 print(d)
489 489 print(nbs)
490 490 self.assertEqual(nbs, [])
491 491
492 492 def test_delete_dirs(self):
493 493 # depth-first delete everything, so we don't try to delete empty directories
494 494 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
495 495 listing = self.api.list(name).json()['content']
496 496 for model in listing:
497 497 self.api.delete(model['path'])
498 498 listing = self.api.list('/').json()['content']
499 499 self.assertEqual(listing, [])
500 500
501 501 def test_delete_non_empty_dir(self):
502 502 """delete non-empty dir raises 400"""
503 503 with assert_http_error(400):
504 504 self.api.delete(u'Ã¥ b')
505 505
506 506 def test_rename(self):
507 507 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
508 508 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
509 509 self.assertEqual(resp.json()['name'], 'z.ipynb')
510 510 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
511 511 assert self.isfile('foo/z.ipynb')
512 512
513 513 nbs = notebooks_only(self.api.list('foo').json())
514 514 nbnames = set(n['name'] for n in nbs)
515 515 self.assertIn('z.ipynb', nbnames)
516 516 self.assertNotIn('a.ipynb', nbnames)
517 517
518 518 def test_checkpoints_follow_file(self):
519 519
520 520 # Read initial file state
521 521 orig = self.api.read('foo/a.ipynb')
522 522
523 523 # Create a checkpoint of initial state
524 524 r = self.api.new_checkpoint('foo/a.ipynb')
525 525 cp1 = r.json()
526 526
527 527 # Modify file and save
528 528 nbcontent = json.loads(orig.text)['content']
529 529 nb = from_dict(nbcontent)
530 530 hcell = new_markdown_cell('Created by test')
531 531 nb.cells.append(hcell)
532 532 nbmodel = {'content': nb, 'type': 'notebook'}
533 533 self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
534 534
535 535 # Rename the file.
536 536 self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
537 537
538 538 # Looking for checkpoints in the old location should yield no results.
539 539 self.assertEqual(self.api.get_checkpoints('foo/a.ipynb').json(), [])
540 540
541 541 # Looking for checkpoints in the new location should work.
542 542 cps = self.api.get_checkpoints('foo/z.ipynb').json()
543 543 self.assertEqual(cps, [cp1])
544 544
545 545 # Delete the file. The checkpoint should be deleted as well.
546 546 self.api.delete('foo/z.ipynb')
547 547 cps = self.api.get_checkpoints('foo/z.ipynb').json()
548 548 self.assertEqual(cps, [])
549 549
550 550 def test_rename_existing(self):
551 551 with assert_http_error(409):
552 552 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
553 553
554 554 def test_save(self):
555 555 resp = self.api.read('foo/a.ipynb')
556 556 nbcontent = json.loads(resp.text)['content']
557 557 nb = from_dict(nbcontent)
558 558 nb.cells.append(new_markdown_cell(u'Created by test ³'))
559 559
560 560 nbmodel = {'content': nb, 'type': 'notebook'}
561 561 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
562 562
563 563 nbcontent = self.api.read('foo/a.ipynb').json()['content']
564 564 newnb = from_dict(nbcontent)
565 565 self.assertEqual(newnb.cells[0].source,
566 566 u'Created by test ³')
567 567
568 568 def test_checkpoints(self):
569 569 resp = self.api.read('foo/a.ipynb')
570 570 r = self.api.new_checkpoint('foo/a.ipynb')
571 571 self.assertEqual(r.status_code, 201)
572 572 cp1 = r.json()
573 573 self.assertEqual(set(cp1), {'id', 'last_modified'})
574 574 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
575 575
576 576 # Modify it
577 577 nbcontent = json.loads(resp.text)['content']
578 578 nb = from_dict(nbcontent)
579 579 hcell = new_markdown_cell('Created by test')
580 580 nb.cells.append(hcell)
581 581 # Save
582 582 nbmodel= {'content': nb, 'type': 'notebook'}
583 583 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
584 584
585 585 # List checkpoints
586 586 cps = self.api.get_checkpoints('foo/a.ipynb').json()
587 587 self.assertEqual(cps, [cp1])
588 588
589 589 nbcontent = self.api.read('foo/a.ipynb').json()['content']
590 590 nb = from_dict(nbcontent)
591 591 self.assertEqual(nb.cells[0].source, 'Created by test')
592 592
593 593 # Restore cp1
594 594 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
595 595 self.assertEqual(r.status_code, 204)
596 596 nbcontent = self.api.read('foo/a.ipynb').json()['content']
597 597 nb = from_dict(nbcontent)
598 598 self.assertEqual(nb.cells, [])
599 599
600 600 # Delete cp1
601 601 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
602 602 self.assertEqual(r.status_code, 204)
603 603 cps = self.api.get_checkpoints('foo/a.ipynb').json()
604 604 self.assertEqual(cps, [])
605 605
606 606 def test_file_checkpoints(self):
607 607 """
608 608 Test checkpointing of non-notebook files.
609 609 """
610 610 filename = 'foo/a.txt'
611 611 resp = self.api.read(filename)
612 612 orig_content = json.loads(resp.text)['content']
613 613
614 614 # Create a checkpoint.
615 615 r = self.api.new_checkpoint(filename)
616 616 self.assertEqual(r.status_code, 201)
617 617 cp1 = r.json()
618 618 self.assertEqual(set(cp1), {'id', 'last_modified'})
619 619 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
620 620
621 621 # Modify the file and save.
622 622 new_content = orig_content + '\nsecond line'
623 623 model = {
624 624 'content': new_content,
625 625 'type': 'file',
626 626 'format': 'text',
627 627 }
628 628 resp = self.api.save(filename, body=json.dumps(model))
629 629
630 630 # List checkpoints
631 631 cps = self.api.get_checkpoints(filename).json()
632 632 self.assertEqual(cps, [cp1])
633 633
634 634 content = self.api.read(filename).json()['content']
635 635 self.assertEqual(content, new_content)
636 636
637 637 # Restore cp1
638 638 r = self.api.restore_checkpoint(filename, cp1['id'])
639 639 self.assertEqual(r.status_code, 204)
640 640 restored_content = self.api.read(filename).json()['content']
641 641 self.assertEqual(restored_content, orig_content)
642 642
643 643 # Delete cp1
644 644 r = self.api.delete_checkpoint(filename, cp1['id'])
645 645 self.assertEqual(r.status_code, 204)
646 646 cps = self.api.get_checkpoints(filename).json()
647 647 self.assertEqual(cps, [])
648 648
649 649 @contextmanager
650 650 def patch_cp_root(self, dirname):
651 651 """
652 652 Temporarily patch the root dir of our checkpoint manager.
653 653 """
654 654 cpm = self.notebook.contents_manager.checkpoints
655 655 old_dirname = cpm.root_dir
656 656 cpm.root_dir = dirname
657 657 try:
658 658 yield
659 659 finally:
660 660 cpm.root_dir = old_dirname
661 661
662 662 def test_checkpoints_separate_root(self):
663 663 """
664 664 Test that FileCheckpoints functions correctly even when it's
665 665 using a different root dir from FileContentsManager. This also keeps
666 666 the implementation honest for use with ContentsManagers that don't map
667 667 models to the filesystem
668 668
669 669 Override this method to a no-op when testing other managers.
670 670 """
671 671 with TemporaryDirectory() as td:
672 672 with self.patch_cp_root(td):
673 673 self.test_checkpoints()
674 674
675 675 with TemporaryDirectory() as td:
676 676 with self.patch_cp_root(td):
677 677 self.test_file_checkpoints()
678 678
679 679
680 680 class GenericFileCheckpointsAPITest(APITest):
681 681 """
682 682 Run the tests from APITest with GenericFileCheckpoints.
683 683 """
684 684 config = Config()
685 685 config.FileContentsManager.checkpoints_class = GenericFileCheckpoints
686 686
687 687 def test_config_did_something(self):
688 688
689 689 self.assertIsInstance(
690 690 self.notebook.contents_manager.checkpoints,
691 691 GenericFileCheckpoints,
692 692 )
693 693
694 694
@@ -1,288 +1,288
1 1 """Tornado handlers for kernels."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7 import logging
8 8 from tornado import gen, web
9 9 from tornado.concurrent import Future
10 10 from tornado.ioloop import IOLoop
11 11
12 12 from jupyter_client.jsonutil import date_default
13 13 from IPython.utils.py3compat import cast_unicode
14 from IPython.html.utils import url_path_join, url_escape
14 from jupyter_notebook.utils import url_path_join, url_escape
15 15
16 16 from ...base.handlers import IPythonHandler, json_errors
17 17 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
18 18
19 19 from IPython.core.release import kernel_protocol_version
20 20
21 21 class MainKernelHandler(IPythonHandler):
22 22
23 23 @web.authenticated
24 24 @json_errors
25 25 def get(self):
26 26 km = self.kernel_manager
27 27 self.finish(json.dumps(km.list_kernels()))
28 28
29 29 @web.authenticated
30 30 @json_errors
31 31 def post(self):
32 32 km = self.kernel_manager
33 33 model = self.get_json_body()
34 34 if model is None:
35 35 model = {
36 36 'name': km.default_kernel_name
37 37 }
38 38 else:
39 39 model.setdefault('name', km.default_kernel_name)
40 40
41 41 kernel_id = km.start_kernel(kernel_name=model['name'])
42 42 model = km.kernel_model(kernel_id)
43 43 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
44 44 self.set_header('Location', url_escape(location))
45 45 self.set_status(201)
46 46 self.finish(json.dumps(model))
47 47
48 48
49 49 class KernelHandler(IPythonHandler):
50 50
51 51 SUPPORTED_METHODS = ('DELETE', 'GET')
52 52
53 53 @web.authenticated
54 54 @json_errors
55 55 def get(self, kernel_id):
56 56 km = self.kernel_manager
57 57 km._check_kernel_id(kernel_id)
58 58 model = km.kernel_model(kernel_id)
59 59 self.finish(json.dumps(model))
60 60
61 61 @web.authenticated
62 62 @json_errors
63 63 def delete(self, kernel_id):
64 64 km = self.kernel_manager
65 65 km.shutdown_kernel(kernel_id)
66 66 self.set_status(204)
67 67 self.finish()
68 68
69 69
70 70 class KernelActionHandler(IPythonHandler):
71 71
72 72 @web.authenticated
73 73 @json_errors
74 74 def post(self, kernel_id, action):
75 75 km = self.kernel_manager
76 76 if action == 'interrupt':
77 77 km.interrupt_kernel(kernel_id)
78 78 self.set_status(204)
79 79 if action == 'restart':
80 80 km.restart_kernel(kernel_id)
81 81 model = km.kernel_model(kernel_id)
82 82 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_url, kernel_id))
83 83 self.write(json.dumps(model))
84 84 self.finish()
85 85
86 86
87 87 class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
88 88
89 89 @property
90 90 def kernel_info_timeout(self):
91 91 return self.settings.get('kernel_info_timeout', 10)
92 92
93 93 def __repr__(self):
94 94 return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
95 95
96 96 def create_stream(self):
97 97 km = self.kernel_manager
98 98 identity = self.session.bsession
99 99 for channel in ('shell', 'iopub', 'stdin'):
100 100 meth = getattr(km, 'connect_' + channel)
101 101 self.channels[channel] = stream = meth(self.kernel_id, identity=identity)
102 102 stream.channel = channel
103 103 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
104 104 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
105 105
106 106 def request_kernel_info(self):
107 107 """send a request for kernel_info"""
108 108 km = self.kernel_manager
109 109 kernel = km.get_kernel(self.kernel_id)
110 110 try:
111 111 # check for previous request
112 112 future = kernel._kernel_info_future
113 113 except AttributeError:
114 114 self.log.debug("Requesting kernel info from %s", self.kernel_id)
115 115 # Create a kernel_info channel to query the kernel protocol version.
116 116 # This channel will be closed after the kernel_info reply is received.
117 117 if self.kernel_info_channel is None:
118 118 self.kernel_info_channel = km.connect_shell(self.kernel_id)
119 119 self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
120 120 self.session.send(self.kernel_info_channel, "kernel_info_request")
121 121 # store the future on the kernel, so only one request is sent
122 122 kernel._kernel_info_future = self._kernel_info_future
123 123 else:
124 124 if not future.done():
125 125 self.log.debug("Waiting for pending kernel_info request")
126 126 future.add_done_callback(lambda f: self._finish_kernel_info(f.result()))
127 127 return self._kernel_info_future
128 128
129 129 def _handle_kernel_info_reply(self, msg):
130 130 """process the kernel_info_reply
131 131
132 132 enabling msg spec adaptation, if necessary
133 133 """
134 134 idents,msg = self.session.feed_identities(msg)
135 135 try:
136 136 msg = self.session.deserialize(msg)
137 137 except:
138 138 self.log.error("Bad kernel_info reply", exc_info=True)
139 139 self._kernel_info_future.set_result({})
140 140 return
141 141 else:
142 142 info = msg['content']
143 143 self.log.debug("Received kernel info: %s", info)
144 144 if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info:
145 145 self.log.error("Kernel info request failed, assuming current %s", info)
146 146 info = {}
147 147 self._finish_kernel_info(info)
148 148
149 149 # close the kernel_info channel, we don't need it anymore
150 150 if self.kernel_info_channel:
151 151 self.kernel_info_channel.close()
152 152 self.kernel_info_channel = None
153 153
154 154 def _finish_kernel_info(self, info):
155 155 """Finish handling kernel_info reply
156 156
157 157 Set up protocol adaptation, if needed,
158 158 and signal that connection can continue.
159 159 """
160 160 protocol_version = info.get('protocol_version', kernel_protocol_version)
161 161 if protocol_version != kernel_protocol_version:
162 162 self.session.adapt_version = int(protocol_version.split('.')[0])
163 163 self.log.info("Adapting to protocol v%s for kernel %s", protocol_version, self.kernel_id)
164 164 if not self._kernel_info_future.done():
165 165 self._kernel_info_future.set_result(info)
166 166
167 167 def initialize(self):
168 168 super(ZMQChannelsHandler, self).initialize()
169 169 self.zmq_stream = None
170 170 self.channels = {}
171 171 self.kernel_id = None
172 172 self.kernel_info_channel = None
173 173 self._kernel_info_future = Future()
174 174
175 175 @gen.coroutine
176 176 def pre_get(self):
177 177 # authenticate first
178 178 super(ZMQChannelsHandler, self).pre_get()
179 179 # then request kernel info, waiting up to a certain time before giving up.
180 180 # We don't want to wait forever, because browsers don't take it well when
181 181 # servers never respond to websocket connection requests.
182 182 kernel = self.kernel_manager.get_kernel(self.kernel_id)
183 183 self.session.key = kernel.session.key
184 184 future = self.request_kernel_info()
185 185
186 186 def give_up():
187 187 """Don't wait forever for the kernel to reply"""
188 188 if future.done():
189 189 return
190 190 self.log.warn("Timeout waiting for kernel_info reply from %s", self.kernel_id)
191 191 future.set_result({})
192 192 loop = IOLoop.current()
193 193 loop.add_timeout(loop.time() + self.kernel_info_timeout, give_up)
194 194 # actually wait for it
195 195 yield future
196 196
197 197 @gen.coroutine
198 198 def get(self, kernel_id):
199 199 self.kernel_id = cast_unicode(kernel_id, 'ascii')
200 200 yield super(ZMQChannelsHandler, self).get(kernel_id=kernel_id)
201 201
202 202 def open(self, kernel_id):
203 203 super(ZMQChannelsHandler, self).open()
204 204 try:
205 205 self.create_stream()
206 206 except web.HTTPError as e:
207 207 self.log.error("Error opening stream: %s", e)
208 208 # WebSockets don't response to traditional error codes so we
209 209 # close the connection.
210 210 for channel, stream in self.channels.items():
211 211 if not stream.closed():
212 212 stream.close()
213 213 self.close()
214 214 else:
215 215 for channel, stream in self.channels.items():
216 216 stream.on_recv_stream(self._on_zmq_reply)
217 217
218 218 def on_message(self, msg):
219 219 if not self.channels:
220 220 # already closed, ignore the message
221 221 self.log.debug("Received message on closed websocket %r", msg)
222 222 return
223 223 if isinstance(msg, bytes):
224 224 msg = deserialize_binary_message(msg)
225 225 else:
226 226 msg = json.loads(msg)
227 227 channel = msg.pop('channel', None)
228 228 if channel is None:
229 229 self.log.warn("No channel specified, assuming shell: %s", msg)
230 230 channel = 'shell'
231 231 if channel not in self.channels:
232 232 self.log.warn("No such channel: %r", channel)
233 233 return
234 234 stream = self.channels[channel]
235 235 self.session.send(stream, msg)
236 236
237 237 def on_close(self):
238 238 km = self.kernel_manager
239 239 if self.kernel_id in km:
240 240 km.remove_restart_callback(
241 241 self.kernel_id, self.on_kernel_restarted,
242 242 )
243 243 km.remove_restart_callback(
244 244 self.kernel_id, self.on_restart_failed, 'dead',
245 245 )
246 246 # This method can be called twice, once by self.kernel_died and once
247 247 # from the WebSocket close event. If the WebSocket connection is
248 248 # closed before the ZMQ streams are setup, they could be None.
249 249 for channel, stream in self.channels.items():
250 250 if stream is not None and not stream.closed():
251 251 stream.on_recv(None)
252 252 # close the socket directly, don't wait for the stream
253 253 socket = stream.socket
254 254 stream.close()
255 255 socket.close()
256 256
257 257 self.channels = {}
258 258
259 259 def _send_status_message(self, status):
260 260 msg = self.session.msg("status",
261 261 {'execution_state': status}
262 262 )
263 263 msg['channel'] = 'iopub'
264 264 self.write_message(json.dumps(msg, default=date_default))
265 265
266 266 def on_kernel_restarted(self):
267 267 logging.warn("kernel %s restarted", self.kernel_id)
268 268 self._send_status_message('restarting')
269 269
270 270 def on_restart_failed(self):
271 271 logging.error("kernel %s restarted failed!", self.kernel_id)
272 272 self._send_status_message('dead')
273 273
274 274
275 275 #-----------------------------------------------------------------------------
276 276 # URL to handler mappings
277 277 #-----------------------------------------------------------------------------
278 278
279 279
280 280 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
281 281 _kernel_action_regex = r"(?P<action>restart|interrupt)"
282 282
283 283 default_handlers = [
284 284 (r"/api/kernels", MainKernelHandler),
285 285 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
286 286 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
287 287 (r"/api/kernels/%s/channels" % _kernel_id_regex, ZMQChannelsHandler),
288 288 ]
@@ -1,123 +1,123
1 1 """A MultiKernelManager for use in the notebook webserver
2 2
3 3 - raises HTTPErrors
4 4 - creates REST API models
5 5 """
6 6
7 7 # Copyright (c) IPython Development Team.
8 8 # Distributed under the terms of the Modified BSD License.
9 9
10 10 import os
11 11
12 12 from tornado import web
13 13
14 14 from IPython.kernel.multikernelmanager import MultiKernelManager
15 15 from IPython.utils.traitlets import List, Unicode, TraitError
16 16
17 from IPython.html.utils import to_os_path
17 from jupyter_notebook.utils import to_os_path
18 18 from IPython.utils.py3compat import getcwd
19 19
20 20
21 21 class MappingKernelManager(MultiKernelManager):
22 22 """A KernelManager that handles notebook mapping and HTTP error handling"""
23 23
24 24 def _kernel_manager_class_default(self):
25 25 return "IPython.kernel.ioloop.IOLoopKernelManager"
26 26
27 27 kernel_argv = List(Unicode)
28 28
29 29 root_dir = Unicode(config=True)
30 30
31 31 def _root_dir_default(self):
32 32 try:
33 33 return self.parent.notebook_dir
34 34 except AttributeError:
35 35 return getcwd()
36 36
37 37 def _root_dir_changed(self, name, old, new):
38 38 """Do a bit of validation of the root dir."""
39 39 if not os.path.isabs(new):
40 40 # If we receive a non-absolute path, make it absolute.
41 41 self.root_dir = os.path.abspath(new)
42 42 return
43 43 if not os.path.exists(new) or not os.path.isdir(new):
44 44 raise TraitError("kernel root dir %r is not a directory" % new)
45 45
46 46 #-------------------------------------------------------------------------
47 47 # Methods for managing kernels and sessions
48 48 #-------------------------------------------------------------------------
49 49
50 50 def _handle_kernel_died(self, kernel_id):
51 51 """notice that a kernel died"""
52 52 self.log.warn("Kernel %s died, removing from map.", kernel_id)
53 53 self.remove_kernel(kernel_id)
54 54
55 55 def cwd_for_path(self, path):
56 56 """Turn API path into absolute OS path."""
57 57 os_path = to_os_path(path, self.root_dir)
58 58 # in the case of notebooks and kernels not being on the same filesystem,
59 59 # walk up to root_dir if the paths don't exist
60 60 while not os.path.isdir(os_path) and os_path != self.root_dir:
61 61 os_path = os.path.dirname(os_path)
62 62 return os_path
63 63
64 64 def start_kernel(self, kernel_id=None, path=None, **kwargs):
65 65 """Start a kernel for a session and return its kernel_id.
66 66
67 67 Parameters
68 68 ----------
69 69 kernel_id : uuid
70 70 The uuid to associate the new kernel with. If this
71 71 is not None, this kernel will be persistent whenever it is
72 72 requested.
73 73 path : API path
74 74 The API path (unicode, '/' delimited) for the cwd.
75 75 Will be transformed to an OS path relative to root_dir.
76 76 kernel_name : str
77 77 The name identifying which kernel spec to launch. This is ignored if
78 78 an existing kernel is returned, but it may be checked in the future.
79 79 """
80 80 if kernel_id is None:
81 81 if path is not None:
82 82 kwargs['cwd'] = self.cwd_for_path(path)
83 83 kernel_id = super(MappingKernelManager, self).start_kernel(
84 84 **kwargs)
85 85 self.log.info("Kernel started: %s" % kernel_id)
86 86 self.log.debug("Kernel args: %r" % kwargs)
87 87 # register callback for failed auto-restart
88 88 self.add_restart_callback(kernel_id,
89 89 lambda : self._handle_kernel_died(kernel_id),
90 90 'dead',
91 91 )
92 92 else:
93 93 self._check_kernel_id(kernel_id)
94 94 self.log.info("Using existing kernel: %s" % kernel_id)
95 95 return kernel_id
96 96
97 97 def shutdown_kernel(self, kernel_id, now=False):
98 98 """Shutdown a kernel by kernel_id"""
99 99 self._check_kernel_id(kernel_id)
100 100 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
101 101
102 102 def kernel_model(self, kernel_id):
103 103 """Return a dictionary of kernel information described in the
104 104 JSON standard model."""
105 105 self._check_kernel_id(kernel_id)
106 106 model = {"id":kernel_id,
107 107 "name": self._kernels[kernel_id].kernel_name}
108 108 return model
109 109
110 110 def list_kernels(self):
111 111 """Returns a list of kernel_id's of kernels running."""
112 112 kernels = []
113 113 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
114 114 for kernel_id in kernel_ids:
115 115 model = self.kernel_model(kernel_id)
116 116 kernels.append(model)
117 117 return kernels
118 118
119 119 # override _check_kernel_id to raise 404 instead of KeyError
120 120 def _check_kernel_id(self, kernel_id):
121 121 """Check a that a kernel_id exists and raise 404 if not."""
122 122 if kernel_id not in self:
123 123 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
@@ -1,141 +1,141
1 1 """Test the kernels service API."""
2 2
3 3 import json
4 4 import requests
5 5
6 6 from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
7 7
8 from IPython.html.utils import url_path_join
9 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
8 from jupyter_notebook.utils import url_path_join
9 from jupyter_notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
10 10
11 11 class KernelAPI(object):
12 12 """Wrapper for kernel REST API requests"""
13 13 def __init__(self, base_url):
14 14 self.base_url = base_url
15 15
16 16 def _req(self, verb, path, body=None):
17 17 response = requests.request(verb,
18 18 url_path_join(self.base_url, 'api/kernels', path), data=body)
19 19
20 20 if 400 <= response.status_code < 600:
21 21 try:
22 22 response.reason = response.json()['message']
23 23 except:
24 24 pass
25 25 response.raise_for_status()
26 26
27 27 return response
28 28
29 29 def list(self):
30 30 return self._req('GET', '')
31 31
32 32 def get(self, id):
33 33 return self._req('GET', id)
34 34
35 35 def start(self, name=NATIVE_KERNEL_NAME):
36 36 body = json.dumps({'name': name})
37 37 return self._req('POST', '', body)
38 38
39 39 def shutdown(self, id):
40 40 return self._req('DELETE', id)
41 41
42 42 def interrupt(self, id):
43 43 return self._req('POST', url_path_join(id, 'interrupt'))
44 44
45 45 def restart(self, id):
46 46 return self._req('POST', url_path_join(id, 'restart'))
47 47
48 48 class KernelAPITest(NotebookTestBase):
49 49 """Test the kernels web service API"""
50 50 def setUp(self):
51 51 self.kern_api = KernelAPI(self.base_url())
52 52
53 53 def tearDown(self):
54 54 for k in self.kern_api.list().json():
55 55 self.kern_api.shutdown(k['id'])
56 56
57 57 def test_no_kernels(self):
58 58 """Make sure there are no kernels running at the start"""
59 59 kernels = self.kern_api.list().json()
60 60 self.assertEqual(kernels, [])
61 61
62 62 def test_default_kernel(self):
63 63 # POST request
64 64 r = self.kern_api._req('POST', '')
65 65 kern1 = r.json()
66 66 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
67 67 self.assertEqual(r.status_code, 201)
68 68 self.assertIsInstance(kern1, dict)
69 69
70 70 self.assertEqual(r.headers['Content-Security-Policy'], (
71 71 "frame-ancestors 'self'; "
72 72 "report-uri /api/security/csp-report;"
73 73 ))
74 74
75 75 def test_main_kernel_handler(self):
76 76 # POST request
77 77 r = self.kern_api.start()
78 78 kern1 = r.json()
79 79 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
80 80 self.assertEqual(r.status_code, 201)
81 81 self.assertIsInstance(kern1, dict)
82 82
83 83 self.assertEqual(r.headers['Content-Security-Policy'], (
84 84 "frame-ancestors 'self'; "
85 85 "report-uri /api/security/csp-report;"
86 86 ))
87 87
88 88 # GET request
89 89 r = self.kern_api.list()
90 90 self.assertEqual(r.status_code, 200)
91 91 assert isinstance(r.json(), list)
92 92 self.assertEqual(r.json()[0]['id'], kern1['id'])
93 93 self.assertEqual(r.json()[0]['name'], kern1['name'])
94 94
95 95 # create another kernel and check that they both are added to the
96 96 # list of kernels from a GET request
97 97 kern2 = self.kern_api.start().json()
98 98 assert isinstance(kern2, dict)
99 99 r = self.kern_api.list()
100 100 kernels = r.json()
101 101 self.assertEqual(r.status_code, 200)
102 102 assert isinstance(kernels, list)
103 103 self.assertEqual(len(kernels), 2)
104 104
105 105 # Interrupt a kernel
106 106 r = self.kern_api.interrupt(kern2['id'])
107 107 self.assertEqual(r.status_code, 204)
108 108
109 109 # Restart a kernel
110 110 r = self.kern_api.restart(kern2['id'])
111 111 self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
112 112 rekern = r.json()
113 113 self.assertEqual(rekern['id'], kern2['id'])
114 114 self.assertEqual(rekern['name'], kern2['name'])
115 115
116 116 def test_kernel_handler(self):
117 117 # GET kernel with given id
118 118 kid = self.kern_api.start().json()['id']
119 119 r = self.kern_api.get(kid)
120 120 kern1 = r.json()
121 121 self.assertEqual(r.status_code, 200)
122 122 assert isinstance(kern1, dict)
123 123 self.assertIn('id', kern1)
124 124 self.assertEqual(kern1['id'], kid)
125 125
126 126 # Request a bad kernel id and check that a JSON
127 127 # message is returned!
128 128 bad_id = '111-111-111-111-111'
129 129 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
130 130 self.kern_api.get(bad_id)
131 131
132 132 # DELETE kernel with id
133 133 r = self.kern_api.shutdown(kid)
134 134 self.assertEqual(r.status_code, 204)
135 135 kernels = self.kern_api.list().json()
136 136 self.assertEqual(kernels, [])
137 137
138 138 # Request to delete a non-existent kernel id
139 139 bad_id = '111-111-111-111-111'
140 140 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
141 141 self.kern_api.shutdown(bad_id)
@@ -1,130 +1,130
1 1 # coding: utf-8
2 2 """Test the kernel specs webservice API."""
3 3
4 4 import errno
5 5 import io
6 6 import json
7 7 import os
8 8 import shutil
9 9
10 10 pjoin = os.path.join
11 11
12 12 import requests
13 13
14 14 from IPython.kernel.kernelspec import NATIVE_KERNEL_NAME
15 from IPython.html.utils import url_path_join
16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
15 from jupyter_notebook.utils import url_path_join
16 from jupyter_notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
17 17
18 18 # Copied from IPython.kernel.tests.test_kernelspec so updating that doesn't
19 19 # break these tests
20 20 sample_kernel_json = {'argv':['cat', '{connection_file}'],
21 21 'display_name':'Test kernel',
22 22 }
23 23
24 24 some_resource = u"The very model of a modern major general"
25 25
26 26
27 27 class KernelSpecAPI(object):
28 28 """Wrapper for notebook API calls."""
29 29 def __init__(self, base_url):
30 30 self.base_url = base_url
31 31
32 32 def _req(self, verb, path, body=None):
33 33 response = requests.request(verb,
34 34 url_path_join(self.base_url, path),
35 35 data=body,
36 36 )
37 37 response.raise_for_status()
38 38 return response
39 39
40 40 def list(self):
41 41 return self._req('GET', 'api/kernelspecs')
42 42
43 43 def kernel_spec_info(self, name):
44 44 return self._req('GET', url_path_join('api/kernelspecs', name))
45 45
46 46 def kernel_resource(self, name, path):
47 47 return self._req('GET', url_path_join('kernelspecs', name, path))
48 48
49 49 class APITest(NotebookTestBase):
50 50 """Test the kernelspec web service API"""
51 51 def setUp(self):
52 52 ipydir = self.ipython_dir.name
53 53 sample_kernel_dir = pjoin(ipydir, 'kernels', 'sample')
54 54 try:
55 55 os.makedirs(sample_kernel_dir)
56 56 except OSError as e:
57 57 if e.errno != errno.EEXIST:
58 58 raise
59 59
60 60 with open(pjoin(sample_kernel_dir, 'kernel.json'), 'w') as f:
61 61 json.dump(sample_kernel_json, f)
62 62
63 63 with io.open(pjoin(sample_kernel_dir, 'resource.txt'), 'w',
64 64 encoding='utf-8') as f:
65 65 f.write(some_resource)
66 66
67 67 self.ks_api = KernelSpecAPI(self.base_url())
68 68
69 69 def test_list_kernelspecs_bad(self):
70 70 """Can list kernelspecs when one is invalid"""
71 71 bad_kernel_dir = pjoin(self.ipython_dir.name, 'kernels', 'bad')
72 72 try:
73 73 os.makedirs(bad_kernel_dir)
74 74 except OSError as e:
75 75 if e.errno != errno.EEXIST:
76 76 raise
77 77
78 78 with open(pjoin(bad_kernel_dir, 'kernel.json'), 'w') as f:
79 79 f.write("garbage")
80 80
81 81 model = self.ks_api.list().json()
82 82 assert isinstance(model, dict)
83 83 self.assertEqual(model['default'], NATIVE_KERNEL_NAME)
84 84 specs = model['kernelspecs']
85 85 assert isinstance(specs, dict)
86 86 # 2: the sample kernelspec created in setUp, and the native Python kernel
87 87 self.assertGreaterEqual(len(specs), 2)
88 88
89 89 shutil.rmtree(bad_kernel_dir)
90 90
91 91 def test_list_kernelspecs(self):
92 92 model = self.ks_api.list().json()
93 93 assert isinstance(model, dict)
94 94 self.assertEqual(model['default'], NATIVE_KERNEL_NAME)
95 95 specs = model['kernelspecs']
96 96 assert isinstance(specs, dict)
97 97
98 98 # 2: the sample kernelspec created in setUp, and the native Python kernel
99 99 self.assertGreaterEqual(len(specs), 2)
100 100
101 101 def is_sample_kernelspec(s):
102 102 return s['name'] == 'sample' and s['spec']['display_name'] == 'Test kernel'
103 103
104 104 def is_default_kernelspec(s):
105 105 return s['name'] == NATIVE_KERNEL_NAME and s['spec']['display_name'].startswith("Python")
106 106
107 107 assert any(is_sample_kernelspec(s) for s in specs.values()), specs
108 108 assert any(is_default_kernelspec(s) for s in specs.values()), specs
109 109
110 110 def test_get_kernelspec(self):
111 111 model = self.ks_api.kernel_spec_info('Sample').json() # Case insensitive
112 112 self.assertEqual(model['name'].lower(), 'sample')
113 113 self.assertIsInstance(model['spec'], dict)
114 114 self.assertEqual(model['spec']['display_name'], 'Test kernel')
115 115 self.assertIsInstance(model['resources'], dict)
116 116
117 117 def test_get_nonexistant_kernelspec(self):
118 118 with assert_http_error(404):
119 119 self.ks_api.kernel_spec_info('nonexistant')
120 120
121 121 def test_get_kernel_resource_file(self):
122 122 res = self.ks_api.kernel_resource('sAmple', 'resource.txt')
123 123 self.assertEqual(res.text, some_resource)
124 124
125 125 def test_get_nonexistant_resource(self):
126 126 with assert_http_error(404):
127 127 self.ks_api.kernel_resource('nonexistant', 'resource.txt')
128 128
129 129 with assert_http_error(404):
130 130 self.ks_api.kernel_resource('sample', 'nonexistant.txt')
@@ -1,31 +1,31
1 1 import requests
2 2
3 from IPython.html.utils import url_path_join
4 from IPython.html.tests.launchnotebook import NotebookTestBase
3 from jupyter_notebook.utils import url_path_join
4 from jupyter_notebook.tests.launchnotebook import NotebookTestBase
5 5
6 6 class NbconvertAPI(object):
7 7 """Wrapper for nbconvert API calls."""
8 8 def __init__(self, base_url):
9 9 self.base_url = base_url
10 10
11 11 def _req(self, verb, path, body=None, params=None):
12 12 response = requests.request(verb,
13 13 url_path_join(self.base_url, 'api/nbconvert', path),
14 14 data=body, params=params,
15 15 )
16 16 response.raise_for_status()
17 17 return response
18 18
19 19 def list_formats(self):
20 20 return self._req('GET', '')
21 21
22 22 class APITest(NotebookTestBase):
23 23 def setUp(self):
24 24 self.nbconvert_api = NbconvertAPI(self.base_url())
25 25
26 26 def test_list_formats(self):
27 27 formats = self.nbconvert_api.list_formats().json()
28 28 self.assertIsInstance(formats, dict)
29 29 self.assertIn('python', formats)
30 30 self.assertIn('html', formats)
31 31 self.assertEqual(formats['python']['output_mimetype'], 'text/x-python') No newline at end of file
@@ -1,122 +1,122
1 1 """Tornado handlers for the sessions web service."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7
8 8 from tornado import web
9 9
10 10 from ...base.handlers import IPythonHandler, json_errors
11 11 from jupyter_client.jsonutil import date_default
12 from IPython.html.utils import url_path_join, url_escape
12 from jupyter_notebook.utils import url_path_join, url_escape
13 13 from IPython.kernel.kernelspec import NoSuchKernel
14 14
15 15
16 16 class SessionRootHandler(IPythonHandler):
17 17
18 18 @web.authenticated
19 19 @json_errors
20 20 def get(self):
21 21 # Return a list of running sessions
22 22 sm = self.session_manager
23 23 sessions = sm.list_sessions()
24 24 self.finish(json.dumps(sessions, default=date_default))
25 25
26 26 @web.authenticated
27 27 @json_errors
28 28 def post(self):
29 29 # Creates a new session
30 30 #(unless a session already exists for the named nb)
31 31 sm = self.session_manager
32 32 cm = self.contents_manager
33 33 km = self.kernel_manager
34 34
35 35 model = self.get_json_body()
36 36 if model is None:
37 37 raise web.HTTPError(400, "No JSON data provided")
38 38 try:
39 39 path = model['notebook']['path']
40 40 except KeyError:
41 41 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
42 42 try:
43 43 kernel_name = model['kernel']['name']
44 44 except KeyError:
45 45 self.log.debug("No kernel name specified, using default kernel")
46 46 kernel_name = None
47 47
48 48 # Check to see if session exists
49 49 if sm.session_exists(path=path):
50 50 model = sm.get_session(path=path)
51 51 else:
52 52 try:
53 53 model = sm.create_session(path=path, kernel_name=kernel_name)
54 54 except NoSuchKernel:
55 55 msg = ("The '%s' kernel is not available. Please pick another "
56 56 "suitable kernel instead, or install that kernel." % kernel_name)
57 57 status_msg = '%s not found' % kernel_name
58 58 self.log.warn('Kernel not found: %s' % kernel_name)
59 59 self.set_status(501)
60 60 self.finish(json.dumps(dict(message=msg, short_message=status_msg)))
61 61 return
62 62
63 63 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
64 64 self.set_header('Location', url_escape(location))
65 65 self.set_status(201)
66 66 self.finish(json.dumps(model, default=date_default))
67 67
68 68 class SessionHandler(IPythonHandler):
69 69
70 70 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
71 71
72 72 @web.authenticated
73 73 @json_errors
74 74 def get(self, session_id):
75 75 # Returns the JSON model for a single session
76 76 sm = self.session_manager
77 77 model = sm.get_session(session_id=session_id)
78 78 self.finish(json.dumps(model, default=date_default))
79 79
80 80 @web.authenticated
81 81 @json_errors
82 82 def patch(self, session_id):
83 83 # Currently, this handler is strictly for renaming notebooks
84 84 sm = self.session_manager
85 85 model = self.get_json_body()
86 86 if model is None:
87 87 raise web.HTTPError(400, "No JSON data provided")
88 88 changes = {}
89 89 if 'notebook' in model:
90 90 notebook = model['notebook']
91 91 if 'path' in notebook:
92 92 changes['path'] = notebook['path']
93 93
94 94 sm.update_session(session_id, **changes)
95 95 model = sm.get_session(session_id=session_id)
96 96 self.finish(json.dumps(model, default=date_default))
97 97
98 98 @web.authenticated
99 99 @json_errors
100 100 def delete(self, session_id):
101 101 # Deletes the session with given session_id
102 102 sm = self.session_manager
103 103 try:
104 104 sm.delete_session(session_id)
105 105 except KeyError:
106 106 # the kernel was deleted but the session wasn't!
107 107 raise web.HTTPError(410, "Kernel deleted before session")
108 108 self.set_status(204)
109 109 self.finish()
110 110
111 111
112 112 #-----------------------------------------------------------------------------
113 113 # URL to handler mappings
114 114 #-----------------------------------------------------------------------------
115 115
116 116 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
117 117
118 118 default_handlers = [
119 119 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
120 120 (r"/api/sessions", SessionRootHandler)
121 121 ]
122 122
@@ -1,208 +1,208
1 1 """A base class session manager."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import uuid
7 7 import sqlite3
8 8
9 9 from tornado import web
10 10
11 11 from IPython.config.configurable import LoggingConfigurable
12 12 from IPython.utils.py3compat import unicode_type
13 13 from IPython.utils.traitlets import Instance
14 14
15 15
16 16 class SessionManager(LoggingConfigurable):
17 17
18 kernel_manager = Instance('IPython.html.services.kernels.kernelmanager.MappingKernelManager')
19 contents_manager = Instance('IPython.html.services.contents.manager.ContentsManager')
18 kernel_manager = Instance('jupyter_notebook.services.kernels.kernelmanager.MappingKernelManager')
19 contents_manager = Instance('jupyter_notebook.services.contents.manager.ContentsManager')
20 20
21 21 # Session database initialized below
22 22 _cursor = None
23 23 _connection = None
24 24 _columns = {'session_id', 'path', 'kernel_id'}
25 25
26 26 @property
27 27 def cursor(self):
28 28 """Start a cursor and create a database called 'session'"""
29 29 if self._cursor is None:
30 30 self._cursor = self.connection.cursor()
31 31 self._cursor.execute("""CREATE TABLE session
32 32 (session_id, path, kernel_id)""")
33 33 return self._cursor
34 34
35 35 @property
36 36 def connection(self):
37 37 """Start a database connection"""
38 38 if self._connection is None:
39 39 self._connection = sqlite3.connect(':memory:')
40 40 self._connection.row_factory = sqlite3.Row
41 41 return self._connection
42 42
43 43 def __del__(self):
44 44 """Close connection once SessionManager closes"""
45 45 self.cursor.close()
46 46
47 47 def session_exists(self, path):
48 48 """Check to see if the session for a given notebook exists"""
49 49 self.cursor.execute("SELECT * FROM session WHERE path=?", (path,))
50 50 reply = self.cursor.fetchone()
51 51 if reply is None:
52 52 return False
53 53 else:
54 54 return True
55 55
56 56 def new_session_id(self):
57 57 "Create a uuid for a new session"
58 58 return unicode_type(uuid.uuid4())
59 59
60 60 def create_session(self, path=None, kernel_name=None):
61 61 """Creates a session and returns its model"""
62 62 session_id = self.new_session_id()
63 63 # allow nbm to specify kernels cwd
64 64 kernel_path = self.contents_manager.get_kernel_path(path=path)
65 65 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
66 66 kernel_name=kernel_name)
67 67 return self.save_session(session_id, path=path,
68 68 kernel_id=kernel_id)
69 69
70 70 def save_session(self, session_id, path=None, kernel_id=None):
71 71 """Saves the items for the session with the given session_id
72 72
73 73 Given a session_id (and any other of the arguments), this method
74 74 creates a row in the sqlite session database that holds the information
75 75 for a session.
76 76
77 77 Parameters
78 78 ----------
79 79 session_id : str
80 80 uuid for the session; this method must be given a session_id
81 81 path : str
82 82 the path for the given notebook
83 83 kernel_id : str
84 84 a uuid for the kernel associated with this session
85 85
86 86 Returns
87 87 -------
88 88 model : dict
89 89 a dictionary of the session model
90 90 """
91 91 self.cursor.execute("INSERT INTO session VALUES (?,?,?)",
92 92 (session_id, path, kernel_id)
93 93 )
94 94 return self.get_session(session_id=session_id)
95 95
96 96 def get_session(self, **kwargs):
97 97 """Returns the model for a particular session.
98 98
99 99 Takes a keyword argument and searches for the value in the session
100 100 database, then returns the rest of the session's info.
101 101
102 102 Parameters
103 103 ----------
104 104 **kwargs : keyword argument
105 105 must be given one of the keywords and values from the session database
106 106 (i.e. session_id, path, kernel_id)
107 107
108 108 Returns
109 109 -------
110 110 model : dict
111 111 returns a dictionary that includes all the information from the
112 112 session described by the kwarg.
113 113 """
114 114 if not kwargs:
115 115 raise TypeError("must specify a column to query")
116 116
117 117 conditions = []
118 118 for column in kwargs.keys():
119 119 if column not in self._columns:
120 120 raise TypeError("No such column: %r", column)
121 121 conditions.append("%s=?" % column)
122 122
123 123 query = "SELECT * FROM session WHERE %s" % (' AND '.join(conditions))
124 124
125 125 self.cursor.execute(query, list(kwargs.values()))
126 126 try:
127 127 row = self.cursor.fetchone()
128 128 except KeyError:
129 129 # The kernel is missing, so the session just got deleted.
130 130 row = None
131 131
132 132 if row is None:
133 133 q = []
134 134 for key, value in kwargs.items():
135 135 q.append("%s=%r" % (key, value))
136 136
137 137 raise web.HTTPError(404, u'Session not found: %s' % (', '.join(q)))
138 138
139 139 return self.row_to_model(row)
140 140
141 141 def update_session(self, session_id, **kwargs):
142 142 """Updates the values in the session database.
143 143
144 144 Changes the values of the session with the given session_id
145 145 with the values from the keyword arguments.
146 146
147 147 Parameters
148 148 ----------
149 149 session_id : str
150 150 a uuid that identifies a session in the sqlite3 database
151 151 **kwargs : str
152 152 the key must correspond to a column title in session database,
153 153 and the value replaces the current value in the session
154 154 with session_id.
155 155 """
156 156 self.get_session(session_id=session_id)
157 157
158 158 if not kwargs:
159 159 # no changes
160 160 return
161 161
162 162 sets = []
163 163 for column in kwargs.keys():
164 164 if column not in self._columns:
165 165 raise TypeError("No such column: %r" % column)
166 166 sets.append("%s=?" % column)
167 167 query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
168 168 self.cursor.execute(query, list(kwargs.values()) + [session_id])
169 169
170 170 def row_to_model(self, row):
171 171 """Takes sqlite database session row and turns it into a dictionary"""
172 172 if row['kernel_id'] not in self.kernel_manager:
173 173 # The kernel was killed or died without deleting the session.
174 174 # We can't use delete_session here because that tries to find
175 175 # and shut down the kernel.
176 176 self.cursor.execute("DELETE FROM session WHERE session_id=?",
177 177 (row['session_id'],))
178 178 raise KeyError
179 179
180 180 model = {
181 181 'id': row['session_id'],
182 182 'notebook': {
183 183 'path': row['path']
184 184 },
185 185 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
186 186 }
187 187 return model
188 188
189 189 def list_sessions(self):
190 190 """Returns a list of dictionaries containing all the information from
191 191 the session database"""
192 192 c = self.cursor.execute("SELECT * FROM session")
193 193 result = []
194 194 # We need to use fetchall() here, because row_to_model can delete rows,
195 195 # which messes up the cursor if we're iterating over rows.
196 196 for row in c.fetchall():
197 197 try:
198 198 result.append(self.row_to_model(row))
199 199 except KeyError:
200 200 pass
201 201 return result
202 202
203 203 def delete_session(self, session_id):
204 204 """Deletes the row in the session database with given session_id"""
205 205 # Check that session exists before deleting
206 206 session = self.get_session(session_id=session_id)
207 207 self.kernel_manager.shutdown_kernel(session['kernel']['id'])
208 208 self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))
@@ -1,162 +1,162
1 1 """Tests for the session manager."""
2 2
3 3 from unittest import TestCase
4 4
5 5 from tornado import web
6 6
7 7 from ..sessionmanager import SessionManager
8 from IPython.html.services.kernels.kernelmanager import MappingKernelManager
9 from IPython.html.services.contents.manager import ContentsManager
8 from jupyter_notebook.services.kernels.kernelmanager import MappingKernelManager
9 from jupyter_notebook.services.contents.manager import ContentsManager
10 10
11 11 class DummyKernel(object):
12 12 def __init__(self, kernel_name='python'):
13 13 self.kernel_name = kernel_name
14 14
15 15 class DummyMKM(MappingKernelManager):
16 16 """MappingKernelManager interface that doesn't start kernels, for testing"""
17 17 def __init__(self, *args, **kwargs):
18 18 super(DummyMKM, self).__init__(*args, **kwargs)
19 19 self.id_letters = iter(u'ABCDEFGHIJK')
20 20
21 21 def _new_id(self):
22 22 return next(self.id_letters)
23 23
24 24 def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
25 25 kernel_id = kernel_id or self._new_id()
26 26 self._kernels[kernel_id] = DummyKernel(kernel_name=kernel_name)
27 27 return kernel_id
28 28
29 29 def shutdown_kernel(self, kernel_id, now=False):
30 30 del self._kernels[kernel_id]
31 31
32 32
33 33 class TestSessionManager(TestCase):
34 34
35 35 def setUp(self):
36 36 self.sm = SessionManager(
37 37 kernel_manager=DummyMKM(),
38 38 contents_manager=ContentsManager(),
39 39 )
40 40
41 41 def test_get_session(self):
42 42 sm = self.sm
43 43 session_id = sm.create_session(path='/path/to/test.ipynb',
44 44 kernel_name='bar')['id']
45 45 model = sm.get_session(session_id=session_id)
46 46 expected = {'id':session_id,
47 47 'notebook':{'path': u'/path/to/test.ipynb'},
48 48 'kernel': {'id':u'A', 'name': 'bar'}}
49 49 self.assertEqual(model, expected)
50 50
51 51 def test_bad_get_session(self):
52 52 # Should raise error if a bad key is passed to the database.
53 53 sm = self.sm
54 54 session_id = sm.create_session(path='/path/to/test.ipynb',
55 55 kernel_name='foo')['id']
56 56 self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
57 57
58 58 def test_get_session_dead_kernel(self):
59 59 sm = self.sm
60 60 session = sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python')
61 61 # kill the kernel
62 62 sm.kernel_manager.shutdown_kernel(session['kernel']['id'])
63 63 with self.assertRaises(KeyError):
64 64 sm.get_session(session_id=session['id'])
65 65 # no sessions left
66 66 listed = sm.list_sessions()
67 67 self.assertEqual(listed, [])
68 68
69 69 def test_list_sessions(self):
70 70 sm = self.sm
71 71 sessions = [
72 72 sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
73 73 sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
74 74 sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
75 75 ]
76 76 sessions = sm.list_sessions()
77 77 expected = [
78 78 {
79 79 'id':sessions[0]['id'],
80 80 'notebook':{'path': u'/path/to/1/test1.ipynb'},
81 81 'kernel':{'id':u'A', 'name':'python'}
82 82 }, {
83 83 'id':sessions[1]['id'],
84 84 'notebook': {'path': u'/path/to/2/test2.ipynb'},
85 85 'kernel':{'id':u'B', 'name':'python'}
86 86 }, {
87 87 'id':sessions[2]['id'],
88 88 'notebook':{'path': u'/path/to/3/test3.ipynb'},
89 89 'kernel':{'id':u'C', 'name':'python'}
90 90 }
91 91 ]
92 92 self.assertEqual(sessions, expected)
93 93
94 94 def test_list_sessions_dead_kernel(self):
95 95 sm = self.sm
96 96 sessions = [
97 97 sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
98 98 sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
99 99 ]
100 100 # kill one of the kernels
101 101 sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
102 102 listed = sm.list_sessions()
103 103 expected = [
104 104 {
105 105 'id': sessions[1]['id'],
106 106 'notebook': {
107 107 'path': u'/path/to/2/test2.ipynb',
108 108 },
109 109 'kernel': {
110 110 'id': u'B',
111 111 'name':'python',
112 112 }
113 113 }
114 114 ]
115 115 self.assertEqual(listed, expected)
116 116
117 117 def test_update_session(self):
118 118 sm = self.sm
119 119 session_id = sm.create_session(path='/path/to/test.ipynb',
120 120 kernel_name='julia')['id']
121 121 sm.update_session(session_id, path='/path/to/new_name.ipynb')
122 122 model = sm.get_session(session_id=session_id)
123 123 expected = {'id':session_id,
124 124 'notebook':{'path': u'/path/to/new_name.ipynb'},
125 125 'kernel':{'id':u'A', 'name':'julia'}}
126 126 self.assertEqual(model, expected)
127 127
128 128 def test_bad_update_session(self):
129 129 # try to update a session with a bad keyword ~ raise error
130 130 sm = self.sm
131 131 session_id = sm.create_session(path='/path/to/test.ipynb',
132 132 kernel_name='ir')['id']
133 133 self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
134 134
135 135 def test_delete_session(self):
136 136 sm = self.sm
137 137 sessions = [
138 138 sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
139 139 sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
140 140 sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
141 141 ]
142 142 sm.delete_session(sessions[1]['id'])
143 143 new_sessions = sm.list_sessions()
144 144 expected = [{
145 145 'id': sessions[0]['id'],
146 146 'notebook': {'path': u'/path/to/1/test1.ipynb'},
147 147 'kernel': {'id':u'A', 'name':'python'}
148 148 }, {
149 149 'id': sessions[2]['id'],
150 150 'notebook': {'path': u'/path/to/3/test3.ipynb'},
151 151 'kernel': {'id':u'C', 'name':'python'}
152 152 }
153 153 ]
154 154 self.assertEqual(new_sessions, expected)
155 155
156 156 def test_bad_delete_session(self):
157 157 # try to delete a session that doesn't exist ~ raise error
158 158 sm = self.sm
159 159 sm.create_session(path='/path/to/test.ipynb', kernel_name='python')
160 160 self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
161 161 self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
162 162
@@ -1,124 +1,124
1 1 """Test the sessions web service API."""
2 2
3 3 import errno
4 4 import io
5 5 import os
6 6 import json
7 7 import requests
8 8 import shutil
9 9 import time
10 10
11 11 pjoin = os.path.join
12 12
13 from IPython.html.utils import url_path_join
14 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 from jupyter_notebook.utils import url_path_join
14 from jupyter_notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
15 15 from IPython.nbformat.v4 import new_notebook
16 16 from IPython.nbformat import write
17 17
18 18 class SessionAPI(object):
19 19 """Wrapper for notebook API calls."""
20 20 def __init__(self, base_url):
21 21 self.base_url = base_url
22 22
23 23 def _req(self, verb, path, body=None):
24 24 response = requests.request(verb,
25 25 url_path_join(self.base_url, 'api/sessions', path), data=body)
26 26
27 27 if 400 <= response.status_code < 600:
28 28 try:
29 29 response.reason = response.json()['message']
30 30 except:
31 31 pass
32 32 response.raise_for_status()
33 33
34 34 return response
35 35
36 36 def list(self):
37 37 return self._req('GET', '')
38 38
39 39 def get(self, id):
40 40 return self._req('GET', id)
41 41
42 42 def create(self, path, kernel_name='python'):
43 43 body = json.dumps({'notebook': {'path':path},
44 44 'kernel': {'name': kernel_name}})
45 45 return self._req('POST', '', body)
46 46
47 47 def modify(self, id, path):
48 48 body = json.dumps({'notebook': {'path':path}})
49 49 return self._req('PATCH', id, body)
50 50
51 51 def delete(self, id):
52 52 return self._req('DELETE', id)
53 53
54 54 class SessionAPITest(NotebookTestBase):
55 55 """Test the sessions web service API"""
56 56 def setUp(self):
57 57 nbdir = self.notebook_dir.name
58 58 try:
59 59 os.mkdir(pjoin(nbdir, 'foo'))
60 60 except OSError as e:
61 61 # Deleting the folder in an earlier test may have failed
62 62 if e.errno != errno.EEXIST:
63 63 raise
64 64
65 65 with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w',
66 66 encoding='utf-8') as f:
67 67 nb = new_notebook()
68 68 write(nb, f, version=4)
69 69
70 70 self.sess_api = SessionAPI(self.base_url())
71 71
72 72 def tearDown(self):
73 73 for session in self.sess_api.list().json():
74 74 self.sess_api.delete(session['id'])
75 75 # This is necessary in some situations on Windows: without it, it
76 76 # fails to delete the directory because something is still using it. I
77 77 # think there is a brief period after the kernel terminates where
78 78 # Windows still treats its working directory as in use. On my Windows
79 79 # VM, 0.01s is not long enough, but 0.1s appears to work reliably.
80 80 # -- TK, 15 December 2014
81 81 time.sleep(0.1)
82 82
83 83 shutil.rmtree(pjoin(self.notebook_dir.name, 'foo'),
84 84 ignore_errors=True)
85 85
86 86 def test_create(self):
87 87 sessions = self.sess_api.list().json()
88 88 self.assertEqual(len(sessions), 0)
89 89
90 90 resp = self.sess_api.create('foo/nb1.ipynb')
91 91 self.assertEqual(resp.status_code, 201)
92 92 newsession = resp.json()
93 93 self.assertIn('id', newsession)
94 94 self.assertEqual(newsession['notebook']['path'], 'foo/nb1.ipynb')
95 95 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
96 96
97 97 sessions = self.sess_api.list().json()
98 98 self.assertEqual(sessions, [newsession])
99 99
100 100 # Retrieve it
101 101 sid = newsession['id']
102 102 got = self.sess_api.get(sid).json()
103 103 self.assertEqual(got, newsession)
104 104
105 105 def test_delete(self):
106 106 newsession = self.sess_api.create('foo/nb1.ipynb').json()
107 107 sid = newsession['id']
108 108
109 109 resp = self.sess_api.delete(sid)
110 110 self.assertEqual(resp.status_code, 204)
111 111
112 112 sessions = self.sess_api.list().json()
113 113 self.assertEqual(sessions, [])
114 114
115 115 with assert_http_error(404):
116 116 self.sess_api.get(sid)
117 117
118 118 def test_modify(self):
119 119 newsession = self.sess_api.create('foo/nb1.ipynb').json()
120 120 sid = newsession['id']
121 121
122 122 changed = self.sess_api.modify(sid, 'nb2.ipynb').json()
123 123 self.assertEqual(changed['id'], sid)
124 124 self.assertEqual(changed['notebook']['path'], 'nb2.ipynb')
@@ -1,489 +1,489
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/utils",
9 9 "base/js/namespace",
10 10 "services/kernels/comm"
11 11 ], function (_, Backbone, $, utils, IPython, comm) {
12 12 "use strict";
13 13 //--------------------------------------------------------------------
14 14 // WidgetManager class
15 15 //--------------------------------------------------------------------
16 16 var WidgetManager = function (comm_manager, notebook) {
17 17 /**
18 18 * Public constructor
19 19 */
20 20 WidgetManager._managers.push(this);
21 21
22 22 // Attach a comm manager to the
23 23 this.keyboard_manager = notebook.keyboard_manager;
24 24 this.notebook = notebook;
25 25 this.comm_manager = comm_manager;
26 26 this.comm_target_name = 'ipython.widget';
27 27 this._models = {}; /* Dictionary of model ids and model instance promises */
28 28
29 29 // Register with the comm manager.
30 30 this.comm_manager.register_target(this.comm_target_name, $.proxy(this._handle_comm_open, this));
31 31
32 32 // Load the initial state of the widget manager if a load callback was
33 33 // registered.
34 34 var that = this;
35 35 if (WidgetManager._load_callback) {
36 36 Promise.resolve().then(function () {
37 37 return WidgetManager._load_callback.call(that);
38 38 }).then(function(state) {
39 39 that.set_state(state);
40 40 }).catch(utils.reject('Error loading widget manager state', true));
41 41 }
42 42
43 43 // Setup state saving code.
44 44 this.notebook.events.on('before_save.Notebook', function() {
45 45 var save_callback = WidgetManager._save_callback;
46 46 var options = WidgetManager._get_state_options;
47 47 if (save_callback) {
48 48 that.get_state(options).then(function(state) {
49 49 save_callback.call(that, state);
50 50 }).catch(utils.reject('Could not call widget save state callback.', true));
51 51 }
52 52 });
53 53 };
54 54
55 55 //--------------------------------------------------------------------
56 56 // Class level
57 57 //--------------------------------------------------------------------
58 58 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
59 59 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
60 60 WidgetManager._managers = []; /* List of widget managers */
61 61 WidgetManager._load_callback = null;
62 62 WidgetManager._save_callback = null;
63 63
64 64 WidgetManager.register_widget_model = function (model_name, model_type) {
65 65 /**
66 66 * Registers a widget model by name.
67 67 */
68 68 WidgetManager._model_types[model_name] = model_type;
69 69 };
70 70
71 71 WidgetManager.register_widget_view = function (view_name, view_type) {
72 72 /**
73 73 * Registers a widget view by name.
74 74 */
75 75 WidgetManager._view_types[view_name] = view_type;
76 76 };
77 77
78 78 WidgetManager.set_state_callbacks = function (load_callback, save_callback, options) {
79 79 /**
80 80 * Registers callbacks for widget state persistence.
81 81 *
82 82 * Parameters
83 83 * ----------
84 84 * load_callback: function()
85 85 * function that is called when the widget manager state should be
86 86 * loaded. This function should return a promise for the widget
87 87 * manager state. An empty state is an empty dictionary `{}`.
88 88 * save_callback: function(state as dictionary)
89 89 * function that is called when the notebook is saved or autosaved.
90 90 * The current state of the widget manager is passed in as the first
91 91 * argument.
92 92 */
93 93 WidgetManager._load_callback = load_callback;
94 94 WidgetManager._save_callback = save_callback;
95 95 WidgetManager._get_state_options = options;
96 96
97 97 // Use the load callback to immediately load widget states.
98 98 WidgetManager._managers.forEach(function(manager) {
99 99 if (load_callback) {
100 100 Promise.resolve().then(function () {
101 101 return load_callback.call(manager);
102 102 }).then(function(state) {
103 103 manager.set_state(state);
104 104 }).catch(utils.reject('Error loading widget manager state', true));
105 105 }
106 106 });
107 107 };
108 108
109 109 // Use local storage to persist widgets across page refresh by default.
110 110 // LocalStorage is per domain, so we need to explicitly set the URL
111 111 // that the widgets are associated with so they don't show on other
112 112 // pages hosted by the noteboook server.
113 113 var url = [window.location.protocol, '//', window.location.host, window.location.pathname].join('');
114 114 var key = 'widgets:' + url;
115 115 WidgetManager.set_state_callbacks(function() {
116 116 if (localStorage[key]) {
117 117 return JSON.parse(localStorage[key]);
118 118 }
119 119 return {};
120 120 }, function(state) {
121 121 localStorage[key] = JSON.stringify(state);
122 122 });
123 123
124 124 //--------------------------------------------------------------------
125 125 // Instance level
126 126 //--------------------------------------------------------------------
127 127 WidgetManager.prototype.display_view = function(msg, model) {
128 128 /**
129 129 * Displays a view for a particular model.
130 130 */
131 131 var cell = this.get_msg_cell(msg.parent_header.msg_id);
132 132 if (cell === null) {
133 133 return Promise.reject(new Error("Could not determine where the display" +
134 134 " message was from. Widget will not be displayed"));
135 135 } else {
136 136 return this.display_view_in_cell(cell, model)
137 137 .catch(utils.reject('Could not display view', true));
138 138 }
139 139 };
140 140
141 141 WidgetManager.prototype.display_view_in_cell = function(cell, model) {
142 142 // Displays a view in a cell.
143 143 if (cell.display_widget_view) {
144 144 var that = this;
145 145 return cell.display_widget_view(this.create_view(model, {
146 146 cell: cell,
147 147 // Only set cell_index when view is displayed as directly.
148 148 cell_index: that.notebook.find_cell_index(cell),
149 149 })).then(function(view) {
150 150 that._handle_display_view(view);
151 151 view.trigger('displayed');
152 152 return view;
153 153 }).catch(utils.reject('Could not create or display view', true));
154 154 } else {
155 155 return Promise.reject(new Error('Cell does not have a `display_widget_view` method'));
156 156 }
157 157 };
158 158
159 159 WidgetManager.prototype._handle_display_view = function (view) {
160 160 /**
161 161 * Have the IPython keyboard manager disable its event
162 162 * handling so the widget can capture keyboard input.
163 163 * Note, this is only done on the outer most widgets.
164 164 */
165 165 if (this.keyboard_manager) {
166 166 this.keyboard_manager.register_events(view.$el);
167 167
168 168 if (view.additional_elements) {
169 169 for (var i = 0; i < view.additional_elements.length; i++) {
170 170 this.keyboard_manager.register_events(view.additional_elements[i]);
171 171 }
172 172 }
173 173 }
174 174 };
175 175
176 176 WidgetManager.prototype.create_view = function(model, options) {
177 177 /**
178 178 * Creates a promise for a view of a given model
179 179 *
180 180 * Make sure the view creation is not out of order with
181 181 * any state updates.
182 182 */
183 183 model.state_change = model.state_change.then(function() {
184 184
185 185 return utils.load_class(model.get('_view_name'), model.get('_view_module'),
186 186 WidgetManager._view_types).then(function(ViewType) {
187 187
188 188 // If a view is passed into the method, use that view's cell as
189 189 // the cell for the view that is created.
190 190 options = options || {};
191 191 if (options.parent !== undefined) {
192 192 options.cell = options.parent.options.cell;
193 193 }
194 194 // Create and render the view...
195 195 var parameters = {model: model, options: options};
196 196 var view = new ViewType(parameters);
197 197 view.listenTo(model, 'destroy', view.remove);
198 198 return Promise.resolve(view.render()).then(function() {return view;});
199 199 }).catch(utils.reject("Couldn't create a view for model id '" + String(model.id) + "'", true));
200 200 });
201 201 var id = utils.uuid();
202 202 model.views[id] = model.state_change;
203 203 model.state_change.then(function(view) {
204 204 view.once('remove', function() {
205 205 delete view.model.views[id];
206 206 }, this);
207 207 });
208 208 return model.state_change;
209 209 };
210 210
211 211 WidgetManager.prototype.get_msg_cell = function (msg_id) {
212 212 var cell = null;
213 213 // First, check to see if the msg was triggered by cell execution.
214 214 if (this.notebook) {
215 215 cell = this.notebook.get_msg_cell(msg_id);
216 216 }
217 217 if (cell !== null) {
218 218 return cell;
219 219 }
220 220 // Second, check to see if a get_cell callback was defined
221 221 // for the message. get_cell callbacks are registered for
222 222 // widget messages, so this block is actually checking to see if the
223 223 // message was triggered by a widget.
224 224 var kernel = this.comm_manager.kernel;
225 225 if (kernel) {
226 226 var callbacks = kernel.get_callbacks_for_msg(msg_id);
227 227 if (callbacks && callbacks.iopub &&
228 228 callbacks.iopub.get_cell !== undefined) {
229 229 return callbacks.iopub.get_cell();
230 230 }
231 231 }
232 232
233 233 // Not triggered by a cell or widget (no get_cell callback
234 234 // exists).
235 235 return null;
236 236 };
237 237
238 238 WidgetManager.prototype.callbacks = function (view) {
239 239 /**
240 240 * callback handlers specific a view
241 241 */
242 242 var callbacks = {};
243 243 if (view && view.options.cell) {
244 244
245 245 // Try to get output handlers
246 246 var cell = view.options.cell;
247 247 var handle_output = null;
248 248 var handle_clear_output = null;
249 249 if (cell.output_area) {
250 250 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
251 251 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
252 252 }
253 253
254 254 // Create callback dictionary using what is known
255 255 var that = this;
256 256 callbacks = {
257 257 iopub : {
258 258 output : handle_output,
259 259 clear_output : handle_clear_output,
260 260
261 261 // Special function only registered by widget messages.
262 262 // Allows us to get the cell for a message so we know
263 263 // where to add widgets if the code requires it.
264 264 get_cell : function () {
265 265 return cell;
266 266 },
267 267 },
268 268 };
269 269 }
270 270 return callbacks;
271 271 };
272 272
273 273 WidgetManager.prototype.get_model = function (model_id) {
274 274 /**
275 275 * Get a promise for a model by model id.
276 276 */
277 277 return this._models[model_id];
278 278 };
279 279
280 280 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
281 281 /**
282 282 * Handle when a comm is opened.
283 283 */
284 284 return this.create_model({
285 285 model_name: msg.content.data.model_name,
286 286 model_module: msg.content.data.model_module,
287 287 comm: comm}).catch(utils.reject("Couldn't create a model.", true));
288 288 };
289 289
290 290 WidgetManager.prototype.create_model = function (options) {
291 291 /**
292 292 * Create and return a promise for a new widget model
293 293 *
294 294 * Minimally, one must provide the model_name and widget_class
295 295 * parameters to create a model from Javascript.
296 296 *
297 297 * Example
298 298 * --------
299 299 * JS:
300 300 * IPython.notebook.kernel.widget_manager.create_model({
301 301 * model_name: 'WidgetModel',
302 * widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
302 * widget_class: 'jupyter_notebook.widgets.widget_int.IntSlider'})
303 303 * .then(function(model) { console.log('Create success!', model); },
304 304 * $.proxy(console.error, console));
305 305 *
306 306 * Parameters
307 307 * ----------
308 308 * options: dictionary
309 309 * Dictionary of options with the following contents:
310 310 * model_name: string
311 311 * Target name of the widget model to create.
312 312 * model_module: (optional) string
313 313 * Module name of the widget model to create.
314 314 * widget_class: (optional) string
315 315 * Target name of the widget in the back-end.
316 316 * comm: (optional) Comm
317 317 *
318 318 * Create a comm if it wasn't provided.
319 319 */
320 320 var comm = options.comm;
321 321 if (!comm) {
322 322 comm = this.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
323 323 }
324 324
325 325 var that = this;
326 326 var model_id = comm.comm_id;
327 327 var model_promise = utils.load_class(options.model_name, options.model_module, WidgetManager._model_types)
328 328 .then(function(ModelType) {
329 329 var widget_model = new ModelType(that, model_id, comm);
330 330 widget_model.once('comm:close', function () {
331 331 delete that._models[model_id];
332 332 });
333 333 widget_model.name = options.model_name;
334 334 widget_model.module = options.model_module;
335 335 return widget_model;
336 336
337 337 }, function(error) {
338 338 delete that._models[model_id];
339 339 var wrapped_error = new utils.WrappedError("Couldn't create model", error);
340 340 return Promise.reject(wrapped_error);
341 341 });
342 342 this._models[model_id] = model_promise;
343 343 return model_promise;
344 344 };
345 345
346 346 WidgetManager.prototype.get_state = function(options) {
347 347 /**
348 348 * Asynchronously get the state of the widget manager.
349 349 *
350 350 * This includes all of the widget models and the cells that they are
351 351 * displayed in.
352 352 *
353 353 * Parameters
354 354 * ----------
355 355 * options: dictionary
356 356 * Dictionary of options with the following contents:
357 357 * only_displayed: (optional) boolean=false
358 358 * Only return models with one or more displayed views.
359 359 * not_live: (optional) boolean=false
360 360 * Include models that have comms with severed connections.
361 361 *
362 362 * Returns
363 363 * -------
364 364 * Promise for a state dictionary
365 365 */
366 366 var that = this;
367 367 return utils.resolve_promises_dict(this._models).then(function(models) {
368 368 var state = {};
369 369
370 370 var model_promises = [];
371 371 for (var model_id in models) {
372 372 if (models.hasOwnProperty(model_id)) {
373 373 var model = models[model_id];
374 374
375 375 // If the model has one or more views defined for it,
376 376 // consider it displayed.
377 377 var displayed_flag = !(options && options.only_displayed) || Object.keys(model.views).length > 0;
378 378 var live_flag = (options && options.not_live) || model.comm_live;
379 379 if (displayed_flag && live_flag) {
380 380 state[model_id] = {
381 381 model_name: model.name,
382 382 model_module: model.module,
383 383 state: model.get_state(),
384 384 views: [],
385 385 };
386 386
387 387 // Get the views that are displayed *now*.
388 388 (function(local_state) {
389 389 model_promises.push(utils.resolve_promises_dict(model.views).then(function(model_views) {
390 390 for (var id in model_views) {
391 391 if (model_views.hasOwnProperty(id)) {
392 392 var view = model_views[id];
393 393 if (view.options.cell_index) {
394 394 local_state.views.push(view.options.cell_index);
395 395 }
396 396 }
397 397 }
398 398 }));
399 399 })(state[model_id]);
400 400 }
401 401 }
402 402 }
403 403 return Promise.all(model_promises).then(function() { return state; });
404 404 }).catch(utils.reject('Could not get state of widget manager', true));
405 405 };
406 406
407 407 WidgetManager.prototype.set_state = function(state) {
408 408 /**
409 409 * Set the notebook's state.
410 410 *
411 411 * Reconstructs all of the widget models and attempts to redisplay the
412 412 * widgets in the appropriate cells by cell index.
413 413 */
414 414
415 415 // Get the kernel when it's available.
416 416 var that = this;
417 417 return this._get_connected_kernel().then(function(kernel) {
418 418
419 419 // Recreate all the widget models for the given state and
420 420 // display the views.
421 421 that.all_views = [];
422 422 var model_ids = Object.keys(state);
423 423 for (var i = 0; i < model_ids.length; i++) {
424 424 var model_id = model_ids[i];
425 425
426 426 // Recreate a comm using the widget's model id (model_id == comm_id).
427 427 var new_comm = new comm.Comm(kernel.widget_manager.comm_target_name, model_id);
428 428 kernel.comm_manager.register_comm(new_comm);
429 429
430 430 // Create the model using the recreated comm. When the model is
431 431 // created we don't know yet if the comm is valid so set_comm_live
432 432 // false. Once we receive the first state push from the back-end
433 433 // we know the comm is alive.
434 434 var views = kernel.widget_manager.create_model({
435 435 comm: new_comm,
436 436 model_name: state[model_id].model_name,
437 437 model_module: state[model_id].model_module})
438 438 .then(function(model) {
439 439
440 440 model.set_comm_live(false);
441 441 var view_promise = Promise.resolve().then(function() {
442 442 return model.set_state(state[model.id].state);
443 443 }).then(function() {
444 444 model.request_state().then(function() {
445 445 model.set_comm_live(true);
446 446 });
447 447
448 448 // Display the views of the model.
449 449 var views = [];
450 450 var model_views = state[model.id].views;
451 451 for (var j=0; j<model_views.length; j++) {
452 452 var cell_index = model_views[j];
453 453 var cell = that.notebook.get_cell(cell_index);
454 454 views.push(that.display_view_in_cell(cell, model));
455 455 }
456 456 return Promise.all(views);
457 457 });
458 458 return view_promise;
459 459 });
460 460 that.all_views.push(views);
461 461 }
462 462 return Promise.all(that.all_views);
463 463 }).catch(utils.reject('Could not set widget manager state.', true));
464 464 };
465 465
466 466 WidgetManager.prototype._get_connected_kernel = function() {
467 467 /**
468 468 * Gets a promise for a connected kernel
469 469 */
470 470 var that = this;
471 471 return new Promise(function(resolve, reject) {
472 472 if (that.comm_manager &&
473 473 that.comm_manager.kernel &&
474 474 that.comm_manager.kernel.is_connected()) {
475 475
476 476 resolve(that.comm_manager.kernel);
477 477 } else {
478 478 that.notebook.events.on('kernel_connected.Kernel', function(event, data) {
479 479 resolve(data.kernel);
480 480 });
481 481 }
482 482 });
483 483 };
484 484
485 485 // Backwards compatibility.
486 486 IPython.WidgetManager = WidgetManager;
487 487
488 488 return {'WidgetManager': WidgetManager};
489 489 });
@@ -1,27 +1,27
1 1 import os
2 2
3 3 import terminado
4 4 from ..utils import check_version
5 5
6 6 if not check_version(terminado.__version__, '0.3.3'):
7 7 raise ImportError("terminado >= 0.3.3 required, found %s" % terminado.__version__)
8 8
9 9 from terminado import NamedTermManager
10 10 from tornado.log import app_log
11 from IPython.html.utils import url_path_join as ujoin
11 from jupyter_notebook.utils import url_path_join as ujoin
12 12 from .handlers import TerminalHandler, TermSocket
13 13 from . import api_handlers
14 14
15 15 def initialize(webapp):
16 16 shell = os.environ.get('SHELL', 'sh')
17 17 terminal_manager = webapp.settings['terminal_manager'] = NamedTermManager(shell_command=[shell])
18 18 terminal_manager.log = app_log
19 19 base_url = webapp.settings['base_url']
20 20 handlers = [
21 21 (ujoin(base_url, r"/terminals/(\w+)"), TerminalHandler),
22 22 (ujoin(base_url, r"/terminals/websocket/(\w+)"), TermSocket,
23 23 {'term_manager': terminal_manager}),
24 24 (ujoin(base_url, r"/api/terminals"), api_handlers.TerminalRootHandler),
25 25 (ujoin(base_url, r"/api/terminals/(\w+)"), api_handlers.TerminalHandler),
26 26 ]
27 27 webapp.add_handlers(".*$", handlers) No newline at end of file
@@ -1,152 +1,152
1 1 # coding: utf-8
2 2 """Test the /files/ handler."""
3 3
4 4 import io
5 5 import os
6 6 from unicodedata import normalize
7 7
8 8 pjoin = os.path.join
9 9
10 10 import requests
11 11 import json
12 12
13 13 from IPython.nbformat import write
14 14 from IPython.nbformat.v4 import (new_notebook,
15 15 new_markdown_cell, new_code_cell,
16 16 new_output)
17 17
18 from IPython.html.utils import url_path_join
18 from jupyter_notebook.utils import url_path_join
19 19 from .launchnotebook import NotebookTestBase
20 20 from IPython.utils import py3compat
21 21
22 22
23 23 class FilesTest(NotebookTestBase):
24 24 def test_hidden_files(self):
25 25 not_hidden = [
26 26 u'Ã¥ b',
27 27 u'å b/ç. d',
28 28 ]
29 29 hidden = [
30 30 u'.Ã¥ b',
31 31 u'å b/.ç d',
32 32 ]
33 33 dirs = not_hidden + hidden
34 34
35 35 nbdir = self.notebook_dir.name
36 36 for d in dirs:
37 37 path = pjoin(nbdir, d.replace('/', os.sep))
38 38 if not os.path.exists(path):
39 39 os.mkdir(path)
40 40 with open(pjoin(path, 'foo'), 'w') as f:
41 41 f.write('foo')
42 42 with open(pjoin(path, '.foo'), 'w') as f:
43 43 f.write('.foo')
44 44 url = self.base_url()
45 45
46 46 for d in not_hidden:
47 47 path = pjoin(nbdir, d.replace('/', os.sep))
48 48 r = requests.get(url_path_join(url, 'files', d, 'foo'))
49 49 r.raise_for_status()
50 50 self.assertEqual(r.text, 'foo')
51 51 r = requests.get(url_path_join(url, 'files', d, '.foo'))
52 52 self.assertEqual(r.status_code, 404)
53 53
54 54 for d in hidden:
55 55 path = pjoin(nbdir, d.replace('/', os.sep))
56 56 for foo in ('foo', '.foo'):
57 57 r = requests.get(url_path_join(url, 'files', d, foo))
58 58 self.assertEqual(r.status_code, 404)
59 59
60 60 def test_contents_manager(self):
61 61 "make sure ContentsManager returns right files (ipynb, bin, txt)."
62 62
63 63 nbdir = self.notebook_dir.name
64 64 base = self.base_url()
65 65
66 66 nb = new_notebook(
67 67 cells=[
68 68 new_markdown_cell(u'Created by test ³'),
69 69 new_code_cell("print(2*6)", outputs=[
70 70 new_output("stream", text="12"),
71 71 ])
72 72 ]
73 73 )
74 74
75 75 with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w',
76 76 encoding='utf-8') as f:
77 77 write(nb, f, version=4)
78 78
79 79 with io.open(pjoin(nbdir, 'test.bin'), 'wb') as f:
80 80 f.write(b'\xff' + os.urandom(5))
81 81 f.close()
82 82
83 83 with io.open(pjoin(nbdir, 'test.txt'), 'w') as f:
84 84 f.write(u'foobar')
85 85 f.close()
86 86
87 87 r = requests.get(url_path_join(base, 'files', 'testnb.ipynb'))
88 88 self.assertEqual(r.status_code, 200)
89 89 self.assertIn('print(2*6)', r.text)
90 90 json.loads(r.text)
91 91
92 92 r = requests.get(url_path_join(base, 'files', 'test.bin'))
93 93 self.assertEqual(r.status_code, 200)
94 94 self.assertEqual(r.headers['content-type'], 'application/octet-stream')
95 95 self.assertEqual(r.content[:1], b'\xff')
96 96 self.assertEqual(len(r.content), 6)
97 97
98 98 r = requests.get(url_path_join(base, 'files', 'test.txt'))
99 99 self.assertEqual(r.status_code, 200)
100 100 self.assertEqual(r.headers['content-type'], 'text/plain')
101 101 self.assertEqual(r.text, 'foobar')
102 102
103 103 def test_download(self):
104 104 nbdir = self.notebook_dir.name
105 105 base = self.base_url()
106 106
107 107 text = 'hello'
108 108 with open(pjoin(nbdir, 'test.txt'), 'w') as f:
109 109 f.write(text)
110 110
111 111 r = requests.get(url_path_join(base, 'files', 'test.txt'))
112 112 disposition = r.headers.get('Content-Disposition', '')
113 113 self.assertNotIn('attachment', disposition)
114 114
115 115 r = requests.get(url_path_join(base, 'files', 'test.txt') + '?download=1')
116 116 disposition = r.headers.get('Content-Disposition', '')
117 117 self.assertIn('attachment', disposition)
118 118 self.assertIn('filename="test.txt"', disposition)
119 119
120 120 def test_old_files_redirect(self):
121 121 """pre-2.0 'files/' prefixed links are properly redirected"""
122 122 nbdir = self.notebook_dir.name
123 123 base = self.base_url()
124 124
125 125 os.mkdir(pjoin(nbdir, 'files'))
126 126 os.makedirs(pjoin(nbdir, 'sub', 'files'))
127 127
128 128 for prefix in ('', 'sub'):
129 129 with open(pjoin(nbdir, prefix, 'files', 'f1.txt'), 'w') as f:
130 130 f.write(prefix + '/files/f1')
131 131 with open(pjoin(nbdir, prefix, 'files', 'f2.txt'), 'w') as f:
132 132 f.write(prefix + '/files/f2')
133 133 with open(pjoin(nbdir, prefix, 'f2.txt'), 'w') as f:
134 134 f.write(prefix + '/f2')
135 135 with open(pjoin(nbdir, prefix, 'f3.txt'), 'w') as f:
136 136 f.write(prefix + '/f3')
137 137
138 138 url = url_path_join(base, 'notebooks', prefix, 'files', 'f1.txt')
139 139 r = requests.get(url)
140 140 self.assertEqual(r.status_code, 200)
141 141 self.assertEqual(r.text, prefix + '/files/f1')
142 142
143 143 url = url_path_join(base, 'notebooks', prefix, 'files', 'f2.txt')
144 144 r = requests.get(url)
145 145 self.assertEqual(r.status_code, 200)
146 146 self.assertEqual(r.text, prefix + '/files/f2')
147 147
148 148 url = url_path_join(base, 'notebooks', prefix, 'files', 'f3.txt')
149 149 r = requests.get(url)
150 150 self.assertEqual(r.status_code, 200)
151 151 self.assertEqual(r.text, prefix + '/f3')
152 152
@@ -1,342 +1,342
1 1 # coding: utf-8
2 2 """Test installation of notebook extensions"""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import glob
8 8 import os
9 9 import re
10 10 import sys
11 11 import tarfile
12 12 import zipfile
13 13 from io import BytesIO, StringIO
14 14 from os.path import basename, join as pjoin
15 15 from unittest import TestCase
16 16
17 17 try:
18 18 from unittest import mock
19 19 except ImportError:
20 20 import mock # py2
21 21
22 22 import IPython.testing.decorators as dec
23 23 from IPython.utils import py3compat
24 24 from IPython.utils.tempdir import TemporaryDirectory
25 from IPython.html import nbextensions
26 from IPython.html.nbextensions import install_nbextension, check_nbextension
25 from jupyter_notebook import nbextensions
26 from jupyter_notebook.nbextensions import install_nbextension, check_nbextension
27 27
28 28
29 29 def touch(file, mtime=None):
30 30 """ensure a file exists, and set its modification time
31 31
32 32 returns the modification time of the file
33 33 """
34 34 open(file, 'a').close()
35 35 # set explicit mtime
36 36 if mtime:
37 37 atime = os.stat(file).st_atime
38 38 os.utime(file, (atime, mtime))
39 39 return os.stat(file).st_mtime
40 40
41 41 class TestInstallNBExtension(TestCase):
42 42
43 43 def tempdir(self):
44 44 td = TemporaryDirectory()
45 45 self.tempdirs.append(td)
46 46 return py3compat.cast_unicode(td.name)
47 47
48 48 def setUp(self):
49 49 self.tempdirs = []
50 50 src = self.src = self.tempdir()
51 51 self.files = files = [
52 52 pjoin(u'Æ’ile'),
53 53 pjoin(u'∂ir', u'ƒile1'),
54 54 pjoin(u'∂ir', u'∂ir2', u'ƒile2'),
55 55 ]
56 56 for file in files:
57 57 fullpath = os.path.join(self.src, file)
58 58 parent = os.path.dirname(fullpath)
59 59 if not os.path.exists(parent):
60 60 os.makedirs(parent)
61 61 touch(fullpath)
62 62
63 63 self.ipdir = self.tempdir()
64 64 self.save_get_ipython_dir = nbextensions.get_ipython_dir
65 65 nbextensions.get_ipython_dir = lambda : self.ipdir
66 66 self.save_system_dir = nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR
67 67 nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR = self.system_nbext = self.tempdir()
68 68
69 69 def tearDown(self):
70 70 nbextensions.get_ipython_dir = self.save_get_ipython_dir
71 71 nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR = self.save_system_dir
72 72 for td in self.tempdirs:
73 73 td.cleanup()
74 74
75 75 def assert_dir_exists(self, path):
76 76 if not os.path.exists(path):
77 77 do_exist = os.listdir(os.path.dirname(path))
78 78 self.fail(u"%s should exist (found %s)" % (path, do_exist))
79 79
80 80 def assert_not_dir_exists(self, path):
81 81 if os.path.exists(path):
82 82 self.fail(u"%s should not exist" % path)
83 83
84 84 def assert_installed(self, relative_path, user=False):
85 85 if user:
86 86 nbext = pjoin(self.ipdir, u'nbextensions')
87 87 else:
88 88 nbext = self.system_nbext
89 89 self.assert_dir_exists(
90 90 pjoin(nbext, relative_path)
91 91 )
92 92
93 93 def assert_not_installed(self, relative_path, user=False):
94 94 if user:
95 95 nbext = pjoin(self.ipdir, u'nbextensions')
96 96 else:
97 97 nbext = self.system_nbext
98 98 self.assert_not_dir_exists(
99 99 pjoin(nbext, relative_path)
100 100 )
101 101
102 102 def test_create_ipython_dir(self):
103 103 """install_nbextension when ipython_dir doesn't exist"""
104 104 with TemporaryDirectory() as td:
105 105 self.ipdir = ipdir = pjoin(td, u'ipython')
106 106 install_nbextension(self.src, user=True)
107 107 self.assert_dir_exists(ipdir)
108 108 for file in self.files:
109 109 self.assert_installed(
110 110 pjoin(basename(self.src), file),
111 111 user=bool(ipdir)
112 112 )
113 113
114 114 def test_create_nbextensions_user(self):
115 115 with TemporaryDirectory() as td:
116 116 self.ipdir = ipdir = pjoin(td, u'ipython')
117 117 install_nbextension(self.src, user=True)
118 118 self.assert_installed(
119 119 pjoin(basename(self.src), u'Æ’ile'),
120 120 user=True
121 121 )
122 122
123 123 def test_create_nbextensions_system(self):
124 124 with TemporaryDirectory() as td:
125 125 nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR = self.system_nbext = pjoin(td, u'nbextensions')
126 126 install_nbextension(self.src, user=False)
127 127 self.assert_installed(
128 128 pjoin(basename(self.src), u'Æ’ile'),
129 129 user=False
130 130 )
131 131
132 132 def test_single_file(self):
133 133 file = self.files[0]
134 134 install_nbextension(pjoin(self.src, file))
135 135 self.assert_installed(file)
136 136
137 137 def test_single_dir(self):
138 138 d = u'∂ir'
139 139 install_nbextension(pjoin(self.src, d))
140 140 self.assert_installed(self.files[-1])
141 141
142 142
143 143 def test_destination_file(self):
144 144 file = self.files[0]
145 145 install_nbextension(pjoin(self.src, file), destination = u'Æ’iledest')
146 146 self.assert_installed(u'Æ’iledest')
147 147
148 148 def test_destination_dir(self):
149 149 d = u'∂ir'
150 150 install_nbextension(pjoin(self.src, d), destination = u'Æ’iledest2')
151 151 self.assert_installed(pjoin(u'ƒiledest2', u'∂ir2', u'ƒile2'))
152 152
153 153 def test_install_nbextension(self):
154 154 with self.assertRaises(TypeError):
155 155 install_nbextension(glob.glob(pjoin(self.src, '*')))
156 156
157 157 def test_overwrite_file(self):
158 158 with TemporaryDirectory() as d:
159 159 fname = u'Æ’.js'
160 160 src = pjoin(d, fname)
161 161 with open(src, 'w') as f:
162 162 f.write('first')
163 163 mtime = touch(src)
164 164 dest = pjoin(self.system_nbext, fname)
165 165 install_nbextension(src)
166 166 with open(src, 'w') as f:
167 167 f.write('overwrite')
168 168 mtime = touch(src, mtime - 100)
169 169 install_nbextension(src, overwrite=True)
170 170 with open(dest) as f:
171 171 self.assertEqual(f.read(), 'overwrite')
172 172
173 173 def test_overwrite_dir(self):
174 174 with TemporaryDirectory() as src:
175 175 base = basename(src)
176 176 fname = u'Æ’.js'
177 177 touch(pjoin(src, fname))
178 178 install_nbextension(src)
179 179 self.assert_installed(pjoin(base, fname))
180 180 os.remove(pjoin(src, fname))
181 181 fname2 = u'∂.js'
182 182 touch(pjoin(src, fname2))
183 183 install_nbextension(src, overwrite=True)
184 184 self.assert_installed(pjoin(base, fname2))
185 185 self.assert_not_installed(pjoin(base, fname))
186 186
187 187 def test_update_file(self):
188 188 with TemporaryDirectory() as d:
189 189 fname = u'Æ’.js'
190 190 src = pjoin(d, fname)
191 191 with open(src, 'w') as f:
192 192 f.write('first')
193 193 mtime = touch(src)
194 194 install_nbextension(src)
195 195 self.assert_installed(fname)
196 196 dest = pjoin(self.system_nbext, fname)
197 197 old_mtime = os.stat(dest).st_mtime
198 198 with open(src, 'w') as f:
199 199 f.write('overwrite')
200 200 touch(src, mtime + 10)
201 201 install_nbextension(src)
202 202 with open(dest) as f:
203 203 self.assertEqual(f.read(), 'overwrite')
204 204
205 205 def test_skip_old_file(self):
206 206 with TemporaryDirectory() as d:
207 207 fname = u'Æ’.js'
208 208 src = pjoin(d, fname)
209 209 mtime = touch(src)
210 210 install_nbextension(src)
211 211 self.assert_installed(fname)
212 212 dest = pjoin(self.system_nbext, fname)
213 213 old_mtime = os.stat(dest).st_mtime
214 214
215 215 mtime = touch(src, mtime - 100)
216 216 install_nbextension(src)
217 217 new_mtime = os.stat(dest).st_mtime
218 218 self.assertEqual(new_mtime, old_mtime)
219 219
220 220 def test_quiet(self):
221 221 stdout = StringIO()
222 222 stderr = StringIO()
223 223 with mock.patch.object(sys, 'stdout', stdout), \
224 224 mock.patch.object(sys, 'stderr', stderr):
225 225 install_nbextension(self.src, verbose=0)
226 226 self.assertEqual(stdout.getvalue(), '')
227 227 self.assertEqual(stderr.getvalue(), '')
228 228
229 229 def test_install_zip(self):
230 230 path = pjoin(self.src, "myjsext.zip")
231 231 with zipfile.ZipFile(path, 'w') as f:
232 232 f.writestr("a.js", b"b();")
233 233 f.writestr("foo/a.js", b"foo();")
234 234 install_nbextension(path)
235 235 self.assert_installed("a.js")
236 236 self.assert_installed(pjoin("foo", "a.js"))
237 237
238 238 def test_install_tar(self):
239 239 def _add_file(f, fname, buf):
240 240 info = tarfile.TarInfo(fname)
241 241 info.size = len(buf)
242 242 f.addfile(info, BytesIO(buf))
243 243
244 244 for i,ext in enumerate((".tar.gz", ".tgz", ".tar.bz2")):
245 245 path = pjoin(self.src, "myjsext" + ext)
246 246 with tarfile.open(path, 'w') as f:
247 247 _add_file(f, "b%i.js" % i, b"b();")
248 248 _add_file(f, "foo/b%i.js" % i, b"foo();")
249 249 install_nbextension(path)
250 250 self.assert_installed("b%i.js" % i)
251 251 self.assert_installed(pjoin("foo", "b%i.js" % i))
252 252
253 253 def test_install_url(self):
254 254 def fake_urlretrieve(url, dest):
255 255 touch(dest)
256 256 save_urlretrieve = nbextensions.urlretrieve
257 257 nbextensions.urlretrieve = fake_urlretrieve
258 258 try:
259 259 install_nbextension("http://example.com/path/to/foo.js")
260 260 self.assert_installed("foo.js")
261 261 install_nbextension("https://example.com/path/to/another/bar.js")
262 262 self.assert_installed("bar.js")
263 263 install_nbextension("https://example.com/path/to/another/bar.js",
264 264 destination = 'foobar.js')
265 265 self.assert_installed("foobar.js")
266 266 finally:
267 267 nbextensions.urlretrieve = save_urlretrieve
268 268
269 269 def test_check_nbextension(self):
270 270 with TemporaryDirectory() as d:
271 271 f = u'Æ’.js'
272 272 src = pjoin(d, f)
273 273 touch(src)
274 274 install_nbextension(src, user=True)
275 275
276 276 assert check_nbextension(f, user=True)
277 277 assert check_nbextension([f], user=True)
278 278 assert not check_nbextension([f, pjoin('dne', f)], user=True)
279 279
280 280 @dec.skip_win32
281 281 def test_install_symlink(self):
282 282 with TemporaryDirectory() as d:
283 283 f = u'Æ’.js'
284 284 src = pjoin(d, f)
285 285 touch(src)
286 286 install_nbextension(src, symlink=True)
287 287 dest = pjoin(self.system_nbext, f)
288 288 assert os.path.islink(dest)
289 289 link = os.readlink(dest)
290 290 self.assertEqual(link, src)
291 291
292 292 @dec.skip_win32
293 293 def test_overwrite_broken_symlink(self):
294 294 with TemporaryDirectory() as d:
295 295 f = u'Æ’.js'
296 296 f2 = u'Æ’2.js'
297 297 src = pjoin(d, f)
298 298 src2 = pjoin(d, f2)
299 299 touch(src)
300 300 install_nbextension(src, symlink=True)
301 301 os.rename(src, src2)
302 302 install_nbextension(src2, symlink=True, overwrite=True, destination=f)
303 303 dest = pjoin(self.system_nbext, f)
304 304 assert os.path.islink(dest)
305 305 link = os.readlink(dest)
306 306 self.assertEqual(link, src2)
307 307
308 308 @dec.skip_win32
309 309 def test_install_symlink_destination(self):
310 310 with TemporaryDirectory() as d:
311 311 f = u'Æ’.js'
312 312 flink = u'Æ’link.js'
313 313 src = pjoin(d, f)
314 314 touch(src)
315 315 install_nbextension(src, symlink=True, destination=flink)
316 316 dest = pjoin(self.system_nbext, flink)
317 317 assert os.path.islink(dest)
318 318 link = os.readlink(dest)
319 319 self.assertEqual(link, src)
320 320
321 321 def test_install_symlink_bad(self):
322 322 with self.assertRaises(ValueError):
323 323 install_nbextension("http://example.com/foo.js", symlink=True)
324 324
325 325 with TemporaryDirectory() as d:
326 326 zf = u'Æ’.zip'
327 327 zsrc = pjoin(d, zf)
328 328 with zipfile.ZipFile(zsrc, 'w') as z:
329 329 z.writestr("a.js", b"b();")
330 330
331 331 with self.assertRaises(ValueError):
332 332 install_nbextension(zsrc, symlink=True)
333 333
334 334 def test_install_destination_bad(self):
335 335 with TemporaryDirectory() as d:
336 336 zf = u'Æ’.zip'
337 337 zsrc = pjoin(d, zf)
338 338 with zipfile.ZipFile(zsrc, 'w') as z:
339 339 z.writestr("a.js", b"b();")
340 340
341 341 with self.assertRaises(ValueError):
342 342 install_nbextension(zsrc, destination='foo')
@@ -1,62 +1,62
1 1 """Test NotebookApp"""
2 2
3 3
4 4 import logging
5 5 import os
6 6 from tempfile import NamedTemporaryFile
7 7
8 8 import nose.tools as nt
9 9
10 10 from traitlets.tests.utils import check_help_all_output
11 11
12 12 from IPython.utils.tempdir import TemporaryDirectory
13 13 from IPython.utils.traitlets import TraitError
14 from IPython.html import notebookapp
14 from jupyter_notebook import notebookapp
15 15 NotebookApp = notebookapp.NotebookApp
16 16
17 17
18 18 def test_help_output():
19 19 """ipython notebook --help-all works"""
20 check_help_all_output('IPython.html')
20 check_help_all_output('jupyter_notebook')
21 21
22 22 def test_server_info_file():
23 23 nbapp = NotebookApp(profile='nbserver_file_test', log=logging.getLogger())
24 24 def get_servers():
25 25 return list(notebookapp.list_running_servers(profile='nbserver_file_test'))
26 26 nbapp.initialize(argv=[])
27 27 nbapp.write_server_info_file()
28 28 servers = get_servers()
29 29 nt.assert_equal(len(servers), 1)
30 30 nt.assert_equal(servers[0]['port'], nbapp.port)
31 31 nt.assert_equal(servers[0]['url'], nbapp.connection_url)
32 32 nbapp.remove_server_info_file()
33 33 nt.assert_equal(get_servers(), [])
34 34
35 35 # The ENOENT error should be silenced.
36 36 nbapp.remove_server_info_file()
37 37
38 38 def test_nb_dir():
39 39 with TemporaryDirectory() as td:
40 40 app = NotebookApp(notebook_dir=td)
41 41 nt.assert_equal(app.notebook_dir, td)
42 42
43 43 def test_no_create_nb_dir():
44 44 with TemporaryDirectory() as td:
45 45 nbdir = os.path.join(td, 'notebooks')
46 46 app = NotebookApp()
47 47 with nt.assert_raises(TraitError):
48 48 app.notebook_dir = nbdir
49 49
50 50 def test_missing_nb_dir():
51 51 with TemporaryDirectory() as td:
52 52 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
53 53 app = NotebookApp()
54 54 with nt.assert_raises(TraitError):
55 55 app.notebook_dir = nbdir
56 56
57 57 def test_invalid_nb_dir():
58 58 with NamedTemporaryFile() as tf:
59 59 app = NotebookApp()
60 60 with nt.assert_raises(TraitError):
61 61 app.notebook_dir = tf
62 62
@@ -1,40 +1,40
1 1
2 2 import re
3 3 import nose.tools as nt
4 4
5 from IPython.html.base.handlers import path_regex
5 from jupyter_notebook.base.handlers import path_regex
6 6
7 7 try: # py3
8 8 assert_regex = nt.assert_regex
9 9 assert_not_regex = nt.assert_not_regex
10 10 except AttributeError: # py2
11 11 assert_regex = nt.assert_regexp_matches
12 12 assert_not_regex = nt.assert_not_regexp_matches
13 13
14 14
15 15 # build regexps that tornado uses:
16 16 path_pat = re.compile('^' + '/x%s' % path_regex + '$')
17 17
18 18 def test_path_regex():
19 19 for path in (
20 20 '/x',
21 21 '/x/',
22 22 '/x/foo',
23 23 '/x/foo.ipynb',
24 24 '/x/foo/bar',
25 25 '/x/foo/bar.txt',
26 26 ):
27 27 assert_regex(path, path_pat)
28 28
29 29 def test_path_regex_bad():
30 30 for path in (
31 31 '/xfoo',
32 32 '/xfoo/',
33 33 '/xfoo/bar',
34 34 '/xfoo/bar/',
35 35 '/x/foo/bar/',
36 36 '/x//foo',
37 37 '/y',
38 38 '/y/x/foo',
39 39 ):
40 40 assert_not_regex(path, path_pat)
@@ -1,66 +1,66
1 1 """Test HTML utils"""
2 2
3 3 # Copyright (c) Jupyter Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import os
7 7
8 8 import nose.tools as nt
9 9
10 10 from traitlets.tests.utils import check_help_all_output
11 from IPython.html.utils import url_escape, url_unescape, is_hidden
11 from jupyter_notebook.utils import url_escape, url_unescape, is_hidden
12 12 from IPython.utils.tempdir import TemporaryDirectory
13 13
14 14
15 15 def test_help_output():
16 16 """jupyter notebook --help-all works"""
17 17 # FIXME: will be jupyter_notebook
18 check_help_all_output('IPython.html')
18 check_help_all_output('jupyter_notebook')
19 19
20 20
21 21 def test_url_escape():
22 22
23 23 # changes path or notebook name with special characters to url encoding
24 24 # these tests specifically encode paths with spaces
25 25 path = url_escape('/this is a test/for spaces/')
26 26 nt.assert_equal(path, '/this%20is%20a%20test/for%20spaces/')
27 27
28 28 path = url_escape('notebook with space.ipynb')
29 29 nt.assert_equal(path, 'notebook%20with%20space.ipynb')
30 30
31 31 path = url_escape('/path with a/notebook and space.ipynb')
32 32 nt.assert_equal(path, '/path%20with%20a/notebook%20and%20space.ipynb')
33 33
34 34 path = url_escape('/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
35 35 nt.assert_equal(path,
36 36 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
37 37
38 38 def test_url_unescape():
39 39
40 40 # decodes a url string to a plain string
41 41 # these tests decode paths with spaces
42 42 path = url_unescape('/this%20is%20a%20test/for%20spaces/')
43 43 nt.assert_equal(path, '/this is a test/for spaces/')
44 44
45 45 path = url_unescape('notebook%20with%20space.ipynb')
46 46 nt.assert_equal(path, 'notebook with space.ipynb')
47 47
48 48 path = url_unescape('/path%20with%20a/notebook%20and%20space.ipynb')
49 49 nt.assert_equal(path, '/path with a/notebook and space.ipynb')
50 50
51 51 path = url_unescape(
52 52 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
53 53 nt.assert_equal(path, '/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
54 54
55 55 def test_is_hidden():
56 56 with TemporaryDirectory() as root:
57 57 subdir1 = os.path.join(root, 'subdir')
58 58 os.makedirs(subdir1)
59 59 nt.assert_equal(is_hidden(subdir1, root), False)
60 60 subdir2 = os.path.join(root, '.subdir2')
61 61 os.makedirs(subdir2)
62 62 nt.assert_equal(is_hidden(subdir2, root), True)
63 63 subdir34 = os.path.join(root, 'subdir3', '.subdir4')
64 64 os.makedirs(subdir34)
65 65 nt.assert_equal(is_hidden(subdir34, root), True)
66 66 nt.assert_equal(is_hidden(subdir34), True)
@@ -1,94 +1,94
1 1 // Test the widget manager.
2 2 casper.notebook_test(function () {
3 3 var index;
4 4
5 5 this.then(function () {
6 6
7 7 // Check if the WidgetManager class is defined.
8 8 this.test.assert(this.evaluate(function() {
9 9 return IPython.WidgetManager !== undefined;
10 10 }), 'WidgetManager class is defined');
11 11
12 12 // Check if the widget manager has been instantiated.
13 13 this.test.assert(this.evaluate(function() {
14 14 return IPython.notebook.kernel.widget_manager !== undefined;
15 15 }), 'Notebook widget manager instantiated');
16 16
17 17 // Try creating a widget from Javascript.
18 18 this.evaluate(function() {
19 19 IPython.notebook.kernel.widget_manager.create_model({
20 20 model_name: 'WidgetModel',
21 widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
21 widget_class: 'jupyter_notebook.widgets.widget_int.IntSlider'})
22 22 .then(function(model) {
23 23 console.log('Create success!', model);
24 24 window.slider_id = model.id;
25 25 }, function(error) { console.log(error); });
26 26 });
27 27 });
28 28
29 29 // Wait for the state to be recieved.
30 30 this.waitFor(function check() {
31 31 return this.evaluate(function() {
32 32 return window.slider_id !== undefined;
33 33 });
34 34 });
35 35
36 36 index = this.append_cell(
37 'from IPython.html.widgets import Widget\n' +
37 'from jupyter_notebook.widgets import Widget\n' +
38 38 'widget = list(Widget.widgets.values())[0]\n' +
39 39 'print(widget.model_id)');
40 40 this.execute_cell_then(index, function(index) {
41 41 var output = this.get_output_cell(index).text.trim();
42 42 var slider_id = this.evaluate(function() { return window.slider_id; });
43 43 this.test.assertEquals(output, slider_id, "Widget created from the front-end.");
44 44 });
45 45
46 46 // Widget persistence tests.
47 47 index = this.append_cell(
48 'from IPython.html.widgets import HTML\n' +
48 'from jupyter_notebook.widgets import HTML\n' +
49 49 'from IPython.display import display\n' +
50 50 'display(HTML(value="<div id=\'hello\'></div>"))');
51 51 this.execute_cell_then(index, function() {});
52 52
53 53 index = this.append_cell(
54 54 'display(HTML(value="<div id=\'world\'></div>"))');
55 55 this.execute_cell_then(index, function() {});
56 56
57 57 var that = this;
58 58 this.then(function() {
59 59 // Wait for the widgets to be shown.
60 60 that.waitForSelector('#hello', function() {
61 61 that.waitForSelector('#world', function() {
62 62 that.test.assertExists('#hello', 'Hello HTML widget constructed.');
63 63 that.test.assertExists('#world', 'World HTML widget constructed.');
64 64
65 65 // Save the notebook.
66 66 that.evaluate(function() {
67 67 IPython.notebook.save_notebook(false).then(function() {
68 68 window.was_saved = true;
69 69 });
70 70 });
71 71 that.waitFor(function check() {
72 72 return that.evaluate(function() {
73 73 return window.was_saved;
74 74 });
75 75 }, function then() {
76 76
77 77 // Reload the page
78 78 that.reload(function() {
79 79
80 80 // Wait for the elements to show up again.
81 81 that.waitForSelector('#hello', function() {
82 82 that.waitForSelector('#world', function() {
83 83 that.test.assertExists('#hello', 'Hello HTML widget persisted.');
84 84 that.test.assertExists('#world', 'World HTML widget persisted.');
85 85 });
86 86 });
87 87 });
88 88 });
89 89 });
90 90 });
91 91 });
92 92
93 93
94 94 });
@@ -1,309 +1,309
1 1 var xor = function (a, b) {return !a ^ !b;};
2 2 var isArray = function (a) {
3 3 try {
4 4 return Object.toString.call(a) === "[object Array]" || Object.toString.call(a) === "[object RuntimeArray]";
5 5 } catch (e) {
6 6 return Array.isArray(a);
7 7 }
8 8 };
9 9 var recursive_compare = function(a, b) {
10 10 // Recursively compare two objects.
11 11 var same = true;
12 12 same = same && !xor(a instanceof Object || typeof a == 'object', b instanceof Object || typeof b == 'object');
13 13 same = same && !xor(isArray(a), isArray(b));
14 14
15 15 if (same) {
16 16 if (a instanceof Object) {
17 17 var key;
18 18 for (key in a) {
19 19 if (a.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
20 20 same = false;
21 21 break;
22 22 }
23 23 }
24 24 for (key in b) {
25 25 if (b.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
26 26 same = false;
27 27 break;
28 28 }
29 29 }
30 30 } else {
31 31 return a === b;
32 32 }
33 33 }
34 34
35 35 return same;
36 36 };
37 37
38 38 // Test the widget framework.
39 39 casper.notebook_test(function () {
40 40 var index;
41 41
42 42 index = this.append_cell(
43 ['from IPython.html import widgets',
43 ['from jupyter_notebook import widgets',
44 44 'from IPython.display import display, clear_output',
45 45 'print("Success")'].join('\n'));
46 46 this.execute_cell_then(index);
47 47
48 48 this.then(function () {
49 49 // Test multi-set, single touch code. First create a custom widget.
50 50 this.thenEvaluate(function() {
51 51 var MultiSetView = IPython.DOMWidgetView.extend({
52 52 render: function(){
53 53 this.model.set('a', 1);
54 54 this.model.set('b', 2);
55 55 this.model.set('c', 3);
56 56 this.touch();
57 57 },
58 58 });
59 59 IPython.WidgetManager.register_widget_view('MultiSetView', MultiSetView);
60 60 }, {});
61 61 });
62 62
63 63 // Try creating the multiset widget, verify that sets the values correctly.
64 64 var multiset = {};
65 65 multiset.index = this.append_cell([
66 66 'from IPython.utils.traitlets import Unicode, CInt',
67 67 'class MultiSetWidget(widgets.Widget):',
68 68 ' _view_name = Unicode("MultiSetView", sync=True)',
69 69 ' a = CInt(0, sync=True)',
70 70 ' b = CInt(0, sync=True)',
71 71 ' c = CInt(0, sync=True)',
72 72 ' d = CInt(-1, sync=True)', // See if it sends a full state.
73 73 ' def set_state(self, sync_data):',
74 74 ' widgets.Widget.set_state(self, sync_data)',
75 75 ' self.d = len(sync_data)',
76 76 'multiset = MultiSetWidget()',
77 77 'display(multiset)',
78 78 'print(multiset.model_id)'].join('\n'));
79 79 this.execute_cell_then(multiset.index, function(index) {
80 80 multiset.model_id = this.get_output_cell(index).text.trim();
81 81 });
82 82
83 83 this.wait_for_widget(multiset);
84 84
85 85 index = this.append_cell(
86 86 'print("%d%d%d" % (multiset.a, multiset.b, multiset.c))');
87 87 this.execute_cell_then(index, function(index) {
88 88 this.test.assertEquals(this.get_output_cell(index).text.trim(), '123',
89 89 'Multiple model.set calls and one view.touch update state in back-end.');
90 90 });
91 91
92 92 index = this.append_cell(
93 93 'print("%d" % (multiset.d))');
94 94 this.execute_cell_then(index, function(index) {
95 95 this.test.assertEquals(this.get_output_cell(index).text.trim(), '3',
96 96 'Multiple model.set calls sent a partial state.');
97 97 });
98 98
99 99 var textbox = {};
100 100 throttle_index = this.append_cell([
101 101 'import time',
102 102 'textbox = widgets.Text()',
103 103 'display(textbox)',
104 104 'textbox._dom_classes = ["my-throttle-textbox"]',
105 105 'def handle_change(name, old, new):',
106 106 ' display(len(new))',
107 107 ' time.sleep(0.5)',
108 108 'textbox.on_trait_change(handle_change, "value")',
109 109 'print(textbox.model_id)'].join('\n'));
110 110 this.execute_cell_then(throttle_index, function(index){
111 111 textbox.model_id = this.get_output_cell(index).text.trim();
112 112
113 113 this.test.assert(this.cell_element_exists(index,
114 114 '.widget-area .widget-subarea'),
115 115 'Widget subarea exists.');
116 116
117 117 this.test.assert(this.cell_element_exists(index,
118 118 '.my-throttle-textbox'), 'Textbox exists.');
119 119
120 120 // Send 20 characters
121 121 this.sendKeys('.my-throttle-textbox input', '12345678901234567890');
122 122 });
123 123
124 124 this.wait_for_widget(textbox);
125 125
126 126 this.then(function () {
127 127 var outputs = this.evaluate(function(i) {
128 128 return IPython.notebook.get_cell(i).output_area.outputs;
129 129 }, {i : throttle_index});
130 130
131 131 // Only 4 outputs should have printed, but because of timing, sometimes
132 132 // 5 outputs will print. All we need to do is verify num outputs <= 5
133 133 // because that is much less than 20.
134 134 this.test.assert(outputs.length <= 5, 'Messages throttled.');
135 135
136 136 // We also need to verify that the last state sent was correct.
137 137 var last_state = outputs[outputs.length-1].data['text/plain'];
138 138 this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
139 139 });
140 140
141 141
142 142 this.thenEvaluate(function() {
143 143 define('TestWidget', ['widgets/js/widget', 'base/js/utils', 'underscore'], function(widget, utils, _) {
144 144 var floatArray = {
145 145 deserialize: function (value, model) {
146 146 if (value===null) {return null;}
147 147 // DataView -> float64 typed array
148 148 return new Float64Array(value.buffer);
149 149 },
150 150 // serialization automatically handled since the
151 151 // attribute is an ArrayBuffer view
152 152 };
153 153
154 154 var floatList = {
155 155 deserialize: function (value, model) {
156 156 // list of floats -> list of strings
157 157 return value.map(function(x) {return x.toString()});
158 158 },
159 159 serialize: function(value, model) {
160 160 // list of strings -> list of floats
161 161 return value.map(function(x) {return parseFloat(x);})
162 162 }
163 163 };
164 164
165 165 var TestWidgetModel = widget.WidgetModel.extend({}, {
166 166 serializers: _.extend({
167 167 array_list: floatList,
168 168 array_binary: floatArray
169 169 }, widget.WidgetModel.serializers)
170 170 });
171 171
172 172 var TestWidgetView = widget.DOMWidgetView.extend({
173 173 render: function () {
174 174 this.listenTo(this.model, 'msg:custom', this.handle_msg);
175 175 },
176 176 handle_msg: function(content, buffers) {
177 177 this.msg = [content, buffers];
178 178 }
179 179 });
180 180
181 181 return {TestWidgetModel: TestWidgetModel, TestWidgetView: TestWidgetView};
182 182 });
183 183 });
184 184
185 185 var testwidget = {};
186 186 this.append_cell_execute_then([
187 'from IPython.html import widgets',
187 'from jupyter_notebook import widgets',
188 188 'from IPython.utils.traitlets import Unicode, Instance, List',
189 189 'from IPython.display import display',
190 190 'from array import array',
191 191 'def _array_to_memoryview(x):',
192 192 ' if x is None: return None',
193 193 ' try:',
194 194 ' y = memoryview(x)',
195 195 ' except TypeError:',
196 196 ' # in python 2, arrays do not support the new buffer protocol',
197 197 ' y = memoryview(buffer(x))',
198 198 ' return y',
199 199 'def _memoryview_to_array(x):',
200 200 ' if x is None: return None',
201 201 ' return array("d", x.tobytes())',
202 202 'arrays_binary = {',
203 203 ' "from_json": _memoryview_to_array,',
204 204 ' "to_json": _array_to_memoryview',
205 205 '}',
206 206 '',
207 207 'def _array_to_list(x):',
208 208 ' return list(x)',
209 209 'def _list_to_array(x):',
210 210 ' return array("d",x)',
211 211 'arrays_list = {',
212 212 ' "from_json": _list_to_array,',
213 213 ' "to_json": _array_to_list',
214 214 '}',
215 215 '',
216 216 'class TestWidget(widgets.DOMWidget):',
217 217 ' _model_module = Unicode("TestWidget", sync=True)',
218 218 ' _model_name = Unicode("TestWidgetModel", sync=True)',
219 219 ' _view_module = Unicode("TestWidget", sync=True)',
220 220 ' _view_name = Unicode("TestWidgetView", sync=True)',
221 221 ' array_binary = Instance(array, allow_none=True, sync=True, **arrays_binary)',
222 222 ' array_list = Instance(array, args=("d", [3.0]), allow_none=False, sync=True, **arrays_list)',
223 223 ' msg = {}',
224 224 ' def __init__(self, **kwargs):',
225 225 ' super(widgets.DOMWidget, self).__init__(**kwargs)',
226 226 ' self.on_msg(self._msg)',
227 227 ' def _msg(self, _, content, buffers):',
228 228 ' self.msg = [content, buffers]',
229 229 'x=TestWidget()',
230 230 'display(x)',
231 231 'print(x.model_id)'].join('\n'), function(index){
232 232 testwidget.index = index;
233 233 testwidget.model_id = this.get_output_cell(index).text.trim();
234 234 });
235 235 this.wait_for_widget(testwidget);
236 236
237 237
238 238 this.append_cell_execute_then('x.array_list = array("d", [1.5, 2.0, 3.1])');
239 239 this.wait_for_widget(testwidget);
240 240 this.then(function() {
241 241 var result = this.evaluate(function(index) {
242 242 var v = IPython.notebook.get_cell(index).widget_views[0];
243 243 var result = v.model.get('array_list');
244 244 var z = result.slice();
245 245 z[0]+="1234";
246 246 z[1]+="5678";
247 247 v.model.set('array_list', z);
248 248 v.touch();
249 249 return result;
250 250 }, testwidget.index);
251 251 this.test.assertEquals(result, ["1.5", "2", "3.1"], "JSON custom serializer kernel -> js");
252 252 });
253 253
254 254 this.assert_output_equals('print(x.array_list.tolist() == [1.51234, 25678.0, 3.1])',
255 255 'True', 'JSON custom serializer js -> kernel');
256 256
257 257 if (this.slimerjs) {
258 258 this.append_cell_execute_then("x.array_binary=array('d', [1.5,2.5,5])", function() {
259 259 this.evaluate(function(index) {
260 260 var v = IPython.notebook.get_cell(index).widget_views[0];
261 261 var z = v.model.get('array_binary');
262 262 z[0]*=3;
263 263 z[1]*=3;
264 264 z[2]*=3;
265 265 // we set to null so that we recognize the change
266 266 // when we set data back to z
267 267 v.model.set('array_binary', null);
268 268 v.model.set('array_binary', z);
269 269 v.touch();
270 270 }, textwidget.index);
271 271 });
272 272 this.wait_for_widget(testwidget);
273 273 this.assert_output_equals('x.array_binary.tolist() == [4.5, 7.5, 15.0]',
274 274 'True\n', 'Binary custom serializer js -> kernel')
275 275
276 276 this.append_cell_execute_then('x.send("some content", [memoryview(b"binarycontent"), memoryview("morecontent")])');
277 277 this.wait_for_widget(testwidget);
278 278
279 279 this.then(function() {
280 280 var result = this.evaluate(function(index) {
281 281 var v = IPython.notebook.get_cell(index).widget_views[0];
282 282 var d = new TextDecoder('utf-8');
283 283 return {text: v.msg[0],
284 284 binary0: d.decode(v.msg[1][0]),
285 285 binary1: d.decode(v.msg[1][1])};
286 286 }, testwidget.index);
287 287 this.test.assertEquals(result, {text: 'some content',
288 288 binary0: 'binarycontent',
289 289 binary1: 'morecontent'},
290 290 "Binary widget messages kernel -> js");
291 291 });
292 292
293 293 this.then(function() {
294 294 this.evaluate(function(index) {
295 295 var v = IPython.notebook.get_cell(index).widget_views[0];
296 296 v.send('content back', [new Uint8Array([1,2,3,4]), new Float64Array([2.1828, 3.14159])])
297 297 }, testwidget.index);
298 298 });
299 299 this.wait_for_widget(testwidget);
300 300 this.assert_output_equals([
301 301 'all([x.msg[0] == "content back",',
302 302 ' x.msg[1][0].tolist() == [1,2,3,4],',
303 303 ' array("d", x.msg[1][1].tobytes()).tolist() == [2.1828, 3.14159]])'].join('\n'),
304 304 'True', 'Binary buffers message js -> kernel');
305 305 } else {
306 306 console.log("skipping binary websocket tests on phantomjs");
307 307 }
308 308
309 309 });
@@ -1,92 +1,92
1 1 // Test widget bool class
2 2 casper.notebook_test(function () {
3 3 "use strict";
4 4
5 5 // Create a checkbox and togglebutton.
6 6 var bool_index = this.append_cell(
7 'from IPython.html import widgets\n' +
7 'from jupyter_notebook import widgets\n' +
8 8 'from IPython.display import display, clear_output\n' +
9 9 'bool_widgets = [widgets.Checkbox(description="Title", value=True),\n' +
10 10 ' widgets.ToggleButton(description="Title", value=True)]\n' +
11 11 'display(bool_widgets[0])\n' +
12 12 'display(bool_widgets[1])\n' +
13 13 'print("Success")');
14 14 this.execute_cell_then(bool_index, function(index){
15 15 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
16 16 'Create bool widget cell executed with correct output.');
17 17 });
18 18
19 19 // Wait for the widgets to actually display.
20 20 var widget_checkbox_selector = '.widget-area .widget-subarea .widget-hbox input';
21 21 var widget_togglebutton_selector = '.widget-area .widget-subarea button';
22 22 this.wait_for_element(bool_index, widget_checkbox_selector);
23 23 this.wait_for_element(bool_index, widget_togglebutton_selector);
24 24
25 25 // Continue the tests.
26 26 this.then(function() {
27 27 this.test.assert(this.cell_element_exists(bool_index,
28 28 '.widget-area .widget-subarea'),
29 29 'Widget subarea exists.');
30 30
31 31 this.test.assert(this.cell_element_exists(bool_index,
32 32 widget_checkbox_selector),
33 33 'Checkbox exists.');
34 34
35 35 this.test.assert(this.cell_element_function(bool_index,
36 36 widget_checkbox_selector, 'prop', ['checked']),
37 37 'Checkbox is checked.');
38 38
39 39 this.test.assert(this.cell_element_exists(bool_index,
40 40 '.widget-area .widget-subarea .widget-hbox .widget-label'),
41 41 'Checkbox label exists.');
42 42
43 43 this.test.assert(this.cell_element_function(bool_index,
44 44 '.widget-area .widget-subarea .widget-hbox .widget-label', 'html')=="Title",
45 45 'Checkbox labeled correctly.');
46 46
47 47 this.test.assert(this.cell_element_exists(bool_index,
48 48 widget_togglebutton_selector),
49 49 'Toggle button exists.');
50 50
51 51 this.test.assert(this.cell_element_function(bool_index,
52 52 widget_togglebutton_selector, 'html')=='<i class="fa"></i>Title',
53 53 'Toggle button labeled correctly.');
54 54
55 55 this.test.assert(this.cell_element_function(bool_index,
56 56 widget_togglebutton_selector, 'hasClass', ['active']),
57 57 'Toggle button is toggled.');
58 58 });
59 59
60 60 // Try changing the state of the widgets programatically.
61 61 var index = this.append_cell(
62 62 'bool_widgets[0].value = False\n' +
63 63 'bool_widgets[1].value = False\n' +
64 64 'print("Success")');
65 65 this.execute_cell_then(index, function(index){
66 66 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
67 67 'Change bool widget value cell executed with correct output.');
68 68
69 69 this.test.assert(! this.cell_element_function(bool_index,
70 70 widget_checkbox_selector, 'prop', ['checked']),
71 71 'Checkbox is not checked. (1)');
72 72
73 73 this.test.assert(! this.cell_element_function(bool_index,
74 74 widget_togglebutton_selector, 'hasClass', ['active']),
75 75 'Toggle button is not toggled. (1)');
76 76
77 77 // Try toggling the bool by clicking on the checkbox.
78 78 this.cell_element_function(bool_index, widget_checkbox_selector, 'click');
79 79
80 80 this.test.assert(this.cell_element_function(bool_index,
81 81 widget_checkbox_selector, 'prop', ['checked']),
82 82 'Checkbox is checked. (2)');
83 83
84 84 // Try toggling the bool by clicking on the toggle button.
85 85 this.cell_element_function(bool_index, widget_togglebutton_selector, 'click');
86 86
87 87 this.test.assert(this.cell_element_function(bool_index,
88 88 widget_togglebutton_selector, 'hasClass', ['active']),
89 89 'Toggle button is toggled. (3)');
90 90
91 91 });
92 92 });
@@ -1,92 +1,92
1 1 // Test container class
2 2 casper.notebook_test(function () {
3 3
4 4 // Create a box widget.
5 5 var container_index = this.append_cell(
6 'from IPython.html import widgets\n' +
6 'from jupyter_notebook import widgets\n' +
7 7 'from IPython.display import display, clear_output\n' +
8 8 'container = widgets.Box()\n' +
9 9 'button = widgets.Button()\n'+
10 10 'container.children = [button]\n'+
11 11 'display(container)\n'+
12 12 'container._dom_classes = ["my-test-class"]\n'+
13 13 'print("Success")\n');
14 14 this.execute_cell_then(container_index, function(index){
15 15 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
16 16 'Create container cell executed with correct output.');
17 17 });
18 18
19 19 // Wait for the widgets to actually display.
20 20 var widget_box_selector = '.widget-area .widget-subarea .widget-box';
21 21 var widget_box_button_selector = '.widget-area .widget-subarea .widget-box button';
22 22 this.wait_for_element(container_index, widget_box_selector);
23 23 this.wait_for_element(container_index, widget_box_button_selector);
24 24
25 25 // Continue with the tests.
26 26 this.then(function() {
27 27 this.test.assert(this.cell_element_exists(container_index,
28 28 '.widget-area .widget-subarea'),
29 29 'Widget subarea exists.');
30 30
31 31 this.test.assert(this.cell_element_exists(container_index,
32 32 widget_box_selector),
33 33 'Widget container exists.');
34 34
35 35 this.test.assert(this.cell_element_exists(container_index,
36 36 '.widget-area .widget-subarea .my-test-class'),
37 37 '_dom_classes works.');
38 38
39 39 this.test.assert(this.cell_element_exists(container_index,
40 40 widget_box_button_selector),
41 41 'Container parent/child relationship works.');
42 42 });
43 43
44 44 index = this.append_cell(
45 45 'container.box_style = "success"\n'+
46 46 'print("Success")\n');
47 47 this.execute_cell_then(index, function(index){
48 48
49 49 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
50 50 'Set box_style cell executed with correct output.');
51 51
52 52 this.test.assert(this.cell_element_exists(container_index,
53 53 '.widget-box.alert-success'),
54 54 'Set box_style works.');
55 55 });
56 56
57 57 index = this.append_cell(
58 58 'container._dom_classes = []\n'+
59 59 'print("Success")\n');
60 60 this.execute_cell_then(index, function(index){
61 61
62 62 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
63 63 'Remove container class cell executed with correct output.');
64 64
65 65 this.test.assert(! this.cell_element_exists(container_index,
66 66 '.widget-area .widget-subarea .my-test-class'),
67 67 '_dom_classes can be used to remove a class.');
68 68 });
69 69
70 70 var boxalone_index = this.append_cell(
71 71 'display(button)\n'+
72 72 'print("Success")\n');
73 73 this.execute_cell_then(boxalone_index, function(index){
74 74 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
75 75 'Display container child executed with correct output.');
76 76 });
77 77
78 78 // Wait for the widget to actually display.
79 79 var widget_button_selector = '.widget-area .widget-subarea button';
80 80 this.wait_for_element(boxalone_index, widget_button_selector);
81 81
82 82 // Continue with the tests.
83 83 this.then(function() {
84 84 this.test.assert(! this.cell_element_exists(boxalone_index,
85 85 widget_box_selector),
86 86 'Parent container not displayed.');
87 87
88 88 this.test.assert(this.cell_element_exists(boxalone_index,
89 89 widget_button_selector),
90 90 'Child displayed.');
91 91 });
92 92 }); No newline at end of file
@@ -1,48 +1,48
1 1 // Test widget button class
2 2 casper.notebook_test(function () {
3 3 var button_index = this.append_cell(
4 'from IPython.html import widgets\n' +
4 'from jupyter_notebook import widgets\n' +
5 5 'from IPython.display import display, clear_output\n' +
6 6 'button = widgets.Button(description="Title")\n' +
7 7 'display(button)\n' +
8 8 'print("Success")\n' +
9 9 'def handle_click(sender):\n' +
10 10 ' display("Clicked")\n' +
11 11 'button.on_click(handle_click)');
12 12 this.execute_cell_then(button_index, function(index){
13 13 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
14 14 'Create button cell executed with correct output.');
15 15 });
16 16
17 17 // Wait for the widgets to actually display.
18 18 var widget_button_selector = '.widget-area .widget-subarea button';
19 19 this.wait_for_element(button_index, widget_button_selector);
20 20
21 21 // Continue with the tests.
22 22 this.then(function() {
23 23 this.test.assert(this.cell_element_exists(button_index,
24 24 '.widget-area .widget-subarea'),
25 25 'Widget subarea exists.');
26 26
27 27 this.test.assert(this.cell_element_exists(button_index,
28 28 widget_button_selector),
29 29 'Widget button exists.');
30 30
31 31 this.test.assert(this.cell_element_function(button_index,
32 32 widget_button_selector, 'html')=='<i class="fa"></i>Title',
33 33 'Set button description.');
34 34
35 35 this.cell_element_function(button_index,
36 36 widget_button_selector, 'click');
37 37 });
38 38
39 39 this.wait_for_output(button_index, 1);
40 40
41 41 this.then(function () {
42 42 var warning_text = this.get_output_cell(button_index, 1).text;
43 43 this.test.assertNotEquals(warning_text.indexOf('Warning'), -1,
44 44 'Importing widgets show a warning');
45 45 this.test.assertEquals(this.get_output_cell(button_index, 2).data['text/plain'], "'Clicked'",
46 46 'Button click event fires.');
47 47 });
48 48 });
@@ -1,107 +1,107
1 1 // Test widget float class
2 2 casper.notebook_test(function () {
3 3 var float_text = {};
4 4 float_text.query = '.widget-area .widget-subarea .my-second-float-text input';
5 5 float_text.index = this.append_cell(
6 'from IPython.html import widgets\n' +
6 'from jupyter_notebook import widgets\n' +
7 7 'from IPython.display import display, clear_output\n' +
8 8 'float_widget = widgets.FloatText()\n' +
9 9 'display(float_widget)\n' +
10 10 'float_widget._dom_classes = ["my-second-float-text"]\n' +
11 11 'print(float_widget.model_id)\n');
12 12 this.execute_cell_then(float_text.index, function(index){
13 13 float_text.model_id = this.get_output_cell(index).text.trim();
14 14 });
15 15
16 16 // Wait for the widget to actually display.
17 17 this.wait_for_element(float_text.index, float_text.query);
18 18
19 19 // Continue with the tests
20 20 this.then(function(){
21 21 this.test.assert(this.cell_element_exists(float_text.index,
22 22 '.widget-area .widget-subarea'),
23 23 'Widget subarea exists.');
24 24
25 25 this.test.assert(this.cell_element_exists(float_text.index, float_text.query),
26 26 'Widget float textbox exists.');
27 27
28 28 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
29 29 this.sendKeys(float_text.query, '1.05');
30 30 });
31 31
32 32 this.wait_for_widget(float_text);
33 33
34 34 index = this.append_cell('print(float_widget.value)\n');
35 35 this.execute_cell_then(index, function(index){
36 36 this.test.assertEquals(this.get_output_cell(index).text, '1.05\n',
37 37 'Float textbox value set.');
38 38 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
39 39 this.sendKeys(float_text.query, '123456789.0');
40 40 });
41 41
42 42 this.wait_for_widget(float_text);
43 43
44 44 index = this.append_cell('print(float_widget.value)\n');
45 45 this.execute_cell_then(index, function(index){
46 46 this.test.assertEquals(this.get_output_cell(index).text, '123456789.0\n',
47 47 'Long float textbox value set (probably triggers throttling).');
48 48 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
49 49 this.sendKeys(float_text.query, '12hello');
50 50 });
51 51
52 52 this.wait_for_widget(float_text);
53 53
54 54 index = this.append_cell('print(float_widget.value)\n');
55 55 this.execute_cell_then(index, function(index){
56 56 this.test.assertEquals(this.get_output_cell(index).text, '12.0\n',
57 57 'Invald float textbox value caught and filtered.');
58 58 });
59 59
60 60 var float_text_query = '.widget-area .widget-subarea .widget-numeric-text';
61 61 var slider = {};
62 62 slider.query = '.widget-area .widget-subarea .slider';
63 63 slider.index = this.append_cell(
64 64 'floatrange = [widgets.BoundedFloatText(), \n' +
65 65 ' widgets.FloatSlider()]\n' +
66 66 '[display(floatrange[i]) for i in range(2)]\n' +
67 67 'print("Success")\n');
68 68 this.execute_cell_then(slider.index, function(index){
69 69 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
70 70 'Create float range cell executed with correct output.');
71 71 });
72 72
73 73 // Wait for the widgets to actually display.
74 74 this.wait_for_element(slider.index, slider.query);
75 75 this.wait_for_element(slider.index, float_text_query);
76 76
77 77 this.then(function(){
78 78 this.test.assert(this.cell_element_exists(slider.index,
79 79 '.widget-area .widget-subarea'),
80 80 'Widget subarea exists.');
81 81
82 82 this.test.assert(this.cell_element_exists(slider.index, slider.query),
83 83 'Widget slider exists.');
84 84
85 85 this.test.assert(this.cell_element_exists(slider.index, float_text_query),
86 86 'Widget float textbox exists.');
87 87 });
88 88
89 89 index = this.append_cell(
90 90 'for widget in floatrange:\n' +
91 91 ' widget.max = 50.0\n' +
92 92 ' widget.min = -50.0\n' +
93 93 ' widget.value = 25.0\n' +
94 94 'print("Success")\n');
95 95 this.execute_cell_then(index, function(index){
96 96
97 97 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
98 98 'Float range properties cell executed with correct output.');
99 99
100 100 this.test.assert(this.cell_element_exists(slider.index, slider.query),
101 101 'Widget slider exists.');
102 102
103 103 this.test.assert(this.cell_element_function(slider.index, slider.query,
104 104 'slider', ['value']) == 25.0,
105 105 'Slider set to Python value.');
106 106 });
107 107 }); No newline at end of file
@@ -1,49 +1,49
1 1 // Test image class
2 2 casper.notebook_test(function () {
3 3 "use strict";
4 4 var index = this.append_cell(
5 'from IPython.html import widgets\n' +
5 'from jupyter_notebook import widgets\n' +
6 6 'from IPython.display import display, clear_output\n' +
7 7 'print("Success")');
8 8 this.execute_cell_then(index);
9 9
10 10 // Get the temporary directory that the test server is running in.
11 11 var cwd = '';
12 12 index = this.append_cell('!echo $(pwd)');
13 13 this.execute_cell_then(index, function(index){
14 14 cwd = this.get_output_cell(index).text.trim();
15 15 });
16 16
17 17 var test_jpg = '/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDACAWGBwYFCAcGhwkIiAmMFA0MCwsMGJGSjpQdGZ6eHJmcG6AkLicgIiuim5woNqirr7EztDOfJri8uDI8LjKzsb/2wBDASIkJDAqMF40NF7GhHCExsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsb/wgARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAAA//EABUBAQEAAAAAAAAAAAAAAAAAAAME/9oADAMBAAIQAxAAAAECv//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAQUCf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Bf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Bf//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEABj8Cf//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAT8hf//aAAwDAQACAAMAAAAQn//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Qf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Qf//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAT8Qf//Z';
18 18
19 19 var image_index = this.append_cell(
20 20 'import base64\n' +
21 21 'data = base64.b64decode("' + test_jpg + '")\n' +
22 22 'image = widgets.Image()\n' +
23 23 'image.format = "jpeg"\n' +
24 24 'image.value = data\n' +
25 25 'image.width = "50px"\n' +
26 26 'image.height = "50px"\n' +
27 27 'display(image)\n' +
28 28 'print("Success")\n');
29 29 this.execute_cell_then(image_index, function(index){
30 30 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
31 31 'Create image executed with correct output.');
32 32 });
33 33
34 34 // Wait for the widget to actually display.
35 35 var img_selector = '.widget-area .widget-subarea img';
36 36 this.wait_for_element(image_index, img_selector);
37 37
38 38 this.then(function(){
39 39 this.test.assert(this.cell_element_exists(image_index,
40 40 '.widget-area .widget-subarea'),
41 41 'Widget subarea exists.');
42 42
43 43 this.test.assert(this.cell_element_exists(image_index, img_selector), 'Image exists.');
44 44
45 45 // Verify that the image's base64 data has made it into the DOM.
46 46 var img_src = this.cell_element_function(image_index, img_selector, 'attr', ['src']);
47 47 this.test.assert(img_src.indexOf(test_jpg) > -1, 'Image src data exists.');
48 48 });
49 49 });
@@ -1,177 +1,177
1 1 // Test widget int class
2 2 casper.notebook_test(function () {
3 3 var int_text = {};
4 4 int_text.query = '.widget-area .widget-subarea .my-second-int-text input';
5 5 int_text.index = this.append_cell(
6 'from IPython.html import widgets\n' +
6 'from jupyter_notebook import widgets\n' +
7 7 'from IPython.display import display, clear_output\n' +
8 8 'int_widget = widgets.IntText()\n' +
9 9 'display(int_widget)\n' +
10 10 'int_widget._dom_classes = ["my-second-int-text"]\n' +
11 11 'print(int_widget.model_id)\n');
12 12 this.execute_cell_then(int_text.index, function(index){
13 13 int_text.model_id = this.get_output_cell(index).text.trim();
14 14 });
15 15
16 16 // Wait for the widget to actually display.
17 17 this.wait_for_element(int_text.index, int_text.query);
18 18
19 19 // Continue with the tests.
20 20 this.then(function() {
21 21 this.test.assert(this.cell_element_exists(int_text.index,
22 22 '.widget-area .widget-subarea'),
23 23 'Widget subarea exists.');
24 24
25 25 this.test.assert(this.cell_element_exists(int_text.index, int_text.query),
26 26 'Widget int textbox exists.');
27 27
28 28 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
29 29 this.sendKeys(int_text.query, '1.05');
30 30 });
31 31
32 32 this.wait_for_widget(int_text);
33 33
34 34 index = this.append_cell('print(int_widget.value)\n');
35 35 this.execute_cell_then(index, function(index){
36 36 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
37 37 'Int textbox value set.');
38 38 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
39 39 this.sendKeys(int_text.query, '123456789');
40 40 });
41 41
42 42 this.wait_for_widget(int_text);
43 43
44 44 index = this.append_cell('print(int_widget.value)\n');
45 45 this.execute_cell_then(index, function(index){
46 46 this.test.assertEquals(this.get_output_cell(index).text, '123456789\n',
47 47 'Long int textbox value set (probably triggers throttling).');
48 48 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
49 49 this.sendKeys(int_text.query, '12hello');
50 50 });
51 51
52 52 this.wait_for_widget(int_text);
53 53
54 54 index = this.append_cell('print(int_widget.value)\n');
55 55 this.execute_cell_then(index, function(index){
56 56 this.test.assertEquals(this.get_output_cell(index).text, '12\n',
57 57 'Invald int textbox value caught and filtered.');
58 58 });
59 59
60 60 var slider_query = '.widget-area .widget-subarea .slider';
61 61 var int_text2 = {};
62 62 int_text2.query = '.widget-area .widget-subarea .my-second-num-test-text input';
63 63 int_text2.index = this.append_cell(
64 64 'intrange = [widgets.BoundedIntTextWidget(),\n' +
65 65 ' widgets.IntSliderWidget()]\n' +
66 66 '[display(intrange[i]) for i in range(2)]\n' +
67 67 'intrange[0]._dom_classes = ["my-second-num-test-text"]\n' +
68 68 'print(intrange[0].model_id)\n');
69 69 this.execute_cell_then(int_text2.index, function(index){
70 70 int_text2.model_id = this.get_output_cell(index).text.trim();
71 71 });
72 72
73 73 // Wait for the widgets to actually display.
74 74 this.wait_for_element(int_text2.index, int_text2.query);
75 75 this.wait_for_element(int_text2.index, slider_query);
76 76
77 77 // Continue with the tests.
78 78 this.then(function(){
79 79 this.test.assert(this.cell_element_exists(int_text2.index,
80 80 '.widget-area .widget-subarea'),
81 81 'Widget subarea exists.');
82 82
83 83 this.test.assert(this.cell_element_exists(int_text2.index, slider_query),
84 84 'Widget slider exists.');
85 85
86 86 this.test.assert(this.cell_element_exists(int_text2.index, int_text2.query),
87 87 'Widget int textbox exists.');
88 88 });
89 89
90 90 index = this.append_cell(
91 91 'for widget in intrange:\n' +
92 92 ' widget.max = 50\n' +
93 93 ' widget.min = -50\n' +
94 94 ' widget.value = 25\n' +
95 95 'print("Success")\n');
96 96 this.execute_cell_then(index, function(index){
97 97
98 98 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
99 99 'Int range properties cell executed with correct output.');
100 100
101 101 this.test.assert(this.cell_element_exists(int_text2.index, slider_query),
102 102 'Widget slider exists.');
103 103
104 104 this.test.assert(this.cell_element_function(int_text2.index, slider_query,
105 105 'slider', ['value']) == 25,
106 106 'Slider set to Python value.');
107 107
108 108 this.test.assert(this.cell_element_function(int_text2.index, int_text2.query,
109 109 'val') == 25, 'Int textbox set to Python value.');
110 110
111 111 // Clear the int textbox value and then set it to 1 by emulating
112 112 // keyboard presses.
113 113 this.evaluate(function(q){
114 114 var textbox = IPython.notebook.element.find(q);
115 115 textbox.val('1');
116 116 textbox.trigger('keyup');
117 117 }, {q: int_text2.query});
118 118 });
119 119
120 120 this.wait_for_widget(int_text2);
121 121
122 122 index = this.append_cell('print(intrange[0].value)\n');
123 123 this.execute_cell_then(index, function(index){
124 124 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
125 125 'Int textbox set int range value');
126 126
127 127 // Clear the int textbox value and then set it to 120 by emulating
128 128 // keyboard presses.
129 129 this.evaluate(function(q){
130 130 var textbox = IPython.notebook.element.find(q);
131 131 textbox.val('120');
132 132 textbox.trigger('keyup');
133 133 }, {q: int_text2.query});
134 134 });
135 135
136 136 this.wait_for_widget(int_text2);
137 137
138 138 index = this.append_cell('print(intrange[0].value)\n');
139 139 this.execute_cell_then(index, function(index){
140 140 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
141 141 'Int textbox value bound');
142 142
143 143 // Clear the int textbox value and then set it to 'hello world' by
144 144 // emulating keyboard presses. 'hello world' should get filtered...
145 145 this.evaluate(function(q){
146 146 var textbox = IPython.notebook.element.find(q);
147 147 textbox.val('hello world');
148 148 textbox.trigger('keyup');
149 149 }, {q: int_text2.query});
150 150 });
151 151
152 152 this.wait_for_widget(int_text2);
153 153
154 154 index = this.append_cell('print(intrange[0].value)\n');
155 155 this.execute_cell_then(index, function(index){
156 156 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
157 157 'Invalid int textbox characters ignored');
158 158 });
159 159
160 160 index = this.append_cell(
161 161 'a = widgets.IntSlider()\n' +
162 162 'display(a)\n' +
163 163 'a.max = -1\n' +
164 164 'print("Success")\n');
165 165 this.execute_cell_then(index, function(index){
166 166 this.test.assertEquals(0, 0, 'Invalid int range max bound does not cause crash.');
167 167 }, true);
168 168
169 169 index = this.append_cell(
170 170 'a = widgets.IntSlider()\n' +
171 171 'display(a)\n' +
172 172 'a.min = 101\n' +
173 173 'print("Success")\n');
174 174 this.execute_cell_then(index, function(index){
175 175 this.test.assertEquals(0, 0, 'Invalid int range min bound does not cause crash.');
176 176 }, true);
177 177 }); No newline at end of file
@@ -1,148 +1,148
1 1 // Test selection class
2 2 casper.notebook_test(function () {
3 3 index = this.append_cell(
4 'from IPython.html import widgets\n' +
4 'from jupyter_notebook import widgets\n' +
5 5 'from IPython.display import display, clear_output\n' +
6 6 'print("Success")');
7 7 this.execute_cell_then(index);
8 8
9 9 var combo_selector = '.widget-area .widget-subarea .widget-hbox .btn-group .widget-combo-btn';
10 10 var multibtn_selector = '.widget-area .widget-subarea .widget-hbox.widget-toggle-buttons .btn-group';
11 11 var radio_selector = '.widget-area .widget-subarea .widget-hbox .widget-radio-box';
12 12 var list_selector = '.widget-area .widget-subarea .widget-hbox .widget-listbox';
13 13
14 14 var selection_index;
15 15 var selection_values = 'abcd';
16 16 var check_state = function(context, index, state){
17 17 if (0 <= index && index < selection_values.length) {
18 18 var multibtn_state = context.cell_element_function(selection_index, multibtn_selector + ' .btn:nth-child(' + (index + 1) + ')', 'hasClass', ['active']);
19 19 var radio_state = context.cell_element_function(selection_index, radio_selector + ' .radio:nth-child(' + (index + 1) + ') input', 'prop', ['checked']);
20 20 var list_val = context.cell_element_function(selection_index, list_selector, 'val');
21 21 var combo_val = context.cell_element_function(selection_index, combo_selector, 'html');
22 22
23 23 var val = selection_values.charAt(index);
24 24 var list_state = (val == list_val);
25 25 var combo_state = (val == combo_val);
26 26
27 27 return multibtn_state == state &&
28 28 radio_state == state &&
29 29 list_state == state &&
30 30 combo_state == state;
31 31 }
32 32 return true;
33 33 };
34 34
35 35 var verify_selection = function(context, index){
36 36 for (var i = 0; i < selection_values.length; i++) {
37 37 if (!check_state(context, i, i==index)) {
38 38 return false;
39 39 }
40 40 }
41 41 return true;
42 42 };
43 43
44 44 //values=["' + selection_values + '"[i] for i in range(4)]
45 45 selection_index = this.append_cell(
46 46 'options=["' + selection_values + '"[i] for i in range(4)]\n' +
47 47 'selection = [widgets.Dropdown(options=options),\n' +
48 48 ' widgets.ToggleButtons(options=options),\n' +
49 49 ' widgets.RadioButtons(options=options),\n' +
50 50 ' widgets.Select(options=options)]\n' +
51 51 '[display(selection[i]) for i in range(4)]\n' +
52 52 'for widget in selection:\n' +
53 53 ' def handle_change(name,old,new):\n' +
54 54 ' for other_widget in selection:\n' +
55 55 ' other_widget.value = new\n' +
56 56 ' widget.on_trait_change(handle_change, "value")\n' +
57 57 'print("Success")\n');
58 58 this.execute_cell_then(selection_index, function(index){
59 59 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
60 60 'Create selection cell executed with correct output.');
61 61 });
62 62
63 63 // Wait for the widgets to actually display.
64 64 this.wait_for_element(selection_index, combo_selector);
65 65 this.wait_for_element(selection_index, multibtn_selector);
66 66 this.wait_for_element(selection_index, radio_selector);
67 67 this.wait_for_element(selection_index, list_selector);
68 68
69 69 // Continue with the tests.
70 70 this.then(function() {
71 71 this.test.assert(this.cell_element_exists(selection_index,
72 72 '.widget-area .widget-subarea'),
73 73 'Widget subarea exists.');
74 74
75 75 this.test.assert(this.cell_element_exists(selection_index, combo_selector),
76 76 'Widget combobox exists.');
77 77
78 78 this.test.assert(this.cell_element_exists(selection_index, multibtn_selector),
79 79 'Widget multibutton exists.');
80 80
81 81 this.test.assert(this.cell_element_exists(selection_index, radio_selector),
82 82 'Widget radio buttons exists.');
83 83
84 84 this.test.assert(this.cell_element_exists(selection_index, list_selector),
85 85 'Widget list exists.');
86 86
87 87 // Verify that no items are selected.
88 88 this.test.assert(verify_selection(this, 0), 'Default first item selected.');
89 89 });
90 90
91 91 index = this.append_cell(
92 92 'for widget in selection:\n' +
93 93 ' widget.value = "a"\n' +
94 94 'print("Success")\n');
95 95 this.execute_cell_then(index, function(index){
96 96 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
97 97 'Python select item executed with correct output.');
98 98
99 99 // Verify that the first item is selected.
100 100 this.test.assert(verify_selection(this, 0), 'Python selected');
101 101
102 102 // Verify that selecting a radio button updates all of the others.
103 103 this.cell_element_function(selection_index, radio_selector + ' .radio:nth-child(2) input', 'click');
104 104 });
105 105 this.wait_for_idle();
106 106 this.then(function () {
107 107 this.test.assert(verify_selection(this, 1), 'Radio button selection updated view states correctly.');
108 108
109 109 // Verify that selecting a list option updates all of the others.
110 110 this.cell_element_function(selection_index, list_selector + ' option:nth-child(3)', 'click');
111 111 });
112 112 this.wait_for_idle();
113 113 this.then(function () {
114 114 this.test.assert(verify_selection(this, 2), 'List selection updated view states correctly.');
115 115
116 116 // Verify that selecting a multibutton option updates all of the others.
117 117 // Bootstrap3 has changed the toggle button group behavior. Two clicks
118 118 // are required to actually select an item.
119 119 this.cell_element_function(selection_index, multibtn_selector + ' .btn:nth-child(4)', 'click');
120 120 this.cell_element_function(selection_index, multibtn_selector + ' .btn:nth-child(4)', 'click');
121 121 });
122 122 this.wait_for_idle();
123 123 this.then(function () {
124 124 this.test.assert(verify_selection(this, 3), 'Multibutton selection updated view states correctly.');
125 125
126 126 // Verify that selecting a combobox option updates all of the others.
127 127 this.cell_element_function(selection_index, '.widget-area .widget-subarea .widget-hbox .btn-group ul.dropdown-menu li:nth-child(3) a', 'click');
128 128 });
129 129 this.wait_for_idle();
130 130 this.then(function () {
131 131 this.test.assert(verify_selection(this, 2), 'Combobox selection updated view states correctly.');
132 132 });
133 133
134 134 this.wait_for_idle();
135 135
136 136 index = this.append_cell(
137 137 'from copy import copy\n' +
138 138 'for widget in selection:\n' +
139 139 ' d = copy(widget.options)\n' +
140 140 ' d.append("z")\n' +
141 141 ' widget.options = d\n' +
142 142 'selection[0].value = "z"');
143 143 this.execute_cell_then(index, function(index){
144 144
145 145 // Verify that selecting a combobox option updates all of the others.
146 146 this.test.assert(verify_selection(this, 4), 'Item added to selection widget.');
147 147 });
148 148 }); No newline at end of file
@@ -1,120 +1,120
1 1 // Test multicontainer class
2 2 casper.notebook_test(function () {
3 3 index = this.append_cell(
4 'from IPython.html import widgets\n' +
4 'from jupyter_notebook import widgets\n' +
5 5 'from IPython.display import display, clear_output\n' +
6 6 'print("Success")');
7 7 this.execute_cell_then(index);
8 8
9 9 // Test tab view
10 10 var multicontainer1_query = '.widget-area .widget-subarea div div.nav-tabs';
11 11 var multicontainer1_index = this.append_cell(
12 12 'multicontainer = widgets.Tab()\n' +
13 13 'page1 = widgets.Text()\n' +
14 14 'page2 = widgets.Text()\n' +
15 15 'page3 = widgets.Text()\n' +
16 16 'multicontainer.children = [page1, page2, page3]\n' +
17 17 'display(multicontainer)\n' +
18 18 'multicontainer.selected_index = 0\n' +
19 19 'print("Success")\n');
20 20 this.execute_cell_then(multicontainer1_index, function(index){
21 21 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
22 22 'Create multicontainer cell executed with correct output. (1)');
23 23 });
24 24
25 25 // Wait for the widget to actually display.
26 26 this.wait_for_element(multicontainer1_index, multicontainer1_query);
27 27
28 28 // Continue with the tests.
29 29 this.then(function() {
30 30 this.test.assert(this.cell_element_exists(multicontainer1_index,
31 31 '.widget-area .widget-subarea'),
32 32 'Widget subarea exists.');
33 33
34 34 this.test.assert(this.cell_element_exists(multicontainer1_index, multicontainer1_query),
35 35 'Widget tab list exists.');
36 36
37 37 // JQuery selector is 1 based
38 38 this.click(multicontainer1_query + ' li:nth-child(2) a');
39 39 });
40 40
41 41 this.wait_for_idle();
42 42
43 43 index = this.append_cell(
44 44 'print(multicontainer.selected_index)\n' +
45 45 'multicontainer.selected_index = 2'); // 0 based
46 46 this.execute_cell_then(index, function(index){
47 47 this.test.assertEquals(this.get_output_cell(index).text, '1\n', // 0 based
48 48 'selected_index property updated with tab change.');
49 49
50 50 // JQuery selector is 1 based
51 51 this.test.assert(!this.cell_element_function(multicontainer1_index, multicontainer1_query + ' li:nth-child(1)', 'hasClass', ['active']),
52 52 "Tab 1 is not selected.");
53 53 this.test.assert(!this.cell_element_function(multicontainer1_index, multicontainer1_query + ' li:nth-child(2)', 'hasClass', ['active']),
54 54 "Tab 2 is not selected.");
55 55 this.test.assert(this.cell_element_function(multicontainer1_index, multicontainer1_query + ' li:nth-child(3)', 'hasClass', ['active']),
56 56 "Tab 3 is selected.");
57 57 });
58 58
59 59 index = this.append_cell('multicontainer.set_title(1, "hello")\nprint("Success")'); // 0 based
60 60 this.execute_cell_then(index, function(index){
61 61 this.test.assert(this.cell_element_function(multicontainer1_index, multicontainer1_query +
62 62 ' li:nth-child(2) a', 'html') == 'hello',
63 63 'Tab page title set (after display).');
64 64 });
65 65
66 66 // Test accordion view
67 67 var multicontainer2_query = '.widget-area .widget-subarea .panel-group';
68 68 var multicontainer2_index = this.append_cell(
69 69 'multicontainer = widgets.Accordion()\n' +
70 70 'page1 = widgets.Text()\n' +
71 71 'page2 = widgets.Text()\n' +
72 72 'page3 = widgets.Text()\n' +
73 73 'multicontainer.children = [page1, page2, page3]\n' +
74 74 'multicontainer.set_title(2, "good")\n' +
75 75 'display(multicontainer)\n' +
76 76 'multicontainer.selected_index = 0\n' +
77 77 'print("Success")\n');
78 78 this.execute_cell_then(multicontainer2_index, function(index){
79 79 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
80 80 'Create multicontainer cell executed with correct output. (2)');
81 81 });
82 82
83 83 // Wait for the widget to actually display.
84 84 this.wait_for_element(multicontainer2_index, multicontainer2_query);
85 85
86 86 // Continue with the tests.
87 87 this.then(function() {
88 88 this.test.assert(this.cell_element_exists(multicontainer2_index,
89 89 '.widget-area .widget-subarea'),
90 90 'Widget subarea exists.');
91 91
92 92 this.test.assert(this.cell_element_exists(multicontainer2_index, multicontainer2_query),
93 93 'Widget accordion exists.');
94 94
95 95 this.test.assert(this.cell_element_exists(multicontainer2_index, multicontainer2_query +
96 96 ' .panel:nth-child(1) .panel-collapse'),
97 97 'First accordion page exists.');
98 98
99 99 // JQuery selector is 1 based
100 100 this.test.assert(this.cell_element_function(multicontainer2_index, multicontainer2_query +
101 101 ' .panel.panel-default:nth-child(3) .panel-heading .accordion-toggle',
102 102 'html')=='good', 'Accordion page title set (before display).');
103 103
104 104 // JQuery selector is 1 based
105 105 this.click(multicontainer2_query + ' .panel:nth-child(2) .panel-heading .accordion-toggle');
106 106 });
107 107
108 108 this.wait_for_idle();
109 109
110 110 index = this.append_cell('print(multicontainer.selected_index)'); // 0 based
111 111 this.execute_cell_then(index, function(index){
112 112 this.test.assertEquals(this.get_output_cell(index).text, '1\n', // 0 based
113 113 'selected_index property updated with tab change.');
114 114
115 115 var is_collapsed = this.evaluate(function(s){
116 116 return $(s + ' div.panel:nth-child(2) a').hasClass('collapsed'); // 1 based
117 117 }, {s: multicontainer2_query});
118 118 this.test.assertEquals(is_collapsed, false, 'Was tab actually opened?');
119 119 });
120 120 }); No newline at end of file
@@ -1,59 +1,59
1 1 // Test widget string class
2 2 casper.notebook_test(function () {
3 3 var string_index = this.append_cell(
4 'from IPython.html import widgets\n' +
4 'from jupyter_notebook import widgets\n' +
5 5 'from IPython.display import display, clear_output\n' +
6 6 'string_widget = [widgets.Text(value = "xyz", placeholder = "abc"),\n' +
7 7 ' widgets.Textarea(value = "xyz", placeholder = "def"),\n' +
8 8 ' widgets.HTML(value = "xyz"),\n' +
9 9 ' widgets.Latex(value = "$\\\\LaTeX{}$")]\n' +
10 10 '[display(widget) for widget in string_widget]\n'+
11 11 'print("Success")');
12 12 this.execute_cell_then(string_index, function(index){
13 13 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
14 14 'Create string widget cell executed with correct output.');
15 15 });
16 16
17 17 // Wait for the widget to actually display.
18 18 var textbox_selector = '.widget-area .widget-subarea .widget-hbox input[type=text]';
19 19 var textarea_selector = '.widget-area .widget-subarea .widget-hbox textarea';
20 20 var latex_selector = '.widget-area .widget-subarea div span.MathJax_Preview';
21 21 this.wait_for_element(string_index, textbox_selector);
22 22 this.wait_for_element(string_index, textarea_selector);
23 23 this.wait_for_element(string_index, latex_selector);
24 24
25 25 // Continue with the tests.
26 26 this.then(function(){
27 27 this.test.assert(this.cell_element_exists(string_index,
28 28 '.widget-area .widget-subarea'),
29 29 'Widget subarea exists.');
30 30
31 31 this.test.assert(this.cell_element_exists(string_index,
32 32 textbox_selector),
33 33 'Textbox exists.');
34 34
35 35 this.test.assert(this.cell_element_exists(string_index,
36 36 textarea_selector),
37 37 'Textarea exists.');
38 38
39 39 this.test.assert(this.cell_element_function(string_index,
40 40 textarea_selector, 'val')=='xyz',
41 41 'Python set textarea value.');
42 42
43 43 this.test.assert(this.cell_element_function(string_index,
44 44 textbox_selector, 'val')=='xyz',
45 45 'Python set textbox value.');
46 46
47 47 this.test.assert(this.cell_element_exists(string_index,
48 48 latex_selector),
49 49 'MathJax parsed the LaTeX successfully.');
50 50
51 51 this.test.assert(this.cell_element_function(string_index,
52 52 textarea_selector, 'attr', ['placeholder'])=='def',
53 53 'Python set textarea placeholder.');
54 54
55 55 this.test.assert(this.cell_element_function(string_index,
56 56 textbox_selector, 'attr', ['placeholder'])=='abc',
57 57 'Python set textbox placehoder.');
58 58 });
59 59 });
@@ -1,32 +1,32
1 1 """Test the /tree handlers"""
2 2 import os
3 3 import io
4 from IPython.html.utils import url_path_join
4 from jupyter_notebook.utils import url_path_join
5 5 from IPython.nbformat import write
6 6 from IPython.nbformat.v4 import new_notebook
7 7
8 8 import requests
9 9
10 from IPython.html.tests.launchnotebook import NotebookTestBase
10 from jupyter_notebook.tests.launchnotebook import NotebookTestBase
11 11
12 12 class TreeTest(NotebookTestBase):
13 13 def setUp(self):
14 14 nbdir = self.notebook_dir.name
15 15 d = os.path.join(nbdir, 'foo')
16 16 os.mkdir(d)
17 17
18 18 with io.open(os.path.join(d, 'bar.ipynb'), 'w', encoding='utf-8') as f:
19 19 nb = new_notebook()
20 20 write(nb, f, version=4)
21 21
22 22 with io.open(os.path.join(d, 'baz.txt'), 'w', encoding='utf-8') as f:
23 23 f.write(u'flamingo')
24 24
25 25 self.base_url()
26 26
27 27 def test_redirect(self):
28 28 r = requests.get(url_path_join(self.base_url(), 'tree/foo/bar.ipynb'))
29 29 self.assertEqual(r.url, self.base_url() + 'notebooks/foo/bar.ipynb')
30 30
31 31 r = requests.get(url_path_join(self.base_url(), 'tree/foo/baz.txt'))
32 32 self.assertEqual(r.url, url_path_join(self.base_url(), 'files/foo/baz.txt'))
@@ -1,40 +1,40
1 1 from .widget import Widget, DOMWidget, CallbackDispatcher, register, widget_serialization
2 2
3 3 from .trait_types import Color, EventfulDict, EventfulList
4 4
5 5 from .widget_bool import Checkbox, ToggleButton, Valid
6 6 from .widget_button import Button
7 7 from .widget_box import Box, FlexBox, HBox, VBox
8 8 from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress, FloatRangeSlider
9 9 from .widget_image import Image
10 10 from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider
11 11 from .widget_output import Output
12 12 from .widget_selection import RadioButtons, ToggleButtons, Dropdown, Select, SelectMultiple
13 13 from .widget_selectioncontainer import Tab, Accordion
14 14 from .widget_string import HTML, Latex, Text, Textarea
15 15 from .interaction import interact, interactive, fixed, interact_manual
16 16 from .widget_link import jslink, jsdlink
17 17
18 18 # Deprecated classes
19 19 from .widget_bool import CheckboxWidget, ToggleButtonWidget
20 20 from .widget_button import ButtonWidget
21 21 from .widget_box import ContainerWidget
22 22 from .widget_float import FloatTextWidget, BoundedFloatTextWidget, FloatSliderWidget, FloatProgressWidget
23 23 from .widget_image import ImageWidget
24 24 from .widget_int import IntTextWidget, BoundedIntTextWidget, IntSliderWidget, IntProgressWidget
25 25 from .widget_selection import RadioButtonsWidget, ToggleButtonsWidget, DropdownWidget, SelectWidget
26 26 from .widget_selectioncontainer import TabWidget, AccordionWidget
27 27 from .widget_string import HTMLWidget, LatexWidget, TextWidget, TextareaWidget
28 28
29 29 # We use warn_explicit so we have very brief messages without file or line numbers.
30 30 # The concern is that file or line numbers will confuse the interactive user.
31 31 # To ignore this warning, do:
32 32 #
33 33 # from warnings import filterwarnings
34 # filterwarnings('ignore', module='IPython.html.widgets')
34 # filterwarnings('ignore', module='jupyter_notebook.widgets')
35 35
36 36 from warnings import warn_explicit
37 37 __warningregistry__ = {}
38 38 warn_explicit("IPython widgets are experimental and may change in the future.",
39 FutureWarning, '', 0, module = 'IPython.html.widgets',
39 FutureWarning, '', 0, module = 'jupyter_notebook.widgets',
40 40 registry = __warningregistry__, module_globals = globals)
@@ -1,344 +1,344
1 1 """Interact with functions using widgets."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import print_function
7 7
8 8 try: # Python >= 3.3
9 9 from inspect import signature, Parameter
10 10 except ImportError:
11 11 from IPython.utils.signatures import signature, Parameter
12 12 from inspect import getcallargs
13 13
14 14 from IPython.core.getipython import get_ipython
15 from IPython.html.widgets import (Widget, Text,
15 from jupyter_notebook.widgets import (Widget, Text,
16 16 FloatSlider, IntSlider, Checkbox, Dropdown,
17 17 Box, Button, DOMWidget)
18 18 from IPython.display import display, clear_output
19 19 from IPython.utils.py3compat import string_types, unicode_type
20 20 from IPython.utils.traitlets import HasTraits, Any, Unicode
21 21
22 22 empty = Parameter.empty
23 23
24 24
25 25 def _matches(o, pattern):
26 26 """Match a pattern of types in a sequence."""
27 27 if not len(o) == len(pattern):
28 28 return False
29 29 comps = zip(o,pattern)
30 30 return all(isinstance(obj,kind) for obj,kind in comps)
31 31
32 32
33 33 def _get_min_max_value(min, max, value=None, step=None):
34 34 """Return min, max, value given input values with possible None."""
35 35 if value is None:
36 36 if not max > min:
37 37 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
38 38 value = min + abs(min-max)/2
39 39 value = type(min)(value)
40 40 elif min is None and max is None:
41 41 if value == 0.0:
42 42 min, max, value = 0.0, 1.0, 0.5
43 43 elif value == 0:
44 44 min, max, value = 0, 1, 0
45 45 elif isinstance(value, (int, float)):
46 46 min, max = (-value, 3*value) if value > 0 else (3*value, -value)
47 47 else:
48 48 raise TypeError('expected a number, got: %r' % value)
49 49 else:
50 50 raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
51 51 if step is not None:
52 52 # ensure value is on a step
53 53 r = (value - min) % step
54 54 value = value - r
55 55 return min, max, value
56 56
57 57 def _widget_abbrev_single_value(o):
58 58 """Make widgets from single values, which can be used as parameter defaults."""
59 59 if isinstance(o, string_types):
60 60 return Text(value=unicode_type(o))
61 61 elif isinstance(o, dict):
62 62 return Dropdown(options=o)
63 63 elif isinstance(o, bool):
64 64 return Checkbox(value=o)
65 65 elif isinstance(o, float):
66 66 min, max, value = _get_min_max_value(None, None, o)
67 67 return FloatSlider(value=o, min=min, max=max)
68 68 elif isinstance(o, int):
69 69 min, max, value = _get_min_max_value(None, None, o)
70 70 return IntSlider(value=o, min=min, max=max)
71 71 else:
72 72 return None
73 73
74 74 def _widget_abbrev(o):
75 75 """Make widgets from abbreviations: single values, lists or tuples."""
76 76 float_or_int = (float, int)
77 77 if isinstance(o, (list, tuple)):
78 78 if o and all(isinstance(x, string_types) for x in o):
79 79 return Dropdown(options=[unicode_type(k) for k in o])
80 80 elif _matches(o, (float_or_int, float_or_int)):
81 81 min, max, value = _get_min_max_value(o[0], o[1])
82 82 if all(isinstance(_, int) for _ in o):
83 83 cls = IntSlider
84 84 else:
85 85 cls = FloatSlider
86 86 return cls(value=value, min=min, max=max)
87 87 elif _matches(o, (float_or_int, float_or_int, float_or_int)):
88 88 step = o[2]
89 89 if step <= 0:
90 90 raise ValueError("step must be >= 0, not %r" % step)
91 91 min, max, value = _get_min_max_value(o[0], o[1], step=step)
92 92 if all(isinstance(_, int) for _ in o):
93 93 cls = IntSlider
94 94 else:
95 95 cls = FloatSlider
96 96 return cls(value=value, min=min, max=max, step=step)
97 97 else:
98 98 return _widget_abbrev_single_value(o)
99 99
100 100 def _widget_from_abbrev(abbrev, default=empty):
101 101 """Build a Widget instance given an abbreviation or Widget."""
102 102 if isinstance(abbrev, Widget) or isinstance(abbrev, fixed):
103 103 return abbrev
104 104
105 105 widget = _widget_abbrev(abbrev)
106 106 if default is not empty and isinstance(abbrev, (list, tuple, dict)):
107 107 # if it's not a single-value abbreviation,
108 108 # set the initial value from the default
109 109 try:
110 110 widget.value = default
111 111 except Exception:
112 112 # ignore failure to set default
113 113 pass
114 114 if widget is None:
115 115 raise ValueError("%r cannot be transformed to a Widget" % (abbrev,))
116 116 return widget
117 117
118 118 def _yield_abbreviations_for_parameter(param, kwargs):
119 119 """Get an abbreviation for a function parameter."""
120 120 name = param.name
121 121 kind = param.kind
122 122 ann = param.annotation
123 123 default = param.default
124 124 not_found = (name, empty, empty)
125 125 if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
126 126 if name in kwargs:
127 127 value = kwargs.pop(name)
128 128 elif ann is not empty:
129 129 value = ann
130 130 elif default is not empty:
131 131 value = default
132 132 else:
133 133 yield not_found
134 134 yield (name, value, default)
135 135 elif kind == Parameter.VAR_KEYWORD:
136 136 # In this case name=kwargs and we yield the items in kwargs with their keys.
137 137 for k, v in kwargs.copy().items():
138 138 kwargs.pop(k)
139 139 yield k, v, empty
140 140
141 141 def _find_abbreviations(f, kwargs):
142 142 """Find the abbreviations for a function and kwargs passed to interact."""
143 143 new_kwargs = []
144 144 for param in signature(f).parameters.values():
145 145 for name, value, default in _yield_abbreviations_for_parameter(param, kwargs):
146 146 if value is empty:
147 147 raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
148 148 new_kwargs.append((name, value, default))
149 149 return new_kwargs
150 150
151 151 def _widgets_from_abbreviations(seq):
152 152 """Given a sequence of (name, abbrev) tuples, return a sequence of Widgets."""
153 153 result = []
154 154 for name, abbrev, default in seq:
155 155 widget = _widget_from_abbrev(abbrev, default)
156 156 if not widget.description:
157 157 widget.description = name
158 158 widget._kwarg = name
159 159 result.append(widget)
160 160 return result
161 161
162 162 def interactive(__interact_f, **kwargs):
163 163 """
164 164 Builds a group of interactive widgets tied to a function and places the
165 165 group into a Box container.
166 166
167 167 Returns
168 168 -------
169 169 container : a Box instance containing multiple widgets
170 170
171 171 Parameters
172 172 ----------
173 173 __interact_f : function
174 174 The function to which the interactive widgets are tied. The `**kwargs`
175 175 should match the function signature.
176 176 **kwargs : various, optional
177 177 An interactive widget is created for each keyword argument that is a
178 178 valid widget abbreviation.
179 179 """
180 180 f = __interact_f
181 181 co = kwargs.pop('clear_output', True)
182 182 manual = kwargs.pop('__manual', False)
183 183 kwargs_widgets = []
184 184 container = Box(_dom_classes=['widget-interact'])
185 185 container.result = None
186 186 container.args = []
187 187 container.kwargs = dict()
188 188 kwargs = kwargs.copy()
189 189
190 190 new_kwargs = _find_abbreviations(f, kwargs)
191 191 # Before we proceed, let's make sure that the user has passed a set of args+kwargs
192 192 # that will lead to a valid call of the function. This protects against unspecified
193 193 # and doubly-specified arguments.
194 194 getcallargs(f, **{n:v for n,v,_ in new_kwargs})
195 195 # Now build the widgets from the abbreviations.
196 196 kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
197 197
198 198 # This has to be done as an assignment, not using container.children.append,
199 199 # so that traitlets notices the update. We skip any objects (such as fixed) that
200 200 # are not DOMWidgets.
201 201 c = [w for w in kwargs_widgets if isinstance(w, DOMWidget)]
202 202
203 203 # If we are only to run the function on demand, add a button to request this
204 204 if manual:
205 205 manual_button = Button(description="Run %s" % f.__name__)
206 206 c.append(manual_button)
207 207 container.children = c
208 208
209 209 # Build the callback
210 210 def call_f(name=None, old=None, new=None):
211 211 container.kwargs = {}
212 212 for widget in kwargs_widgets:
213 213 value = widget.value
214 214 container.kwargs[widget._kwarg] = value
215 215 if co:
216 216 clear_output(wait=True)
217 217 if manual:
218 218 manual_button.disabled = True
219 219 try:
220 220 container.result = f(**container.kwargs)
221 221 except Exception as e:
222 222 ip = get_ipython()
223 223 if ip is None:
224 224 container.log.warn("Exception in interact callback: %s", e, exc_info=True)
225 225 else:
226 226 ip.showtraceback()
227 227 finally:
228 228 if manual:
229 229 manual_button.disabled = False
230 230
231 231 # Wire up the widgets
232 232 # If we are doing manual running, the callback is only triggered by the button
233 233 # Otherwise, it is triggered for every trait change received
234 234 # On-demand running also suppresses running the function with the initial parameters
235 235 if manual:
236 236 manual_button.on_click(call_f)
237 237 else:
238 238 for widget in kwargs_widgets:
239 239 widget.on_trait_change(call_f, 'value')
240 240
241 241 container.on_displayed(lambda _: call_f(None, None, None))
242 242
243 243 return container
244 244
245 245 def interact(__interact_f=None, **kwargs):
246 246 """
247 247 Displays interactive widgets which are tied to a function.
248 248 Expects the first argument to be a function. Parameters to this function are
249 249 widget abbreviations passed in as keyword arguments (`**kwargs`). Can be used
250 250 as a decorator (see examples).
251 251
252 252 Returns
253 253 -------
254 254 f : __interact_f with interactive widget attached to it.
255 255
256 256 Parameters
257 257 ----------
258 258 __interact_f : function
259 259 The function to which the interactive widgets are tied. The `**kwargs`
260 260 should match the function signature. Passed to :func:`interactive()`
261 261 **kwargs : various, optional
262 262 An interactive widget is created for each keyword argument that is a
263 263 valid widget abbreviation. Passed to :func:`interactive()`
264 264
265 265 Examples
266 266 --------
267 267 Render an interactive text field that shows the greeting with the passed in
268 268 text::
269 269
270 270 # 1. Using interact as a function
271 271 def greeting(text="World"):
272 272 print "Hello {}".format(text)
273 273 interact(greeting, text="IPython Widgets")
274 274
275 275 # 2. Using interact as a decorator
276 276 @interact
277 277 def greeting(text="World"):
278 278 print "Hello {}".format(text)
279 279
280 280 # 3. Using interact as a decorator with named parameters
281 281 @interact(text="IPython Widgets")
282 282 def greeting(text="World"):
283 283 print "Hello {}".format(text)
284 284
285 285 Render an interactive slider widget and prints square of number::
286 286
287 287 # 1. Using interact as a function
288 288 def square(num=1):
289 289 print "{} squared is {}".format(num, num*num)
290 290 interact(square, num=5)
291 291
292 292 # 2. Using interact as a decorator
293 293 @interact
294 294 def square(num=2):
295 295 print "{} squared is {}".format(num, num*num)
296 296
297 297 # 3. Using interact as a decorator with named parameters
298 298 @interact(num=5)
299 299 def square(num=2):
300 300 print "{} squared is {}".format(num, num*num)
301 301 """
302 302 # positional arg support in: https://gist.github.com/8851331
303 303 if __interact_f is not None:
304 304 # This branch handles the cases 1 and 2
305 305 # 1. interact(f, **kwargs)
306 306 # 2. @interact
307 307 # def f(*args, **kwargs):
308 308 # ...
309 309 f = __interact_f
310 310 w = interactive(f, **kwargs)
311 311 try:
312 312 f.widget = w
313 313 except AttributeError:
314 314 # some things (instancemethods) can't have attributes attached,
315 315 # so wrap in a lambda
316 316 f = lambda *args, **kwargs: __interact_f(*args, **kwargs)
317 317 f.widget = w
318 318 display(w)
319 319 return f
320 320 else:
321 321 # This branch handles the case 3
322 322 # @interact(a=30, b=40)
323 323 # def f(*args, **kwargs):
324 324 # ...
325 325 def dec(f):
326 326 return interact(f, **kwargs)
327 327 return dec
328 328
329 329 def interact_manual(__interact_f=None, **kwargs):
330 330 """interact_manual(f, **kwargs)
331 331
332 332 As `interact()`, generates widgets for each argument, but rather than running
333 333 the function after each widget change, adds a "Run" button and waits for it
334 334 to be clicked. Useful if the function is long-running and has several
335 335 parameters to change.
336 336 """
337 337 return interact(__interact_f, __manual=True, **kwargs)
338 338
339 339 class fixed(HasTraits):
340 340 """A pseudo-widget whose value is fixed and never synced to the client."""
341 341 value = Any(help="Any Python object")
342 342 description = Unicode('', help="Any Python object")
343 343 def __init__(self, value, **kwargs):
344 344 super(fixed, self).__init__(value=value, **kwargs)
@@ -1,693 +1,693
1 1 """Test interact and interactive."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import print_function
7 7
8 8 try:
9 9 from unittest.mock import patch
10 10 except ImportError:
11 11 from mock import patch
12 12
13 13 import nose.tools as nt
14 14
15 15 from IPython.kernel.comm import Comm
16 from IPython.html import widgets
17 from IPython.html.widgets import interact, interactive, Widget, interaction
16 from jupyter_notebook import widgets
17 from jupyter_notebook.widgets import interact, interactive, Widget, interaction
18 18 from IPython.utils.py3compat import annotate
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Utility stuff
22 22 #-----------------------------------------------------------------------------
23 23
24 24 class DummyComm(Comm):
25 25 comm_id = 'a-b-c-d'
26 26
27 27 def open(self, *args, **kwargs):
28 28 pass
29 29
30 30 def send(self, *args, **kwargs):
31 31 pass
32 32
33 33 def close(self, *args, **kwargs):
34 34 pass
35 35
36 36 _widget_attrs = {}
37 37 displayed = []
38 38 undefined = object()
39 39
40 40 def setup():
41 41 _widget_attrs['_comm_default'] = getattr(Widget, '_comm_default', undefined)
42 42 Widget._comm_default = lambda self: DummyComm()
43 43 _widget_attrs['_ipython_display_'] = Widget._ipython_display_
44 44 def raise_not_implemented(*args, **kwargs):
45 45 raise NotImplementedError()
46 46 Widget._ipython_display_ = raise_not_implemented
47 47
48 48 def teardown():
49 49 for attr, value in _widget_attrs.items():
50 50 if value is undefined:
51 51 delattr(Widget, attr)
52 52 else:
53 53 setattr(Widget, attr, value)
54 54
55 55 def f(**kwargs):
56 56 pass
57 57
58 58 def clear_display():
59 59 global displayed
60 60 displayed = []
61 61
62 62 def record_display(*args):
63 63 displayed.extend(args)
64 64
65 65 #-----------------------------------------------------------------------------
66 66 # Actual tests
67 67 #-----------------------------------------------------------------------------
68 68
69 69 def check_widget(w, **d):
70 70 """Check a single widget against a dict"""
71 71 for attr, expected in d.items():
72 72 if attr == 'cls':
73 73 nt.assert_is(w.__class__, expected)
74 74 else:
75 75 value = getattr(w, attr)
76 76 nt.assert_equal(value, expected,
77 77 "%s.%s = %r != %r" % (w.__class__.__name__, attr, value, expected)
78 78 )
79 79
80 80 def check_widgets(container, **to_check):
81 81 """Check that widgets are created as expected"""
82 82 # build a widget dictionary, so it matches
83 83 widgets = {}
84 84 for w in container.children:
85 85 widgets[w.description] = w
86 86
87 87 for key, d in to_check.items():
88 88 nt.assert_in(key, widgets)
89 89 check_widget(widgets[key], **d)
90 90
91 91
92 92 def test_single_value_string():
93 93 a = u'hello'
94 94 c = interactive(f, a=a)
95 95 w = c.children[0]
96 96 check_widget(w,
97 97 cls=widgets.Text,
98 98 description='a',
99 99 value=a,
100 100 )
101 101
102 102 def test_single_value_bool():
103 103 for a in (True, False):
104 104 c = interactive(f, a=a)
105 105 w = c.children[0]
106 106 check_widget(w,
107 107 cls=widgets.Checkbox,
108 108 description='a',
109 109 value=a,
110 110 )
111 111
112 112 def test_single_value_dict():
113 113 for d in [
114 114 dict(a=5),
115 115 dict(a=5, b='b', c=dict),
116 116 ]:
117 117 c = interactive(f, d=d)
118 118 w = c.children[0]
119 119 check_widget(w,
120 120 cls=widgets.Dropdown,
121 121 description='d',
122 122 options=d,
123 123 value=next(iter(d.values())),
124 124 )
125 125
126 126 def test_single_value_float():
127 127 for a in (2.25, 1.0, -3.5):
128 128 c = interactive(f, a=a)
129 129 w = c.children[0]
130 130 check_widget(w,
131 131 cls=widgets.FloatSlider,
132 132 description='a',
133 133 value=a,
134 134 min= -a if a > 0 else 3*a,
135 135 max= 3*a if a > 0 else -a,
136 136 step=0.1,
137 137 readout=True,
138 138 )
139 139
140 140 def test_single_value_int():
141 141 for a in (1, 5, -3):
142 142 c = interactive(f, a=a)
143 143 nt.assert_equal(len(c.children), 1)
144 144 w = c.children[0]
145 145 check_widget(w,
146 146 cls=widgets.IntSlider,
147 147 description='a',
148 148 value=a,
149 149 min= -a if a > 0 else 3*a,
150 150 max= 3*a if a > 0 else -a,
151 151 step=1,
152 152 readout=True,
153 153 )
154 154
155 155 def test_list_tuple_2_int():
156 156 with nt.assert_raises(ValueError):
157 157 c = interactive(f, tup=(1,1))
158 158 with nt.assert_raises(ValueError):
159 159 c = interactive(f, tup=(1,-1))
160 160 for min, max in [ (0,1), (1,10), (1,2), (-5,5), (-20,-19) ]:
161 161 c = interactive(f, tup=(min, max), lis=[min, max])
162 162 nt.assert_equal(len(c.children), 2)
163 163 d = dict(
164 164 cls=widgets.IntSlider,
165 165 min=min,
166 166 max=max,
167 167 step=1,
168 168 readout=True,
169 169 )
170 170 check_widgets(c, tup=d, lis=d)
171 171
172 172 def test_list_tuple_3_int():
173 173 with nt.assert_raises(ValueError):
174 174 c = interactive(f, tup=(1,2,0))
175 175 with nt.assert_raises(ValueError):
176 176 c = interactive(f, tup=(1,2,-1))
177 177 for min, max, step in [ (0,2,1), (1,10,2), (1,100,2), (-5,5,4), (-100,-20,4) ]:
178 178 c = interactive(f, tup=(min, max, step), lis=[min, max, step])
179 179 nt.assert_equal(len(c.children), 2)
180 180 d = dict(
181 181 cls=widgets.IntSlider,
182 182 min=min,
183 183 max=max,
184 184 step=step,
185 185 readout=True,
186 186 )
187 187 check_widgets(c, tup=d, lis=d)
188 188
189 189 def test_list_tuple_2_float():
190 190 with nt.assert_raises(ValueError):
191 191 c = interactive(f, tup=(1.0,1.0))
192 192 with nt.assert_raises(ValueError):
193 193 c = interactive(f, tup=(0.5,-0.5))
194 194 for min, max in [ (0.5, 1.5), (1.1,10.2), (1,2.2), (-5.,5), (-20,-19.) ]:
195 195 c = interactive(f, tup=(min, max), lis=[min, max])
196 196 nt.assert_equal(len(c.children), 2)
197 197 d = dict(
198 198 cls=widgets.FloatSlider,
199 199 min=min,
200 200 max=max,
201 201 step=.1,
202 202 readout=True,
203 203 )
204 204 check_widgets(c, tup=d, lis=d)
205 205
206 206 def test_list_tuple_3_float():
207 207 with nt.assert_raises(ValueError):
208 208 c = interactive(f, tup=(1,2,0.0))
209 209 with nt.assert_raises(ValueError):
210 210 c = interactive(f, tup=(-1,-2,1.))
211 211 with nt.assert_raises(ValueError):
212 212 c = interactive(f, tup=(1,2.,-1.))
213 213 for min, max, step in [ (0.,2,1), (1,10.,2), (1,100,2.), (-5.,5.,4), (-100,-20.,4.) ]:
214 214 c = interactive(f, tup=(min, max, step), lis=[min, max, step])
215 215 nt.assert_equal(len(c.children), 2)
216 216 d = dict(
217 217 cls=widgets.FloatSlider,
218 218 min=min,
219 219 max=max,
220 220 step=step,
221 221 readout=True,
222 222 )
223 223 check_widgets(c, tup=d, lis=d)
224 224
225 225 def test_list_tuple_str():
226 226 values = ['hello', 'there', 'guy']
227 227 first = values[0]
228 228 c = interactive(f, tup=tuple(values), lis=list(values))
229 229 nt.assert_equal(len(c.children), 2)
230 230 d = dict(
231 231 cls=widgets.Dropdown,
232 232 value=first,
233 233 options=values
234 234 )
235 235 check_widgets(c, tup=d, lis=d)
236 236
237 237 def test_list_tuple_invalid():
238 238 for bad in [
239 239 (),
240 240 (5, 'hi'),
241 241 ('hi', 5),
242 242 ({},),
243 243 (None,),
244 244 ]:
245 245 with nt.assert_raises(ValueError):
246 246 print(bad) # because there is no custom message in assert_raises
247 247 c = interactive(f, tup=bad)
248 248
249 249 def test_defaults():
250 250 @annotate(n=10)
251 251 def f(n, f=4.5, g=1):
252 252 pass
253 253
254 254 c = interactive(f)
255 255 check_widgets(c,
256 256 n=dict(
257 257 cls=widgets.IntSlider,
258 258 value=10,
259 259 ),
260 260 f=dict(
261 261 cls=widgets.FloatSlider,
262 262 value=4.5,
263 263 ),
264 264 g=dict(
265 265 cls=widgets.IntSlider,
266 266 value=1,
267 267 ),
268 268 )
269 269
270 270 def test_default_values():
271 271 @annotate(n=10, f=(0, 10.), g=5, h={'a': 1, 'b': 2}, j=['hi', 'there'])
272 272 def f(n, f=4.5, g=1, h=2, j='there'):
273 273 pass
274 274
275 275 c = interactive(f)
276 276 check_widgets(c,
277 277 n=dict(
278 278 cls=widgets.IntSlider,
279 279 value=10,
280 280 ),
281 281 f=dict(
282 282 cls=widgets.FloatSlider,
283 283 value=4.5,
284 284 ),
285 285 g=dict(
286 286 cls=widgets.IntSlider,
287 287 value=5,
288 288 ),
289 289 h=dict(
290 290 cls=widgets.Dropdown,
291 291 options={'a': 1, 'b': 2},
292 292 value=2
293 293 ),
294 294 j=dict(
295 295 cls=widgets.Dropdown,
296 296 options=['hi', 'there'],
297 297 value='there'
298 298 ),
299 299 )
300 300
301 301 def test_default_out_of_bounds():
302 302 @annotate(f=(0, 10.), h={'a': 1}, j=['hi', 'there'])
303 303 def f(f='hi', h=5, j='other'):
304 304 pass
305 305
306 306 c = interactive(f)
307 307 check_widgets(c,
308 308 f=dict(
309 309 cls=widgets.FloatSlider,
310 310 value=5.,
311 311 ),
312 312 h=dict(
313 313 cls=widgets.Dropdown,
314 314 options={'a': 1},
315 315 value=1,
316 316 ),
317 317 j=dict(
318 318 cls=widgets.Dropdown,
319 319 options=['hi', 'there'],
320 320 value='hi',
321 321 ),
322 322 )
323 323
324 324 def test_annotations():
325 325 @annotate(n=10, f=widgets.FloatText())
326 326 def f(n, f):
327 327 pass
328 328
329 329 c = interactive(f)
330 330 check_widgets(c,
331 331 n=dict(
332 332 cls=widgets.IntSlider,
333 333 value=10,
334 334 ),
335 335 f=dict(
336 336 cls=widgets.FloatText,
337 337 ),
338 338 )
339 339
340 340 def test_priority():
341 341 @annotate(annotate='annotate', kwarg='annotate')
342 342 def f(kwarg='default', annotate='default', default='default'):
343 343 pass
344 344
345 345 c = interactive(f, kwarg='kwarg')
346 346 check_widgets(c,
347 347 kwarg=dict(
348 348 cls=widgets.Text,
349 349 value='kwarg',
350 350 ),
351 351 annotate=dict(
352 352 cls=widgets.Text,
353 353 value='annotate',
354 354 ),
355 355 )
356 356
357 357 @nt.with_setup(clear_display)
358 358 def test_decorator_kwarg():
359 359 with patch.object(interaction, 'display', record_display):
360 360 @interact(a=5)
361 361 def foo(a):
362 362 pass
363 363 nt.assert_equal(len(displayed), 1)
364 364 w = displayed[0].children[0]
365 365 check_widget(w,
366 366 cls=widgets.IntSlider,
367 367 value=5,
368 368 )
369 369
370 370 @nt.with_setup(clear_display)
371 371 def test_interact_instancemethod():
372 372 class Foo(object):
373 373 def show(self, x):
374 374 print(x)
375 375
376 376 f = Foo()
377 377
378 378 with patch.object(interaction, 'display', record_display):
379 379 g = interact(f.show, x=(1,10))
380 380 nt.assert_equal(len(displayed), 1)
381 381 w = displayed[0].children[0]
382 382 check_widget(w,
383 383 cls=widgets.IntSlider,
384 384 value=5,
385 385 )
386 386
387 387 @nt.with_setup(clear_display)
388 388 def test_decorator_no_call():
389 389 with patch.object(interaction, 'display', record_display):
390 390 @interact
391 391 def foo(a='default'):
392 392 pass
393 393 nt.assert_equal(len(displayed), 1)
394 394 w = displayed[0].children[0]
395 395 check_widget(w,
396 396 cls=widgets.Text,
397 397 value='default',
398 398 )
399 399
400 400 @nt.with_setup(clear_display)
401 401 def test_call_interact():
402 402 def foo(a='default'):
403 403 pass
404 404 with patch.object(interaction, 'display', record_display):
405 405 ifoo = interact(foo)
406 406 nt.assert_equal(len(displayed), 1)
407 407 w = displayed[0].children[0]
408 408 check_widget(w,
409 409 cls=widgets.Text,
410 410 value='default',
411 411 )
412 412
413 413 @nt.with_setup(clear_display)
414 414 def test_call_interact_kwargs():
415 415 def foo(a='default'):
416 416 pass
417 417 with patch.object(interaction, 'display', record_display):
418 418 ifoo = interact(foo, a=10)
419 419 nt.assert_equal(len(displayed), 1)
420 420 w = displayed[0].children[0]
421 421 check_widget(w,
422 422 cls=widgets.IntSlider,
423 423 value=10,
424 424 )
425 425
426 426 @nt.with_setup(clear_display)
427 427 def test_call_decorated_on_trait_change():
428 428 """test calling @interact decorated functions"""
429 429 d = {}
430 430 with patch.object(interaction, 'display', record_display):
431 431 @interact
432 432 def foo(a='default'):
433 433 d['a'] = a
434 434 return a
435 435 nt.assert_equal(len(displayed), 1)
436 436 w = displayed[0].children[0]
437 437 check_widget(w,
438 438 cls=widgets.Text,
439 439 value='default',
440 440 )
441 441 # test calling the function directly
442 442 a = foo('hello')
443 443 nt.assert_equal(a, 'hello')
444 444 nt.assert_equal(d['a'], 'hello')
445 445
446 446 # test that setting trait values calls the function
447 447 w.value = 'called'
448 448 nt.assert_equal(d['a'], 'called')
449 449
450 450 @nt.with_setup(clear_display)
451 451 def test_call_decorated_kwargs_on_trait_change():
452 452 """test calling @interact(foo=bar) decorated functions"""
453 453 d = {}
454 454 with patch.object(interaction, 'display', record_display):
455 455 @interact(a='kwarg')
456 456 def foo(a='default'):
457 457 d['a'] = a
458 458 return a
459 459 nt.assert_equal(len(displayed), 1)
460 460 w = displayed[0].children[0]
461 461 check_widget(w,
462 462 cls=widgets.Text,
463 463 value='kwarg',
464 464 )
465 465 # test calling the function directly
466 466 a = foo('hello')
467 467 nt.assert_equal(a, 'hello')
468 468 nt.assert_equal(d['a'], 'hello')
469 469
470 470 # test that setting trait values calls the function
471 471 w.value = 'called'
472 472 nt.assert_equal(d['a'], 'called')
473 473
474 474 def test_fixed():
475 475 c = interactive(f, a=widgets.fixed(5), b='text')
476 476 nt.assert_equal(len(c.children), 1)
477 477 w = c.children[0]
478 478 check_widget(w,
479 479 cls=widgets.Text,
480 480 value='text',
481 481 description='b',
482 482 )
483 483
484 484 def test_default_description():
485 485 c = interactive(f, b='text')
486 486 w = c.children[0]
487 487 check_widget(w,
488 488 cls=widgets.Text,
489 489 value='text',
490 490 description='b',
491 491 )
492 492
493 493 def test_custom_description():
494 494 d = {}
495 495 def record_kwargs(**kwargs):
496 496 d.clear()
497 497 d.update(kwargs)
498 498
499 499 c = interactive(record_kwargs, b=widgets.Text(value='text', description='foo'))
500 500 w = c.children[0]
501 501 check_widget(w,
502 502 cls=widgets.Text,
503 503 value='text',
504 504 description='foo',
505 505 )
506 506 w.value = 'different text'
507 507 nt.assert_equal(d, {'b': 'different text'})
508 508
509 509 def test_interact_manual_button():
510 510 c = interactive(f, __manual=True)
511 511 w = c.children[0]
512 512 check_widget(w, cls=widgets.Button)
513 513
514 514 def test_interact_manual_nocall():
515 515 callcount = 0
516 516 def calltest(testarg):
517 517 callcount += 1
518 518 c = interactive(calltest, testarg=5, __manual=True)
519 519 c.children[0].value = 10
520 520 nt.assert_equal(callcount, 0)
521 521
522 522 def test_int_range_logic():
523 523 irsw = widgets.IntRangeSlider
524 524 w = irsw(value=(2, 4), min=0, max=6)
525 525 check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
526 526 w.value = (4, 2)
527 527 check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
528 528 w.value = (-1, 7)
529 529 check_widget(w, cls=irsw, value=(0, 6), min=0, max=6)
530 530 w.min = 3
531 531 check_widget(w, cls=irsw, value=(3, 6), min=3, max=6)
532 532 w.max = 3
533 533 check_widget(w, cls=irsw, value=(3, 3), min=3, max=3)
534 534
535 535 w.min = 0
536 536 w.max = 6
537 537 w.lower = 2
538 538 w.upper = 4
539 539 check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
540 540 w.value = (0, 1) #lower non-overlapping range
541 541 check_widget(w, cls=irsw, value=(0, 1), min=0, max=6)
542 542 w.value = (5, 6) #upper non-overlapping range
543 543 check_widget(w, cls=irsw, value=(5, 6), min=0, max=6)
544 544 w.value = (-1, 4) #semi out-of-range
545 545 check_widget(w, cls=irsw, value=(0, 4), min=0, max=6)
546 546 w.lower = 2
547 547 check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
548 548 w.value = (-2, -1) #wholly out of range
549 549 check_widget(w, cls=irsw, value=(0, 0), min=0, max=6)
550 550 w.value = (7, 8)
551 551 check_widget(w, cls=irsw, value=(6, 6), min=0, max=6)
552 552
553 553 with nt.assert_raises(ValueError):
554 554 w.min = 7
555 555 with nt.assert_raises(ValueError):
556 556 w.max = -1
557 557 with nt.assert_raises(ValueError):
558 558 w.lower = 5
559 559 with nt.assert_raises(ValueError):
560 560 w.upper = 1
561 561
562 562 w = irsw(min=2, max=3)
563 563 check_widget(w, min=2, max=3)
564 564 w = irsw(min=100, max=200)
565 565 check_widget(w, lower=125, upper=175, value=(125, 175))
566 566
567 567 with nt.assert_raises(ValueError):
568 568 irsw(value=(2, 4), lower=3)
569 569 with nt.assert_raises(ValueError):
570 570 irsw(value=(2, 4), upper=3)
571 571 with nt.assert_raises(ValueError):
572 572 irsw(value=(2, 4), lower=3, upper=3)
573 573 with nt.assert_raises(ValueError):
574 574 irsw(min=2, max=1)
575 575 with nt.assert_raises(ValueError):
576 576 irsw(lower=5)
577 577 with nt.assert_raises(ValueError):
578 578 irsw(upper=5)
579 579
580 580
581 581 def test_float_range_logic():
582 582 frsw = widgets.FloatRangeSlider
583 583 w = frsw(value=(.2, .4), min=0., max=.6)
584 584 check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
585 585 w.value = (.4, .2)
586 586 check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
587 587 w.value = (-.1, .7)
588 588 check_widget(w, cls=frsw, value=(0., .6), min=0., max=.6)
589 589 w.min = .3
590 590 check_widget(w, cls=frsw, value=(.3, .6), min=.3, max=.6)
591 591 w.max = .3
592 592 check_widget(w, cls=frsw, value=(.3, .3), min=.3, max=.3)
593 593
594 594 w.min = 0.
595 595 w.max = .6
596 596 w.lower = .2
597 597 w.upper = .4
598 598 check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
599 599 w.value = (0., .1) #lower non-overlapping range
600 600 check_widget(w, cls=frsw, value=(0., .1), min=0., max=.6)
601 601 w.value = (.5, .6) #upper non-overlapping range
602 602 check_widget(w, cls=frsw, value=(.5, .6), min=0., max=.6)
603 603 w.value = (-.1, .4) #semi out-of-range
604 604 check_widget(w, cls=frsw, value=(0., .4), min=0., max=.6)
605 605 w.lower = .2
606 606 check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
607 607 w.value = (-.2, -.1) #wholly out of range
608 608 check_widget(w, cls=frsw, value=(0., 0.), min=0., max=.6)
609 609 w.value = (.7, .8)
610 610 check_widget(w, cls=frsw, value=(.6, .6), min=.0, max=.6)
611 611
612 612 with nt.assert_raises(ValueError):
613 613 w.min = .7
614 614 with nt.assert_raises(ValueError):
615 615 w.max = -.1
616 616 with nt.assert_raises(ValueError):
617 617 w.lower = .5
618 618 with nt.assert_raises(ValueError):
619 619 w.upper = .1
620 620
621 621 w = frsw(min=2, max=3)
622 622 check_widget(w, min=2, max=3)
623 623 w = frsw(min=1., max=2.)
624 624 check_widget(w, lower=1.25, upper=1.75, value=(1.25, 1.75))
625 625
626 626 with nt.assert_raises(ValueError):
627 627 frsw(value=(2, 4), lower=3)
628 628 with nt.assert_raises(ValueError):
629 629 frsw(value=(2, 4), upper=3)
630 630 with nt.assert_raises(ValueError):
631 631 frsw(value=(2, 4), lower=3, upper=3)
632 632 with nt.assert_raises(ValueError):
633 633 frsw(min=.2, max=.1)
634 634 with nt.assert_raises(ValueError):
635 635 frsw(lower=5)
636 636 with nt.assert_raises(ValueError):
637 637 frsw(upper=5)
638 638
639 639
640 640 def test_multiple_selection():
641 641 smw = widgets.SelectMultiple
642 642
643 643 # degenerate multiple select
644 644 w = smw()
645 645 check_widget(w, value=tuple(), options=None, selected_labels=tuple())
646 646
647 647 # don't accept random other value when no options
648 648 with nt.assert_raises(KeyError):
649 649 w.value = (2,)
650 650 check_widget(w, value=tuple(), selected_labels=tuple())
651 651
652 652 # basic multiple select
653 653 w = smw(options=[(1, 1)], value=[1])
654 654 check_widget(w, cls=smw, value=(1,), options=[(1, 1)])
655 655
656 656 # don't accept random other value
657 657 with nt.assert_raises(KeyError):
658 658 w.value = w.value + (2,)
659 659 check_widget(w, value=(1,), selected_labels=(1,))
660 660
661 661 # change options
662 662 w.options = w.options + [(2, 2)]
663 663 check_widget(w, options=[(1, 1), (2,2)])
664 664
665 665 # change value
666 666 w.value = w.value + (2,)
667 667 check_widget(w, value=(1, 2), selected_labels=(1, 2))
668 668
669 669 # change value name
670 670 w.selected_labels = (1,)
671 671 check_widget(w, value=(1,))
672 672
673 673 # don't accept random other names when no options
674 674 with nt.assert_raises(KeyError):
675 675 w.selected_labels = (3,)
676 676 check_widget(w, value=(1,))
677 677
678 678 # don't accept selected_label (from superclass)
679 679 with nt.assert_raises(AttributeError):
680 680 w.selected_label = 3
681 681
682 682 # don't return selected_label (from superclass)
683 683 with nt.assert_raises(AttributeError):
684 684 print(w.selected_label)
685 685
686 686 # dict style
687 687 w.options = {1: 1}
688 688 check_widget(w, options={1: 1})
689 689
690 690 # updating
691 691 with nt.assert_raises(KeyError):
692 692 w.value = (2,)
693 693 check_widget(w, options={1: 1})
@@ -1,83 +1,83
1 1 """Test trait types of the widget packages."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from unittest import TestCase
7 7 from IPython.utils.traitlets import HasTraits
8 8 from traitlets.tests.test_traitlets import TraitTestBase
9 from IPython.html.widgets import Color, EventfulDict, EventfulList
9 from jupyter_notebook.widgets import Color, EventfulDict, EventfulList
10 10
11 11
12 12 class ColorTrait(HasTraits):
13 13 value = Color("black")
14 14
15 15
16 16 class TestColor(TraitTestBase):
17 17 obj = ColorTrait()
18 18
19 19 _good_values = ["blue", "#AA0", "#FFFFFF"]
20 20 _bad_values = ["vanilla", "blues"]
21 21
22 22
23 23 class TestEventful(TestCase):
24 24
25 25 def test_list(self):
26 26 """Does the EventfulList work?"""
27 27 event_cache = []
28 28
29 29 class A(HasTraits):
30 30 x = EventfulList([c for c in 'abc'])
31 31 a = A()
32 32 a.x.on_events(lambda i, x: event_cache.append('insert'), \
33 33 lambda i, x: event_cache.append('set'), \
34 34 lambda i: event_cache.append('del'), \
35 35 lambda: event_cache.append('reverse'), \
36 36 lambda *p, **k: event_cache.append('sort'))
37 37
38 38 a.x.remove('c')
39 39 # ab
40 40 a.x.insert(0, 'z')
41 41 # zab
42 42 del a.x[1]
43 43 # zb
44 44 a.x.reverse()
45 45 # bz
46 46 a.x[1] = 'o'
47 47 # bo
48 48 a.x.append('a')
49 49 # boa
50 50 a.x.sort()
51 51 # abo
52 52
53 53 # Were the correct events captured?
54 54 self.assertEqual(event_cache, ['del', 'insert', 'del', 'reverse', 'set', 'set', 'sort'])
55 55
56 56 # Is the output correct?
57 57 self.assertEqual(a.x, [c for c in 'abo'])
58 58
59 59 def test_dict(self):
60 60 """Does the EventfulDict work?"""
61 61 event_cache = []
62 62
63 63 class A(HasTraits):
64 64 x = EventfulDict({c: c for c in 'abc'})
65 65 a = A()
66 66 a.x.on_events(lambda k, v: event_cache.append('add'), \
67 67 lambda k, v: event_cache.append('set'), \
68 68 lambda k: event_cache.append('del'))
69 69
70 70 del a.x['c']
71 71 # ab
72 72 a.x['z'] = 1
73 73 # abz
74 74 a.x['z'] = 'z'
75 75 # abz
76 76 a.x.pop('a')
77 77 # bz
78 78
79 79 # Were the correct events captured?
80 80 self.assertEqual(event_cache, ['del', 'add', 'set', 'del'])
81 81
82 82 # Is the output correct?
83 83 self.assertEqual(a.x, {c: c for c in 'bz'})
@@ -1,76 +1,76
1 1 """Output class.
2 2
3 3 Represents a widget that can be used to display output within the widget area.
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 from .widget import DOMWidget
10 10 import sys
11 11 from IPython.utils.traitlets import Unicode, List
12 12 from IPython.display import clear_output
13 13 from IPython.kernel.zmq.session import Message
14 14
15 15 class Output(DOMWidget):
16 16 """Widget used as a context manager to display output.
17 17
18 18 This widget can capture and display stdout, stderr, and rich output. To use
19 19 it, create an instance of it and display it. Then use it as a context
20 20 manager. Any output produced while in it's context will be captured and
21 21 displayed in it instead of the standard output area.
22 22
23 23 Example
24 from IPython.html import widgets
24 from jupyter_notebook import widgets
25 25 from IPython.display import display
26 26 out = widgets.Output()
27 27 display(out)
28 28
29 29 print('prints to output area')
30 30
31 31 with out:
32 32 print('prints to output widget')"""
33 33 _view_name = Unicode('OutputView', sync=True)
34 34
35 35 def clear_output(self, *pargs, **kwargs):
36 36 with self:
37 37 clear_output(*pargs, **kwargs)
38 38
39 39 def __enter__(self):
40 40 """Called upon entering output widget context manager."""
41 41 self._flush()
42 42 kernel = get_ipython().kernel
43 43 session = kernel.session
44 44 send = session.send
45 45 self._original_send = send
46 46 self._session = session
47 47
48 48 def send_hook(stream, msg_or_type, content=None, parent=None, ident=None,
49 49 buffers=None, track=False, header=None, metadata=None):
50 50
51 51 # Handle both prebuild messages and unbuilt messages.
52 52 if isinstance(msg_or_type, (Message, dict)):
53 53 msg_type = msg_or_type['msg_type']
54 54 msg = dict(msg_or_type)
55 55 else:
56 56 msg_type = msg_or_type
57 57 msg = session.msg(msg_type, content=content, parent=parent,
58 58 header=header, metadata=metadata)
59 59
60 60 # If this is a message type that we want to forward, forward it.
61 61 if stream is kernel.iopub_socket and msg_type in ['clear_output', 'stream', 'display_data']:
62 62 self.send(msg)
63 63 else:
64 64 send(stream, msg, ident=ident, buffers=buffers, track=track)
65 65
66 66 session.send = send_hook
67 67
68 68 def __exit__(self, exception_type, exception_value, traceback):
69 69 """Called upon exiting output widget context manager."""
70 70 self._flush()
71 71 self._session.send = self._original_send
72 72
73 73 def _flush(self):
74 74 """Flush stdout and stderr buffers."""
75 75 sys.stdout.flush()
76 76 sys.stderr.flush()
General Comments 0
You need to be logged in to leave comments. Login now