Show More
@@ -1,3 +1,3 | |||
|
1 | 1 | if __name__ == '__main__': |
|
2 |
from |
|
|
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 |
|
|
32 | from jupyter_notebook.utils import is_hidden, url_path_join, url_escape | |
|
33 | 33 | |
|
34 |
from |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
12 |
from |
|
|
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 |
|
|
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 = ' |
|
|
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 |
|
|
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 |
|
|
9 |
from |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
10 | from jupyter_notebook.utils import url_path_join, url_escape | |
|
11 | 11 | from jupyter_client.jsonutil import date_default |
|
12 | 12 | |
|
13 |
from |
|
|
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 |
|
|
20 |
from |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
9 |
from |
|
|
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 |
|
|
16 |
from |
|
|
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 |
|
|
4 |
from |
|
|
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 |
|
|
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(' |
|
|
19 |
contents_manager = Instance(' |
|
|
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 |
|
|
9 |
from |
|
|
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 |
|
|
14 |
from |
|
|
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: ' |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
26 |
from |
|
|
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 |
|
|
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(' |
|
|
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 |
|
|
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 |
|
|
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(' |
|
|
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: ' |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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=' |
|
|
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 = ' |
|
|
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 |
|
|
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 |
|
|
17 |
from |
|
|
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 |
|
|
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 |
|
|
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