##// END OF EJS Templates
Merge remote-tracking branch 'public-upstream/master' into links-rebase...
Jason Grout -
r19202:bab26ab3 merge
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,311 b''
1 """WebsocketProtocol76 from tornado 3.2.2 for tornado >= 4.0
2
3 The contents of this file are Copyright (c) Tornado
4 Used under the Apache 2.0 license
5 """
6
7
8 from __future__ import absolute_import, division, print_function, with_statement
9 # Author: Jacob Kristhammar, 2010
10
11 import functools
12 import hashlib
13 import struct
14 import time
15 import tornado.escape
16 import tornado.web
17
18 from tornado.log import gen_log, app_log
19 from tornado.util import bytes_type, unicode_type
20
21 from tornado.websocket import WebSocketHandler, WebSocketProtocol13
22
23 class AllowDraftWebSocketHandler(WebSocketHandler):
24 """Restore Draft76 support for tornado 4
25
26 Remove when we can run tests without phantomjs + qt4
27 """
28
29 # get is unmodified except between the BEGIN/END PATCH lines
30 @tornado.web.asynchronous
31 def get(self, *args, **kwargs):
32 self.open_args = args
33 self.open_kwargs = kwargs
34
35 # Upgrade header should be present and should be equal to WebSocket
36 if self.request.headers.get("Upgrade", "").lower() != 'websocket':
37 self.set_status(400)
38 self.finish("Can \"Upgrade\" only to \"WebSocket\".")
39 return
40
41 # Connection header should be upgrade. Some proxy servers/load balancers
42 # might mess with it.
43 headers = self.request.headers
44 connection = map(lambda s: s.strip().lower(), headers.get("Connection", "").split(","))
45 if 'upgrade' not in connection:
46 self.set_status(400)
47 self.finish("\"Connection\" must be \"Upgrade\".")
48 return
49
50 # Handle WebSocket Origin naming convention differences
51 # The difference between version 8 and 13 is that in 8 the
52 # client sends a "Sec-Websocket-Origin" header and in 13 it's
53 # simply "Origin".
54 if "Origin" in self.request.headers:
55 origin = self.request.headers.get("Origin")
56 else:
57 origin = self.request.headers.get("Sec-Websocket-Origin", None)
58
59
60 # If there was an origin header, check to make sure it matches
61 # according to check_origin. When the origin is None, we assume it
62 # did not come from a browser and that it can be passed on.
63 if origin is not None and not self.check_origin(origin):
64 self.set_status(403)
65 self.finish("Cross origin websockets not allowed")
66 return
67
68 self.stream = self.request.connection.detach()
69 self.stream.set_close_callback(self.on_connection_close)
70
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 self.ws_connection = WebSocketProtocol13(self)
73 self.ws_connection.accept_connection()
74 #--------------- BEGIN PATCH ----------------
75 elif (self.allow_draft76() and
76 "Sec-WebSocket-Version" not in self.request.headers):
77 self.ws_connection = WebSocketProtocol76(self)
78 self.ws_connection.accept_connection()
79 #--------------- END PATCH ----------------
80 else:
81 if not self.stream.closed():
82 self.stream.write(tornado.escape.utf8(
83 "HTTP/1.1 426 Upgrade Required\r\n"
84 "Sec-WebSocket-Version: 8\r\n\r\n"))
85 self.stream.close()
86
87 # 3.2 methods removed in 4.0:
88 def allow_draft76(self):
89 """Using this class allows draft76 connections by default"""
90 return True
91
92 def get_websocket_scheme(self):
93 """Return the url scheme used for this request, either "ws" or "wss".
94 This is normally decided by HTTPServer, but applications
95 may wish to override this if they are using an SSL proxy
96 that does not provide the X-Scheme header as understood
97 by HTTPServer.
98 Note that this is only used by the draft76 protocol.
99 """
100 return "wss" if self.request.protocol == "https" else "ws"
101
102
103
104 # No modifications from tornado-3.2.2 below this line
105
106 class WebSocketProtocol(object):
107 """Base class for WebSocket protocol versions.
108 """
109 def __init__(self, handler):
110 self.handler = handler
111 self.request = handler.request
112 self.stream = handler.stream
113 self.client_terminated = False
114 self.server_terminated = False
115
116 def async_callback(self, callback, *args, **kwargs):
117 """Wrap callbacks with this if they are used on asynchronous requests.
118
119 Catches exceptions properly and closes this WebSocket if an exception
120 is uncaught.
121 """
122 if args or kwargs:
123 callback = functools.partial(callback, *args, **kwargs)
124
125 def wrapper(*args, **kwargs):
126 try:
127 return callback(*args, **kwargs)
128 except Exception:
129 app_log.error("Uncaught exception in %s",
130 self.request.path, exc_info=True)
131 self._abort()
132 return wrapper
133
134 def on_connection_close(self):
135 self._abort()
136
137 def _abort(self):
138 """Instantly aborts the WebSocket connection by closing the socket"""
139 self.client_terminated = True
140 self.server_terminated = True
141 self.stream.close() # forcibly tear down the connection
142 self.close() # let the subclass cleanup
143
144
145 class WebSocketProtocol76(WebSocketProtocol):
146 """Implementation of the WebSockets protocol, version hixie-76.
147
148 This class provides basic functionality to process WebSockets requests as
149 specified in
150 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
151 """
152 def __init__(self, handler):
153 WebSocketProtocol.__init__(self, handler)
154 self.challenge = None
155 self._waiting = None
156
157 def accept_connection(self):
158 try:
159 self._handle_websocket_headers()
160 except ValueError:
161 gen_log.debug("Malformed WebSocket request received")
162 self._abort()
163 return
164
165 scheme = self.handler.get_websocket_scheme()
166
167 # draft76 only allows a single subprotocol
168 subprotocol_header = ''
169 subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
170 if subprotocol:
171 selected = self.handler.select_subprotocol([subprotocol])
172 if selected:
173 assert selected == subprotocol
174 subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
175
176 # Write the initial headers before attempting to read the challenge.
177 # This is necessary when using proxies (such as HAProxy), which
178 # need to see the Upgrade headers before passing through the
179 # non-HTTP traffic that follows.
180 self.stream.write(tornado.escape.utf8(
181 "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
182 "Upgrade: WebSocket\r\n"
183 "Connection: Upgrade\r\n"
184 "Server: TornadoServer/%(version)s\r\n"
185 "Sec-WebSocket-Origin: %(origin)s\r\n"
186 "Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
187 "%(subprotocol)s"
188 "\r\n" % (dict(
189 version=tornado.version,
190 origin=self.request.headers["Origin"],
191 scheme=scheme,
192 host=self.request.host,
193 uri=self.request.uri,
194 subprotocol=subprotocol_header))))
195 self.stream.read_bytes(8, self._handle_challenge)
196
197 def challenge_response(self, challenge):
198 """Generates the challenge response that's needed in the handshake
199
200 The challenge parameter should be the raw bytes as sent from the
201 client.
202 """
203 key_1 = self.request.headers.get("Sec-Websocket-Key1")
204 key_2 = self.request.headers.get("Sec-Websocket-Key2")
205 try:
206 part_1 = self._calculate_part(key_1)
207 part_2 = self._calculate_part(key_2)
208 except ValueError:
209 raise ValueError("Invalid Keys/Challenge")
210 return self._generate_challenge_response(part_1, part_2, challenge)
211
212 def _handle_challenge(self, challenge):
213 try:
214 challenge_response = self.challenge_response(challenge)
215 except ValueError:
216 gen_log.debug("Malformed key data in WebSocket request")
217 self._abort()
218 return
219 self._write_response(challenge_response)
220
221 def _write_response(self, challenge):
222 self.stream.write(challenge)
223 self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
224 self._receive_message()
225
226 def _handle_websocket_headers(self):
227 """Verifies all invariant- and required headers
228
229 If a header is missing or have an incorrect value ValueError will be
230 raised
231 """
232 fields = ("Origin", "Host", "Sec-Websocket-Key1",
233 "Sec-Websocket-Key2")
234 if not all(map(lambda f: self.request.headers.get(f), fields)):
235 raise ValueError("Missing/Invalid WebSocket headers")
236
237 def _calculate_part(self, key):
238 """Processes the key headers and calculates their key value.
239
240 Raises ValueError when feed invalid key."""
241 # pyflakes complains about variable reuse if both of these lines use 'c'
242 number = int(''.join(c for c in key if c.isdigit()))
243 spaces = len([c2 for c2 in key if c2.isspace()])
244 try:
245 key_number = number // spaces
246 except (ValueError, ZeroDivisionError):
247 raise ValueError
248 return struct.pack(">I", key_number)
249
250 def _generate_challenge_response(self, part_1, part_2, part_3):
251 m = hashlib.md5()
252 m.update(part_1)
253 m.update(part_2)
254 m.update(part_3)
255 return m.digest()
256
257 def _receive_message(self):
258 self.stream.read_bytes(1, self._on_frame_type)
259
260 def _on_frame_type(self, byte):
261 frame_type = ord(byte)
262 if frame_type == 0x00:
263 self.stream.read_until(b"\xff", self._on_end_delimiter)
264 elif frame_type == 0xff:
265 self.stream.read_bytes(1, self._on_length_indicator)
266 else:
267 self._abort()
268
269 def _on_end_delimiter(self, frame):
270 if not self.client_terminated:
271 self.async_callback(self.handler.on_message)(
272 frame[:-1].decode("utf-8", "replace"))
273 if not self.client_terminated:
274 self._receive_message()
275
276 def _on_length_indicator(self, byte):
277 if ord(byte) != 0x00:
278 self._abort()
279 return
280 self.client_terminated = True
281 self.close()
282
283 def write_message(self, message, binary=False):
284 """Sends the given message to the client of this Web Socket."""
285 if binary:
286 raise ValueError(
287 "Binary messages not supported by this version of websockets")
288 if isinstance(message, unicode_type):
289 message = message.encode("utf-8")
290 assert isinstance(message, bytes_type)
291 self.stream.write(b"\x00" + message + b"\xff")
292
293 def write_ping(self, data):
294 """Send ping frame."""
295 raise ValueError("Ping messages not supported by this version of websockets")
296
297 def close(self):
298 """Closes the WebSocket connection."""
299 if not self.server_terminated:
300 if not self.stream.closed():
301 self.stream.write("\xff\x00")
302 self.server_terminated = True
303 if self.client_terminated:
304 if self._waiting is not None:
305 self.stream.io_loop.remove_timeout(self._waiting)
306 self._waiting = None
307 self.stream.close()
308 elif self._waiting is None:
309 self._waiting = self.stream.io_loop.add_timeout(
310 time.time() + 5, self._abort)
311
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,29 b''
1 #encoding: utf-8
2 """Tornado handlers for the terminal emulator."""
3
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
6
7 from tornado import web
8 from ..base.handlers import IPythonHandler, path_regex
9 from ..utils import url_escape
10
11 class EditorHandler(IPythonHandler):
12 """Render the text editor interface."""
13 @web.authenticated
14 def get(self, path):
15 path = path.strip('/')
16 if not self.contents_manager.file_exists(path):
17 raise web.HTTPError(404, u'File does not exist: %s' % path)
18
19 basename = path.rsplit('/', 1)[-1]
20 self.write(self.render_template('edit.html',
21 file_path=url_escape(path),
22 basename=basename,
23 page_title=basename + " (editing)",
24 )
25 )
26
27 default_handlers = [
28 (r"/edit%s" % path_regex, EditorHandler),
29 ] No newline at end of file
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,54 b''
1 """Serve files directly from the ContentsManager."""
2
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5
6 import os
7 import mimetypes
8 import json
9 import base64
10
11 from tornado import web
12
13 from IPython.html.base.handlers import IPythonHandler
14
15 class FilesHandler(IPythonHandler):
16 """serve files via ContentsManager"""
17
18 @web.authenticated
19 def get(self, path):
20 cm = self.contents_manager
21 if cm.is_hidden(path):
22 self.log.info("Refusing to serve hidden file, via 404 Error")
23 raise web.HTTPError(404)
24
25 path = path.strip('/')
26 if '/' in path:
27 _, name = path.rsplit('/', 1)
28 else:
29 name = path
30
31 model = cm.get(path)
32
33 if self.get_argument("download", False):
34 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
35
36 if model['type'] == 'notebook':
37 self.set_header('Content-Type', 'application/json')
38 else:
39 cur_mime = mimetypes.guess_type(name)[0]
40 if cur_mime is not None:
41 self.set_header('Content-Type', cur_mime)
42
43 if model['format'] == 'base64':
44 b64_bytes = model['content'].encode('ascii')
45 self.write(base64.decodestring(b64_bytes))
46 elif model['format'] == 'json':
47 self.write(json.dumps(model['content']))
48 else:
49 self.write(model['content'])
50 self.flush()
51
52 default_handlers = [
53 (r"/files/(.*)", FilesHandler),
54 ] No newline at end of file
@@ -0,0 +1,1 b''
1 from .manager import ConfigManager
@@ -0,0 +1,44 b''
1 """Tornado handlers for frontend config storage."""
2
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5 import json
6 import os
7 import io
8 import errno
9 from tornado import web
10
11 from IPython.utils.py3compat import PY3
12 from ...base.handlers import IPythonHandler, json_errors
13
14 class ConfigHandler(IPythonHandler):
15 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH')
16
17 @web.authenticated
18 @json_errors
19 def get(self, section_name):
20 self.set_header("Content-Type", 'application/json')
21 self.finish(json.dumps(self.config_manager.get(section_name)))
22
23 @web.authenticated
24 @json_errors
25 def put(self, section_name):
26 data = self.get_json_body() # Will raise 400 if content is not valid JSON
27 self.config_manager.set(section_name, data)
28 self.set_status(204)
29
30 @web.authenticated
31 @json_errors
32 def patch(self, section_name):
33 new_data = self.get_json_body()
34 section = self.config_manager.update(section_name, new_data)
35 self.finish(json.dumps(section))
36
37
38 # URL to handler mappings
39
40 section_name_regex = r"(?P<section_name>\w+)"
41
42 default_handlers = [
43 (r"/api/config/%s" % section_name_regex, ConfigHandler),
44 ]
@@ -0,0 +1,90 b''
1 """Manager to read and modify frontend config data in JSON files.
2 """
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5 import errno
6 import io
7 import json
8 import os
9
10 from IPython.config import LoggingConfigurable
11 from IPython.utils.path import locate_profile
12 from IPython.utils.py3compat import PY3
13 from IPython.utils.traitlets import Unicode
14
15
16 def recursive_update(target, new):
17 """Recursively update one dictionary using another.
18
19 None values will delete their keys.
20 """
21 for k, v in new.items():
22 if isinstance(v, dict):
23 if k not in target:
24 target[k] = {}
25 recursive_update(target[k], v)
26 if not target[k]:
27 # Prune empty subdicts
28 del target[k]
29
30 elif v is None:
31 target.pop(k, None)
32
33 else:
34 target[k] = v
35
36
37 class ConfigManager(LoggingConfigurable):
38 profile_dir = Unicode()
39 def _profile_dir_default(self):
40 return locate_profile()
41
42 @property
43 def config_dir(self):
44 return os.path.join(self.profile_dir, 'nbconfig')
45
46 def ensure_config_dir_exists(self):
47 try:
48 os.mkdir(self.config_dir, 0o755)
49 except OSError as e:
50 if e.errno != errno.EEXIST:
51 raise
52
53 def file_name(self, section_name):
54 return os.path.join(self.config_dir, section_name+'.json')
55
56 def get(self, section_name):
57 """Retrieve the config data for the specified section.
58
59 Returns the data as a dictionary, or an empty dictionary if the file
60 doesn't exist.
61 """
62 filename = self.file_name(section_name)
63 if os.path.isfile(filename):
64 with io.open(filename, encoding='utf-8') as f:
65 return json.load(f)
66 else:
67 return {}
68
69 def set(self, section_name, data):
70 """Store the given config data.
71 """
72 filename = self.file_name(section_name)
73 self.ensure_config_dir_exists()
74
75 if PY3:
76 f = io.open(filename, 'w', encoding='utf-8')
77 else:
78 f = open(filename, 'wb')
79 with f:
80 json.dump(data, f)
81
82 def update(self, section_name, new_data):
83 """Modify the config section by recursively updating it with new_data.
84
85 Returns the modified config data as a dictionary.
86 """
87 data = self.get(section_name)
88 recursive_update(data, new_data)
89 self.set(section_name, data)
90 return data
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,68 b''
1 # coding: utf-8
2 """Test the config webservice API."""
3
4 import json
5
6 import requests
7
8 from IPython.html.utils import url_path_join
9 from IPython.html.tests.launchnotebook import NotebookTestBase
10
11
12 class ConfigAPI(object):
13 """Wrapper for notebook API calls."""
14 def __init__(self, base_url):
15 self.base_url = base_url
16
17 def _req(self, verb, section, body=None):
18 response = requests.request(verb,
19 url_path_join(self.base_url, 'api/config', section),
20 data=body,
21 )
22 response.raise_for_status()
23 return response
24
25 def get(self, section):
26 return self._req('GET', section)
27
28 def set(self, section, values):
29 return self._req('PUT', section, json.dumps(values))
30
31 def modify(self, section, values):
32 return self._req('PATCH', section, json.dumps(values))
33
34 class APITest(NotebookTestBase):
35 """Test the config web service API"""
36 def setUp(self):
37 self.config_api = ConfigAPI(self.base_url())
38
39 def test_create_retrieve_config(self):
40 sample = {'foo': 'bar', 'baz': 73}
41 r = self.config_api.set('example', sample)
42 self.assertEqual(r.status_code, 204)
43
44 r = self.config_api.get('example')
45 self.assertEqual(r.status_code, 200)
46 self.assertEqual(r.json(), sample)
47
48 def test_modify(self):
49 sample = {'foo': 'bar', 'baz': 73,
50 'sub': {'a': 6, 'b': 7}, 'sub2': {'c': 8}}
51 self.config_api.set('example', sample)
52
53 r = self.config_api.modify('example', {'foo': None, # should delete foo
54 'baz': 75,
55 'wib': [1,2,3],
56 'sub': {'a': 8, 'b': None, 'd': 9},
57 'sub2': {'c': None} # should delete sub2
58 })
59 self.assertEqual(r.status_code, 200)
60 self.assertEqual(r.json(), {'baz': 75, 'wib': [1,2,3],
61 'sub': {'a': 8, 'd': 9}})
62
63 def test_get_unknown(self):
64 # We should get an empty config dictionary instead of a 404
65 r = self.config_api.get('nonexistant')
66 self.assertEqual(r.status_code, 200)
67 self.assertEqual(r.json(), {})
68
@@ -0,0 +1,23 b''
1 """A dummy contents manager for when the logic is done client side (in JavaScript)."""
2
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5
6 from .manager import ContentsManager
7
8 class ClientSideContentsManager(ContentsManager):
9 """Dummy contents manager for use with client-side contents APIs like GDrive
10
11 The view handlers for notebooks and directories (/tree/) check with the
12 ContentsManager that their target exists so they can return 404 if not. Using
13 this class as the contents manager allows those pages to render without
14 checking something that the server doesn't know about.
15 """
16 def dir_exists(self, path):
17 return True
18
19 def is_hidden(self, path):
20 return False
21
22 def file_exists(self, name, path=''):
23 return True
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,4 b''
1 # URI for the CSP Report. Included here to prevent a cyclic dependency.
2 # csp_report_uri is needed both by the BaseHandler (for setting the report-uri)
3 # and by the CSPReportHandler (which depends on the BaseHandler).
4 csp_report_uri = r"/api/security/csp-report"
@@ -0,0 +1,23 b''
1 """Tornado handlers for security logging."""
2
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5
6 from tornado import gen, web
7
8 from ...base.handlers import IPythonHandler, json_errors
9 from . import csp_report_uri
10
11 class CSPReportHandler(IPythonHandler):
12 '''Accepts a content security policy violation report'''
13 @web.authenticated
14 @json_errors
15 def post(self):
16 '''Log a content security policy violation report'''
17 csp_report = self.get_json_body()
18 self.log.warn("Content security violation: %s",
19 self.request.body.decode('utf8', 'replace'))
20
21 default_handlers = [
22 (csp_report_uri, CSPReportHandler)
23 ]
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,83 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 define([
5 'jquery',
6 'base/js/notificationwidget',
7 ], function($, notificationwidget) {
8 "use strict";
9
10 // store reference to the NotificationWidget class
11 var NotificationWidget = notificationwidget.NotificationWidget;
12
13 /**
14 * Construct the NotificationArea object. Options are:
15 * events: $(Events) instance
16 * save_widget: SaveWidget instance
17 * notebook: Notebook instance
18 * keyboard_manager: KeyboardManager instance
19 *
20 * @constructor
21 * @param {string} selector - a jQuery selector string for the
22 * notification area element
23 * @param {Object} [options] - a dictionary of keyword arguments.
24 */
25 var NotificationArea = function (selector, options) {
26 this.selector = selector;
27 this.events = options.events;
28 if (this.selector !== undefined) {
29 this.element = $(selector);
30 }
31 this.widget_dict = {};
32 };
33
34 /**
35 * Get a widget by name, creating it if it doesn't exist.
36 *
37 * @method widget
38 * @param {string} name - the widget name
39 */
40 NotificationArea.prototype.widget = function (name) {
41 if (this.widget_dict[name] === undefined) {
42 return this.new_notification_widget(name);
43 }
44 return this.get_widget(name);
45 };
46
47 /**
48 * Get a widget by name, throwing an error if it doesn't exist.
49 *
50 * @method get_widget
51 * @param {string} name - the widget name
52 */
53 NotificationArea.prototype.get_widget = function (name) {
54 if(this.widget_dict[name] === undefined) {
55 throw('no widgets with this name');
56 }
57 return this.widget_dict[name];
58 };
59
60 /**
61 * Create a new notification widget with the given name. The
62 * widget must not already exist.
63 *
64 * @method new_notification_widget
65 * @param {string} name - the widget name
66 */
67 NotificationArea.prototype.new_notification_widget = function (name) {
68 if (this.widget_dict[name] !== undefined) {
69 throw('widget with that name already exists!');
70 }
71
72 // create the element for the notification widget and add it
73 // to the notification aread element
74 var div = $('<div/>').attr('id', 'notification_' + name);
75 $(this.selector).append(div);
76
77 // create the widget object and return it
78 this.widget_dict[name] = new NotificationWidget('#notification_' + name);
79 return this.widget_dict[name];
80 };
81
82 return {'NotificationArea': NotificationArea};
83 });
@@ -0,0 +1,78 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 define([
5 'jquery',
6 'base/js/utils',
7 'codemirror/lib/codemirror',
8 'codemirror/mode/meta',
9 'codemirror/addon/search/search'
10 ],
11 function($,
12 utils,
13 CodeMirror
14 ) {
15 var Editor = function(selector, options) {
16 this.selector = selector;
17 this.contents = options.contents;
18 this.events = options.events;
19 this.base_url = options.base_url;
20 this.file_path = options.file_path;
21
22 this.codemirror = CodeMirror($(this.selector)[0]);
23
24 // It appears we have to set commands on the CodeMirror class, not the
25 // instance. I'd like to be wrong, but since there should only be one CM
26 // instance on the page, this is good enough for now.
27 CodeMirror.commands.save = $.proxy(this.save, this);
28
29 this.save_enabled = false;
30 };
31
32 Editor.prototype.load = function() {
33 var that = this;
34 var cm = this.codemirror;
35 this.contents.get(this.file_path, {type: 'file', format: 'text'})
36 .then(function(model) {
37 cm.setValue(model.content);
38
39 // Setting the file's initial value creates a history entry,
40 // which we don't want.
41 cm.clearHistory();
42
43 // Find and load the highlighting mode
44 var modeinfo = CodeMirror.findModeByMIME(model.mimetype);
45 if (modeinfo) {
46 utils.requireCodeMirrorMode(modeinfo.mode, function() {
47 cm.setOption('mode', modeinfo.mode);
48 });
49 }
50 that.save_enabled = true;
51 },
52 function(error) {
53 cm.setValue("Error! " + error.message +
54 "\nSaving disabled.");
55 that.save_enabled = false;
56 }
57 );
58 };
59
60 Editor.prototype.save = function() {
61 if (!this.save_enabled) {
62 console.log("Not saving, save disabled");
63 return;
64 }
65 var model = {
66 path: this.file_path,
67 type: 'file',
68 format: 'text',
69 content: this.codemirror.getValue(),
70 };
71 var that = this;
72 this.contents.save(this.file_path, model).then(function() {
73 that.events.trigger("save_succeeded.TextEditor");
74 });
75 };
76
77 return {Editor: Editor};
78 });
@@ -0,0 +1,64 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 require([
5 'base/js/namespace',
6 'base/js/utils',
7 'base/js/page',
8 'base/js/events',
9 'contents',
10 'services/config',
11 'edit/js/editor',
12 'edit/js/menubar',
13 'edit/js/notificationarea',
14 'custom/custom',
15 ], function(
16 IPython,
17 utils,
18 page,
19 events,
20 contents,
21 configmod,
22 editor,
23 menubar,
24 notificationarea
25 ){
26 page = new page.Page();
27
28 var base_url = utils.get_body_data('baseUrl');
29 var file_path = utils.get_body_data('filePath');
30 contents = new contents.Contents({base_url: base_url});
31 var config = new configmod.ConfigSection('edit', {base_url: base_url})
32 config.load();
33
34 var editor = new editor.Editor('#texteditor-container', {
35 base_url: base_url,
36 events: events,
37 contents: contents,
38 file_path: file_path,
39 });
40
41 // Make it available for debugging
42 IPython.editor = editor;
43
44 var menus = new menubar.MenuBar('#menubar', {
45 base_url: base_url,
46 editor: editor,
47 });
48
49 var notification_area = new notificationarea.EditorNotificationArea(
50 '#notification_area', {
51 events: events,
52 });
53 notification_area.init_notification_widgets();
54
55 config.loaded.then(function() {
56 if (config.data.load_extensions) {
57 var nbextension_paths = Object.getOwnPropertyNames(
58 config.data.load_extensions);
59 IPython.load_extensions.apply(this, nbextension_paths);
60 }
61 });
62 editor.load();
63 page.show();
64 });
@@ -0,0 +1,50 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 define([
5 'base/js/namespace',
6 'jquery',
7 'base/js/utils',
8 'bootstrap',
9 ], function(IPython, $, utils, bootstrap) {
10 "use strict";
11
12 var MenuBar = function (selector, options) {
13 /**
14 * Constructor
15 *
16 * A MenuBar Class to generate the menubar of IPython notebook
17 *
18 * Parameters:
19 * selector: string
20 * options: dictionary
21 * Dictionary of keyword arguments.
22 * codemirror: CodeMirror instance
23 * contents: ContentManager instance
24 * events: $(Events) instance
25 * base_url : string
26 * file_path : string
27 */
28 options = options || {};
29 this.base_url = options.base_url || utils.get_body_data("baseUrl");
30 this.selector = selector;
31 this.editor = options.editor;
32
33 if (this.selector !== undefined) {
34 this.element = $(selector);
35 this.bind_events();
36 }
37 };
38
39 MenuBar.prototype.bind_events = function () {
40 /**
41 * File
42 */
43 var that = this;
44 this.element.find('#save_file').click(function () {
45 that.editor.save();
46 });
47 };
48
49 return {'MenuBar': MenuBar};
50 });
@@ -0,0 +1,29 b''
1 define([
2 'base/js/notificationarea'
3 ], function(notificationarea) {
4 "use strict";
5 var NotificationArea = notificationarea.NotificationArea;
6
7 var EditorNotificationArea = function(selector, options) {
8 NotificationArea.apply(this, [selector, options]);
9 }
10
11 EditorNotificationArea.prototype = Object.create(NotificationArea.prototype);
12
13 /**
14 * Initialize the default set of notification widgets.
15 *
16 * @method init_notification_widgets
17 */
18 EditorNotificationArea.prototype.init_notification_widgets = function () {
19 var that = this;
20 var enw = this.new_notification_widget('editor');
21
22 this.events.on("save_succeeded.TextEditor", function() {
23 enw.set_message("File saved", 2000);
24 });
25 };
26
27
28 return {EditorNotificationArea: EditorNotificationArea};
29 });
@@ -0,0 +1,38 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3 require([
4 'jquery',
5 'base/js/dialog',
6 'underscore',
7 'base/js/namespace'
8 ], function ($, dialog, _, IPython) {
9 'use strict';
10 $('#notebook_about').click(function () {
11 // use underscore template to auto html escape
12 var text = 'You are using IPython notebook.<br/><br/>';
13 text = text + 'The version of the notebook server is ';
14 text = text + _.template('<b><%- version %></b>')({ version: sys_info.ipython_version });
15 if (sys_info.commit_hash) {
16 text = text + _.template('-<%- hash %>')({ hash: sys_info.commit_hash });
17 }
18 text = text + _.template(' and is running on:<br/><pre>Python <%- pyver %></pre>')({ pyver: sys_info.sys_version });
19 var kinfo = $('<div/>').attr('id', '#about-kinfo').text('Waiting for kernel to be available...');
20 var body = $('<div/>');
21 body.append($('<h4/>').text('Server Information:'));
22 body.append($('<p/>').html(text));
23 body.append($('<h4/>').text('Current Kernel Information:'));
24 body.append(kinfo);
25 dialog.modal({
26 title: 'About IPython Notebook',
27 body: body,
28 buttons: { 'OK': {} }
29 });
30 try {
31 IPython.notebook.session.kernel.kernel_info(function (data) {
32 kinfo.html($('<pre/>').text(data.content.banner));
33 });
34 } catch (e) {
35 kinfo.html($('<p/>').text('unable to contact kernel'));
36 }
37 });
38 });
This diff has been collapsed as it changes many lines, (503 lines changed) Show them Hide them
@@ -0,0 +1,503 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 define(['require'
5 ], function(require) {
6 "use strict";
7
8 var ActionHandler = function (env) {
9 this.env = env || {};
10 Object.seal(this);
11 };
12
13 /**
14 * A bunch of predefined `Simple Actions` used by IPython.
15 * `Simple Actions` have the following keys:
16 * help (optional): a short string the describe the action.
17 * will be used in various context, like as menu name, tool tips on buttons,
18 * and short description in help menu.
19 * help_index (optional): a string used to sort action in help menu.
20 * icon (optional): a short string that represent the icon that have to be used with this
21 * action. this should mainly correspond to a Font_awesome class.
22 * handler : a function which is called when the action is activated. It will receive at first parameter
23 * a dictionary containing various handle to element of the notebook.
24 *
25 * action need to be registered with a **name** that can be use to refer to this action.
26 *
27 *
28 * if `help` is not provided it will be derived by replacing any dash by space
29 * in the **name** of the action. It is advised to provide a prefix to action name to
30 * avoid conflict the prefix should be all lowercase and end with a dot `.`
31 * in the absence of a prefix the behavior of the action is undefined.
32 *
33 * All action provided by IPython are prefixed with `ipython.`.
34 *
35 * One can register extra actions or replace an existing action with another one is possible
36 * but is considered undefined behavior.
37 *
38 **/
39 var _action = {
40 'run-select-next': {
41 icon: 'fa-play',
42 help : 'run cell, select below',
43 help_index : 'ba',
44 handler : function (env) {
45 env.notebook.execute_cell_and_select_below();
46 }
47 },
48 'execute-in-place':{
49 help : 'run cell',
50 help_index : 'bb',
51 handler : function (env) {
52 env.notebook.execute_cell();
53 }
54 },
55 'execute-and-insert-after':{
56 help : 'run cell, insert below',
57 help_index : 'bc',
58 handler : function (env) {
59 env.notebook.execute_cell_and_insert_below();
60 }
61 },
62 'go-to-command-mode': {
63 help : 'command mode',
64 help_index : 'aa',
65 handler : function (env) {
66 env.notebook.command_mode();
67 }
68 },
69 'split-cell-at-cursor': {
70 help : 'split cell',
71 help_index : 'ea',
72 handler : function (env) {
73 env.notebook.split_cell();
74 }
75 },
76 'enter-edit-mode' : {
77 help_index : 'aa',
78 handler : function (env) {
79 env.notebook.edit_mode();
80 }
81 },
82 'select-previous-cell' : {
83 help_index : 'da',
84 handler : function (env) {
85 var index = env.notebook.get_selected_index();
86 if (index !== 0 && index !== null) {
87 env.notebook.select_prev();
88 env.notebook.focus_cell();
89 }
90 }
91 },
92 'select-next-cell' : {
93 help_index : 'db',
94 handler : function (env) {
95 var index = env.notebook.get_selected_index();
96 if (index !== (env.notebook.ncells()-1) && index !== null) {
97 env.notebook.select_next();
98 env.notebook.focus_cell();
99 }
100 }
101 },
102 'cut-selected-cell' : {
103 icon: 'fa-cut',
104 help_index : 'ee',
105 handler : function (env) {
106 env.notebook.cut_cell();
107 }
108 },
109 'copy-selected-cell' : {
110 icon: 'fa-copy',
111 help_index : 'ef',
112 handler : function (env) {
113 env.notebook.copy_cell();
114 }
115 },
116 'paste-cell-before' : {
117 help_index : 'eg',
118 handler : function (env) {
119 env.notebook.paste_cell_above();
120 }
121 },
122 'paste-cell-after' : {
123 icon: 'fa-paste',
124 help_index : 'eh',
125 handler : function (env) {
126 env.notebook.paste_cell_below();
127 }
128 },
129 'insert-cell-before' : {
130 help_index : 'ec',
131 handler : function (env) {
132 env.notebook.insert_cell_above();
133 env.notebook.select_prev();
134 env.notebook.focus_cell();
135 }
136 },
137 'insert-cell-after' : {
138 icon : 'fa-plus',
139 help_index : 'ed',
140 handler : function (env) {
141 env.notebook.insert_cell_below();
142 env.notebook.select_next();
143 env.notebook.focus_cell();
144 }
145 },
146 'change-selected-cell-to-code-cell' : {
147 help : 'to code',
148 help_index : 'ca',
149 handler : function (env) {
150 env.notebook.to_code();
151 }
152 },
153 'change-selected-cell-to-markdown-cell' : {
154 help : 'to markdown',
155 help_index : 'cb',
156 handler : function (env) {
157 env.notebook.to_markdown();
158 }
159 },
160 'change-selected-cell-to-raw-cell' : {
161 help : 'to raw',
162 help_index : 'cc',
163 handler : function (env) {
164 env.notebook.to_raw();
165 }
166 },
167 'change-selected-cell-to-heading-1' : {
168 help : 'to heading 1',
169 help_index : 'cd',
170 handler : function (env) {
171 env.notebook.to_heading(undefined, 1);
172 }
173 },
174 'change-selected-cell-to-heading-2' : {
175 help : 'to heading 2',
176 help_index : 'ce',
177 handler : function (env) {
178 env.notebook.to_heading(undefined, 2);
179 }
180 },
181 'change-selected-cell-to-heading-3' : {
182 help : 'to heading 3',
183 help_index : 'cf',
184 handler : function (env) {
185 env.notebook.to_heading(undefined, 3);
186 }
187 },
188 'change-selected-cell-to-heading-4' : {
189 help : 'to heading 4',
190 help_index : 'cg',
191 handler : function (env) {
192 env.notebook.to_heading(undefined, 4);
193 }
194 },
195 'change-selected-cell-to-heading-5' : {
196 help : 'to heading 5',
197 help_index : 'ch',
198 handler : function (env) {
199 env.notebook.to_heading(undefined, 5);
200 }
201 },
202 'change-selected-cell-to-heading-6' : {
203 help : 'to heading 6',
204 help_index : 'ci',
205 handler : function (env) {
206 env.notebook.to_heading(undefined, 6);
207 }
208 },
209 'toggle-output-visibility-selected-cell' : {
210 help : 'toggle output',
211 help_index : 'gb',
212 handler : function (env) {
213 env.notebook.toggle_output();
214 }
215 },
216 'toggle-output-scrolling-selected-cell' : {
217 help : 'toggle output scrolling',
218 help_index : 'gc',
219 handler : function (env) {
220 env.notebook.toggle_output_scroll();
221 }
222 },
223 'move-selected-cell-down' : {
224 icon: 'fa-arrow-down',
225 help_index : 'eb',
226 handler : function (env) {
227 env.notebook.move_cell_down();
228 }
229 },
230 'move-selected-cell-up' : {
231 icon: 'fa-arrow-up',
232 help_index : 'ea',
233 handler : function (env) {
234 env.notebook.move_cell_up();
235 }
236 },
237 'toggle-line-number-selected-cell' : {
238 help : 'toggle line numbers',
239 help_index : 'ga',
240 handler : function (env) {
241 env.notebook.cell_toggle_line_numbers();
242 }
243 },
244 'show-keyboard-shortcut-help-dialog' : {
245 help_index : 'ge',
246 handler : function (env) {
247 env.quick_help.show_keyboard_shortcuts();
248 }
249 },
250 'delete-cell': {
251 help_index : 'ej',
252 handler : function (env) {
253 env.notebook.delete_cell();
254 }
255 },
256 'interrupt-kernel':{
257 icon: 'fa-stop',
258 help_index : 'ha',
259 handler : function (env) {
260 env.notebook.kernel.interrupt();
261 }
262 },
263 'restart-kernel':{
264 icon: 'fa-repeat',
265 help_index : 'hb',
266 handler : function (env) {
267 env.notebook.restart_kernel();
268 }
269 },
270 'undo-last-cell-deletion' : {
271 help_index : 'ei',
272 handler : function (env) {
273 env.notebook.undelete_cell();
274 }
275 },
276 'merge-selected-cell-with-cell-after' : {
277 help : 'merge cell below',
278 help_index : 'ek',
279 handler : function (env) {
280 env.notebook.merge_cell_below();
281 }
282 },
283 'close-pager' : {
284 help_index : 'gd',
285 handler : function (env) {
286 env.pager.collapse();
287 }
288 }
289
290 };
291
292 /**
293 * A bunch of `Advance actions` for IPython.
294 * Cf `Simple Action` plus the following properties.
295 *
296 * handler: first argument of the handler is the event that triggerd the action
297 * (typically keypress). The handler is responsible for any modification of the
298 * event and event propagation.
299 * Is also responsible for returning false if the event have to be further ignored,
300 * true, to tell keyboard manager that it ignored the event.
301 *
302 * the second parameter of the handler is the environemnt passed to Simple Actions
303 *
304 **/
305 var custom_ignore = {
306 'ignore':{
307 handler : function () {
308 return true;
309 }
310 },
311 'move-cursor-up-or-previous-cell':{
312 handler : function (env, event) {
313 var index = env.notebook.get_selected_index();
314 var cell = env.notebook.get_cell(index);
315 var cm = env.notebook.get_selected_cell().code_mirror;
316 var cur = cm.getCursor();
317 if (cell && cell.at_top() && index !== 0 && cur.ch === 0) {
318 if(event){
319 event.preventDefault();
320 }
321 env.notebook.command_mode();
322 env.notebook.select_prev();
323 env.notebook.edit_mode();
324 cm = env.notebook.get_selected_cell().code_mirror;
325 cm.setCursor(cm.lastLine(), 0);
326 }
327 return false;
328 }
329 },
330 'move-cursor-down-or-next-cell':{
331 handler : function (env, event) {
332 var index = env.notebook.get_selected_index();
333 var cell = env.notebook.get_cell(index);
334 if (cell.at_bottom() && index !== (env.notebook.ncells()-1)) {
335 if(event){
336 event.preventDefault();
337 }
338 env.notebook.command_mode();
339 env.notebook.select_next();
340 env.notebook.edit_mode();
341 var cm = env.notebook.get_selected_cell().code_mirror;
342 cm.setCursor(0, 0);
343 }
344 return false;
345 }
346 },
347 'scroll-down': {
348 handler: function(env, event) {
349 if(event){
350 event.preventDefault();
351 }
352 return env.notebook.scroll_manager.scroll(1);
353 },
354 },
355 'scroll-up': {
356 handler: function(env, event) {
357 if(event){
358 event.preventDefault();
359 }
360 return env.notebook.scroll_manager.scroll(-1);
361 },
362 },
363 'save-notebook':{
364 help: "Save and Checkpoint",
365 help_index : 'fb',
366 icon: 'fa-save',
367 handler : function (env, event) {
368 env.notebook.save_checkpoint();
369 if(event){
370 event.preventDefault();
371 }
372 return false;
373 }
374 },
375 };
376
377 // private stuff that prepend `.ipython` to actions names
378 // and uniformize/fill in missing pieces in of an action.
379 var _prepare_handler = function(registry, subkey, source){
380 registry['ipython.'+subkey] = {};
381 registry['ipython.'+subkey].help = source[subkey].help||subkey.replace(/-/g,' ');
382 registry['ipython.'+subkey].help_index = source[subkey].help_index;
383 registry['ipython.'+subkey].icon = source[subkey].icon;
384 return source[subkey].handler;
385 };
386
387 // Will actually generate/register all the IPython actions
388 var fun = function(){
389 var final_actions = {};
390 for(var k in _action){
391 // Js closure are function level not block level need to wrap in a IIFE
392 // and append ipython to event name these things do intercept event so are wrapped
393 // in a function that return false.
394 var handler = _prepare_handler(final_actions, k, _action);
395 (function(key, handler){
396 final_actions['ipython.'+key].handler = function(env, event){
397 handler(env);
398 if(event){
399 event.preventDefault();
400 }
401 return false;
402 };
403 })(k, handler);
404 }
405
406 for(var k in custom_ignore){
407 // Js closure are function level not block level need to wrap in a IIFE
408 // same as above, but decide for themselves wether or not they intercept events.
409 var handler = _prepare_handler(final_actions, k, custom_ignore);
410 (function(key, handler){
411 final_actions['ipython.'+key].handler = function(env, event){
412 return handler(env, event);
413 };
414 })(k, handler);
415 }
416
417 return final_actions;
418 };
419 ActionHandler.prototype._actions = fun();
420
421
422 /**
423 * extend the environment variable that will be pass to handlers
424 **/
425 ActionHandler.prototype.extend_env = function(env){
426 for(var k in env){
427 this.env[k] = env[k];
428 }
429 };
430
431 ActionHandler.prototype.register = function(action, name, prefix){
432 /**
433 * Register an `action` with an optional name and prefix.
434 *
435 * if name and prefix are not given they will be determined automatically.
436 * if action if just a `function` it will be wrapped in an anonymous action.
437 *
438 * @return the full name to access this action .
439 **/
440 action = this.normalise(action);
441 if( !name ){
442 name = 'autogenerated-'+String(action.handler);
443 }
444 prefix = prefix || 'auto';
445 var full_name = prefix+'.'+name;
446 this._actions[full_name] = action;
447 return full_name;
448
449 };
450
451
452 ActionHandler.prototype.normalise = function(data){
453 /**
454 * given an `action` or `function`, return a normalised `action`
455 * by setting all known attributes and removing unknown attributes;
456 **/
457 if(typeof(data) === 'function'){
458 data = {handler:data};
459 }
460 if(typeof(data.handler) !== 'function'){
461 throw('unknown datatype, cannot register');
462 }
463 var _data = data;
464 data = {};
465 data.handler = _data.handler;
466 data.help = data.help || '';
467 data.icon = data.icon || '';
468 data.help_index = data.help_index || '';
469 return data;
470 };
471
472 ActionHandler.prototype.get_name = function(name_or_data){
473 /**
474 * given an `action` or `name` of a action, return the name attached to this action.
475 * if given the name of and corresponding actions does not exist in registry, return `null`.
476 **/
477
478 if(typeof(name_or_data) === 'string'){
479 if(this.exists(name_or_data)){
480 return name_or_data;
481 } else {
482 return null;
483 }
484 } else {
485 return this.register(name_or_data);
486 }
487 };
488
489 ActionHandler.prototype.get = function(name){
490 return this._actions[name];
491 };
492
493 ActionHandler.prototype.call = function(name, event, env){
494 return this._actions[name].handler(env|| this.env, event);
495 };
496
497 ActionHandler.prototype.exists = function(name){
498 return (typeof(this._actions[name]) !== 'undefined');
499 };
500
501 return {init:ActionHandler};
502
503 });
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -5,6 +5,7 b' _build'
5 docs/man/*.gz
5 docs/man/*.gz
6 docs/source/api/generated
6 docs/source/api/generated
7 docs/source/config/options
7 docs/source/config/options
8 docs/source/interactive/magics-generated.txt
8 docs/gh-pages
9 docs/gh-pages
9 IPython/html/notebook/static/mathjax
10 IPython/html/notebook/static/mathjax
10 IPython/html/static/style/*.map
11 IPython/html/static/style/*.map
@@ -16,3 +17,6 b' __pycache__'
16 .ipynb_checkpoints
17 .ipynb_checkpoints
17 .tox
18 .tox
18 .DS_Store
19 .DS_Store
20 \#*#
21 .#*
22 .coverage
@@ -11,14 +11,15 b' before_install:'
11 # workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
11 # workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
12 - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
12 - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
13 # Pierre Carrier's PPA for PhantomJS and CasperJS
13 # Pierre Carrier's PPA for PhantomJS and CasperJS
14 - time sudo add-apt-repository -y ppa:pcarrier/ppa
14 - sudo add-apt-repository -y ppa:pcarrier/ppa
15 - time sudo apt-get update
15 # Needed to get recent version of pandoc in ubntu 12.04
16 - time sudo apt-get install pandoc casperjs libzmq3-dev
16 - sudo add-apt-repository -y ppa:marutter/c2d4u
17 # pin tornado < 4 for js tests while phantom is on super old webkit
17 - sudo apt-get update
18 - if [[ $GROUP == 'js' ]]; then pip install 'tornado<4'; fi
18 - sudo apt-get install pandoc casperjs libzmq3-dev
19 - time pip install -f https://nipy.bic.berkeley.edu/wheelhouse/travis jinja2 sphinx pygments tornado requests mock pyzmq jsonschema jsonpointer mistune
19 - git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
20 - 'if [[ $GROUP == js* ]]; then python -m IPython.external.mathjax; fi'
20 install:
21 install:
21 - time python setup.py install -q
22 - pip install -f travis-wheels/wheelhouse file://$PWD#egg=ipython[all]
22 script:
23 script:
23 - cd /tmp && iptest $GROUP
24 - cd /tmp && iptest $GROUP
24
25
@@ -5,13 +5,11 b' FROM ubuntu:14.04'
5
5
6 MAINTAINER IPython Project <ipython-dev@scipy.org>
6 MAINTAINER IPython Project <ipython-dev@scipy.org>
7
7
8 # Make sure apt is up to date
8 ENV DEBIAN_FRONTEND noninteractive
9 RUN apt-get update
10 RUN apt-get upgrade -y
11
9
12 # Not essential, but wise to set the lang
10 # Not essential, but wise to set the lang
13 # Note: Users with other languages should set this in their derivative image
11 # Note: Users with other languages should set this in their derivative image
14 RUN apt-get install -y language-pack-en
12 RUN apt-get update && apt-get install -y language-pack-en
15 ENV LANGUAGE en_US.UTF-8
13 ENV LANGUAGE en_US.UTF-8
16 ENV LANG en_US.UTF-8
14 ENV LANG en_US.UTF-8
17 ENV LC_ALL en_US.UTF-8
15 ENV LC_ALL en_US.UTF-8
@@ -20,14 +18,32 b' RUN locale-gen en_US.UTF-8'
20 RUN dpkg-reconfigure locales
18 RUN dpkg-reconfigure locales
21
19
22 # Python binary dependencies, developer tools
20 # Python binary dependencies, developer tools
23 RUN apt-get install -y -q build-essential make gcc zlib1g-dev git && \
21 RUN apt-get update && apt-get install -y -q \
24 apt-get install -y -q python python-dev python-pip python3-dev python3-pip && \
22 build-essential \
25 apt-get install -y -q libzmq3-dev sqlite3 libsqlite3-dev pandoc libcurl4-openssl-dev nodejs nodejs-legacy npm
23 make \
24 gcc \
25 zlib1g-dev \
26 git \
27 python \
28 python-dev \
29 python-pip \
30 python3-dev \
31 python3-pip \
32 python-sphinx \
33 python3-sphinx \
34 libzmq3-dev \
35 sqlite3 \
36 libsqlite3-dev \
37 pandoc \
38 libcurl4-openssl-dev \
39 nodejs \
40 nodejs-legacy \
41 npm
26
42
27 # In order to build from source, need less
43 # In order to build from source, need less
28 RUN npm install -g less
44 RUN npm install -g less@1.7.5
29
45
30 RUN apt-get -y install fabric
46 RUN pip install invoke
31
47
32 RUN mkdir -p /srv/
48 RUN mkdir -p /srv/
33 WORKDIR /srv/
49 WORKDIR /srv/
@@ -37,10 +53,14 b' RUN chmod -R +rX /srv/ipython'
37
53
38 # .[all] only works with -e, so use file://path#egg
54 # .[all] only works with -e, so use file://path#egg
39 # Can't use -e because ipython2 and ipython3 will clobber each other
55 # Can't use -e because ipython2 and ipython3 will clobber each other
40 RUN pip2 install --upgrade file:///srv/ipython#egg=ipython[all]
56 RUN pip2 install file:///srv/ipython#egg=ipython[all]
41 RUN pip3 install --upgrade file:///srv/ipython#egg=ipython[all]
57 RUN pip3 install file:///srv/ipython#egg=ipython[all]
42
58
43 # install kernels
59 # install kernels
44 RUN python2 -m IPython kernelspec install-self --system
60 RUN python2 -m IPython kernelspec install-self --system
45 RUN python3 -m IPython kernelspec install-self --system
61 RUN python3 -m IPython kernelspec install-self --system
46
62
63 WORKDIR /tmp/
64
65 RUN iptest2
66 RUN iptest3
@@ -6,6 +6,7 b''
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import json
9 import logging
10 import logging
10 import os
11 import os
11 import re
12 import re
@@ -123,7 +124,16 b' class Application(SingletonConfigurable):'
123
124
124 # A sequence of Configurable subclasses whose config=True attributes will
125 # A sequence of Configurable subclasses whose config=True attributes will
125 # be exposed at the command line.
126 # be exposed at the command line.
126 classes = List([])
127 classes = []
128 @property
129 def _help_classes(self):
130 """Define `App.help_classes` if CLI classes should differ from config file classes"""
131 return getattr(self, 'help_classes', self.classes)
132
133 @property
134 def _config_classes(self):
135 """Define `App.config_classes` if config file classes should differ from CLI classes."""
136 return getattr(self, 'config_classes', self.classes)
127
137
128 # The version string of this application.
138 # The version string of this application.
129 version = Unicode(u'0.0')
139 version = Unicode(u'0.0')
@@ -256,7 +266,7 b' class Application(SingletonConfigurable):'
256
266
257 lines = []
267 lines = []
258 classdict = {}
268 classdict = {}
259 for cls in self.classes:
269 for cls in self._help_classes:
260 # include all parents (up to, but excluding Configurable) in available names
270 # include all parents (up to, but excluding Configurable) in available names
261 for c in cls.mro()[:-3]:
271 for c in cls.mro()[:-3]:
262 classdict[c.__name__] = c
272 classdict[c.__name__] = c
@@ -331,7 +341,8 b' class Application(SingletonConfigurable):'
331 self.print_options()
341 self.print_options()
332
342
333 if classes:
343 if classes:
334 if self.classes:
344 help_classes = self._help_classes
345 if help_classes:
335 print("Class parameters")
346 print("Class parameters")
336 print("----------------")
347 print("----------------")
337 print()
348 print()
@@ -339,7 +350,7 b' class Application(SingletonConfigurable):'
339 print(p)
350 print(p)
340 print()
351 print()
341
352
342 for cls in self.classes:
353 for cls in help_classes:
343 cls.class_print_help()
354 cls.class_print_help()
344 print()
355 print()
345 else:
356 else:
@@ -412,7 +423,7 b' class Application(SingletonConfigurable):'
412 # it will be a dict by parent classname of classes in our list
423 # it will be a dict by parent classname of classes in our list
413 # that are descendents
424 # that are descendents
414 mro_tree = defaultdict(list)
425 mro_tree = defaultdict(list)
415 for cls in self.classes:
426 for cls in self._help_classes:
416 clsname = cls.__name__
427 clsname = cls.__name__
417 for parent in cls.mro()[1:-3]:
428 for parent in cls.mro()[1:-3]:
418 # exclude cls itself and Configurable,HasTraits,object
429 # exclude cls itself and Configurable,HasTraits,object
@@ -491,27 +502,32 b' class Application(SingletonConfigurable):'
491
502
492 yield each config object in turn.
503 yield each config object in turn.
493 """
504 """
494 pyloader = PyFileConfigLoader(basefilename+'.py', path=path, log=log)
505
495 jsonloader = JSONFileConfigLoader(basefilename+'.json', path=path, log=log)
506 if not isinstance(path, list):
496 config = None
507 path = [path]
497 for loader in [pyloader, jsonloader]:
508 for path in path[::-1]:
498 try:
509 # path list is in descending priority order, so load files backwards:
499 config = loader.load_config()
510 pyloader = PyFileConfigLoader(basefilename+'.py', path=path, log=log)
500 except ConfigFileNotFound:
511 jsonloader = JSONFileConfigLoader(basefilename+'.json', path=path, log=log)
501 pass
512 config = None
502 except Exception:
513 for loader in [pyloader, jsonloader]:
503 # try to get the full filename, but it will be empty in the
514 try:
504 # unlikely event that the error raised before filefind finished
515 config = loader.load_config()
505 filename = loader.full_filename or basefilename
516 except ConfigFileNotFound:
506 # problem while running the file
517 pass
507 if log:
518 except Exception:
508 log.error("Exception while loading config file %s",
519 # try to get the full filename, but it will be empty in the
509 filename, exc_info=True)
520 # unlikely event that the error raised before filefind finished
510 else:
521 filename = loader.full_filename or basefilename
511 if log:
522 # problem while running the file
512 log.debug("Loaded config file: %s", loader.full_filename)
523 if log:
513 if config:
524 log.error("Exception while loading config file %s",
514 yield config
525 filename, exc_info=True)
526 else:
527 if log:
528 log.debug("Loaded config file: %s", loader.full_filename)
529 if config:
530 yield config
515
531
516 raise StopIteration
532 raise StopIteration
517
533
@@ -520,8 +536,17 b' class Application(SingletonConfigurable):'
520 def load_config_file(self, filename, path=None):
536 def load_config_file(self, filename, path=None):
521 """Load config files by filename and path."""
537 """Load config files by filename and path."""
522 filename, ext = os.path.splitext(filename)
538 filename, ext = os.path.splitext(filename)
539 loaded = []
523 for config in self._load_config_files(filename, path=path, log=self.log):
540 for config in self._load_config_files(filename, path=path, log=self.log):
541 loaded.append(config)
524 self.update_config(config)
542 self.update_config(config)
543 if len(loaded) > 1:
544 collisions = loaded[0].collisions(loaded[1])
545 if collisions:
546 self.log.warn("Collisions detected in {0}.py and {0}.json config files."
547 " {0}.json has higher priority: {1}".format(
548 filename, json.dumps(collisions, indent=2),
549 ))
525
550
526
551
527 def generate_config_file(self):
552 def generate_config_file(self):
@@ -530,7 +555,7 b' class Application(SingletonConfigurable):'
530 lines.append('')
555 lines.append('')
531 lines.append('c = get_config()')
556 lines.append('c = get_config()')
532 lines.append('')
557 lines.append('')
533 for cls in self.classes:
558 for cls in self._config_classes:
534 lines.append(cls.class_config_section())
559 lines.append(cls.class_config_section())
535 return '\n'.join(lines)
560 return '\n'.join(lines)
536
561
@@ -193,7 +193,27 b' class Config(dict):'
193 to_update[k] = copy.deepcopy(v)
193 to_update[k] = copy.deepcopy(v)
194
194
195 self.update(to_update)
195 self.update(to_update)
196
196
197 def collisions(self, other):
198 """Check for collisions between two config objects.
199
200 Returns a dict of the form {"Class": {"trait": "collision message"}}`,
201 indicating which values have been ignored.
202
203 An empty dict indicates no collisions.
204 """
205 collisions = {}
206 for section in self:
207 if section not in other:
208 continue
209 mine = self[section]
210 theirs = other[section]
211 for key in mine:
212 if key in theirs and mine[key] != theirs[key]:
213 collisions.setdefault(section, {})
214 collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key])
215 return collisions
216
197 def __contains__(self, key):
217 def __contains__(self, key):
198 # allow nested contains of the form `"Section.key" in config`
218 # allow nested contains of the form `"Section.key" in config`
199 if '.' in key:
219 if '.' in key:
@@ -565,7 +585,7 b' class KeyValueConfigLoader(CommandLineConfigLoader):'
565
585
566
586
567 def _decode_argv(self, argv, enc=None):
587 def _decode_argv(self, argv, enc=None):
568 """decode argv if bytes, using stin.encoding, falling back on default enc"""
588 """decode argv if bytes, using stdin.encoding, falling back on default enc"""
569 uargv = []
589 uargv = []
570 if enc is None:
590 if enc is None:
571 enc = DEFAULT_ENCODING
591 enc = DEFAULT_ENCODING
@@ -1,27 +1,18 b''
1 # coding: utf-8
1 # coding: utf-8
2 """
2 """
3 Tests for IPython.config.application.Application
3 Tests for IPython.config.application.Application
4
5 Authors:
6
7 * Brian Granger
8 """
4 """
9
5
10 #-----------------------------------------------------------------------------
6 # Copyright (c) IPython Development Team.
11 # Copyright (C) 2008-2011 The IPython Development Team
7 # Distributed under the terms of the Modified BSD License.
12 #
13 # Distributed under the terms of the BSD License. The full license is in
14 # the file COPYING, distributed as part of this software.
15 #-----------------------------------------------------------------------------
16
17 #-----------------------------------------------------------------------------
18 # Imports
19 #-----------------------------------------------------------------------------
20
8
21 import logging
9 import logging
10 import os
22 from io import StringIO
11 from io import StringIO
23 from unittest import TestCase
12 from unittest import TestCase
24
13
14 pjoin = os.path.join
15
25 import nose.tools as nt
16 import nose.tools as nt
26
17
27 from IPython.config.configurable import Configurable
18 from IPython.config.configurable import Configurable
@@ -31,13 +22,11 b' from IPython.config.application import ('
31 Application
22 Application
32 )
23 )
33
24
25 from IPython.utils.tempdir import TemporaryDirectory
34 from IPython.utils.traitlets import (
26 from IPython.utils.traitlets import (
35 Bool, Unicode, Integer, List, Dict
27 Bool, Unicode, Integer, List, Dict
36 )
28 )
37
29
38 #-----------------------------------------------------------------------------
39 # Code
40 #-----------------------------------------------------------------------------
41
30
42 class Foo(Configurable):
31 class Foo(Configurable):
43
32
@@ -189,5 +178,21 b' class TestApplication(TestCase):'
189 def test_unicode_argv(self):
178 def test_unicode_argv(self):
190 app = MyApp()
179 app = MyApp()
191 app.parse_command_line(['ünîcødé'])
180 app.parse_command_line(['ünîcødé'])
192
181
182 def test_multi_file(self):
183 app = MyApp()
184 app.log = logging.getLogger()
185 name = 'config.py'
186 with TemporaryDirectory('_1') as td1:
187 with open(pjoin(td1, name), 'w') as f1:
188 f1.write("get_config().MyApp.Bar.b = 1")
189 with TemporaryDirectory('_2') as td2:
190 with open(pjoin(td2, name), 'w') as f2:
191 f2.write("get_config().MyApp.Bar.b = 2")
192 app.load_config_file(name, path=[td2, td1])
193 app.init_bar()
194 self.assertEqual(app.bar.b, 2)
195 app.load_config_file(name, path=[td1, td2])
196 app.init_bar()
197 self.assertEqual(app.bar.b, 1)
193
198
@@ -1,28 +1,12 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """Tests for IPython.config.loader"""
3 Tests for IPython.config.loader
4
5 Authors:
6
7 * Brian Granger
8 * Fernando Perez (design help)
9 """
10
3
11 #-----------------------------------------------------------------------------
4 # Copyright (c) IPython Development Team.
12 # Copyright (C) 2008 The IPython Development Team
5 # Distributed under the terms of the Modified BSD License.
13 #
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
16 #-----------------------------------------------------------------------------
17
18 #-----------------------------------------------------------------------------
19 # Imports
20 #-----------------------------------------------------------------------------
21
6
22 import os
7 import os
23 import pickle
8 import pickle
24 import sys
9 import sys
25 import json
26
10
27 from tempfile import mkstemp
11 from tempfile import mkstemp
28 from unittest import TestCase
12 from unittest import TestCase
@@ -43,10 +27,6 b' from IPython.config.loader import ('
43 ConfigError,
27 ConfigError,
44 )
28 )
45
29
46 #-----------------------------------------------------------------------------
47 # Actual tests
48 #-----------------------------------------------------------------------------
49
50
30
51 pyfile = """
31 pyfile = """
52 c = get_config()
32 c = get_config()
@@ -117,6 +97,34 b' class TestFileCL(TestCase):'
117 cl = JSONFileConfigLoader(fname, log=log)
97 cl = JSONFileConfigLoader(fname, log=log)
118 config = cl.load_config()
98 config = cl.load_config()
119 self._check_conf(config)
99 self._check_conf(config)
100
101 def test_collision(self):
102 a = Config()
103 b = Config()
104 self.assertEqual(a.collisions(b), {})
105 a.A.trait1 = 1
106 b.A.trait2 = 2
107 self.assertEqual(a.collisions(b), {})
108 b.A.trait1 = 1
109 self.assertEqual(a.collisions(b), {})
110 b.A.trait1 = 0
111 self.assertEqual(a.collisions(b), {
112 'A': {
113 'trait1': "1 ignored, using 0",
114 }
115 })
116 self.assertEqual(b.collisions(a), {
117 'A': {
118 'trait1': "0 ignored, using 1",
119 }
120 })
121 a.A.trait2 = 3
122 self.assertEqual(b.collisions(a), {
123 'A': {
124 'trait1': "0 ignored, using 1",
125 'trait2': "2 ignored, using 3",
126 }
127 })
120
128
121 def test_v2raise(self):
129 def test_v2raise(self):
122 fd, fname = mkstemp('.json')
130 fd, fname = mkstemp('.json')
@@ -7,11 +7,6 b' refactoring of what used to be the IPython/qt/console/qtconsoleapp.py'
7 # Copyright (c) IPython Development Team.
7 # Copyright (c) IPython Development Team.
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9
9
10 #-----------------------------------------------------------------------------
11 # Imports
12 #-----------------------------------------------------------------------------
13
14 # stdlib imports
15 import atexit
10 import atexit
16 import os
11 import os
17 import signal
12 import signal
@@ -19,7 +14,6 b' import sys'
19 import uuid
14 import uuid
20
15
21
16
22 # Local imports
23 from IPython.config.application import boolean_flag
17 from IPython.config.application import boolean_flag
24 from IPython.core.profiledir import ProfileDir
18 from IPython.core.profiledir import ProfileDir
25 from IPython.kernel.blocking import BlockingKernelClient
19 from IPython.kernel.blocking import BlockingKernelClient
@@ -40,18 +34,9 b' from IPython.kernel.zmq.session import Session, default_secure'
40 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
34 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
41 from IPython.kernel.connect import ConnectionFileMixin
35 from IPython.kernel.connect import ConnectionFileMixin
42
36
43 #-----------------------------------------------------------------------------
44 # Network Constants
45 #-----------------------------------------------------------------------------
46
47 from IPython.utils.localinterfaces import localhost
37 from IPython.utils.localinterfaces import localhost
48
38
49 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
50 # Globals
51 #-----------------------------------------------------------------------------
52
53
54 #-----------------------------------------------------------------------------
55 # Aliases and Flags
40 # Aliases and Flags
56 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
57
42
@@ -98,11 +83,7 b' aliases.update(app_aliases)'
98 # Classes
83 # Classes
99 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
100
85
101 #-----------------------------------------------------------------------------
86 classes = [KernelManager, ProfileDir, Session]
102 # IPythonConsole
103 #-----------------------------------------------------------------------------
104
105 classes = [IPKernelApp, ZMQInteractiveShell, KernelManager, ProfileDir, Session, InlineBackend]
106
87
107 class IPythonConsoleApp(ConnectionFileMixin):
88 class IPythonConsoleApp(ConnectionFileMixin):
108 name = 'ipython-console-mixin'
89 name = 'ipython-console-mixin'
@@ -158,8 +139,15 b' class IPythonConsoleApp(ConnectionFileMixin):'
158 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
139 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
159 to force a direct exit without any confirmation.""",
140 to force a direct exit without any confirmation.""",
160 )
141 )
161
142
162
143 @property
144 def help_classes(self):
145 """ConsoleApps can configure kernels on the command-line
146
147 But this shouldn't be written to a file
148 """
149 return self.classes + [IPKernelApp] + IPKernelApp.classes
150
163 def build_kernel_argv(self, argv=None):
151 def build_kernel_argv(self, argv=None):
164 """build argv to be passed to kernel subprocess"""
152 """build argv to be passed to kernel subprocess"""
165 if argv is None:
153 if argv is None:
@@ -303,7 +291,11 b' class IPythonConsoleApp(ConnectionFileMixin):'
303 self.exit(1)
291 self.exit(1)
304
292
305 self.kernel_manager.client_factory = self.kernel_client_class
293 self.kernel_manager.client_factory = self.kernel_client_class
306 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
294 # FIXME: remove special treatment of IPython kernels
295 kwargs = {}
296 if self.kernel_manager.ipython_kernel:
297 kwargs['extra_arguments'] = self.kernel_argv
298 self.kernel_manager.start_kernel(**kwargs)
307 atexit.register(self.kernel_manager.cleanup_ipc_files)
299 atexit.register(self.kernel_manager.cleanup_ipc_files)
308
300
309 if self.sshserver:
301 if self.sshserver:
@@ -69,6 +69,21 b' def default_aliases():'
69 # things which are executable
69 # things which are executable
70 ('lx', 'ls -F -o --color %l | grep ^-..x'),
70 ('lx', 'ls -F -o --color %l | grep ^-..x'),
71 ]
71 ]
72 elif sys.platform.startswith('openbsd') or sys.platform.startswith('netbsd'):
73 # OpenBSD, NetBSD. The ls implementation on these platforms do not support
74 # the -G switch and lack the ability to use colorized output.
75 ls_aliases = [('ls', 'ls -F'),
76 # long ls
77 ('ll', 'ls -F -l'),
78 # ls normal files only
79 ('lf', 'ls -F -l %l | grep ^-'),
80 # ls symbolic links
81 ('lk', 'ls -F -l %l | grep ^l'),
82 # directories or links to directories,
83 ('ldir', 'ls -F -l %l | grep /$'),
84 # things which are executable
85 ('lx', 'ls -F -l %l | grep ^-..x'),
86 ]
72 else:
87 else:
73 # BSD, OSX, etc.
88 # BSD, OSX, etc.
74 ls_aliases = [('ls', 'ls -F -G'),
89 ls_aliases = [('ls', 'ls -F -G'),
@@ -7,25 +7,10 b' handling configuration and creating configurables.'
7
7
8 The job of an :class:`Application` is to create the master configuration
8 The job of an :class:`Application` is to create the master configuration
9 object and then create the configurable objects, passing the config to them.
9 object and then create the configurable objects, passing the config to them.
10
11 Authors:
12
13 * Brian Granger
14 * Fernando Perez
15 * Min RK
16
17 """
10 """
18
11
19 #-----------------------------------------------------------------------------
12 # Copyright (c) IPython Development Team.
20 # Copyright (C) 2008 The IPython Development Team
13 # Distributed under the terms of the Modified BSD License.
21 #
22 # Distributed under the terms of the BSD License. The full license is in
23 # the file COPYING, distributed as part of this software.
24 #-----------------------------------------------------------------------------
25
26 #-----------------------------------------------------------------------------
27 # Imports
28 #-----------------------------------------------------------------------------
29
14
30 import atexit
15 import atexit
31 import glob
16 import glob
@@ -42,14 +27,18 b' from IPython.utils.path import get_ipython_dir, get_ipython_package_dir, ensure_'
42 from IPython.utils import py3compat
27 from IPython.utils import py3compat
43 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict, Set, Instance
28 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict, Set, Instance
44
29
45 #-----------------------------------------------------------------------------
30 if os.name == 'nt':
46 # Classes and functions
31 programdata = os.environ.get('PROGRAMDATA', None)
47 #-----------------------------------------------------------------------------
32 if programdata:
48
33 SYSTEM_CONFIG_DIRS = [os.path.join(programdata, 'ipython')]
34 else: # PROGRAMDATA is not defined by default on XP.
35 SYSTEM_CONFIG_DIRS = []
36 else:
37 SYSTEM_CONFIG_DIRS = [
38 "/usr/local/etc/ipython",
39 "/etc/ipython",
40 ]
49
41
50 #-----------------------------------------------------------------------------
51 # Base Application Class
52 #-----------------------------------------------------------------------------
53
42
54 # aliases and flags
43 # aliases and flags
55
44
@@ -100,7 +89,7 b' class BaseIPythonApplication(Application):'
100 builtin_profile_dir = Unicode(
89 builtin_profile_dir = Unicode(
101 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
90 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
102 )
91 )
103
92
104 config_file_paths = List(Unicode)
93 config_file_paths = List(Unicode)
105 def _config_file_paths_default(self):
94 def _config_file_paths_default(self):
106 return [py3compat.getcwd()]
95 return [py3compat.getcwd()]
@@ -210,11 +199,12 b' class BaseIPythonApplication(Application):'
210 return crashhandler.crash_handler_lite(etype, evalue, tb)
199 return crashhandler.crash_handler_lite(etype, evalue, tb)
211
200
212 def _ipython_dir_changed(self, name, old, new):
201 def _ipython_dir_changed(self, name, old, new):
213 str_old = py3compat.cast_bytes_py2(os.path.abspath(old),
202 if old is not None:
214 sys.getfilesystemencoding()
203 str_old = py3compat.cast_bytes_py2(os.path.abspath(old),
215 )
204 sys.getfilesystemencoding()
216 if str_old in sys.path:
205 )
217 sys.path.remove(str_old)
206 if str_old in sys.path:
207 sys.path.remove(str_old)
218 str_path = py3compat.cast_bytes_py2(os.path.abspath(new),
208 str_path = py3compat.cast_bytes_py2(os.path.abspath(new),
219 sys.getfilesystemencoding()
209 sys.getfilesystemencoding()
220 )
210 )
@@ -336,6 +326,7 b' class BaseIPythonApplication(Application):'
336
326
337 def init_config_files(self):
327 def init_config_files(self):
338 """[optionally] copy default config files into profile dir."""
328 """[optionally] copy default config files into profile dir."""
329 self.config_file_paths.extend(SYSTEM_CONFIG_DIRS)
339 # copy config files
330 # copy config files
340 path = self.builtin_profile_dir
331 path = self.builtin_profile_dir
341 if self.copy_config_files:
332 if self.copy_config_files:
@@ -277,7 +277,7 b' class Pdb(OldPdb):'
277 try:
277 try:
278 OldPdb.interaction(self, frame, traceback)
278 OldPdb.interaction(self, frame, traceback)
279 except KeyboardInterrupt:
279 except KeyboardInterrupt:
280 self.shell.write("\nKeyboardInterrupt\n")
280 self.shell.write('\n' + self.shell.get_exception_only())
281 break
281 break
282 else:
282 else:
283 break
283 break
@@ -21,6 +21,7 b' from __future__ import print_function'
21
21
22 import os
22 import os
23 import struct
23 import struct
24 import mimetypes
24
25
25 from IPython.core.formatters import _safe_get_formatter_method
26 from IPython.core.formatters import _safe_get_formatter_method
26 from IPython.utils.py3compat import (string_types, cast_bytes_py2, cast_unicode,
27 from IPython.utils.py3compat import (string_types, cast_bytes_py2, cast_unicode,
@@ -781,6 +782,90 b' class Image(DisplayObject):'
781 def _find_ext(self, s):
782 def _find_ext(self, s):
782 return unicode_type(s.split('.')[-1].lower())
783 return unicode_type(s.split('.')[-1].lower())
783
784
785 class Video(DisplayObject):
786
787 def __init__(self, data=None, url=None, filename=None, embed=None, mimetype=None):
788 """Create a video object given raw data or an URL.
789
790 When this object is returned by an input cell or passed to the
791 display function, it will result in the video being displayed
792 in the frontend.
793
794 Parameters
795 ----------
796 data : unicode, str or bytes
797 The raw image data or a URL or filename to load the data from.
798 This always results in embedded image data.
799 url : unicode
800 A URL to download the data from. If you specify `url=`,
801 the image data will not be embedded unless you also specify `embed=True`.
802 filename : unicode
803 Path to a local file to load the data from.
804 Videos from a file are always embedded.
805 embed : bool
806 Should the image data be embedded using a data URI (True) or be
807 loaded using an <img> tag. Set this to True if you want the image
808 to be viewable later with no internet connection in the notebook.
809
810 Default is `True`, unless the keyword argument `url` is set, then
811 default value is `False`.
812
813 Note that QtConsole is not able to display images if `embed` is set to `False`
814 mimetype: unicode
815 Specify the mimetype in case you load in a encoded video.
816 Examples
817 --------
818 Video('https://archive.org/download/Sita_Sings_the_Blues/Sita_Sings_the_Blues_small.mp4')
819 Video('path/to/video.mp4')
820 Video('path/to/video.mp4', embed=False)
821 """
822 if url is None and (data.startswith('http') or data.startswith('https')):
823 url = data
824 data = None
825 embed = False
826 elif os.path.exists(data):
827 filename = data
828 data = None
829
830 self.mimetype = mimetype
831 self.embed = embed if embed is not None else (filename is not None)
832 super(Video, self).__init__(data=data, url=url, filename=filename)
833
834 def _repr_html_(self):
835 # External URLs and potentially local files are not embedded into the
836 # notebook output.
837 if not self.embed:
838 url = self.url if self.url is not None else self.filename
839 output = """<video src="{0}" controls>
840 Your browser does not support the <code>video</code> element.
841 </video>""".format(url)
842 return output
843 # Embedded videos uses base64 encoded videos.
844 if self.filename is not None:
845 mimetypes.init()
846 mimetype, encoding = mimetypes.guess_type(self.filename)
847
848 video = open(self.filename, 'rb').read()
849 video_encoded = video.encode('base64')
850 else:
851 video_encoded = self.data
852 mimetype = self.mimetype
853 output = """<video controls>
854 <source src="data:{0};base64,{1}" type="{0}">
855 Your browser does not support the video tag.
856 </video>""".format(mimetype, video_encoded)
857 return output
858
859 def reload(self):
860 # TODO
861 pass
862
863 def _repr_png_(self):
864 # TODO
865 pass
866 def _repr_jpeg_(self):
867 # TODO
868 pass
784
869
785 def clear_output(wait=False):
870 def clear_output(wait=False):
786 """Clear the output of the current cell receiving output.
871 """Clear the output of the current cell receiving output.
@@ -2,25 +2,11 b''
2 """Displayhook for IPython.
2 """Displayhook for IPython.
3
3
4 This defines a callable class that IPython uses for `sys.displayhook`.
4 This defines a callable class that IPython uses for `sys.displayhook`.
5
6 Authors:
7
8 * Fernando Perez
9 * Brian Granger
10 * Robert Kern
11 """
5 """
12
6
13 #-----------------------------------------------------------------------------
7 # Copyright (c) IPython Development Team.
14 # Copyright (C) 2008-2011 The IPython Development Team
8 # Distributed under the terms of the Modified BSD License.
15 # Copyright (C) 2001-2007 Fernando Perez <fperez@colorado.edu>
9
16 #
17 # Distributed under the terms of the BSD License. The full license is in
18 # the file COPYING, distributed as part of this software.
19 #-----------------------------------------------------------------------------
20
21 #-----------------------------------------------------------------------------
22 # Imports
23 #-----------------------------------------------------------------------------
24 from __future__ import print_function
10 from __future__ import print_function
25
11
26 import sys
12 import sys
@@ -29,13 +15,9 b' from IPython.core.formatters import _safe_get_formatter_method'
29 from IPython.config.configurable import Configurable
15 from IPython.config.configurable import Configurable
30 from IPython.utils import io
16 from IPython.utils import io
31 from IPython.utils.py3compat import builtin_mod
17 from IPython.utils.py3compat import builtin_mod
32 from IPython.utils.traitlets import Instance
18 from IPython.utils.traitlets import Instance, Float
33 from IPython.utils.warn import warn
19 from IPython.utils.warn import warn
34
20
35 #-----------------------------------------------------------------------------
36 # Main displayhook class
37 #-----------------------------------------------------------------------------
38
39 # TODO: Move the various attributes (cache_size, [others now moved]). Some
21 # TODO: Move the various attributes (cache_size, [others now moved]). Some
40 # of these are also attributes of InteractiveShell. They should be on ONE object
22 # of these are also attributes of InteractiveShell. They should be on ONE object
41 # only and the other objects should ask that one object for their values.
23 # only and the other objects should ask that one object for their values.
@@ -48,10 +30,10 b' class DisplayHook(Configurable):'
48 """
30 """
49
31
50 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
32 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
33 cull_fraction = Float(0.2)
51
34
52 def __init__(self, shell=None, cache_size=1000, **kwargs):
35 def __init__(self, shell=None, cache_size=1000, **kwargs):
53 super(DisplayHook, self).__init__(shell=shell, **kwargs)
36 super(DisplayHook, self).__init__(shell=shell, **kwargs)
54
55 cache_size_min = 3
37 cache_size_min = 3
56 if cache_size <= 0:
38 if cache_size <= 0:
57 self.do_full_cache = 0
39 self.do_full_cache = 0
@@ -168,6 +150,9 b' class DisplayHook(Configurable):'
168 md_dict : dict (optional)
150 md_dict : dict (optional)
169 The metadata dict to be associated with the display data.
151 The metadata dict to be associated with the display data.
170 """
152 """
153 if 'text/plain' not in format_dict:
154 # nothing to do
155 return
171 # We want to print because we want to always make sure we have a
156 # We want to print because we want to always make sure we have a
172 # newline, even if all the prompt separators are ''. This is the
157 # newline, even if all the prompt separators are ''. This is the
173 # standard IPython behavior.
158 # standard IPython behavior.
@@ -193,13 +178,7 b' class DisplayHook(Configurable):'
193 # Avoid recursive reference when displaying _oh/Out
178 # Avoid recursive reference when displaying _oh/Out
194 if result is not self.shell.user_ns['_oh']:
179 if result is not self.shell.user_ns['_oh']:
195 if len(self.shell.user_ns['_oh']) >= self.cache_size and self.do_full_cache:
180 if len(self.shell.user_ns['_oh']) >= self.cache_size and self.do_full_cache:
196 warn('Output cache limit (currently '+
181 self.cull_cache()
197 repr(self.cache_size)+' entries) hit.\n'
198 'Flushing cache and resetting history counter...\n'
199 'The only history variables available will be _,__,___ and _1\n'
200 'with the current result.')
201
202 self.flush()
203 # Don't overwrite '_' and friends if '_' is in __builtin__ (otherwise
182 # Don't overwrite '_' and friends if '_' is in __builtin__ (otherwise
204 # we cause buggy behavior for things like gettext).
183 # we cause buggy behavior for things like gettext).
205
184
@@ -221,6 +200,9 b' class DisplayHook(Configurable):'
221
200
222 def log_output(self, format_dict):
201 def log_output(self, format_dict):
223 """Log the output."""
202 """Log the output."""
203 if 'text/plain' not in format_dict:
204 # nothing to do
205 return
224 if self.shell.logger.log_output:
206 if self.shell.logger.log_output:
225 self.shell.logger.log_write(format_dict['text/plain'], 'output')
207 self.shell.logger.log_write(format_dict['text/plain'], 'output')
226 self.shell.history_manager.output_hist_reprs[self.prompt_count] = \
208 self.shell.history_manager.output_hist_reprs[self.prompt_count] = \
@@ -255,6 +237,21 b' class DisplayHook(Configurable):'
255 self.log_output(format_dict)
237 self.log_output(format_dict)
256 self.finish_displayhook()
238 self.finish_displayhook()
257
239
240 def cull_cache(self):
241 """Output cache is full, cull the oldest entries"""
242 oh = self.shell.user_ns.get('_oh', {})
243 sz = len(oh)
244 cull_count = max(int(sz * self.cull_fraction), 2)
245 warn('Output cache limit (currently {sz} entries) hit.\n'
246 'Flushing oldest {cull_count} entries.'.format(sz=sz, cull_count=cull_count))
247
248 for i, n in enumerate(sorted(oh)):
249 if i >= cull_count:
250 break
251 self.shell.user_ns.pop('_%i' % n, None)
252 oh.pop(n, None)
253
254
258 def flush(self):
255 def flush(self):
259 if not self.do_full_cache:
256 if not self.do_full_cache:
260 raise ValueError("You shouldn't have reached the cache flush "
257 raise ValueError("You shouldn't have reached the cache flush "
@@ -63,14 +63,6 b' class EventManager(object):'
63 """Remove a callback from the given event."""
63 """Remove a callback from the given event."""
64 self.callbacks[event].remove(function)
64 self.callbacks[event].remove(function)
65
65
66 def reset(self, event):
67 """Clear all callbacks for the given event."""
68 self.callbacks[event] = []
69
70 def reset_all(self):
71 """Clear all callbacks for all events."""
72 self.callbacks = {n:[] for n in self.callbacks}
73
74 def trigger(self, event, *args, **kwargs):
66 def trigger(self, event, *args, **kwargs):
75 """Call callbacks for ``event``.
67 """Call callbacks for ``event``.
76
68
@@ -5,35 +5,22 b' Inheritance diagram:'
5
5
6 .. inheritance-diagram:: IPython.core.formatters
6 .. inheritance-diagram:: IPython.core.formatters
7 :parts: 3
7 :parts: 3
8
9 Authors:
10
11 * Robert Kern
12 * Brian Granger
13 """
8 """
14 #-----------------------------------------------------------------------------
15 # Copyright (C) 2010-2011, IPython Development Team.
16 #
17 # Distributed under the terms of the Modified BSD License.
18 #
19 # The full license is in the file COPYING.txt, distributed with this software.
20 #-----------------------------------------------------------------------------
21
9
22 #-----------------------------------------------------------------------------
10 # Copyright (c) IPython Development Team.
23 # Imports
11 # Distributed under the terms of the Modified BSD License.
24 #-----------------------------------------------------------------------------
25
12
26 # Stdlib imports
27 import abc
13 import abc
28 import inspect
14 import inspect
29 import sys
15 import sys
16 import traceback
30 import types
17 import types
31 import warnings
18 import warnings
32
19
33 from IPython.external.decorator import decorator
20 from IPython.external.decorator import decorator
34
21
35 # Our own imports
36 from IPython.config.configurable import Configurable
22 from IPython.config.configurable import Configurable
23 from IPython.core.getipython import get_ipython
37 from IPython.lib import pretty
24 from IPython.lib import pretty
38 from IPython.utils.traitlets import (
25 from IPython.utils.traitlets import (
39 Bool, Dict, Integer, Unicode, CUnicode, ObjectName, List,
26 Bool, Dict, Integer, Unicode, CUnicode, ObjectName, List,
@@ -223,6 +210,18 b' class DisplayFormatter(Configurable):'
223 # Formatters for specific format types (text, html, svg, etc.)
210 # Formatters for specific format types (text, html, svg, etc.)
224 #-----------------------------------------------------------------------------
211 #-----------------------------------------------------------------------------
225
212
213
214 def _safe_repr(obj):
215 """Try to return a repr of an object
216
217 always returns a string, at least.
218 """
219 try:
220 return repr(obj)
221 except Exception as e:
222 return "un-repr-able object (%r)" % e
223
224
226 class FormatterWarning(UserWarning):
225 class FormatterWarning(UserWarning):
227 """Warning class for errors in formatters"""
226 """Warning class for errors in formatters"""
228
227
@@ -231,13 +230,16 b' def warn_format_error(method, self, *args, **kwargs):'
231 """decorator for warning on failed format call"""
230 """decorator for warning on failed format call"""
232 try:
231 try:
233 r = method(self, *args, **kwargs)
232 r = method(self, *args, **kwargs)
234 except NotImplementedError as e:
233 except NotImplementedError:
235 # don't warn on NotImplementedErrors
234 # don't warn on NotImplementedErrors
236 return None
235 return None
237 except Exception as e:
236 except Exception:
238 warnings.warn("Exception in %s formatter: %s" % (self.format_type, e),
237 exc_info = sys.exc_info()
239 FormatterWarning,
238 ip = get_ipython()
240 )
239 if ip is not None:
240 ip.showtraceback(exc_info)
241 else:
242 traceback.print_exception(*exc_info)
241 return None
243 return None
242 if r is None or isinstance(r, self._return_type) or \
244 if r is None or isinstance(r, self._return_type) or \
243 (isinstance(r, tuple) and r and isinstance(r[0], self._return_type)):
245 (isinstance(r, tuple) and r and isinstance(r[0], self._return_type)):
@@ -245,7 +247,7 b' def warn_format_error(method, self, *args, **kwargs):'
245 else:
247 else:
246 warnings.warn(
248 warnings.warn(
247 "%s formatter returned invalid type %s (expected %s) for object: %s" % \
249 "%s formatter returned invalid type %s (expected %s) for object: %s" % \
248 (self.format_type, type(r), self._return_type, pretty._safe_repr(args[0])),
250 (self.format_type, type(r), self._return_type, _safe_repr(args[0])),
249 FormatterWarning
251 FormatterWarning
250 )
252 )
251
253
@@ -588,7 +590,14 b' class PlainTextFormatter(BaseFormatter):'
588 # This subclass ignores this attribute as it always need to return
590 # This subclass ignores this attribute as it always need to return
589 # something.
591 # something.
590 enabled = Bool(True, config=False)
592 enabled = Bool(True, config=False)
591
593
594 max_seq_length = Integer(pretty.MAX_SEQ_LENGTH, config=True,
595 help="""Truncate large collections (lists, dicts, tuples, sets) to this size.
596
597 Set to 0 to disable truncation.
598 """
599 )
600
592 # Look for a _repr_pretty_ methods to use for pretty printing.
601 # Look for a _repr_pretty_ methods to use for pretty printing.
593 print_method = ObjectName('_repr_pretty_')
602 print_method = ObjectName('_repr_pretty_')
594
603
@@ -672,7 +681,7 b' class PlainTextFormatter(BaseFormatter):'
672 def __call__(self, obj):
681 def __call__(self, obj):
673 """Compute the pretty representation of the object."""
682 """Compute the pretty representation of the object."""
674 if not self.pprint:
683 if not self.pprint:
675 return pretty._safe_repr(obj)
684 return repr(obj)
676 else:
685 else:
677 # This uses use StringIO, as cStringIO doesn't handle unicode.
686 # This uses use StringIO, as cStringIO doesn't handle unicode.
678 stream = StringIO()
687 stream = StringIO()
@@ -681,6 +690,7 b' class PlainTextFormatter(BaseFormatter):'
681 # or it will cause trouble.
690 # or it will cause trouble.
682 printer = pretty.RepresentationPrinter(stream, self.verbose,
691 printer = pretty.RepresentationPrinter(stream, self.verbose,
683 self.max_width, unicode_to_str(self.newline),
692 self.max_width, unicode_to_str(self.newline),
693 max_seq_length=self.max_seq_length,
684 singleton_pprinters=self.singleton_printers,
694 singleton_pprinters=self.singleton_printers,
685 type_pprinters=self.type_printers,
695 type_pprinters=self.type_printers,
686 deferred_pprinters=self.deferred_printers)
696 deferred_pprinters=self.deferred_printers)
@@ -836,6 +846,8 b' class PDFFormatter(BaseFormatter):'
836
846
837 print_method = ObjectName('_repr_pdf_')
847 print_method = ObjectName('_repr_pdf_')
838
848
849 _return_type = (bytes, unicode_type)
850
839
851
840 FormatterABC.register(BaseFormatter)
852 FormatterABC.register(BaseFormatter)
841 FormatterABC.register(PlainTextFormatter)
853 FormatterABC.register(PlainTextFormatter)
@@ -98,9 +98,24 b' def catch_corrupt_db(f, self, *a, **kw):'
98 # The hist_file is probably :memory: or something else.
98 # The hist_file is probably :memory: or something else.
99 raise
99 raise
100
100
101 class HistoryAccessorBase(Configurable):
102 """An abstract class for History Accessors """
101
103
104 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
105 raise NotImplementedError
106
107 def search(self, pattern="*", raw=True, search_raw=True,
108 output=False, n=None, unique=False):
109 raise NotImplementedError
110
111 def get_range(self, session, start=1, stop=None, raw=True,output=False):
112 raise NotImplementedError
102
113
103 class HistoryAccessor(Configurable):
114 def get_range_by_str(self, rangestr, raw=True, output=False):
115 raise NotImplementedError
116
117
118 class HistoryAccessor(HistoryAccessorBase):
104 """Access the history database without adding to it.
119 """Access the history database without adding to it.
105
120
106 This is intended for use by standalone history tools. IPython shells use
121 This is intended for use by standalone history tools. IPython shells use
@@ -544,7 +559,7 b' class HistoryManager(HistoryAccessor):'
544 self.input_hist_parsed[:] = [""]
559 self.input_hist_parsed[:] = [""]
545 self.input_hist_raw[:] = [""]
560 self.input_hist_raw[:] = [""]
546 self.new_session()
561 self.new_session()
547
562
548 # ------------------------------
563 # ------------------------------
549 # Methods for retrieving history
564 # Methods for retrieving history
550 # ------------------------------
565 # ------------------------------
@@ -20,6 +20,7 b' import ast'
20 import codeop
20 import codeop
21 import re
21 import re
22 import sys
22 import sys
23 import warnings
23
24
24 from IPython.utils.py3compat import cast_unicode
25 from IPython.utils.py3compat import cast_unicode
25 from IPython.core.inputtransformer import (leading_indent,
26 from IPython.core.inputtransformer import (leading_indent,
@@ -208,6 +209,8 b' class InputSplitter(object):'
208 _full_dedent = False
209 _full_dedent = False
209 # Boolean indicating whether the current block is complete
210 # Boolean indicating whether the current block is complete
210 _is_complete = None
211 _is_complete = None
212 # Boolean indicating whether the current block has an unrecoverable syntax error
213 _is_invalid = False
211
214
212 def __init__(self):
215 def __init__(self):
213 """Create a new InputSplitter instance.
216 """Create a new InputSplitter instance.
@@ -223,6 +226,7 b' class InputSplitter(object):'
223 self.source = ''
226 self.source = ''
224 self.code = None
227 self.code = None
225 self._is_complete = False
228 self._is_complete = False
229 self._is_invalid = False
226 self._full_dedent = False
230 self._full_dedent = False
227
231
228 def source_reset(self):
232 def source_reset(self):
@@ -232,6 +236,42 b' class InputSplitter(object):'
232 self.reset()
236 self.reset()
233 return out
237 return out
234
238
239 def check_complete(self, source):
240 """Return whether a block of code is ready to execute, or should be continued
241
242 This is a non-stateful API, and will reset the state of this InputSplitter.
243
244 Parameters
245 ----------
246 source : string
247 Python input code, which can be multiline.
248
249 Returns
250 -------
251 status : str
252 One of 'complete', 'incomplete', or 'invalid' if source is not a
253 prefix of valid code.
254 indent_spaces : int or None
255 The number of spaces by which to indent the next line of code. If
256 status is not 'incomplete', this is None.
257 """
258 self.reset()
259 try:
260 self.push(source)
261 except SyntaxError:
262 # Transformers in IPythonInputSplitter can raise SyntaxError,
263 # which push() will not catch.
264 return 'invalid', None
265 else:
266 if self._is_invalid:
267 return 'invalid', None
268 elif self.push_accepts_more():
269 return 'incomplete', self.indent_spaces
270 else:
271 return 'complete', None
272 finally:
273 self.reset()
274
235 def push(self, lines):
275 def push(self, lines):
236 """Push one or more lines of input.
276 """Push one or more lines of input.
237
277
@@ -261,6 +301,7 b' class InputSplitter(object):'
261 # exception is raised in compilation, we don't mislead by having
301 # exception is raised in compilation, we don't mislead by having
262 # inconsistent code/source attributes.
302 # inconsistent code/source attributes.
263 self.code, self._is_complete = None, None
303 self.code, self._is_complete = None, None
304 self._is_invalid = False
264
305
265 # Honor termination lines properly
306 # Honor termination lines properly
266 if source.endswith('\\\n'):
307 if source.endswith('\\\n'):
@@ -268,15 +309,18 b' class InputSplitter(object):'
268
309
269 self._update_indent(lines)
310 self._update_indent(lines)
270 try:
311 try:
271 self.code = self._compile(source, symbol="exec")
312 with warnings.catch_warnings():
313 warnings.simplefilter('error', SyntaxWarning)
314 self.code = self._compile(source, symbol="exec")
272 # Invalid syntax can produce any of a number of different errors from
315 # Invalid syntax can produce any of a number of different errors from
273 # inside the compiler, so we have to catch them all. Syntax errors
316 # inside the compiler, so we have to catch them all. Syntax errors
274 # immediately produce a 'ready' block, so the invalid Python can be
317 # immediately produce a 'ready' block, so the invalid Python can be
275 # sent to the kernel for evaluation with possible ipython
318 # sent to the kernel for evaluation with possible ipython
276 # special-syntax conversion.
319 # special-syntax conversion.
277 except (SyntaxError, OverflowError, ValueError, TypeError,
320 except (SyntaxError, OverflowError, ValueError, TypeError,
278 MemoryError):
321 MemoryError, SyntaxWarning):
279 self._is_complete = True
322 self._is_complete = True
323 self._is_invalid = True
280 else:
324 else:
281 # Compilation didn't produce any exceptions (though it may not have
325 # Compilation didn't produce any exceptions (though it may not have
282 # given a complete code object)
326 # given a complete code object)
@@ -461,7 +461,7 b' def classic_prompt():'
461 def ipy_prompt():
461 def ipy_prompt():
462 """Strip IPython's In [1]:/...: prompts."""
462 """Strip IPython's In [1]:/...: prompts."""
463 # FIXME: non-capturing version (?:...) usable?
463 # FIXME: non-capturing version (?:...) usable?
464 prompt_re = re.compile(r'^(In \[\d+\]: |\ {3,}\.{3,}: )')
464 prompt_re = re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)')
465 return _strip_prompts(prompt_re)
465 return _strip_prompts(prompt_re)
466
466
467
467
@@ -22,6 +22,7 b' import re'
22 import runpy
22 import runpy
23 import sys
23 import sys
24 import tempfile
24 import tempfile
25 import traceback
25 import types
26 import types
26 import subprocess
27 import subprocess
27 from io import open as io_open
28 from io import open as io_open
@@ -424,7 +425,7 b' class InteractiveShell(SingletonConfigurable):'
424 display_trap = Instance('IPython.core.display_trap.DisplayTrap')
425 display_trap = Instance('IPython.core.display_trap.DisplayTrap')
425 extension_manager = Instance('IPython.core.extensions.ExtensionManager')
426 extension_manager = Instance('IPython.core.extensions.ExtensionManager')
426 payload_manager = Instance('IPython.core.payload.PayloadManager')
427 payload_manager = Instance('IPython.core.payload.PayloadManager')
427 history_manager = Instance('IPython.core.history.HistoryManager')
428 history_manager = Instance('IPython.core.history.HistoryAccessorBase')
428 magics_manager = Instance('IPython.core.magic.MagicsManager')
429 magics_manager = Instance('IPython.core.magic.MagicsManager')
429
430
430 profile_dir = Instance('IPython.core.application.ProfileDir')
431 profile_dir = Instance('IPython.core.application.ProfileDir')
@@ -523,7 +524,6 b' class InteractiveShell(SingletonConfigurable):'
523 self.init_pdb()
524 self.init_pdb()
524 self.init_extension_manager()
525 self.init_extension_manager()
525 self.init_payload()
526 self.init_payload()
526 self.init_comms()
527 self.hooks.late_startup_hook()
527 self.hooks.late_startup_hook()
528 self.events.trigger('shell_initialized', self)
528 self.events.trigger('shell_initialized', self)
529 atexit.register(self.atexit_operations)
529 atexit.register(self.atexit_operations)
@@ -874,6 +874,8 b' class InteractiveShell(SingletonConfigurable):'
874 def init_events(self):
874 def init_events(self):
875 self.events = EventManager(self, available_events)
875 self.events = EventManager(self, available_events)
876
876
877 self.events.register("pre_execute", self._clear_warning_registry)
878
877 def register_post_execute(self, func):
879 def register_post_execute(self, func):
878 """DEPRECATED: Use ip.events.register('post_run_cell', func)
880 """DEPRECATED: Use ip.events.register('post_run_cell', func)
879
881
@@ -883,6 +885,13 b' class InteractiveShell(SingletonConfigurable):'
883 "ip.events.register('post_run_cell', func) instead.")
885 "ip.events.register('post_run_cell', func) instead.")
884 self.events.register('post_run_cell', func)
886 self.events.register('post_run_cell', func)
885
887
888 def _clear_warning_registry(self):
889 # clear the warning registry, so that different code blocks with
890 # overlapping line number ranges don't cause spurious suppression of
891 # warnings (see gh-6611 for details)
892 if "__warningregistry__" in self.user_global_ns:
893 del self.user_global_ns["__warningregistry__"]
894
886 #-------------------------------------------------------------------------
895 #-------------------------------------------------------------------------
887 # Things related to the "main" module
896 # Things related to the "main" module
888 #-------------------------------------------------------------------------
897 #-------------------------------------------------------------------------
@@ -1778,6 +1787,15 b' class InteractiveShell(SingletonConfigurable):'
1778 """
1787 """
1779 self.write_err("UsageError: %s" % exc)
1788 self.write_err("UsageError: %s" % exc)
1780
1789
1790 def get_exception_only(self, exc_tuple=None):
1791 """
1792 Return as a string (ending with a newline) the exception that
1793 just occurred, without any traceback.
1794 """
1795 etype, value, tb = self._get_exc_info(exc_tuple)
1796 msg = traceback.format_exception_only(etype, value)
1797 return ''.join(msg)
1798
1781 def showtraceback(self, exc_tuple=None, filename=None, tb_offset=None,
1799 def showtraceback(self, exc_tuple=None, filename=None, tb_offset=None,
1782 exception_only=False):
1800 exception_only=False):
1783 """Display the exception that just occurred.
1801 """Display the exception that just occurred.
@@ -1830,7 +1848,7 b' class InteractiveShell(SingletonConfigurable):'
1830 self._showtraceback(etype, value, stb)
1848 self._showtraceback(etype, value, stb)
1831
1849
1832 except KeyboardInterrupt:
1850 except KeyboardInterrupt:
1833 self.write_err("\nKeyboardInterrupt\n")
1851 self.write_err('\n' + self.get_exception_only())
1834
1852
1835 def _showtraceback(self, etype, evalue, stb):
1853 def _showtraceback(self, etype, evalue, stb):
1836 """Actually show a traceback.
1854 """Actually show a traceback.
@@ -2344,22 +2362,38 b' class InteractiveShell(SingletonConfigurable):'
2344 if path is not None:
2362 if path is not None:
2345 cmd = '"pushd %s &&"%s' % (path, cmd)
2363 cmd = '"pushd %s &&"%s' % (path, cmd)
2346 cmd = py3compat.unicode_to_str(cmd)
2364 cmd = py3compat.unicode_to_str(cmd)
2347 ec = os.system(cmd)
2365 try:
2366 ec = os.system(cmd)
2367 except KeyboardInterrupt:
2368 self.write_err('\n' + self.get_exception_only())
2369 ec = -2
2348 else:
2370 else:
2349 cmd = py3compat.unicode_to_str(cmd)
2371 cmd = py3compat.unicode_to_str(cmd)
2350 # Call the cmd using the OS shell, instead of the default /bin/sh, if set.
2372 # For posix the result of the subprocess.call() below is an exit
2351 ec = subprocess.call(cmd, shell=True, executable=os.environ.get('SHELL', None))
2373 # code, which by convention is zero for success, positive for
2352 # exit code is positive for program failure, or negative for
2374 # program failure. Exit codes above 128 are reserved for signals,
2353 # terminating signal number.
2375 # and the formula for converting a signal to an exit code is usually
2354
2376 # signal_number+128. To more easily differentiate between exit
2355 # Interpret ec > 128 as signal
2377 # codes and signals, ipython uses negative numbers. For instance
2356 # Some shells (csh, fish) don't follow sh/bash conventions for exit codes
2378 # since control-c is signal 2 but exit code 130, ipython's
2379 # _exit_code variable will read -2. Note that some shells like
2380 # csh and fish don't follow sh/bash conventions for exit codes.
2381 executable = os.environ.get('SHELL', None)
2382 try:
2383 # Use env shell instead of default /bin/sh
2384 ec = subprocess.call(cmd, shell=True, executable=executable)
2385 except KeyboardInterrupt:
2386 # intercept control-C; a long traceback is not useful here
2387 self.write_err('\n' + self.get_exception_only())
2388 ec = 130
2357 if ec > 128:
2389 if ec > 128:
2358 ec = -(ec - 128)
2390 ec = -(ec - 128)
2359
2391
2360 # We explicitly do NOT return the subprocess status code, because
2392 # We explicitly do NOT return the subprocess status code, because
2361 # a non-None value would trigger :func:`sys.displayhook` calls.
2393 # a non-None value would trigger :func:`sys.displayhook` calls.
2362 # Instead, we store the exit_code in user_ns.
2394 # Instead, we store the exit_code in user_ns. Note the semantics
2395 # of _exit_code: for control-c, _exit_code == -signal.SIGNIT,
2396 # but raising SystemExit(_exit_code) will give status 254!
2363 self.user_ns['_exit_code'] = ec
2397 self.user_ns['_exit_code'] = ec
2364
2398
2365 # use piped system by default, because it is better behaved
2399 # use piped system by default, because it is better behaved
@@ -2419,14 +2453,6 b' class InteractiveShell(SingletonConfigurable):'
2419 self.configurables.append(self.payload_manager)
2453 self.configurables.append(self.payload_manager)
2420
2454
2421 #-------------------------------------------------------------------------
2455 #-------------------------------------------------------------------------
2422 # Things related to widgets
2423 #-------------------------------------------------------------------------
2424
2425 def init_comms(self):
2426 # not implemented in the base class
2427 pass
2428
2429 #-------------------------------------------------------------------------
2430 # Things related to the prefilter
2456 # Things related to the prefilter
2431 #-------------------------------------------------------------------------
2457 #-------------------------------------------------------------------------
2432
2458
@@ -2565,10 +2591,16 b' class InteractiveShell(SingletonConfigurable):'
2565 silenced for zero status, as it is so common).
2591 silenced for zero status, as it is so common).
2566 raise_exceptions : bool (False)
2592 raise_exceptions : bool (False)
2567 If True raise exceptions everywhere. Meant for testing.
2593 If True raise exceptions everywhere. Meant for testing.
2594 shell_futures : bool (False)
2595 If True, the code will share future statements with the interactive
2596 shell. It will both be affected by previous __future__ imports, and
2597 any __future__ imports in the code will affect the shell. If False,
2598 __future__ imports are not shared in either direction.
2568
2599
2569 """
2600 """
2570 kw.setdefault('exit_ignore', False)
2601 kw.setdefault('exit_ignore', False)
2571 kw.setdefault('raise_exceptions', False)
2602 kw.setdefault('raise_exceptions', False)
2603 kw.setdefault('shell_futures', False)
2572
2604
2573 fname = os.path.abspath(os.path.expanduser(fname))
2605 fname = os.path.abspath(os.path.expanduser(fname))
2574
2606
@@ -2587,7 +2619,10 b' class InteractiveShell(SingletonConfigurable):'
2587
2619
2588 with prepended_to_syspath(dname):
2620 with prepended_to_syspath(dname):
2589 try:
2621 try:
2590 py3compat.execfile(fname,*where)
2622 glob, loc = (where + (None, ))[:2]
2623 py3compat.execfile(
2624 fname, glob, loc,
2625 self.compile if kw['shell_futures'] else None)
2591 except SystemExit as status:
2626 except SystemExit as status:
2592 # If the call was made with 0 or None exit status (sys.exit(0)
2627 # If the call was made with 0 or None exit status (sys.exit(0)
2593 # or sys.exit() ), don't bother showing a traceback, as both of
2628 # or sys.exit() ), don't bother showing a traceback, as both of
@@ -2608,7 +2643,7 b' class InteractiveShell(SingletonConfigurable):'
2608 # tb offset is 2 because we wrap execfile
2643 # tb offset is 2 because we wrap execfile
2609 self.showtraceback(tb_offset=2)
2644 self.showtraceback(tb_offset=2)
2610
2645
2611 def safe_execfile_ipy(self, fname):
2646 def safe_execfile_ipy(self, fname, shell_futures=False):
2612 """Like safe_execfile, but for .ipy or .ipynb files with IPython syntax.
2647 """Like safe_execfile, but for .ipy or .ipynb files with IPython syntax.
2613
2648
2614 Parameters
2649 Parameters
@@ -2616,6 +2651,11 b' class InteractiveShell(SingletonConfigurable):'
2616 fname : str
2651 fname : str
2617 The name of the file to execute. The filename must have a
2652 The name of the file to execute. The filename must have a
2618 .ipy or .ipynb extension.
2653 .ipy or .ipynb extension.
2654 shell_futures : bool (False)
2655 If True, the code will share future statements with the interactive
2656 shell. It will both be affected by previous __future__ imports, and
2657 any __future__ imports in the code will affect the shell. If False,
2658 __future__ imports are not shared in either direction.
2619 """
2659 """
2620 fname = os.path.abspath(os.path.expanduser(fname))
2660 fname = os.path.abspath(os.path.expanduser(fname))
2621
2661
@@ -2635,14 +2675,14 b' class InteractiveShell(SingletonConfigurable):'
2635 def get_cells():
2675 def get_cells():
2636 """generator for sequence of code blocks to run"""
2676 """generator for sequence of code blocks to run"""
2637 if fname.endswith('.ipynb'):
2677 if fname.endswith('.ipynb'):
2638 from IPython.nbformat import current
2678 from IPython.nbformat import read
2639 with open(fname) as f:
2679 with io_open(fname) as f:
2640 nb = current.read(f, 'json')
2680 nb = read(f, as_version=4)
2641 if not nb.worksheets:
2681 if not nb.cells:
2642 return
2682 return
2643 for cell in nb.worksheets[0].cells:
2683 for cell in nb.cells:
2644 if cell.cell_type == 'code':
2684 if cell.cell_type == 'code':
2645 yield cell.input
2685 yield cell.source
2646 else:
2686 else:
2647 with open(fname) as f:
2687 with open(fname) as f:
2648 yield f.read()
2688 yield f.read()
@@ -2654,7 +2694,7 b' class InteractiveShell(SingletonConfigurable):'
2654 # raised in user code. It would be nice if there were
2694 # raised in user code. It would be nice if there were
2655 # versions of run_cell that did raise, so
2695 # versions of run_cell that did raise, so
2656 # we could catch the errors.
2696 # we could catch the errors.
2657 self.run_cell(cell, silent=True, shell_futures=False)
2697 self.run_cell(cell, silent=True, shell_futures=shell_futures)
2658 except:
2698 except:
2659 self.showtraceback()
2699 self.showtraceback()
2660 warn('Unknown failure executing file: <%s>' % fname)
2700 warn('Unknown failure executing file: <%s>' % fname)
@@ -3072,7 +3112,15 b' class InteractiveShell(SingletonConfigurable):'
3072 namespace.
3112 namespace.
3073 """
3113 """
3074 ns = self.user_ns.copy()
3114 ns = self.user_ns.copy()
3075 ns.update(sys._getframe(depth+1).f_locals)
3115 try:
3116 frame = sys._getframe(depth+1)
3117 except ValueError:
3118 # This is thrown if there aren't that many frames on the stack,
3119 # e.g. if a script called run_line_magic() directly.
3120 pass
3121 else:
3122 ns.update(frame.f_locals)
3123
3076 try:
3124 try:
3077 # We have to use .vformat() here, because 'self' is a valid and common
3125 # We have to use .vformat() here, because 'self' is a valid and common
3078 # name, and expanding **ns for .format() would make it collide with
3126 # name, and expanding **ns for .format() would make it collide with
@@ -1,25 +1,12 b''
1 """Implementation of basic magic functions.
1 """Implementation of basic magic functions."""
2 """
2
3 #-----------------------------------------------------------------------------
4 # Copyright (c) 2012 The IPython Development Team.
5 #
6 # Distributed under the terms of the Modified BSD License.
7 #
8 # The full license is in the file COPYING.txt, distributed with this software.
9 #-----------------------------------------------------------------------------
10
11 #-----------------------------------------------------------------------------
12 # Imports
13 #-----------------------------------------------------------------------------
14 from __future__ import print_function
3 from __future__ import print_function
15
4
16 # Stdlib
17 import io
5 import io
18 import json
6 import json
19 import sys
7 import sys
20 from pprint import pformat
8 from pprint import pformat
21
9
22 # Our own packages
23 from IPython.core import magic_arguments, page
10 from IPython.core import magic_arguments, page
24 from IPython.core.error import UsageError
11 from IPython.core.error import UsageError
25 from IPython.core.magic import Magics, magics_class, line_magic, magic_escapes
12 from IPython.core.magic import Magics, magics_class, line_magic, magic_escapes
@@ -30,9 +17,6 b' from IPython.utils.path import unquote_filename'
30 from IPython.utils.py3compat import unicode_type
17 from IPython.utils.py3compat import unicode_type
31 from IPython.utils.warn import warn, error
18 from IPython.utils.warn import warn, error
32
19
33 #-----------------------------------------------------------------------------
34 # Magics class implementation
35 #-----------------------------------------------------------------------------
36
20
37 class MagicsDisplay(object):
21 class MagicsDisplay(object):
38 def __init__(self, magics_manager):
22 def __init__(self, magics_manager):
@@ -362,9 +346,6 b' Currently the magic system has the following functions:""",'
362 Proper color support under MS Windows requires the pyreadline library.
346 Proper color support under MS Windows requires the pyreadline library.
363 You can find it at:
347 You can find it at:
364 http://ipython.org/pyreadline.html
348 http://ipython.org/pyreadline.html
365 Gary's readline needs the ctypes module, from:
366 http://starship.python.net/crew/theller/ctypes
367 (Note that ctypes is already part of Python versions 2.5 and newer).
368
349
369 Defaulting color scheme to 'NoColor'"""
350 Defaulting color scheme to 'NoColor'"""
370 new_scheme = 'NoColor'
351 new_scheme = 'NoColor'
@@ -602,13 +583,6 b' Defaulting color scheme to \'NoColor\'"""'
602 'file extension will write the notebook as a Python script'
583 'file extension will write the notebook as a Python script'
603 )
584 )
604 @magic_arguments.argument(
585 @magic_arguments.argument(
605 '-f', '--format',
606 help='Convert an existing IPython notebook to a new format. This option '
607 'specifies the new format and can have the values: json, py. '
608 'The target filename is chosen automatically based on the new '
609 'format. The filename argument gives the name of the source file.'
610 )
611 @magic_arguments.argument(
612 'filename', type=unicode_type,
586 'filename', type=unicode_type,
613 help='Notebook name or filename'
587 help='Notebook name or filename'
614 )
588 )
@@ -616,41 +590,22 b' Defaulting color scheme to \'NoColor\'"""'
616 def notebook(self, s):
590 def notebook(self, s):
617 """Export and convert IPython notebooks.
591 """Export and convert IPython notebooks.
618
592
619 This function can export the current IPython history to a notebook file
593 This function can export the current IPython history to a notebook file.
620 or can convert an existing notebook file into a different format. For
594 For example, to export the history to "foo.ipynb" do "%notebook -e foo.ipynb".
621 example, to export the history to "foo.ipynb" do "%notebook -e foo.ipynb".
595 To export the history to "foo.py" do "%notebook -e foo.py".
622 To export the history to "foo.py" do "%notebook -e foo.py". To convert
623 "foo.ipynb" to "foo.json" do "%notebook -f json foo.ipynb". Possible
624 formats include (json/ipynb, py).
625 """
596 """
626 args = magic_arguments.parse_argstring(self.notebook, s)
597 args = magic_arguments.parse_argstring(self.notebook, s)
627
598
628 from IPython.nbformat import current
599 from IPython.nbformat import write, v4
629 args.filename = unquote_filename(args.filename)
600 args.filename = unquote_filename(args.filename)
630 if args.export:
601 if args.export:
631 fname, name, format = current.parse_filename(args.filename)
632 cells = []
602 cells = []
633 hist = list(self.shell.history_manager.get_range())
603 hist = list(self.shell.history_manager.get_range())
634 for session, prompt_number, input in hist[:-1]:
604 for session, execution_count, input in hist[:-1]:
635 cells.append(current.new_code_cell(prompt_number=prompt_number,
605 cells.append(v4.new_code_cell(
636 input=input))
606 execution_count=execution_count,
637 worksheet = current.new_worksheet(cells=cells)
607 source=source
638 nb = current.new_notebook(name=name,worksheets=[worksheet])
608 ))
639 with io.open(fname, 'w', encoding='utf-8') as f:
609 nb = v4.new_notebook(cells=cells)
640 current.write(nb, f, format);
610 with io.open(args.filename, 'w', encoding='utf-8') as f:
641 elif args.format is not None:
611 write(nb, f, version=4)
642 old_fname, old_name, old_format = current.parse_filename(args.filename)
643 new_format = args.format
644 if new_format == u'xml':
645 raise ValueError('Notebooks cannot be written as xml.')
646 elif new_format == u'ipynb' or new_format == u'json':
647 new_fname = old_name + u'.ipynb'
648 new_format = u'json'
649 elif new_format == u'py':
650 new_fname = old_name + u'.py'
651 else:
652 raise ValueError('Invalid notebook format: %s' % new_format)
653 with io.open(old_fname, 'r', encoding='utf-8') as f:
654 nb = current.read(f, old_format)
655 with io.open(new_fname, 'w', encoding='utf-8') as f:
656 current.write(nb, f, new_format)
@@ -1027,7 +1027,10 b' python-profiler package from non-free.""")'
1027 worst = max(worst, worst_tuning)
1027 worst = max(worst, worst_tuning)
1028 # Check best timing is greater than zero to avoid a
1028 # Check best timing is greater than zero to avoid a
1029 # ZeroDivisionError.
1029 # ZeroDivisionError.
1030 if worst > 4 * best and best > 0:
1030 # In cases where the slowest timing is lesser than a micosecond
1031 # we assume that it does not really matter if the fastest
1032 # timing is 4 times faster than the slowest timing or not.
1033 if worst > 4 * best and best > 0 and worst > 1e-6:
1031 print("The slowest run took %0.2f times longer than the "
1034 print("The slowest run took %0.2f times longer than the "
1032 "fastest. This could mean that an intermediate result "
1035 "fastest. This could mean that an intermediate result "
1033 "is being cached " % (worst / best))
1036 "is being cached " % (worst / best))
@@ -1057,7 +1060,7 b' python-profiler package from non-free.""")'
1057 following statement raises an error).
1060 following statement raises an error).
1058
1061
1059 This function provides very basic timing functionality. Use the timeit
1062 This function provides very basic timing functionality. Use the timeit
1060 magic for more controll over the measurement.
1063 magic for more control over the measurement.
1061
1064
1062 Examples
1065 Examples
1063 --------
1066 --------
@@ -371,14 +371,52 b' class OSMagics(Magics):'
371 if not 'q' in opts and self.shell.user_ns['_dh']:
371 if not 'q' in opts and self.shell.user_ns['_dh']:
372 print(self.shell.user_ns['_dh'][-1])
372 print(self.shell.user_ns['_dh'][-1])
373
373
374
375 @line_magic
374 @line_magic
376 def env(self, parameter_s=''):
375 def env(self, parameter_s=''):
377 """List environment variables."""
376 """List environment variables."""
378
377 if parameter_s.strip():
378 split = '=' if '=' in parameter_s else ' '
379 bits = parameter_s.split(split)
380 if len(bits) == 1:
381 key = parameter_s.strip()
382 if key in os.environ:
383 return os.environ[key]
384 else:
385 err = "Environment does not have key: {0}".format(key)
386 raise UsageError(err)
387 if len(bits) > 1:
388 return self.set_env(parameter_s)
379 return dict(os.environ)
389 return dict(os.environ)
380
390
381 @line_magic
391 @line_magic
392 def set_env(self, parameter_s):
393 """Set environment variables. Assumptions are that either "val" is a
394 name in the user namespace, or val is something that evaluates to a
395 string.
396
397 Usage:\\
398 %set_env var val
399 """
400 split = '=' if '=' in parameter_s else ' '
401 bits = parameter_s.split(split, 1)
402 if not parameter_s.strip() or len(bits)<2:
403 raise UsageError("usage is 'set_env var=val'")
404 var = bits[0].strip()
405 val = bits[1].strip()
406 if re.match(r'.*\s.*', var):
407 # an environment variable with whitespace is almost certainly
408 # not what the user intended. what's more likely is the wrong
409 # split was chosen, ie for "set_env cmd_args A=B", we chose
410 # '=' for the split and should have chosen ' '. to get around
411 # this, users should just assign directly to os.environ or use
412 # standard magic {var} expansion.
413 err = "refusing to set env var with whitespace: '{0}'"
414 err = err.format(val)
415 raise UsageError(err)
416 os.environ[py3compat.cast_bytes_py2(var)] = py3compat.cast_bytes_py2(val)
417 print('env: {0}={1}'.format(var,val))
418
419 @line_magic
382 def pushd(self, parameter_s=''):
420 def pushd(self, parameter_s=''):
383 """Place the current dir on stack and change directory.
421 """Place the current dir on stack and change directory.
384
422
@@ -40,6 +40,7 b' from IPython.utils.text import indent'
40 from IPython.utils.wildcard import list_namespace
40 from IPython.utils.wildcard import list_namespace
41 from IPython.utils.coloransi import TermColors, ColorScheme, ColorSchemeTable
41 from IPython.utils.coloransi import TermColors, ColorScheme, ColorSchemeTable
42 from IPython.utils.py3compat import cast_unicode, string_types, PY3
42 from IPython.utils.py3compat import cast_unicode, string_types, PY3
43 from IPython.utils.signatures import signature
43
44
44 # builtin docstrings to ignore
45 # builtin docstrings to ignore
45 _func_call_docstring = types.FunctionType.__call__.__doc__
46 _func_call_docstring = types.FunctionType.__call__.__doc__
@@ -390,7 +391,7 b' class Inspector:'
390 If any exception is generated, None is returned instead and the
391 If any exception is generated, None is returned instead and the
391 exception is suppressed."""
392 exception is suppressed."""
392 try:
393 try:
393 hdef = oname + inspect.formatargspec(*getargspec(obj))
394 hdef = oname + str(signature(obj))
394 return cast_unicode(hdef)
395 return cast_unicode(hdef)
395 except:
396 except:
396 return None
397 return None
@@ -38,7 +38,6 b' def page(strng, start=0, screen_lines=0, pager_cmd=None):'
38 source='page',
38 source='page',
39 data=data,
39 data=data,
40 start=start,
40 start=start,
41 screen_lines=screen_lines,
42 )
41 )
43 shell.payload_manager.write_payload(payload)
42 shell.payload_manager.write_payload(payload)
44
43
@@ -261,6 +261,8 b' class ProfileCreate(BaseIPythonApplication):'
261 from IPython.terminal.ipapp import TerminalIPythonApp
261 from IPython.terminal.ipapp import TerminalIPythonApp
262 apps = [TerminalIPythonApp]
262 apps = [TerminalIPythonApp]
263 for app_path in (
263 for app_path in (
264 'IPython.kernel.zmq.kernelapp.IPKernelApp',
265 'IPython.terminal.console.app.ZMQTerminalIPythonApp',
264 'IPython.qt.console.qtconsoleapp.IPythonQtConsoleApp',
266 'IPython.qt.console.qtconsoleapp.IPythonQtConsoleApp',
265 'IPython.html.notebookapp.NotebookApp',
267 'IPython.html.notebookapp.NotebookApp',
266 'IPython.nbconvert.nbconvertapp.NbConvertApp',
268 'IPython.nbconvert.nbconvertapp.NbConvertApp',
@@ -1,24 +1,9 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Pylab (matplotlib) support utilities.
2 """Pylab (matplotlib) support utilities."""
3
4 Authors
5 -------
6
7 * Fernando Perez.
8 * Brian Granger
9 """
10 from __future__ import print_function
3 from __future__ import print_function
11
4
12 #-----------------------------------------------------------------------------
5 # Copyright (c) IPython Development Team.
13 # Copyright (C) 2009 The IPython Development Team
6 # Distributed under the terms of the Modified BSD License.
14 #
15 # Distributed under the terms of the BSD License. The full license is in
16 # the file COPYING, distributed as part of this software.
17 #-----------------------------------------------------------------------------
18
19 #-----------------------------------------------------------------------------
20 # Imports
21 #-----------------------------------------------------------------------------
22
7
23 from io import BytesIO
8 from io import BytesIO
24
9
@@ -34,7 +19,9 b" backends = {'tk': 'TkAgg',"
34 'wx': 'WXAgg',
19 'wx': 'WXAgg',
35 'qt': 'Qt4Agg', # qt3 not supported
20 'qt': 'Qt4Agg', # qt3 not supported
36 'qt4': 'Qt4Agg',
21 'qt4': 'Qt4Agg',
22 'qt5': 'Qt5Agg',
37 'osx': 'MacOSX',
23 'osx': 'MacOSX',
24 'nbagg': 'nbAgg',
38 'inline' : 'module://IPython.kernel.zmq.pylab.backend_inline'}
25 'inline' : 'module://IPython.kernel.zmq.pylab.backend_inline'}
39
26
40 # We also need a reverse backends2guis mapping that will properly choose which
27 # We also need a reverse backends2guis mapping that will properly choose which
@@ -324,7 +324,7 b' class InteractiveShellApp(Configurable):'
324 self.log.warn("Unknown error in handling IPythonApp.exec_lines:")
324 self.log.warn("Unknown error in handling IPythonApp.exec_lines:")
325 self.shell.showtraceback()
325 self.shell.showtraceback()
326
326
327 def _exec_file(self, fname):
327 def _exec_file(self, fname, shell_futures=False):
328 try:
328 try:
329 full_filename = filefind(fname, [u'.', self.ipython_dir])
329 full_filename = filefind(fname, [u'.', self.ipython_dir])
330 except IOError as e:
330 except IOError as e:
@@ -346,11 +346,13 b' class InteractiveShellApp(Configurable):'
346 with preserve_keys(self.shell.user_ns, '__file__'):
346 with preserve_keys(self.shell.user_ns, '__file__'):
347 self.shell.user_ns['__file__'] = fname
347 self.shell.user_ns['__file__'] = fname
348 if full_filename.endswith('.ipy'):
348 if full_filename.endswith('.ipy'):
349 self.shell.safe_execfile_ipy(full_filename)
349 self.shell.safe_execfile_ipy(full_filename,
350 shell_futures=shell_futures)
350 else:
351 else:
351 # default to python, even without extension
352 # default to python, even without extension
352 self.shell.safe_execfile(full_filename,
353 self.shell.safe_execfile(full_filename,
353 self.shell.user_ns)
354 self.shell.user_ns,
355 shell_futures=shell_futures)
354 finally:
356 finally:
355 sys.argv = save_argv
357 sys.argv = save_argv
356
358
@@ -418,7 +420,7 b' class InteractiveShellApp(Configurable):'
418 elif self.file_to_run:
420 elif self.file_to_run:
419 fname = self.file_to_run
421 fname = self.file_to_run
420 try:
422 try:
421 self._exec_file(fname)
423 self._exec_file(fname, shell_futures=True)
422 except:
424 except:
423 self.log.warn("Error in executing file in user namespace: %s" %
425 self.log.warn("Error in executing file in user namespace: %s" %
424 fname)
426 fname)
@@ -52,9 +52,9 b' def test_image_filename_defaults():'
52 nt.assert_raises(ValueError, display.Image, data='this is not an image', format='badformat', embed=True)
52 nt.assert_raises(ValueError, display.Image, data='this is not an image', format='badformat', embed=True)
53 from IPython.html import DEFAULT_STATIC_FILES_PATH
53 from IPython.html import DEFAULT_STATIC_FILES_PATH
54 # check boths paths to allow packages to test at build and install time
54 # check boths paths to allow packages to test at build and install time
55 imgfile = os.path.join(tpath, 'html/static/base/images/ipynblogo.png')
55 imgfile = os.path.join(tpath, 'html/static/base/images/logo.png')
56 if not os.path.exists(imgfile):
56 if not os.path.exists(imgfile):
57 imgfile = os.path.join(DEFAULT_STATIC_FILES_PATH, 'base/images/ipynblogo.png')
57 imgfile = os.path.join(DEFAULT_STATIC_FILES_PATH, 'base/images/logo.png')
58 img = display.Image(filename=imgfile)
58 img = display.Image(filename=imgfile)
59 nt.assert_equal('png', img.format)
59 nt.assert_equal('png', img.format)
60 nt.assert_is_not_none(img._repr_png_())
60 nt.assert_is_not_none(img._repr_png_())
@@ -25,22 +25,8 b' class CallbackTests(unittest.TestCase):'
25 self.em.trigger('ping_received')
25 self.em.trigger('ping_received')
26 self.assertEqual(cb.call_count, 1)
26 self.assertEqual(cb.call_count, 1)
27
27
28 def test_reset(self):
29 cb = Mock()
30 self.em.register('ping_received', cb)
31 self.em.reset('ping_received')
32 self.em.trigger('ping_received')
33 assert not cb.called
34
35 def test_reset_all(self):
36 cb = Mock()
37 self.em.register('ping_received', cb)
38 self.em.reset_all()
39 self.em.trigger('ping_received')
40 assert not cb.called
41
42 def test_cb_error(self):
28 def test_cb_error(self):
43 cb = Mock(side_effect=ValueError)
29 cb = Mock(side_effect=ValueError)
44 self.em.register('ping_received', cb)
30 self.em.register('ping_received', cb)
45 with tt.AssertPrints("Error in callback"):
31 with tt.AssertPrints("Error in callback"):
46 self.em.trigger('ping_received') No newline at end of file
32 self.em.trigger('ping_received')
@@ -25,6 +25,10 b' class B(A):'
25 class C:
25 class C:
26 pass
26 pass
27
27
28 class BadRepr(object):
29 def __repr__(self):
30 raise ValueError("bad repr")
31
28 class BadPretty(object):
32 class BadPretty(object):
29 _repr_pretty_ = None
33 _repr_pretty_ = None
30
34
@@ -234,30 +238,30 b' def test_pop_string():'
234 nt.assert_is(f.pop(type_str, None), None)
238 nt.assert_is(f.pop(type_str, None), None)
235
239
236
240
237 def test_warn_error_method():
241 def test_error_method():
238 f = HTMLFormatter()
242 f = HTMLFormatter()
239 class BadHTML(object):
243 class BadHTML(object):
240 def _repr_html_(self):
244 def _repr_html_(self):
241 return 1/0
245 raise ValueError("Bad HTML")
242 bad = BadHTML()
246 bad = BadHTML()
243 with capture_output() as captured:
247 with capture_output() as captured:
244 result = f(bad)
248 result = f(bad)
245 nt.assert_is(result, None)
249 nt.assert_is(result, None)
246 nt.assert_in("FormatterWarning", captured.stderr)
250 nt.assert_in("Traceback", captured.stdout)
247 nt.assert_in("text/html", captured.stderr)
251 nt.assert_in("Bad HTML", captured.stdout)
248 nt.assert_in("zero", captured.stderr)
252 nt.assert_in("_repr_html_", captured.stdout)
249
253
250 def test_nowarn_notimplemented():
254 def test_nowarn_notimplemented():
251 f = HTMLFormatter()
255 f = HTMLFormatter()
252 class HTMLNotImplemented(object):
256 class HTMLNotImplemented(object):
253 def _repr_html_(self):
257 def _repr_html_(self):
254 raise NotImplementedError
258 raise NotImplementedError
255 return 1/0
256 h = HTMLNotImplemented()
259 h = HTMLNotImplemented()
257 with capture_output() as captured:
260 with capture_output() as captured:
258 result = f(h)
261 result = f(h)
259 nt.assert_is(result, None)
262 nt.assert_is(result, None)
260 nt.assert_not_in("FormatterWarning", captured.stderr)
263 nt.assert_equal("", captured.stderr)
264 nt.assert_equal("", captured.stdout)
261
265
262 def test_warn_error_for_type():
266 def test_warn_error_for_type():
263 f = HTMLFormatter()
267 f = HTMLFormatter()
@@ -265,11 +269,11 b' def test_warn_error_for_type():'
265 with capture_output() as captured:
269 with capture_output() as captured:
266 result = f(5)
270 result = f(5)
267 nt.assert_is(result, None)
271 nt.assert_is(result, None)
268 nt.assert_in("FormatterWarning", captured.stderr)
272 nt.assert_in("Traceback", captured.stdout)
269 nt.assert_in("text/html", captured.stderr)
273 nt.assert_in("NameError", captured.stdout)
270 nt.assert_in("name_error", captured.stderr)
274 nt.assert_in("name_error", captured.stdout)
271
275
272 def test_warn_error_pretty_method():
276 def test_error_pretty_method():
273 f = PlainTextFormatter()
277 f = PlainTextFormatter()
274 class BadPretty(object):
278 class BadPretty(object):
275 def _repr_pretty_(self):
279 def _repr_pretty_(self):
@@ -278,9 +282,23 b' def test_warn_error_pretty_method():'
278 with capture_output() as captured:
282 with capture_output() as captured:
279 result = f(bad)
283 result = f(bad)
280 nt.assert_is(result, None)
284 nt.assert_is(result, None)
281 nt.assert_in("FormatterWarning", captured.stderr)
285 nt.assert_in("Traceback", captured.stdout)
282 nt.assert_in("text/plain", captured.stderr)
286 nt.assert_in("_repr_pretty_", captured.stdout)
283 nt.assert_in("argument", captured.stderr)
287 nt.assert_in("given", captured.stdout)
288 nt.assert_in("argument", captured.stdout)
289
290
291 def test_bad_repr_traceback():
292 f = PlainTextFormatter()
293 bad = BadRepr()
294 with capture_output() as captured:
295 result = f(bad)
296 # catches error, returns None
297 nt.assert_is(result, None)
298 nt.assert_in("Traceback", captured.stdout)
299 nt.assert_in("__repr__", captured.stdout)
300 nt.assert_in("ValueError", captured.stdout)
301
284
302
285 class MakePDF(object):
303 class MakePDF(object):
286 def _repr_pdf_(self):
304 def _repr_pdf_(self):
@@ -320,3 +338,15 b' def test_format_config():'
320 result = f(Config)
338 result = f(Config)
321 nt.assert_is(result, None)
339 nt.assert_is(result, None)
322 nt.assert_equal(captured.stderr, "")
340 nt.assert_equal(captured.stderr, "")
341
342 def test_pretty_max_seq_length():
343 f = PlainTextFormatter(max_seq_length=1)
344 lis = list(range(3))
345 text = f(lis)
346 nt.assert_equal(text, '[0, ...]')
347 f.max_seq_length = 0
348 text = f(lis)
349 nt.assert_equal(text, '[0, 1, 2]')
350 text = f(list(range(1024)))
351 lines = text.splitlines()
352 nt.assert_equal(len(lines), 1024)
@@ -342,6 +342,14 b' class InputSplitterTestCase(unittest.TestCase):'
342 isp.push(r"(1 \ ")
342 isp.push(r"(1 \ ")
343 self.assertFalse(isp.push_accepts_more())
343 self.assertFalse(isp.push_accepts_more())
344
344
345 def test_check_complete(self):
346 isp = self.isp
347 self.assertEqual(isp.check_complete("a = 1"), ('complete', None))
348 self.assertEqual(isp.check_complete("for a in range(5):"), ('incomplete', 4))
349 self.assertEqual(isp.check_complete("raise = 2"), ('invalid', None))
350 self.assertEqual(isp.check_complete("a = [1,\n2,"), ('incomplete', 0))
351 self.assertEqual(isp.check_complete("def a():\n x=1\n global x"), ('invalid', None))
352
345 class InteractiveLoopTestCase(unittest.TestCase):
353 class InteractiveLoopTestCase(unittest.TestCase):
346 """Tests for an interactive loop like a python shell.
354 """Tests for an interactive loop like a python shell.
347 """
355 """
@@ -228,6 +228,16 b' syntax_ml = \\'
228 (' ...: print i',' print i'),
228 (' ...: print i',' print i'),
229 (' ...: ', ''),
229 (' ...: ', ''),
230 ],
230 ],
231 [('In [24]: for i in range(10):','for i in range(10):'),
232 # Sometimes whitespace preceding '...' has been removed
233 ('...: print i',' print i'),
234 ('...: ', ''),
235 ],
236 [('In [24]: for i in range(10):','for i in range(10):'),
237 # Space after last continuation prompt has been removed (issue #6674)
238 ('...: print i',' print i'),
239 ('...:', ''),
240 ],
231 [('In [2]: a="""','a="""'),
241 [('In [2]: a="""','a="""'),
232 (' ...: 123"""','123"""'),
242 (' ...: 123"""','123"""'),
233 ],
243 ],
@@ -301,7 +301,10 b' class InteractiveShellTestCase(unittest.TestCase):'
301 assert post_explicit.called
301 assert post_explicit.called
302 finally:
302 finally:
303 # remove post-exec
303 # remove post-exec
304 ip.events.reset_all()
304 ip.events.unregister('pre_run_cell', pre_explicit)
305 ip.events.unregister('pre_execute', pre_always)
306 ip.events.unregister('post_run_cell', post_explicit)
307 ip.events.unregister('post_execute', post_always)
305
308
306 def test_silent_noadvance(self):
309 def test_silent_noadvance(self):
307 """run_cell(silent=True) doesn't advance execution_count"""
310 """run_cell(silent=True) doesn't advance execution_count"""
@@ -479,6 +482,24 b' class InteractiveShellTestCase(unittest.TestCase):'
479 mod = ip.new_main_mod(u'%s.py' % name, name)
482 mod = ip.new_main_mod(u'%s.py' % name, name)
480 self.assertEqual(mod.__name__, name)
483 self.assertEqual(mod.__name__, name)
481
484
485 def test_get_exception_only(self):
486 try:
487 raise KeyboardInterrupt
488 except KeyboardInterrupt:
489 msg = ip.get_exception_only()
490 self.assertEqual(msg, 'KeyboardInterrupt\n')
491
492 class DerivedInterrupt(KeyboardInterrupt):
493 pass
494 try:
495 raise DerivedInterrupt("foo")
496 except KeyboardInterrupt:
497 msg = ip.get_exception_only()
498 if sys.version_info[0] <= 2:
499 self.assertEqual(msg, 'DerivedInterrupt: foo\n')
500 else:
501 self.assertEqual(msg, 'IPython.core.tests.test_interactiveshell.DerivedInterrupt: foo\n')
502
482 class TestSafeExecfileNonAsciiPath(unittest.TestCase):
503 class TestSafeExecfileNonAsciiPath(unittest.TestCase):
483
504
484 @onlyif_unicode_paths
505 @onlyif_unicode_paths
@@ -541,6 +562,16 b' class TestSystemRaw(unittest.TestCase, ExitCodeChecks):'
541 cmd = u'''python -c "'åäö'" '''
562 cmd = u'''python -c "'åäö'" '''
542 ip.system_raw(cmd)
563 ip.system_raw(cmd)
543
564
565 @mock.patch('subprocess.call', side_effect=KeyboardInterrupt)
566 @mock.patch('os.system', side_effect=KeyboardInterrupt)
567 def test_control_c(self, *mocks):
568 try:
569 self.system("sleep 1 # wont happen")
570 except KeyboardInterrupt:
571 self.fail("system call should intercept "
572 "keyboard interrupt from subprocess.call")
573 self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGINT)
574
544 # TODO: Exit codes are currently ignored on Windows.
575 # TODO: Exit codes are currently ignored on Windows.
545 class TestSystemPipedExitCode(unittest.TestCase, ExitCodeChecks):
576 class TestSystemPipedExitCode(unittest.TestCase, ExitCodeChecks):
546 system = ip.system_piped
577 system = ip.system_piped
@@ -840,3 +871,17 b' class TestSyntaxErrorTransformer(unittest.TestCase):'
840
871
841
872
842
873
874 def test_warning_suppression():
875 ip.run_cell("import warnings")
876 try:
877 with tt.AssertPrints("UserWarning: asdf", channel="stderr"):
878 ip.run_cell("warnings.warn('asdf')")
879 # Here's the real test -- if we run that again, we should get the
880 # warning again. Traditionally, each warning was only issued once per
881 # IPython session (approximately), even if the user typed in new and
882 # different code that should have also triggered the warning, leading
883 # to much confusion.
884 with tt.AssertPrints("UserWarning: asdf", channel="stderr"):
885 ip.run_cell("warnings.warn('asdf')")
886 finally:
887 ip.run_cell("del warnings")
@@ -5,10 +5,6 b' Needs to be run by nose (to make ipython session available).'
5 """
5 """
6 from __future__ import absolute_import
6 from __future__ import absolute_import
7
7
8 #-----------------------------------------------------------------------------
9 # Imports
10 #-----------------------------------------------------------------------------
11
12 import io
8 import io
13 import os
9 import os
14 import sys
10 import sys
@@ -23,6 +19,7 b' except ImportError:'
23 import nose.tools as nt
19 import nose.tools as nt
24
20
25 from IPython.core import magic
21 from IPython.core import magic
22 from IPython.core.error import UsageError
26 from IPython.core.magic import (Magics, magics_class, line_magic,
23 from IPython.core.magic import (Magics, magics_class, line_magic,
27 cell_magic, line_cell_magic,
24 cell_magic, line_cell_magic,
28 register_line_magic, register_cell_magic,
25 register_line_magic, register_cell_magic,
@@ -40,9 +37,6 b' if py3compat.PY3:'
40 else:
37 else:
41 from StringIO import StringIO
38 from StringIO import StringIO
42
39
43 #-----------------------------------------------------------------------------
44 # Test functions begin
45 #-----------------------------------------------------------------------------
46
40
47 @magic.magics_class
41 @magic.magics_class
48 class DummyMagics(magic.Magics): pass
42 class DummyMagics(magic.Magics): pass
@@ -624,7 +618,7 b' def test_extension():'
624
618
625
619
626 # The nose skip decorator doesn't work on classes, so this uses unittest's skipIf
620 # The nose skip decorator doesn't work on classes, so this uses unittest's skipIf
627 @skipIf(dec.module_not_available('IPython.nbformat.current'), 'nbformat not importable')
621 @skipIf(dec.module_not_available('IPython.nbformat'), 'nbformat not importable')
628 class NotebookExportMagicTests(TestCase):
622 class NotebookExportMagicTests(TestCase):
629 def test_notebook_export_json(self):
623 def test_notebook_export_json(self):
630 with TemporaryDirectory() as td:
624 with TemporaryDirectory() as td:
@@ -632,39 +626,36 b' class NotebookExportMagicTests(TestCase):'
632 _ip.ex(py3compat.u_format(u"u = {u}'héllo'"))
626 _ip.ex(py3compat.u_format(u"u = {u}'héllo'"))
633 _ip.magic("notebook -e %s" % outfile)
627 _ip.magic("notebook -e %s" % outfile)
634
628
635 def test_notebook_export_py(self):
636 with TemporaryDirectory() as td:
637 outfile = os.path.join(td, "nb.py")
638 _ip.ex(py3compat.u_format(u"u = {u}'héllo'"))
639 _ip.magic("notebook -e %s" % outfile)
640
629
641 def test_notebook_reformat_py(self):
630 class TestEnv(TestCase):
642 from IPython.nbformat.v3.tests.nbexamples import nb0
643 from IPython.nbformat import current
644 with TemporaryDirectory() as td:
645 infile = os.path.join(td, "nb.ipynb")
646 with io.open(infile, 'w', encoding='utf-8') as f:
647 current.write(nb0, f, 'json')
648
631
649 _ip.ex(py3compat.u_format(u"u = {u}'héllo'"))
632 def test_env(self):
650 _ip.magic("notebook -f py %s" % infile)
633 env = _ip.magic("env")
634 self.assertTrue(isinstance(env, dict))
651
635
652 def test_notebook_reformat_json(self):
636 def test_env_get_set_simple(self):
653 from IPython.nbformat.v3.tests.nbexamples import nb0
637 env = _ip.magic("env var val1")
654 from IPython.nbformat import current
638 self.assertEqual(env, None)
655 with TemporaryDirectory() as td:
639 self.assertEqual(os.environ['var'], 'val1')
656 infile = os.path.join(td, "nb.py")
640 self.assertEqual(_ip.magic("env var"), 'val1')
657 with io.open(infile, 'w', encoding='utf-8') as f:
641 env = _ip.magic("env var=val2")
658 current.write(nb0, f, 'py')
642 self.assertEqual(env, None)
643 self.assertEqual(os.environ['var'], 'val2')
659
644
660 _ip.ex(py3compat.u_format(u"u = {u}'héllo'"))
645 def test_env_get_set_complex(self):
661 _ip.magic("notebook -f ipynb %s" % infile)
646 env = _ip.magic("env var 'val1 '' 'val2")
662 _ip.magic("notebook -f json %s" % infile)
647 self.assertEqual(env, None)
648 self.assertEqual(os.environ['var'], "'val1 '' 'val2")
649 self.assertEqual(_ip.magic("env var"), "'val1 '' 'val2")
650 env = _ip.magic('env var=val2 val3="val4')
651 self.assertEqual(env, None)
652 self.assertEqual(os.environ['var'], 'val2 val3="val4')
663
653
654 def test_env_set_bad_input(self):
655 self.assertRaises(UsageError, lambda: _ip.magic("set_env var"))
664
656
665 def test_env():
657 def test_env_set_whitespace(self):
666 env = _ip.magic("env")
658 self.assertRaises(UsageError, lambda: _ip.magic("env var A=B"))
667 assert isinstance(env, dict), type(env)
668
659
669
660
670 class CellMagicTestCase(TestCase):
661 class CellMagicTestCase(TestCase):
@@ -7,11 +7,12 b' will be kept in this separate file. This makes it easier to aggregate in one'
7 place the tricks needed to handle it; most other magics are much easier to test
7 place the tricks needed to handle it; most other magics are much easier to test
8 and we do so in a common test_magic file.
8 and we do so in a common test_magic file.
9 """
9 """
10
11 # Copyright (c) IPython Development Team.
12 # Distributed under the terms of the Modified BSD License.
13
10 from __future__ import absolute_import
14 from __future__ import absolute_import
11
15
12 #-----------------------------------------------------------------------------
13 # Imports
14 #-----------------------------------------------------------------------------
15
16
16 import functools
17 import functools
17 import os
18 import os
@@ -32,9 +33,6 b' from IPython.utils.io import capture_output'
32 from IPython.utils.tempdir import TemporaryDirectory
33 from IPython.utils.tempdir import TemporaryDirectory
33 from IPython.core import debugger
34 from IPython.core import debugger
34
35
35 #-----------------------------------------------------------------------------
36 # Test functions begin
37 #-----------------------------------------------------------------------------
38
36
39 def doctest_refbug():
37 def doctest_refbug():
40 """Very nasty problem with references held by multiple runs of a script.
38 """Very nasty problem with references held by multiple runs of a script.
@@ -372,19 +370,17 b' tclass.py: deleting object: C-third'
372 with tt.AssertNotPrints('SystemExit'):
370 with tt.AssertNotPrints('SystemExit'):
373 _ip.magic('run -e %s' % self.fname)
371 _ip.magic('run -e %s' % self.fname)
374
372
375 @dec.skip_without('IPython.nbformat.current') # Requires jsonschema
373 @dec.skip_without('IPython.nbformat') # Requires jsonschema
376 def test_run_nb(self):
374 def test_run_nb(self):
377 """Test %run notebook.ipynb"""
375 """Test %run notebook.ipynb"""
378 from IPython.nbformat import current
376 from IPython.nbformat import v4, writes
379 nb = current.new_notebook(
377 nb = v4.new_notebook(
380 worksheets=[
378 cells=[
381 current.new_worksheet(cells=[
379 v4.new_markdown_cell("The Ultimate Question of Everything"),
382 current.new_text_cell("The Ultimate Question of Everything"),
380 v4.new_code_cell("answer=42")
383 current.new_code_cell("answer=42")
384 ])
385 ]
381 ]
386 )
382 )
387 src = current.writes(nb, 'json')
383 src = writes(nb, version=4)
388 self.mktmp(src, ext='.ipynb')
384 self.mktmp(src, ext='.ipynb')
389
385
390 _ip.magic("run %s" % self.fname)
386 _ip.magic("run %s" % self.fname)
@@ -19,6 +19,11 b' import unittest'
19
19
20 from IPython.testing import decorators as dec
20 from IPython.testing import decorators as dec
21 from IPython.testing import tools as tt
21 from IPython.testing import tools as tt
22 from IPython.utils.py3compat import PY3
23
24 sqlite_err_maybe = dec.module_not_available('sqlite3')
25 SQLITE_NOT_AVAILABLE_ERROR = ('WARNING: IPython History requires SQLite,'
26 ' your history will not be saved\n')
22
27
23 class TestFileToRun(unittest.TestCase, tt.TempFileMixin):
28 class TestFileToRun(unittest.TestCase, tt.TempFileMixin):
24 """Test the behavior of the file_to_run parameter."""
29 """Test the behavior of the file_to_run parameter."""
@@ -28,10 +33,7 b' class TestFileToRun(unittest.TestCase, tt.TempFileMixin):'
28 src = "print(__file__)\n"
33 src = "print(__file__)\n"
29 self.mktmp(src)
34 self.mktmp(src)
30
35
31 if dec.module_not_available('sqlite3'):
36 err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None
32 err = 'WARNING: IPython History requires SQLite, your history will not be saved\n'
33 else:
34 err = None
35 tt.ipexec_validate(self.fname, self.fname, err)
37 tt.ipexec_validate(self.fname, self.fname, err)
36
38
37 def test_ipy_script_file_attribute(self):
39 def test_ipy_script_file_attribute(self):
@@ -39,11 +41,28 b' class TestFileToRun(unittest.TestCase, tt.TempFileMixin):'
39 src = "print(__file__)\n"
41 src = "print(__file__)\n"
40 self.mktmp(src, ext='.ipy')
42 self.mktmp(src, ext='.ipy')
41
43
42 if dec.module_not_available('sqlite3'):
44 err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None
43 err = 'WARNING: IPython History requires SQLite, your history will not be saved\n'
44 else:
45 err = None
46 tt.ipexec_validate(self.fname, self.fname, err)
45 tt.ipexec_validate(self.fname, self.fname, err)
47
46
48 # Ideally we would also test that `__file__` is not set in the
47 # The commands option to ipexec_validate doesn't work on Windows, and it
49 # interactive namespace after running `ipython -i <file>`.
48 # doesn't seem worth fixing
49 @dec.skip_win32
50 def test_py_script_file_attribute_interactively(self):
51 """Test that `__file__` is not set after `ipython -i file.py`"""
52 src = "True\n"
53 self.mktmp(src)
54
55 err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None
56 tt.ipexec_validate(self.fname, 'False', err, options=['-i'],
57 commands=['"__file__" in globals()', 'exit()'])
58
59 @dec.skip_win32
60 @dec.skipif(PY3)
61 def test_py_script_file_compiler_directive(self):
62 """Test `__future__` compiler directives with `ipython -i file.py`"""
63 src = "from __future__ import division\n"
64 self.mktmp(src)
65
66 err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None
67 tt.ipexec_validate(self.fname, 'float', err, options=['-i'],
68 commands=['type(1/2)', 'exit()'])
@@ -722,15 +722,23 b' class VerboseTB(TBTools):'
722 #print '*** record:',file,lnum,func,lines,index # dbg
722 #print '*** record:',file,lnum,func,lines,index # dbg
723 if not file:
723 if not file:
724 file = '?'
724 file = '?'
725 elif not (file.startswith(str("<")) and file.endswith(str(">"))):
725 elif file.startswith(str("<")) and file.endswith(str(">")):
726 # Guess that filenames like <string> aren't real filenames, so
726 # Not a real filename, no problem...
727 # don't call abspath on them.
727 pass
728 try:
728 elif not os.path.isabs(file):
729 file = abspath(file)
729 # Try to make the filename absolute by trying all
730 except OSError:
730 # sys.path entries (which is also what linecache does)
731 # Not sure if this can still happen: abspath now works with
731 for dirname in sys.path:
732 # file names like <string>
732 try:
733 pass
733 fullname = os.path.join(dirname, file)
734 if os.path.isfile(fullname):
735 file = os.path.abspath(fullname)
736 break
737 except Exception:
738 # Just in case that sys.path contains very
739 # strange entries...
740 pass
741
734 file = py3compat.cast_unicode(file, util_path.fs_encoding)
742 file = py3compat.cast_unicode(file, util_path.fs_encoding)
735 link = tpl_link % file
743 link = tpl_link % file
736 args, varargs, varkw, locals = inspect.getargvalues(frame)
744 args, varargs, varkw, locals = inspect.getargvalues(frame)
@@ -103,11 +103,6 b' MAIN FEATURES'
103 If you just want to see an object's docstring, type '%pdoc object' (without
103 If you just want to see an object's docstring, type '%pdoc object' (without
104 quotes, and without % if you have automagic on).
104 quotes, and without % if you have automagic on).
105
105
106 Both %pdoc and ?/?? give you access to documentation even on things which are
107 not explicitely defined. Try for example typing {}.get? or after import os,
108 type os.path.abspath??. The magic functions %pdef, %source and %file operate
109 similarly.
110
111 * Completion in the local namespace, by typing TAB at the prompt.
106 * Completion in the local namespace, by typing TAB at the prompt.
112
107
113 At any time, hitting tab will complete any available python commands or
108 At any time, hitting tab will complete any available python commands or
@@ -183,7 +183,7 b' class ModuleReloader(object):'
183 return top_module, top_name
183 return top_module, top_name
184
184
185 def filename_and_mtime(self, module):
185 def filename_and_mtime(self, module):
186 if not hasattr(module, '__file__'):
186 if not hasattr(module, '__file__') or module.__file__ is None:
187 return None, None
187 return None, None
188
188
189 if module.__name__ == '__main__':
189 if module.__name__ == '__main__':
@@ -1,37 +1,10 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 =====================
3 The cython magic has been integrated into Cython itself,
4 Cython related magics
4 which is now released in version 0.21.
5 =====================
6
5
7 Magic command interface for interactive work with Cython
6 cf github `Cython` organisation, `Cython` repo, under the
8
7 file `Cython/Build/IpythonMagic.py`
9 .. note::
10
11 The ``Cython`` package needs to be installed separately. It
12 can be obtained using ``easy_install`` or ``pip``.
13
14 Usage
15 =====
16
17 To enable the magics below, execute ``%load_ext cythonmagic``.
18
19 ``%%cython``
20
21 {CYTHON_DOC}
22
23 ``%%cython_inline``
24
25 {CYTHON_INLINE_DOC}
26
27 ``%%cython_pyximport``
28
29 {CYTHON_PYXIMPORT_DOC}
30
31 Author:
32 * Brian Granger
33
34 Parts of this code were taken from Cython.inline.
35 """
8 """
36 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
37 # Copyright (C) 2010-2011, IPython Development Team.
10 # Copyright (C) 2010-2011, IPython Development Team.
@@ -43,303 +16,28 b' Parts of this code were taken from Cython.inline.'
43
16
44 from __future__ import print_function
17 from __future__ import print_function
45
18
46 import imp
19 import IPython.utils.version as version
47 import io
48 import os
49 import re
50 import sys
51 import time
52
20
53 try:
21 try:
54 reload
22 import Cython
55 except NameError: # Python 3
23 except:
56 from imp import reload
24 Cython = None
57
25
58 try:
26 try:
59 import hashlib
27 from Cython.Build.IpythonMagic import CythonMagics
60 except ImportError:
28 except :
61 import md5 as hashlib
29 pass
62
63 from distutils.core import Distribution, Extension
64 from distutils.command.build_ext import build_ext
65
66 from IPython.core import display
67 from IPython.core import magic_arguments
68 from IPython.core.magic import Magics, magics_class, cell_magic
69 from IPython.utils import py3compat
70 from IPython.utils.path import get_ipython_cache_dir
71 from IPython.utils.text import dedent
72
73 import Cython
74 from Cython.Compiler.Errors import CompileError
75 from Cython.Build.Dependencies import cythonize
76
77
78 @magics_class
79 class CythonMagics(Magics):
80
81 def __init__(self, shell):
82 super(CythonMagics,self).__init__(shell)
83 self._reloads = {}
84 self._code_cache = {}
85
86 def _import_all(self, module):
87 for k,v in module.__dict__.items():
88 if not k.startswith('__'):
89 self.shell.push({k:v})
90
91 @cell_magic
92 def cython_inline(self, line, cell):
93 """Compile and run a Cython code cell using Cython.inline.
94
95 This magic simply passes the body of the cell to Cython.inline
96 and returns the result. If the variables `a` and `b` are defined
97 in the user's namespace, here is a simple example that returns
98 their sum::
99
100 %%cython_inline
101 return a+b
102
103 For most purposes, we recommend the usage of the `%%cython` magic.
104 """
105 locs = self.shell.user_global_ns
106 globs = self.shell.user_ns
107 return Cython.inline(cell, locals=locs, globals=globs)
108
109 @cell_magic
110 def cython_pyximport(self, line, cell):
111 """Compile and import a Cython code cell using pyximport.
112
30
113 The contents of the cell are written to a `.pyx` file in the current
114 working directory, which is then imported using `pyximport`. This
115 magic requires a module name to be passed::
116
117 %%cython_pyximport modulename
118 def f(x):
119 return 2.0*x
120
121 The compiled module is then imported and all of its symbols are
122 injected into the user's namespace. For most purposes, we recommend
123 the usage of the `%%cython` magic.
124 """
125 module_name = line.strip()
126 if not module_name:
127 raise ValueError('module name must be given')
128 fname = module_name + '.pyx'
129 with io.open(fname, 'w', encoding='utf-8') as f:
130 f.write(cell)
131 if 'pyximport' not in sys.modules:
132 import pyximport
133 pyximport.install(reload_support=True)
134 if module_name in self._reloads:
135 module = self._reloads[module_name]
136 reload(module)
137 else:
138 __import__(module_name)
139 module = sys.modules[module_name]
140 self._reloads[module_name] = module
141 self._import_all(module)
142
143 @magic_arguments.magic_arguments()
144 @magic_arguments.argument(
145 '-c', '--compile-args', action='append', default=[],
146 help="Extra flags to pass to compiler via the `extra_compile_args` "
147 "Extension flag (can be specified multiple times)."
148 )
149 @magic_arguments.argument(
150 '--link-args', action='append', default=[],
151 help="Extra flags to pass to linker via the `extra_link_args` "
152 "Extension flag (can be specified multiple times)."
153 )
154 @magic_arguments.argument(
155 '-l', '--lib', action='append', default=[],
156 help="Add a library to link the extension against (can be specified "
157 "multiple times)."
158 )
159 @magic_arguments.argument(
160 '-n', '--name',
161 help="Specify a name for the Cython module."
162 )
163 @magic_arguments.argument(
164 '-L', dest='library_dirs', metavar='dir', action='append', default=[],
165 help="Add a path to the list of libary directories (can be specified "
166 "multiple times)."
167 )
168 @magic_arguments.argument(
169 '-I', '--include', action='append', default=[],
170 help="Add a path to the list of include directories (can be specified "
171 "multiple times)."
172 )
173 @magic_arguments.argument(
174 '-+', '--cplus', action='store_true', default=False,
175 help="Output a C++ rather than C file."
176 )
177 @magic_arguments.argument(
178 '-f', '--force', action='store_true', default=False,
179 help="Force the compilation of a new module, even if the source has been "
180 "previously compiled."
181 )
182 @magic_arguments.argument(
183 '-a', '--annotate', action='store_true', default=False,
184 help="Produce a colorized HTML version of the source."
185 )
186 @cell_magic
187 def cython(self, line, cell):
188 """Compile and import everything from a Cython code cell.
189
190 The contents of the cell are written to a `.pyx` file in the
191 directory `IPYTHONDIR/cython` using a filename with the hash of the
192 code. This file is then cythonized and compiled. The resulting module
193 is imported and all of its symbols are injected into the user's
194 namespace. The usage is similar to that of `%%cython_pyximport` but
195 you don't have to pass a module name::
196
197 %%cython
198 def f(x):
199 return 2.0*x
200
201 To compile OpenMP codes, pass the required `--compile-args`
202 and `--link-args`. For example with gcc::
203
204 %%cython --compile-args=-fopenmp --link-args=-fopenmp
205 ...
206 """
207 args = magic_arguments.parse_argstring(self.cython, line)
208 code = cell if cell.endswith('\n') else cell+'\n'
209 lib_dir = os.path.join(get_ipython_cache_dir(), 'cython')
210 quiet = True
211 key = code, sys.version_info, sys.executable, Cython.__version__
212
213 if not os.path.exists(lib_dir):
214 os.makedirs(lib_dir)
215
216 if args.force:
217 # Force a new module name by adding the current time to the
218 # key which is hashed to determine the module name.
219 key += time.time(),
220
221 if args.name:
222 module_name = py3compat.unicode_to_str(args.name)
223 else:
224 module_name = "_cython_magic_" + hashlib.md5(str(key).encode('utf-8')).hexdigest()
225 module_path = os.path.join(lib_dir, module_name + self.so_ext)
226
227 have_module = os.path.isfile(module_path)
228 need_cythonize = not have_module
229
230 if args.annotate:
231 html_file = os.path.join(lib_dir, module_name + '.html')
232 if not os.path.isfile(html_file):
233 need_cythonize = True
234
235 if need_cythonize:
236 c_include_dirs = args.include
237 if 'numpy' in code:
238 import numpy
239 c_include_dirs.append(numpy.get_include())
240 pyx_file = os.path.join(lib_dir, module_name + '.pyx')
241 pyx_file = py3compat.cast_bytes_py2(pyx_file, encoding=sys.getfilesystemencoding())
242 with io.open(pyx_file, 'w', encoding='utf-8') as f:
243 f.write(code)
244 extension = Extension(
245 name = module_name,
246 sources = [pyx_file],
247 include_dirs = c_include_dirs,
248 library_dirs = args.library_dirs,
249 extra_compile_args = args.compile_args,
250 extra_link_args = args.link_args,
251 libraries = args.lib,
252 language = 'c++' if args.cplus else 'c',
253 )
254 build_extension = self._get_build_extension()
255 try:
256 opts = dict(
257 quiet=quiet,
258 annotate = args.annotate,
259 force = True,
260 )
261 build_extension.extensions = cythonize([extension], **opts)
262 except CompileError:
263 return
264
265 if not have_module:
266 build_extension.build_temp = os.path.dirname(pyx_file)
267 build_extension.build_lib = lib_dir
268 build_extension.run()
269 self._code_cache[key] = module_name
270
271 module = imp.load_dynamic(module_name, module_path)
272 self._import_all(module)
273
274 if args.annotate:
275 try:
276 with io.open(html_file, encoding='utf-8') as f:
277 annotated_html = f.read()
278 except IOError as e:
279 # File could not be opened. Most likely the user has a version
280 # of Cython before 0.15.1 (when `cythonize` learned the
281 # `force` keyword argument) and has already compiled this
282 # exact source without annotation.
283 print('Cython completed successfully but the annotated '
284 'source could not be read.', file=sys.stderr)
285 print(e, file=sys.stderr)
286 else:
287 return display.HTML(self.clean_annotated_html(annotated_html))
288
289 @property
290 def so_ext(self):
291 """The extension suffix for compiled modules."""
292 try:
293 return self._so_ext
294 except AttributeError:
295 self._so_ext = self._get_build_extension().get_ext_filename('')
296 return self._so_ext
297
298 def _clear_distutils_mkpath_cache(self):
299 """clear distutils mkpath cache
300
301 prevents distutils from skipping re-creation of dirs that have been removed
302 """
303 try:
304 from distutils.dir_util import _path_created
305 except ImportError:
306 pass
307 else:
308 _path_created.clear()
309
310 def _get_build_extension(self):
311 self._clear_distutils_mkpath_cache()
312 dist = Distribution()
313 config_files = dist.find_config_files()
314 try:
315 config_files.remove('setup.cfg')
316 except ValueError:
317 pass
318 dist.parse_config_files(config_files)
319 build_extension = build_ext(dist)
320 build_extension.finalize_options()
321 return build_extension
322
323 @staticmethod
324 def clean_annotated_html(html):
325 """Clean up the annotated HTML source.
326
327 Strips the link to the generated C or C++ file, which we do not
328 present to the user.
329 """
330 r = re.compile('<p>Raw output: <a href="(.*)">(.*)</a>')
331 html = '\n'.join(l for l in html.splitlines() if not r.match(l))
332 return html
333
334 __doc__ = __doc__.format(
335 # rST doesn't see the -+ flag as part of an option list, so we
336 # hide it from the module-level docstring.
337 CYTHON_DOC = dedent(CythonMagics.cython.__doc__\
338 .replace('-+, --cplus','--cplus ')),
339 CYTHON_INLINE_DOC = dedent(CythonMagics.cython_inline.__doc__),
340 CYTHON_PYXIMPORT_DOC = dedent(CythonMagics.cython_pyximport.__doc__),
341 )
342
31
32 ## still load the magic in IPython 3.x, remove completely in future versions.
343 def load_ipython_extension(ip):
33 def load_ipython_extension(ip):
344 """Load the extension in IPython."""
34 """Load the extension in IPython."""
345 ip.register_magics(CythonMagics)
35
36 print("""The Cython magic has been move to the Cython package, hence """)
37 print("""`%load_ext cythonmagic` is deprecated; Please use `%load_ext Cython` instead.""")
38
39 if Cython is None or not version.check_version(Cython.__version__, "0.21"):
40 print("You need Cython version >=0.21 to use the Cython magic")
41 return
42 print("""\nThough, because I am nice, I'll still try to load it for you this time.""")
43 Cython.load_ipython_extension(ip)
@@ -132,7 +132,6 b' def extract_zip(fd, dest):'
132 z.extractall(parent)
132 z.extractall(parent)
133
133
134 # it will be mathjax-MathJax-<sha>, rename to just mathjax
134 # it will be mathjax-MathJax-<sha>, rename to just mathjax
135 d = os.path.join(parent, topdir)
136 os.rename(os.path.join(parent, topdir), dest)
135 os.rename(os.path.join(parent, topdir), dest)
137
136
138
137
@@ -57,11 +57,11 b' def commit_api(api):'
57 if api == QT_API_PYSIDE:
57 if api == QT_API_PYSIDE:
58 ID.forbid('PyQt4')
58 ID.forbid('PyQt4')
59 ID.forbid('PyQt5')
59 ID.forbid('PyQt5')
60 elif api == QT_API_PYQT:
60 elif api == QT_API_PYQT5:
61 ID.forbid('PySide')
61 ID.forbid('PySide')
62 ID.forbid('PyQt5')
63 else:
64 ID.forbid('PyQt4')
62 ID.forbid('PyQt4')
63 else: # There are three other possibilities, all representing PyQt4
64 ID.forbid('PyQt5')
65 ID.forbid('PySide')
65 ID.forbid('PySide')
66
66
67
67
@@ -241,7 +241,7 b' def load_qt(api_options):'
241 ----------
241 ----------
242 api_options: List of strings
242 api_options: List of strings
243 The order of APIs to try. Valid items are 'pyside',
243 The order of APIs to try. Valid items are 'pyside',
244 'pyqt', 'pyqt5' and 'pyqtv1'
244 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
245
245
246 Returns
246 Returns
247 -------
247 -------
@@ -4,10 +4,9 b''
4
4
5 Developers of the IPython Notebook will need to install the following tools:
5 Developers of the IPython Notebook will need to install the following tools:
6
6
7 * fabric
7 * invoke
8 * node.js
8 * node.js
9 * less (`npm install -g less`)
9 * less (`npm install -g less`)
10 * bower (`npm install -g bower`)
11
10
12 ## Components
11 ## Components
13
12
@@ -15,14 +14,13 b' We are moving to a model where our JavaScript dependencies are managed using'
15 [bower](http://bower.io/). These packages are installed in `static/components`
14 [bower](http://bower.io/). These packages are installed in `static/components`
16 and committed into a separate git repo [ipython/ipython-components](ipython/ipython-components).
15 and committed into a separate git repo [ipython/ipython-components](ipython/ipython-components).
17 Our dependencies are described in the file
16 Our dependencies are described in the file
18 `static/components/bower.json`. To update our bower packages, run `fab update`
17 `static/components/bower.json`. To update our bower packages, run `bower install`
19 in this directory.
18 in this directory.
20
19
21 ## less
20 ## less
22
21
23 If you edit our `.less` files you will need to run the less compiler to build
22 If you edit our `.less` files you will need to run the less compiler to build
24 our minified css files. This can be done by running `fab css` from this directory,
23 our minified css files. This can be done by running `python setup.py css` from the root of the repository.
25 or `python setup.py css` from the root of the repository.
26 If you are working frequently with `.less` files please consider installing git hooks that
24 If you are working frequently with `.less` files please consider installing git hooks that
27 rebuild the css files and corresponding maps in `${RepoRoot}/git-hooks/install-hooks.sh`.
25 rebuild the css files and corresponding maps in `${RepoRoot}/git-hooks/install-hooks.sh`.
28
26
@@ -4,6 +4,22 b' import os'
4 # Packagers: modify this line if you store the notebook static files elsewhere
4 # Packagers: modify this line if you store the notebook static files elsewhere
5 DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
5 DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
6
6
7 # Packagers: modify the next line if you store the notebook template files
8 # elsewhere
9
10 # Include both IPython/html/ and IPython/html/templates/. This makes it
11 # possible for users to override a template with a file that inherits from that
12 # template.
13 #
14 # For example, if you want to override a specific block of notebook.html, you
15 # can create a file called notebook.html that inherits from
16 # templates/notebook.html, and the latter will resolve correctly to the base
17 # implementation.
18 DEFAULT_TEMPLATE_PATH_LIST = [
19 os.path.dirname(__file__),
20 os.path.join(os.path.dirname(__file__), "templates"),
21 ]
22
7 del os
23 del os
8
24
9 from .nbextensions import install_nbextension No newline at end of file
25 from .nbextensions import install_nbextension
@@ -24,33 +24,45 b' try:'
24 except ImportError:
24 except ImportError:
25 app_log = logging.getLogger()
25 app_log = logging.getLogger()
26
26
27 import IPython
28 from IPython.utils.sysinfo import get_sys_info
29
27 from IPython.config import Application
30 from IPython.config import Application
28 from IPython.utils.path import filefind
31 from IPython.utils.path import filefind
29 from IPython.utils.py3compat import string_types
32 from IPython.utils.py3compat import string_types
30 from IPython.html.utils import is_hidden, url_path_join, url_escape
33 from IPython.html.utils import is_hidden, url_path_join, url_escape
31
34
35 from IPython.html.services.security import csp_report_uri
36
32 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
33 # Top-level handlers
38 # Top-level handlers
34 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
40 non_alphanum = re.compile(r'[^A-Za-z0-9]')
36
41
42 sys_info = json.dumps(get_sys_info())
43
37 class AuthenticatedHandler(web.RequestHandler):
44 class AuthenticatedHandler(web.RequestHandler):
38 """A RequestHandler with an authenticated user."""
45 """A RequestHandler with an authenticated user."""
39
46
40 def set_default_headers(self):
47 def set_default_headers(self):
41 headers = self.settings.get('headers', {})
48 headers = self.settings.get('headers', {})
42
49
43 if "X-Frame-Options" not in headers:
50 if "Content-Security-Policy" not in headers:
44 headers["X-Frame-Options"] = "SAMEORIGIN"
51 headers["Content-Security-Policy"] = (
52 "frame-ancestors 'self'; "
53 # Make sure the report-uri is relative to the base_url
54 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
55 )
45
56
57 # Allow for overriding headers
46 for header_name,value in headers.items() :
58 for header_name,value in headers.items() :
47 try:
59 try:
48 self.set_header(header_name, value)
60 self.set_header(header_name, value)
49 except Exception:
61 except Exception as e:
50 # tornado raise Exception (not a subclass)
62 # tornado raise Exception (not a subclass)
51 # if method is unsupported (websocket and Access-Control-Allow-Origin
63 # if method is unsupported (websocket and Access-Control-Allow-Origin
52 # for example, so just ignore)
64 # for example, so just ignore)
53 pass
65 self.log.debug(e)
54
66
55 def clear_login_cookie(self):
67 def clear_login_cookie(self):
56 self.clear_cookie(self.cookie_name)
68 self.clear_cookie(self.cookie_name)
@@ -121,6 +133,11 b' class IPythonHandler(AuthenticatedHandler):'
121 #---------------------------------------------------------------
133 #---------------------------------------------------------------
122
134
123 @property
135 @property
136 def version_hash(self):
137 """The version hash to use for cache hints for static files"""
138 return self.settings.get('version_hash', '')
139
140 @property
124 def mathjax_url(self):
141 def mathjax_url(self):
125 return self.settings.get('mathjax_url', '')
142 return self.settings.get('mathjax_url', '')
126
143
@@ -131,6 +148,12 b' class IPythonHandler(AuthenticatedHandler):'
131 @property
148 @property
132 def ws_url(self):
149 def ws_url(self):
133 return self.settings.get('websocket_url', '')
150 return self.settings.get('websocket_url', '')
151
152 @property
153 def contents_js_source(self):
154 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
155 'services/contents'))
156 return self.settings.get('contents_js_source', 'services/contents')
134
157
135 #---------------------------------------------------------------
158 #---------------------------------------------------------------
136 # Manager objects
159 # Manager objects
@@ -153,9 +176,17 b' class IPythonHandler(AuthenticatedHandler):'
153 return self.settings['session_manager']
176 return self.settings['session_manager']
154
177
155 @property
178 @property
179 def terminal_manager(self):
180 return self.settings['terminal_manager']
181
182 @property
156 def kernel_spec_manager(self):
183 def kernel_spec_manager(self):
157 return self.settings['kernel_spec_manager']
184 return self.settings['kernel_spec_manager']
158
185
186 @property
187 def config_manager(self):
188 return self.settings['config_manager']
189
159 #---------------------------------------------------------------
190 #---------------------------------------------------------------
160 # CORS
191 # CORS
161 #---------------------------------------------------------------
192 #---------------------------------------------------------------
@@ -219,6 +250,9 b' class IPythonHandler(AuthenticatedHandler):'
219 logged_in=self.logged_in,
250 logged_in=self.logged_in,
220 login_available=self.login_available,
251 login_available=self.login_available,
221 static_url=self.static_url,
252 static_url=self.static_url,
253 sys_info=sys_info,
254 contents_js_source=self.contents_js_source,
255 version_hash=self.version_hash,
222 )
256 )
223
257
224 def get_json_body(self):
258 def get_json_body(self):
@@ -285,12 +319,18 b' class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):'
285 @web.authenticated
319 @web.authenticated
286 def get(self, path):
320 def get(self, path):
287 if os.path.splitext(path)[1] == '.ipynb':
321 if os.path.splitext(path)[1] == '.ipynb':
288 name = os.path.basename(path)
322 name = path.rsplit('/', 1)[-1]
289 self.set_header('Content-Type', 'application/json')
323 self.set_header('Content-Type', 'application/json')
290 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
324 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
291
325
292 return web.StaticFileHandler.get(self, path)
326 return web.StaticFileHandler.get(self, path)
293
327
328 def set_headers(self):
329 super(AuthenticatedFileHandler, self).set_headers()
330 # disable browser caching, rely on 304 replies for savings
331 if "v" not in self.request.arguments:
332 self.add_header("Cache-Control", "no-cache")
333
294 def compute_etag(self):
334 def compute_etag(self):
295 return None
335 return None
296
336
@@ -359,7 +399,16 b' class FileFindHandler(web.StaticFileHandler):'
359 # cache search results, don't search for files more than once
399 # cache search results, don't search for files more than once
360 _static_paths = {}
400 _static_paths = {}
361
401
362 def initialize(self, path, default_filename=None):
402 def set_headers(self):
403 super(FileFindHandler, self).set_headers()
404 # disable browser caching, rely on 304 replies for savings
405 if "v" not in self.request.arguments or \
406 any(self.request.path.startswith(path) for path in self.no_cache_paths):
407 self.add_header("Cache-Control", "no-cache")
408
409 def initialize(self, path, default_filename=None, no_cache_paths=None):
410 self.no_cache_paths = no_cache_paths or []
411
363 if isinstance(path, string_types):
412 if isinstance(path, string_types):
364 path = [path]
413 path = [path]
365
414
@@ -398,43 +447,49 b' class FileFindHandler(web.StaticFileHandler):'
398 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
447 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
399
448
400
449
450 class ApiVersionHandler(IPythonHandler):
451
452 @json_errors
453 def get(self):
454 # not authenticated, so give as few info as possible
455 self.finish(json.dumps({"version":IPython.__version__}))
456
457
401 class TrailingSlashHandler(web.RequestHandler):
458 class TrailingSlashHandler(web.RequestHandler):
402 """Simple redirect handler that strips trailing slashes
459 """Simple redirect handler that strips trailing slashes
403
460
404 This should be the first, highest priority handler.
461 This should be the first, highest priority handler.
405 """
462 """
406
463
407 SUPPORTED_METHODS = ['GET']
408
409 def get(self):
464 def get(self):
410 self.redirect(self.request.uri.rstrip('/'))
465 self.redirect(self.request.uri.rstrip('/'))
466
467 post = put = get
411
468
412
469
413 class FilesRedirectHandler(IPythonHandler):
470 class FilesRedirectHandler(IPythonHandler):
414 """Handler for redirecting relative URLs to the /files/ handler"""
471 """Handler for redirecting relative URLs to the /files/ handler"""
415 def get(self, path=''):
472 def get(self, path=''):
416 cm = self.contents_manager
473 cm = self.contents_manager
417 if cm.path_exists(path):
474 if cm.dir_exists(path):
418 # it's a *directory*, redirect to /tree
475 # it's a *directory*, redirect to /tree
419 url = url_path_join(self.base_url, 'tree', path)
476 url = url_path_join(self.base_url, 'tree', path)
420 else:
477 else:
421 orig_path = path
478 orig_path = path
422 # otherwise, redirect to /files
479 # otherwise, redirect to /files
423 parts = path.split('/')
480 parts = path.split('/')
424 path = '/'.join(parts[:-1])
425 name = parts[-1]
426
481
427 if not cm.file_exists(name=name, path=path) and 'files' in parts:
482 if not cm.file_exists(path=path) and 'files' in parts:
428 # redirect without files/ iff it would 404
483 # redirect without files/ iff it would 404
429 # this preserves pre-2.0-style 'files/' links
484 # this preserves pre-2.0-style 'files/' links
430 self.log.warn("Deprecated files/ URL: %s", orig_path)
485 self.log.warn("Deprecated files/ URL: %s", orig_path)
431 parts.remove('files')
486 parts.remove('files')
432 path = '/'.join(parts[:-1])
487 path = '/'.join(parts)
433
488
434 if not cm.file_exists(name=name, path=path):
489 if not cm.file_exists(path=path):
435 raise web.HTTPError(404)
490 raise web.HTTPError(404)
436
491
437 url = url_path_join(self.base_url, 'files', path, name)
492 url = url_path_join(self.base_url, 'files', path)
438 url = url_escape(url)
493 url = url_escape(url)
439 self.log.debug("Redirecting %s to %s", self.request.path, url)
494 self.log.debug("Redirecting %s to %s", self.request.path, url)
440 self.redirect(url)
495 self.redirect(url)
@@ -444,11 +499,9 b' class FilesRedirectHandler(IPythonHandler):'
444 # URL pattern fragments for re-use
499 # URL pattern fragments for re-use
445 #-----------------------------------------------------------------------------
500 #-----------------------------------------------------------------------------
446
501
447 path_regex = r"(?P<path>(?:/.*)*)"
502 # path matches any number of `/foo[/bar...]` or just `/` or ''
448 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
503 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
449 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
504 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
450 file_name_regex = r"(?P<name>[^/]+)"
451 file_path_regex = "%s/%s" % (path_regex, file_name_regex)
452
505
453 #-----------------------------------------------------------------------------
506 #-----------------------------------------------------------------------------
454 # URL to handler mappings
507 # URL to handler mappings
@@ -456,5 +509,6 b' file_path_regex = "%s/%s" % (path_regex, file_name_regex)'
456
509
457
510
458 default_handlers = [
511 default_handlers = [
459 (r".*/", TrailingSlashHandler)
512 (r".*/", TrailingSlashHandler),
513 (r"api", ApiVersionHandler)
460 ]
514 ]
@@ -1,34 +1,98 b''
1 # coding: utf-8
1 """Tornado handlers for WebSocket <-> ZMQ sockets."""
2 """Tornado handlers for WebSocket <-> ZMQ sockets."""
2
3
3 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
5
6
7 import os
6 import json
8 import json
9 import struct
10 import warnings
7
11
8 try:
12 try:
9 from urllib.parse import urlparse # Py 3
13 from urllib.parse import urlparse # Py 3
10 except ImportError:
14 except ImportError:
11 from urlparse import urlparse # Py 2
15 from urlparse import urlparse # Py 2
12
16
13 try:
14 from http.cookies import SimpleCookie # Py 3
15 except ImportError:
16 from Cookie import SimpleCookie # Py 2
17 import logging
18
19 import tornado
17 import tornado
20 from tornado import ioloop
18 from tornado import gen, ioloop, web
21 from tornado import web
19 from tornado.websocket import WebSocketHandler
22 from tornado import websocket
23
20
24 from IPython.kernel.zmq.session import Session
21 from IPython.kernel.zmq.session import Session
25 from IPython.utils.jsonutil import date_default
22 from IPython.utils.jsonutil import date_default, extract_dates
26 from IPython.utils.py3compat import PY3, cast_unicode
23 from IPython.utils.py3compat import cast_unicode
27
24
28 from .handlers import IPythonHandler
25 from .handlers import IPythonHandler
29
26
27 def serialize_binary_message(msg):
28 """serialize a message as a binary blob
29
30 Header:
31
32 4 bytes: number of msg parts (nbufs) as 32b int
33 4 * nbufs bytes: offset for each buffer as integer as 32b int
34
35 Offsets are from the start of the buffer, including the header.
36
37 Returns
38 -------
39
40 The message serialized to bytes.
41
42 """
43 # don't modify msg or buffer list in-place
44 msg = msg.copy()
45 buffers = list(msg.pop('buffers'))
46 bmsg = json.dumps(msg, default=date_default).encode('utf8')
47 buffers.insert(0, bmsg)
48 nbufs = len(buffers)
49 offsets = [4 * (nbufs + 1)]
50 for buf in buffers[:-1]:
51 offsets.append(offsets[-1] + len(buf))
52 offsets_buf = struct.pack('!' + 'I' * (nbufs + 1), nbufs, *offsets)
53 buffers.insert(0, offsets_buf)
54 return b''.join(buffers)
55
56
57 def deserialize_binary_message(bmsg):
58 """deserialize a message from a binary blog
59
60 Header:
61
62 4 bytes: number of msg parts (nbufs) as 32b int
63 4 * nbufs bytes: offset for each buffer as integer as 32b int
30
64
31 class ZMQStreamHandler(websocket.WebSocketHandler):
65 Offsets are from the start of the buffer, including the header.
66
67 Returns
68 -------
69
70 message dictionary
71 """
72 nbufs = struct.unpack('!i', bmsg[:4])[0]
73 offsets = list(struct.unpack('!' + 'I' * nbufs, bmsg[4:4*(nbufs+1)]))
74 offsets.append(None)
75 bufs = []
76 for start, stop in zip(offsets[:-1], offsets[1:]):
77 bufs.append(bmsg[start:stop])
78 msg = json.loads(bufs[0].decode('utf8'))
79 msg['header'] = extract_dates(msg['header'])
80 msg['parent_header'] = extract_dates(msg['parent_header'])
81 msg['buffers'] = bufs[1:]
82 return msg
83
84 # ping interval for keeping websockets alive (30 seconds)
85 WS_PING_INTERVAL = 30000
86
87 if os.environ.get('IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS', False):
88 warnings.warn("""Allowing draft76 websocket connections!
89 This should only be done for testing with phantomjs!""")
90 from IPython.html import allow76
91 WebSocketHandler = allow76.AllowDraftWebSocketHandler
92 # draft 76 doesn't support ping
93 WS_PING_INTERVAL = 0
94
95 class ZMQStreamHandler(WebSocketHandler):
32
96
33 def check_origin(self, origin):
97 def check_origin(self, origin):
34 """Check Origin == Host or Access-Control-Allow-Origin.
98 """Check Origin == Host or Access-Control-Allow-Origin.
@@ -77,23 +141,19 b' class ZMQStreamHandler(websocket.WebSocketHandler):'
77 def _reserialize_reply(self, msg_list):
141 def _reserialize_reply(self, msg_list):
78 """Reserialize a reply message using JSON.
142 """Reserialize a reply message using JSON.
79
143
80 This takes the msg list from the ZMQ socket, unserializes it using
144 This takes the msg list from the ZMQ socket, deserializes it using
81 self.session and then serializes the result using JSON. This method
145 self.session and then serializes the result using JSON. This method
82 should be used by self._on_zmq_reply to build messages that can
146 should be used by self._on_zmq_reply to build messages that can
83 be sent back to the browser.
147 be sent back to the browser.
84 """
148 """
85 idents, msg_list = self.session.feed_identities(msg_list)
149 idents, msg_list = self.session.feed_identities(msg_list)
86 msg = self.session.unserialize(msg_list)
150 msg = self.session.deserialize(msg_list)
87 try:
151 if msg['buffers']:
88 msg['header'].pop('date')
152 buf = serialize_binary_message(msg)
89 except KeyError:
153 return buf
90 pass
154 else:
91 try:
155 smsg = json.dumps(msg, default=date_default)
92 msg['parent_header'].pop('date')
156 return cast_unicode(smsg)
93 except KeyError:
94 pass
95 msg.pop('buffers')
96 return json.dumps(msg, default=date_default)
97
157
98 def _on_zmq_reply(self, msg_list):
158 def _on_zmq_reply(self, msg_list):
99 # Sometimes this gets triggered when the on_close method is scheduled in the
159 # Sometimes this gets triggered when the on_close method is scheduled in the
@@ -104,18 +164,7 b' class ZMQStreamHandler(websocket.WebSocketHandler):'
104 except Exception:
164 except Exception:
105 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
165 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
106 else:
166 else:
107 self.write_message(msg)
167 self.write_message(msg, binary=isinstance(msg, bytes))
108
109 def allow_draft76(self):
110 """Allow draft 76, until browsers such as Safari update to RFC 6455.
111
112 This has been disabled by default in tornado in release 2.2.0, and
113 support will be removed in later versions.
114 """
115 return True
116
117 # ping interval for keeping websockets alive (30 seconds)
118 WS_PING_INTERVAL = 30000
119
168
120 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
169 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
121 ping_callback = None
170 ping_callback = None
@@ -146,18 +195,37 b' class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):'
146 which doesn't make sense for websockets
195 which doesn't make sense for websockets
147 """
196 """
148 pass
197 pass
149
198
150 def open(self, kernel_id):
199 def pre_get(self):
151 self.kernel_id = cast_unicode(kernel_id, 'ascii')
200 """Run before finishing the GET request
152 # Check to see that origin matches host directly, including ports
201
153 # Tornado 4 already does CORS checking
202 Extend this method to add logic that should fire before
154 if tornado.version_info[0] < 4:
203 the websocket finishes completing.
155 if not self.check_origin(self.get_origin()):
204 """
156 raise web.HTTPError(403)
205 # authenticate the request before opening the websocket
157
206 if self.get_current_user() is None:
207 self.log.warn("Couldn't authenticate WebSocket connection")
208 raise web.HTTPError(403)
209
210 if self.get_argument('session_id', False):
211 self.session.session = cast_unicode(self.get_argument('session_id'))
212 else:
213 self.log.warn("No session ID specified")
214
215 @gen.coroutine
216 def get(self, *args, **kwargs):
217 # pre_get can be a coroutine in subclasses
218 # assign and yield in two step to avoid tornado 3 issues
219 res = self.pre_get()
220 yield gen.maybe_future(res)
221 super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
222
223 def initialize(self):
224 self.log.debug("Initializing websocket connection %s", self.request.path)
158 self.session = Session(config=self.config)
225 self.session = Session(config=self.config)
159 self.save_on_message = self.on_message
226
160 self.on_message = self.on_first_message
227 def open(self, *args, **kwargs):
228 self.log.debug("Opening websocket %s", self.request.path)
161
229
162 # start the pinging
230 # start the pinging
163 if self.ping_interval > 0:
231 if self.ping_interval > 0:
@@ -187,28 +255,3 b' class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):'
187
255
188 def on_pong(self, data):
256 def on_pong(self, data):
189 self.last_pong = ioloop.IOLoop.instance().time()
257 self.last_pong = ioloop.IOLoop.instance().time()
190
191 def _inject_cookie_message(self, msg):
192 """Inject the first message, which is the document cookie,
193 for authentication."""
194 if not PY3 and isinstance(msg, unicode):
195 # Cookie constructor doesn't accept unicode strings
196 # under Python 2.x for some reason
197 msg = msg.encode('utf8', 'replace')
198 try:
199 identity, msg = msg.split(':', 1)
200 self.session.session = cast_unicode(identity, 'ascii')
201 except Exception:
202 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
203
204 try:
205 self.request._cookies = SimpleCookie(msg)
206 except:
207 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
208
209 def on_first_message(self, msg):
210 self._inject_cookie_message(msg)
211 if self.get_current_user() is None:
212 self.log.warn("Couldn't authenticate WebSocket connection")
213 raise web.HTTPError(403)
214 self.on_message = self.save_on_message
@@ -13,7 +13,7 b' from ..base.handlers import ('
13 IPythonHandler, FilesRedirectHandler,
13 IPythonHandler, FilesRedirectHandler,
14 notebook_path_regex, path_regex,
14 notebook_path_regex, path_regex,
15 )
15 )
16 from IPython.nbformat.current import to_notebook_json
16 from IPython.nbformat import from_dict
17
17
18 from IPython.utils.py3compat import cast_bytes
18 from IPython.utils.py3compat import cast_bytes
19
19
@@ -43,7 +43,7 b' def respond_zip(handler, name, output, resources):'
43 # Prepare the zip file
43 # Prepare the zip file
44 buffer = io.BytesIO()
44 buffer = io.BytesIO()
45 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
45 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
46 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
46 output_filename = os.path.splitext(name)[0] + resources['output_extension']
47 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
47 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
48 for filename, data in output_files.items():
48 for filename, data in output_files.items():
49 zipf.writestr(os.path.basename(filename), data)
49 zipf.writestr(os.path.basename(filename), data)
@@ -76,12 +76,13 b' class NbconvertFileHandler(IPythonHandler):'
76 SUPPORTED_METHODS = ('GET',)
76 SUPPORTED_METHODS = ('GET',)
77
77
78 @web.authenticated
78 @web.authenticated
79 def get(self, format, path='', name=None):
79 def get(self, format, path):
80
80
81 exporter = get_exporter(format, config=self.config, log=self.log)
81 exporter = get_exporter(format, config=self.config, log=self.log)
82
82
83 path = path.strip('/')
83 path = path.strip('/')
84 model = self.contents_manager.get_model(name=name, path=path)
84 model = self.contents_manager.get(path=path)
85 name = model['name']
85
86
86 self.set_header('Last-Modified', model['last_modified'])
87 self.set_header('Last-Modified', model['last_modified'])
87
88
@@ -95,7 +96,7 b' class NbconvertFileHandler(IPythonHandler):'
95
96
96 # Force download if requested
97 # Force download if requested
97 if self.get_argument('download', 'false').lower() == 'true':
98 if self.get_argument('download', 'false').lower() == 'true':
98 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
99 filename = os.path.splitext(name)[0] + resources['output_extension']
99 self.set_header('Content-Disposition',
100 self.set_header('Content-Disposition',
100 'attachment; filename="%s"' % filename)
101 'attachment; filename="%s"' % filename)
101
102
@@ -109,19 +110,20 b' class NbconvertFileHandler(IPythonHandler):'
109 class NbconvertPostHandler(IPythonHandler):
110 class NbconvertPostHandler(IPythonHandler):
110 SUPPORTED_METHODS = ('POST',)
111 SUPPORTED_METHODS = ('POST',)
111
112
112 @web.authenticated
113 @web.authenticated
113 def post(self, format):
114 def post(self, format):
114 exporter = get_exporter(format, config=self.config)
115 exporter = get_exporter(format, config=self.config)
115
116
116 model = self.get_json_body()
117 model = self.get_json_body()
117 nbnode = to_notebook_json(model['content'])
118 name = model.get('name', 'notebook.ipynb')
119 nbnode = from_dict(model['content'])
118
120
119 try:
121 try:
120 output, resources = exporter.from_notebook_node(nbnode)
122 output, resources = exporter.from_notebook_node(nbnode)
121 except Exception as e:
123 except Exception as e:
122 raise web.HTTPError(500, "nbconvert failed: %s" % e)
124 raise web.HTTPError(500, "nbconvert failed: %s" % e)
123
125
124 if respond_zip(self, nbnode.metadata.name, output, resources):
126 if respond_zip(self, name, output, resources):
125 return
127 return
126
128
127 # MIME type
129 # MIME type
@@ -10,9 +10,10 b' import requests'
10
10
11 from IPython.html.utils import url_path_join
11 from IPython.html.utils import url_path_join
12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 from IPython.nbformat.current import (new_notebook, write, new_worksheet,
13 from IPython.nbformat import write
14 new_heading_cell, new_code_cell,
14 from IPython.nbformat.v4 import (
15 new_output)
15 new_notebook, new_markdown_cell, new_code_cell, new_output,
16 )
16
17
17 from IPython.testing.decorators import onlyif_cmds_exist
18 from IPython.testing.decorators import onlyif_cmds_exist
18
19
@@ -43,7 +44,8 b' class NbconvertAPI(object):'
43
44
44 png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
45 png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
45 b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
46 b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
46 b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82')
47 b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82'
48 ).decode('ascii')
47
49
48 class APITest(NotebookTestBase):
50 class APITest(NotebookTestBase):
49 def setUp(self):
51 def setUp(self):
@@ -52,19 +54,20 b' class APITest(NotebookTestBase):'
52 if not os.path.isdir(pjoin(nbdir, 'foo')):
54 if not os.path.isdir(pjoin(nbdir, 'foo')):
53 os.mkdir(pjoin(nbdir, 'foo'))
55 os.mkdir(pjoin(nbdir, 'foo'))
54
56
55 nb = new_notebook(name='testnb')
57 nb = new_notebook()
56
58
57 ws = new_worksheet()
59 nb.cells.append(new_markdown_cell(u'Created by test ³'))
58 nb.worksheets = [ws]
60 cc1 = new_code_cell(source=u'print(2*6)')
59 ws.cells.append(new_heading_cell(u'Created by test ³'))
61 cc1.outputs.append(new_output(output_type="stream", text=u'12'))
60 cc1 = new_code_cell(input=u'print(2*6)')
62 cc1.outputs.append(new_output(output_type="execute_result",
61 cc1.outputs.append(new_output(output_text=u'12', output_type='stream'))
63 data={'image/png' : png_green_pixel},
62 cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout'))
64 execution_count=1,
63 ws.cells.append(cc1)
65 ))
66 nb.cells.append(cc1)
64
67
65 with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
68 with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
66 encoding='utf-8') as f:
69 encoding='utf-8') as f:
67 write(nb, f, format='ipynb')
70 write(nb, f, version=4)
68
71
69 self.nbconvert_api = NbconvertAPI(self.base_url())
72 self.nbconvert_api = NbconvertAPI(self.base_url())
70
73
@@ -93,7 +93,9 b' def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None,'
93 If True, always install the files, regardless of what may already be installed.
93 If True, always install the files, regardless of what may already be installed.
94 symlink : bool [default: False]
94 symlink : bool [default: False]
95 If True, create a symlink in nbextensions, rather than copying files.
95 If True, create a symlink in nbextensions, rather than copying files.
96 Not allowed with URLs or archives.
96 Not allowed with URLs or archives. Windows support for symlinks requires
97 Vista or above, Python 3, and a permission bit which only admin users
98 have by default, so don't rely on it.
97 ipython_dir : str [optional]
99 ipython_dir : str [optional]
98 The path to an IPython directory, if the default value is not desired.
100 The path to an IPython directory, if the default value is not desired.
99 get_ipython_dir() is used by default.
101 get_ipython_dir() is used by default.
@@ -147,7 +149,7 b' def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None,'
147 if overwrite and os.path.exists(dest):
149 if overwrite and os.path.exists(dest):
148 if verbose >= 1:
150 if verbose >= 1:
149 print("removing %s" % dest)
151 print("removing %s" % dest)
150 if os.path.isdir(dest):
152 if os.path.isdir(dest) and not os.path.islink(dest):
151 shutil.rmtree(dest)
153 shutil.rmtree(dest)
152 else:
154 else:
153 os.remove(dest)
155 os.remove(dest)
@@ -17,18 +17,16 b' from ..utils import url_escape'
17 class NotebookHandler(IPythonHandler):
17 class NotebookHandler(IPythonHandler):
18
18
19 @web.authenticated
19 @web.authenticated
20 def get(self, path='', name=None):
20 def get(self, path):
21 """get renders the notebook template if a name is given, or
21 """get renders the notebook template if a name is given, or
22 redirects to the '/files/' handler if the name is not given."""
22 redirects to the '/files/' handler if the name is not given."""
23 path = path.strip('/')
23 path = path.strip('/')
24 cm = self.contents_manager
24 cm = self.contents_manager
25 if name is None:
26 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
27
25
28 # a .ipynb filename was given
26 # a .ipynb filename was given
29 if not cm.file_exists(name, path):
27 if not cm.file_exists(path):
30 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
28 raise web.HTTPError(404, u'Notebook does not exist: %s' % path)
31 name = url_escape(name)
29 name = url_escape(path.rsplit('/', 1)[-1])
32 path = url_escape(path)
30 path = url_escape(path)
33 self.write(self.render_template('notebook.html',
31 self.write(self.render_template('notebook.html',
34 notebook_path=path,
32 notebook_path=path,
@@ -7,6 +7,7 b''
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import base64
9 import base64
10 import datetime
10 import errno
11 import errno
11 import io
12 import io
12 import json
13 import json
@@ -35,7 +36,7 b' from zmq.eventloop import ioloop'
35 ioloop.install()
36 ioloop.install()
36
37
37 # check for tornado 3.1.0
38 # check for tornado 3.1.0
38 msg = "The IPython Notebook requires tornado >= 3.1.0"
39 msg = "The IPython Notebook requires tornado >= 4.0"
39 try:
40 try:
40 import tornado
41 import tornado
41 except ImportError:
42 except ImportError:
@@ -44,14 +45,17 b' try:'
44 version_info = tornado.version_info
45 version_info = tornado.version_info
45 except AttributeError:
46 except AttributeError:
46 raise ImportError(msg + ", but you have < 1.1.0")
47 raise ImportError(msg + ", but you have < 1.1.0")
47 if version_info < (3,1,0):
48 if version_info < (4,0):
48 raise ImportError(msg + ", but you have %s" % tornado.version)
49 raise ImportError(msg + ", but you have %s" % tornado.version)
49
50
50 from tornado import httpserver
51 from tornado import httpserver
51 from tornado import web
52 from tornado import web
52 from tornado.log import LogFormatter
53 from tornado.log import LogFormatter, app_log, access_log, gen_log
53
54
54 from IPython.html import DEFAULT_STATIC_FILES_PATH
55 from IPython.html import (
56 DEFAULT_STATIC_FILES_PATH,
57 DEFAULT_TEMPLATE_PATH_LIST,
58 )
55 from .base.handlers import Template404
59 from .base.handlers import Template404
56 from .log import log_request
60 from .log import log_request
57 from .services.kernels.kernelmanager import MappingKernelManager
61 from .services.kernels.kernelmanager import MappingKernelManager
@@ -81,6 +85,7 b' from IPython.utils.traitlets import ('
81 )
85 )
82 from IPython.utils import py3compat
86 from IPython.utils import py3compat
83 from IPython.utils.path import filefind, get_ipython_dir
87 from IPython.utils.path import filefind, get_ipython_dir
88 from IPython.utils.sysinfo import get_sys_info
84
89
85 from .utils import url_path_join
90 from .utils import url_path_join
86
91
@@ -122,37 +127,43 b' def load_handlers(name):'
122 class NotebookWebApplication(web.Application):
127 class NotebookWebApplication(web.Application):
123
128
124 def __init__(self, ipython_app, kernel_manager, contents_manager,
129 def __init__(self, ipython_app, kernel_manager, contents_manager,
125 cluster_manager, session_manager, kernel_spec_manager, log,
130 cluster_manager, session_manager, kernel_spec_manager,
131 config_manager, log,
126 base_url, default_url, settings_overrides, jinja_env_options):
132 base_url, default_url, settings_overrides, jinja_env_options):
127
133
128 settings = self.init_settings(
134 settings = self.init_settings(
129 ipython_app, kernel_manager, contents_manager, cluster_manager,
135 ipython_app, kernel_manager, contents_manager, cluster_manager,
130 session_manager, kernel_spec_manager, log, base_url, default_url,
136 session_manager, kernel_spec_manager, config_manager, log, base_url,
131 settings_overrides, jinja_env_options)
137 default_url, settings_overrides, jinja_env_options)
132 handlers = self.init_handlers(settings)
138 handlers = self.init_handlers(settings)
133
139
134 super(NotebookWebApplication, self).__init__(handlers, **settings)
140 super(NotebookWebApplication, self).__init__(handlers, **settings)
135
141
136 def init_settings(self, ipython_app, kernel_manager, contents_manager,
142 def init_settings(self, ipython_app, kernel_manager, contents_manager,
137 cluster_manager, session_manager, kernel_spec_manager,
143 cluster_manager, session_manager, kernel_spec_manager,
144 config_manager,
138 log, base_url, default_url, settings_overrides,
145 log, base_url, default_url, settings_overrides,
139 jinja_env_options=None):
146 jinja_env_options=None):
140 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
147
141 # base_url will always be unicode, which will in turn
148 _template_path = settings_overrides.get(
142 # make the patterns unicode, and ultimately result in unicode
149 "template_path",
143 # keys in kwargs to handler._execute(**kwargs) in tornado.
150 ipython_app.template_file_path,
144 # This enforces that base_url be ascii in that situation.
151 )
145 #
146 # Note that the URLs these patterns check against are escaped,
147 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
148 base_url = py3compat.unicode_to_str(base_url, 'ascii')
149 _template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
150 if isinstance(_template_path, str):
152 if isinstance(_template_path, str):
151 _template_path = (_template_path,)
153 _template_path = (_template_path,)
152 template_path = [os.path.expanduser(path) for path in _template_path]
154 template_path = [os.path.expanduser(path) for path in _template_path]
153
155
154 jenv_opt = jinja_env_options if jinja_env_options else {}
156 jenv_opt = jinja_env_options if jinja_env_options else {}
155 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
157 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
158
159 sys_info = get_sys_info()
160 if sys_info['commit_source'] == 'repository':
161 # don't cache (rely on 304) when working from master
162 version_hash = ''
163 else:
164 # reset the cache on server restart
165 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
166
156 settings = dict(
167 settings = dict(
157 # basics
168 # basics
158 log_function=log_request,
169 log_function=log_request,
@@ -162,6 +173,11 b' class NotebookWebApplication(web.Application):'
162 static_path=ipython_app.static_file_path,
173 static_path=ipython_app.static_file_path,
163 static_handler_class = FileFindHandler,
174 static_handler_class = FileFindHandler,
164 static_url_prefix = url_path_join(base_url,'/static/'),
175 static_url_prefix = url_path_join(base_url,'/static/'),
176 static_handler_args = {
177 # don't cache custom.js
178 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
179 },
180 version_hash=version_hash,
165
181
166 # authentication
182 # authentication
167 cookie_secret=ipython_app.cookie_secret,
183 cookie_secret=ipython_app.cookie_secret,
@@ -174,6 +190,7 b' class NotebookWebApplication(web.Application):'
174 cluster_manager=cluster_manager,
190 cluster_manager=cluster_manager,
175 session_manager=session_manager,
191 session_manager=session_manager,
176 kernel_spec_manager=kernel_spec_manager,
192 kernel_spec_manager=kernel_spec_manager,
193 config_manager=config_manager,
177
194
178 # IPython stuff
195 # IPython stuff
179 nbextensions_path = ipython_app.nbextensions_path,
196 nbextensions_path = ipython_app.nbextensions_path,
@@ -181,6 +198,7 b' class NotebookWebApplication(web.Application):'
181 mathjax_url=ipython_app.mathjax_url,
198 mathjax_url=ipython_app.mathjax_url,
182 config=ipython_app.config,
199 config=ipython_app.config,
183 jinja2_env=env,
200 jinja2_env=env,
201 terminals_available=False, # Set later if terminals are available
184 )
202 )
185
203
186 # allow custom overrides for the tornado web app.
204 # allow custom overrides for the tornado web app.
@@ -188,30 +206,34 b' class NotebookWebApplication(web.Application):'
188 return settings
206 return settings
189
207
190 def init_handlers(self, settings):
208 def init_handlers(self, settings):
191 # Load the (URL pattern, handler) tuples for each component.
209 """Load the (URL pattern, handler) tuples for each component."""
210
211 # Order matters. The first handler to match the URL will handle the request.
192 handlers = []
212 handlers = []
193 handlers.extend(load_handlers('base.handlers'))
194 handlers.extend(load_handlers('tree.handlers'))
213 handlers.extend(load_handlers('tree.handlers'))
195 handlers.extend(load_handlers('auth.login'))
214 handlers.extend(load_handlers('auth.login'))
196 handlers.extend(load_handlers('auth.logout'))
215 handlers.extend(load_handlers('auth.logout'))
216 handlers.extend(load_handlers('files.handlers'))
197 handlers.extend(load_handlers('notebook.handlers'))
217 handlers.extend(load_handlers('notebook.handlers'))
198 handlers.extend(load_handlers('nbconvert.handlers'))
218 handlers.extend(load_handlers('nbconvert.handlers'))
199 handlers.extend(load_handlers('kernelspecs.handlers'))
219 handlers.extend(load_handlers('kernelspecs.handlers'))
220 handlers.extend(load_handlers('edit.handlers'))
221 handlers.extend(load_handlers('services.config.handlers'))
200 handlers.extend(load_handlers('services.kernels.handlers'))
222 handlers.extend(load_handlers('services.kernels.handlers'))
201 handlers.extend(load_handlers('services.contents.handlers'))
223 handlers.extend(load_handlers('services.contents.handlers'))
202 handlers.extend(load_handlers('services.clusters.handlers'))
224 handlers.extend(load_handlers('services.clusters.handlers'))
203 handlers.extend(load_handlers('services.sessions.handlers'))
225 handlers.extend(load_handlers('services.sessions.handlers'))
204 handlers.extend(load_handlers('services.nbconvert.handlers'))
226 handlers.extend(load_handlers('services.nbconvert.handlers'))
205 handlers.extend(load_handlers('services.kernelspecs.handlers'))
227 handlers.extend(load_handlers('services.kernelspecs.handlers'))
206 # FIXME: /files/ should be handled by the Contents service when it exists
228 handlers.extend(load_handlers('services.security.handlers'))
207 cm = settings['contents_manager']
208 if hasattr(cm, 'root_dir'):
209 handlers.append(
210 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : cm.root_dir}),
211 )
212 handlers.append(
229 handlers.append(
213 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
230 (r"/nbextensions/(.*)", FileFindHandler, {
231 'path': settings['nbextensions_path'],
232 'no_cache_paths': ['/'], # don't cache anything in nbextensions
233 }),
214 )
234 )
235 # register base handlers last
236 handlers.extend(load_handlers('base.handlers'))
215 # set the URL that will be redirected from `/`
237 # set the URL that will be redirected from `/`
216 handlers.append(
238 handlers.append(
217 (r'/?', web.RedirectHandler, {
239 (r'/?', web.RedirectHandler, {
@@ -325,7 +347,7 b' class NotebookApp(BaseIPythonApplication):'
325 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
347 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
326 )
348 )
327
349
328 kernel_argv = List(Unicode)
350 ipython_kernel_argv = List(Unicode)
329
351
330 _log_formatter_cls = LogFormatter
352 _log_formatter_cls = LogFormatter
331
353
@@ -345,11 +367,6 b' class NotebookApp(BaseIPythonApplication):'
345
367
346 # file to be opened in the notebook server
368 # file to be opened in the notebook server
347 file_to_run = Unicode('', config=True)
369 file_to_run = Unicode('', config=True)
348 def _file_to_run_changed(self, name, old, new):
349 path, base = os.path.split(new)
350 if path:
351 self.file_to_run = base
352 self.notebook_dir = path
353
370
354 # Network related information
371 # Network related information
355
372
@@ -531,7 +548,20 b' class NotebookApp(BaseIPythonApplication):'
531 def static_file_path(self):
548 def static_file_path(self):
532 """return extra paths + the default location"""
549 """return extra paths + the default location"""
533 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
550 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
534
551
552 extra_template_paths = List(Unicode, config=True,
553 help="""Extra paths to search for serving jinja templates.
554
555 Can be used to override templates from IPython.html.templates."""
556 )
557 def _extra_template_paths_default(self):
558 return []
559
560 @property
561 def template_file_path(self):
562 """return extra paths + the default locations"""
563 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
564
535 nbextensions_path = List(Unicode, config=True,
565 nbextensions_path = List(Unicode, config=True,
536 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
566 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
537 )
567 )
@@ -599,26 +629,38 b' class NotebookApp(BaseIPythonApplication):'
599 help='The cluster manager class to use.'
629 help='The cluster manager class to use.'
600 )
630 )
601
631
632 config_manager_class = DottedObjectName('IPython.html.services.config.manager.ConfigManager',
633 config = True,
634 help='The config manager class to use'
635 )
636
602 kernel_spec_manager = Instance(KernelSpecManager)
637 kernel_spec_manager = Instance(KernelSpecManager)
603
638
604 def _kernel_spec_manager_default(self):
639 def _kernel_spec_manager_default(self):
605 return KernelSpecManager(ipython_dir=self.ipython_dir)
640 return KernelSpecManager(ipython_dir=self.ipython_dir)
606
641
642
643 kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager',
644 config=True,
645 help="""
646 The kernel spec manager class to use. Should be a subclass
647 of `IPython.kernel.kernelspec.KernelSpecManager`.
648
649 The Api of KernelSpecManager is provisional and might change
650 without warning between this version of IPython and the next stable one.
651 """)
652
607 trust_xheaders = Bool(False, config=True,
653 trust_xheaders = Bool(False, config=True,
608 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
654 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
609 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
655 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
610 )
656 )
611
657
612 info_file = Unicode()
658 info_file = Unicode()
613
659
614 def _info_file_default(self):
660 def _info_file_default(self):
615 info_file = "nbserver-%s.json"%os.getpid()
661 info_file = "nbserver-%s.json"%os.getpid()
616 return os.path.join(self.profile_dir.security_dir, info_file)
662 return os.path.join(self.profile_dir.security_dir, info_file)
617
663
618 notebook_dir = Unicode(py3compat.getcwd(), config=True,
619 help="The directory to use for notebooks and kernels."
620 )
621
622 pylab = Unicode('disabled', config=True,
664 pylab = Unicode('disabled', config=True,
623 help="""
665 help="""
624 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
666 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
@@ -636,6 +678,16 b' class NotebookApp(BaseIPythonApplication):'
636 )
678 )
637 self.exit(1)
679 self.exit(1)
638
680
681 notebook_dir = Unicode(config=True,
682 help="The directory to use for notebooks and kernels."
683 )
684
685 def _notebook_dir_default(self):
686 if self.file_to_run:
687 return os.path.dirname(os.path.abspath(self.file_to_run))
688 else:
689 return py3compat.getcwd()
690
639 def _notebook_dir_changed(self, name, old, new):
691 def _notebook_dir_changed(self, name, old, new):
640 """Do a bit of validation of the notebook dir."""
692 """Do a bit of validation of the notebook dir."""
641 if not os.path.isabs(new):
693 if not os.path.isabs(new):
@@ -671,16 +723,20 b' class NotebookApp(BaseIPythonApplication):'
671 self.update_config(c)
723 self.update_config(c)
672
724
673 def init_kernel_argv(self):
725 def init_kernel_argv(self):
674 """construct the kernel arguments"""
726 """add the profile-dir to arguments to be passed to IPython kernels"""
727 # FIXME: remove special treatment of IPython kernels
675 # Kernel should get *absolute* path to profile directory
728 # Kernel should get *absolute* path to profile directory
676 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
729 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
677
730
678 def init_configurables(self):
731 def init_configurables(self):
679 # force Session default to be secure
732 # force Session default to be secure
680 default_secure(self.config)
733 default_secure(self.config)
734 kls = import_item(self.kernel_spec_manager_class)
735 self.kernel_spec_manager = kls(ipython_dir=self.ipython_dir)
736
681 kls = import_item(self.kernel_manager_class)
737 kls = import_item(self.kernel_manager_class)
682 self.kernel_manager = kls(
738 self.kernel_manager = kls(
683 parent=self, log=self.log, kernel_argv=self.kernel_argv,
739 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
684 connection_dir = self.profile_dir.security_dir,
740 connection_dir = self.profile_dir.security_dir,
685 )
741 )
686 kls = import_item(self.contents_manager_class)
742 kls = import_item(self.contents_manager_class)
@@ -693,12 +749,19 b' class NotebookApp(BaseIPythonApplication):'
693 self.cluster_manager = kls(parent=self, log=self.log)
749 self.cluster_manager = kls(parent=self, log=self.log)
694 self.cluster_manager.update_profiles()
750 self.cluster_manager.update_profiles()
695
751
752 kls = import_item(self.config_manager_class)
753 self.config_manager = kls(parent=self, log=self.log,
754 profile_dir=self.profile_dir.location)
755
696 def init_logging(self):
756 def init_logging(self):
697 # This prevents double log messages because tornado use a root logger that
757 # This prevents double log messages because tornado use a root logger that
698 # self.log is a child of. The logging module dipatches log messages to a log
758 # self.log is a child of. The logging module dipatches log messages to a log
699 # and all of its ancenstors until propagate is set to False.
759 # and all of its ancenstors until propagate is set to False.
700 self.log.propagate = False
760 self.log.propagate = False
701
761
762 for log in app_log, access_log, gen_log:
763 # consistent log output name (NotebookApp instead of tornado.access, etc.)
764 log.name = self.log.name
702 # hook up tornado 3's loggers to our app handlers
765 # hook up tornado 3's loggers to our app handlers
703 logger = logging.getLogger('tornado')
766 logger = logging.getLogger('tornado')
704 logger.propagate = True
767 logger.propagate = True
@@ -715,6 +778,7 b' class NotebookApp(BaseIPythonApplication):'
715 self.web_app = NotebookWebApplication(
778 self.web_app = NotebookWebApplication(
716 self, self.kernel_manager, self.contents_manager,
779 self, self.kernel_manager, self.contents_manager,
717 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
780 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
781 self.config_manager,
718 self.log, self.base_url, self.default_url, self.tornado_settings,
782 self.log, self.base_url, self.default_url, self.tornado_settings,
719 self.jinja_environment_options
783 self.jinja_environment_options
720 )
784 )
@@ -771,6 +835,14 b' class NotebookApp(BaseIPythonApplication):'
771 proto = 'https' if self.certfile else 'http'
835 proto = 'https' if self.certfile else 'http'
772 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
836 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
773
837
838 def init_terminals(self):
839 try:
840 from .terminal import initialize
841 initialize(self.web_app)
842 self.web_app.settings['terminals_available'] = True
843 except ImportError as e:
844 self.log.info("Terminals not available (error was %s)", e)
845
774 def init_signal(self):
846 def init_signal(self):
775 if not sys.platform.startswith('win'):
847 if not sys.platform.startswith('win'):
776 signal.signal(signal.SIGINT, self._handle_sigint)
848 signal.signal(signal.SIGINT, self._handle_sigint)
@@ -850,6 +922,7 b' class NotebookApp(BaseIPythonApplication):'
850 self.init_configurables()
922 self.init_configurables()
851 self.init_components()
923 self.init_components()
852 self.init_webapp()
924 self.init_webapp()
925 self.init_terminals()
853 self.init_signal()
926 self.init_signal()
854
927
855 def cleanup_kernels(self):
928 def cleanup_kernels(self):
@@ -917,12 +990,12 b' class NotebookApp(BaseIPythonApplication):'
917 browser = None
990 browser = None
918
991
919 if self.file_to_run:
992 if self.file_to_run:
920 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
993 if not os.path.exists(self.file_to_run):
921 if not os.path.exists(fullpath):
994 self.log.critical("%s does not exist" % self.file_to_run)
922 self.log.critical("%s does not exist" % fullpath)
923 self.exit(1)
995 self.exit(1)
924
996
925 uri = url_path_join('notebooks', self.file_to_run)
997 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
998 uri = url_path_join('notebooks', *relpath.split(os.sep))
926 else:
999 else:
927 uri = 'tree'
1000 uri = 'tree'
928 if browser:
1001 if browser:
@@ -1,44 +1,23 b''
1 """Manage IPython.parallel clusters in the notebook.
1 """Manage IPython.parallel clusters in the notebook."""
2
2
3 Authors:
3 # Copyright (c) IPython Development Team.
4
4 # Distributed under the terms of the Modified BSD License.
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
5
19 from tornado import web
6 from tornado import web
20 from zmq.eventloop import ioloop
21
7
22 from IPython.config.configurable import LoggingConfigurable
8 from IPython.config.configurable import LoggingConfigurable
23 from IPython.utils.traitlets import Dict, Instance, CFloat
9 from IPython.utils.traitlets import Dict, Instance, Float
24 from IPython.core.profileapp import list_profiles_in
10 from IPython.core.profileapp import list_profiles_in
25 from IPython.core.profiledir import ProfileDir
11 from IPython.core.profiledir import ProfileDir
26 from IPython.utils import py3compat
12 from IPython.utils import py3compat
27 from IPython.utils.path import get_ipython_dir
13 from IPython.utils.path import get_ipython_dir
28
14
29
15
30 #-----------------------------------------------------------------------------
31 # Classes
32 #-----------------------------------------------------------------------------
33
34
35
36
37 class ClusterManager(LoggingConfigurable):
16 class ClusterManager(LoggingConfigurable):
38
17
39 profiles = Dict()
18 profiles = Dict()
40
19
41 delay = CFloat(1., config=True,
20 delay = Float(1., config=True,
42 help="delay (in s) between starting the controller and the engines")
21 help="delay (in s) between starting the controller and the engines")
43
22
44 loop = Instance('zmq.eventloop.ioloop.IOLoop')
23 loop = Instance('zmq.eventloop.ioloop.IOLoop')
@@ -75,16 +54,24 b' class ClusterManager(LoggingConfigurable):'
75 def update_profiles(self):
54 def update_profiles(self):
76 """List all profiles in the ipython_dir and cwd.
55 """List all profiles in the ipython_dir and cwd.
77 """
56 """
57
58 stale = set(self.profiles)
78 for path in [get_ipython_dir(), py3compat.getcwd()]:
59 for path in [get_ipython_dir(), py3compat.getcwd()]:
79 for profile in list_profiles_in(path):
60 for profile in list_profiles_in(path):
61 if profile in stale:
62 stale.remove(profile)
80 pd = self.get_profile_dir(profile, path)
63 pd = self.get_profile_dir(profile, path)
81 if profile not in self.profiles:
64 if profile not in self.profiles:
82 self.log.debug("Adding cluster profile '%s'" % profile)
65 self.log.debug("Adding cluster profile '%s'", profile)
83 self.profiles[profile] = {
66 self.profiles[profile] = {
84 'profile': profile,
67 'profile': profile,
85 'profile_dir': pd,
68 'profile_dir': pd,
86 'status': 'stopped'
69 'status': 'stopped'
87 }
70 }
71 for profile in stale:
72 # remove profiles that no longer exist
73 self.log.debug("Profile '%s' no longer exists", profile)
74 self.profiles.pop(stale)
88
75
89 def list_profiles(self):
76 def list_profiles(self):
90 self.update_profiles()
77 self.update_profiles()
@@ -133,11 +120,13 b' class ClusterManager(LoggingConfigurable):'
133 esl.stop()
120 esl.stop()
134 clean_data()
121 clean_data()
135 cl.on_stop(controller_stopped)
122 cl.on_stop(controller_stopped)
136
123 loop = self.loop
137 dc = ioloop.DelayedCallback(lambda: cl.start(), 0, self.loop)
124
138 dc.start()
125 def start():
139 dc = ioloop.DelayedCallback(lambda: esl.start(n), 1000*self.delay, self.loop)
126 """start the controller, then the engines after a delay"""
140 dc.start()
127 cl.start()
128 loop.add_timeout(self.loop.time() + self.delay, lambda : esl.start(n))
129 self.loop.add_callback(start)
141
130
142 self.log.debug('Cluster started')
131 self.log.debug('Cluster started')
143 data['controller_launcher'] = cl
132 data['controller_launcher'] = cl
@@ -4,27 +4,66 b''
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import base64
6 import base64
7 import errno
7 import io
8 import io
8 import os
9 import os
9 import glob
10 import shutil
10 import shutil
11 from contextlib import contextmanager
12 import mimetypes
11
13
12 from tornado import web
14 from tornado import web
13
15
14 from .manager import ContentsManager
16 from .manager import ContentsManager
15 from IPython.nbformat import current
17 from IPython import nbformat
16 from IPython.utils.io import atomic_writing
18 from IPython.utils.io import atomic_writing
17 from IPython.utils.path import ensure_dir_exists
19 from IPython.utils.path import ensure_dir_exists
18 from IPython.utils.traitlets import Unicode, Bool, TraitError
20 from IPython.utils.traitlets import Unicode, Bool, TraitError
19 from IPython.utils.py3compat import getcwd
21 from IPython.utils.py3compat import getcwd, str_to_unicode
20 from IPython.utils import tz
22 from IPython.utils import tz
21 from IPython.html.utils import is_hidden, to_os_path, url_path_join
23 from IPython.html.utils import is_hidden, to_os_path, to_api_path
22
24
23
25
24 class FileContentsManager(ContentsManager):
26 class FileContentsManager(ContentsManager):
25
27
26 root_dir = Unicode(getcwd(), config=True)
28 root_dir = Unicode(config=True)
27
29
30 def _root_dir_default(self):
31 try:
32 return self.parent.notebook_dir
33 except AttributeError:
34 return getcwd()
35
36 @contextmanager
37 def perm_to_403(self, os_path=''):
38 """context manager for turning permission errors into 403"""
39 try:
40 yield
41 except OSError as e:
42 if e.errno in {errno.EPERM, errno.EACCES}:
43 # make 403 error message without root prefix
44 # this may not work perfectly on unicode paths on Python 2,
45 # but nobody should be doing that anyway.
46 if not os_path:
47 os_path = str_to_unicode(e.filename or 'unknown file')
48 path = to_api_path(os_path, self.root_dir)
49 raise web.HTTPError(403, u'Permission denied: %s' % path)
50 else:
51 raise
52
53 @contextmanager
54 def open(self, os_path, *args, **kwargs):
55 """wrapper around io.open that turns permission errors into 403"""
56 with self.perm_to_403(os_path):
57 with io.open(os_path, *args, **kwargs) as f:
58 yield f
59
60 @contextmanager
61 def atomic_writing(self, os_path, *args, **kwargs):
62 """wrapper around atomic_writing that turns permission errors into 403"""
63 with self.perm_to_403(os_path):
64 with atomic_writing(os_path, *args, **kwargs) as f:
65 yield f
66
28 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
67 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
29 def _save_script_changed(self):
68 def _save_script_changed(self):
30 self.log.warn("""
69 self.log.warn("""
@@ -61,27 +100,22 b' class FileContentsManager(ContentsManager):'
61 except OSError as e:
100 except OSError as e:
62 self.log.debug("copystat on %s failed", dest, exc_info=True)
101 self.log.debug("copystat on %s failed", dest, exc_info=True)
63
102
64 def _get_os_path(self, name=None, path=''):
103 def _get_os_path(self, path):
65 """Given a filename and API path, return its file system
104 """Given an API path, return its file system path.
66 path.
67
105
68 Parameters
106 Parameters
69 ----------
107 ----------
70 name : string
71 A filename
72 path : string
108 path : string
73 The relative API path to the named file.
109 The relative API path to the named file.
74
110
75 Returns
111 Returns
76 -------
112 -------
77 path : string
113 path : string
78 API path to be evaluated relative to root_dir.
114 Native, absolute OS path to for a file.
79 """
115 """
80 if name is not None:
81 path = url_path_join(path, name)
82 return to_os_path(path, self.root_dir)
116 return to_os_path(path, self.root_dir)
83
117
84 def path_exists(self, path):
118 def dir_exists(self, path):
85 """Does the API-style path refer to an extant directory?
119 """Does the API-style path refer to an extant directory?
86
120
87 API-style wrapper for os.path.isdir
121 API-style wrapper for os.path.isdir
@@ -112,25 +146,22 b' class FileContentsManager(ContentsManager):'
112
146
113 Returns
147 Returns
114 -------
148 -------
115 exists : bool
149 hidden : bool
116 Whether the path is hidden.
150 Whether the path exists and is hidden.
117
118 """
151 """
119 path = path.strip('/')
152 path = path.strip('/')
120 os_path = self._get_os_path(path=path)
153 os_path = self._get_os_path(path=path)
121 return is_hidden(os_path, self.root_dir)
154 return is_hidden(os_path, self.root_dir)
122
155
123 def file_exists(self, name, path=''):
156 def file_exists(self, path):
124 """Returns True if the file exists, else returns False.
157 """Returns True if the file exists, else returns False.
125
158
126 API-style wrapper for os.path.isfile
159 API-style wrapper for os.path.isfile
127
160
128 Parameters
161 Parameters
129 ----------
162 ----------
130 name : string
131 The name of the file you are checking.
132 path : string
163 path : string
133 The relative path to the file's directory (with '/' as separator)
164 The relative path to the file (with '/' as separator)
134
165
135 Returns
166 Returns
136 -------
167 -------
@@ -138,20 +169,18 b' class FileContentsManager(ContentsManager):'
138 Whether the file exists.
169 Whether the file exists.
139 """
170 """
140 path = path.strip('/')
171 path = path.strip('/')
141 nbpath = self._get_os_path(name, path=path)
172 os_path = self._get_os_path(path)
142 return os.path.isfile(nbpath)
173 return os.path.isfile(os_path)
143
174
144 def exists(self, name=None, path=''):
175 def exists(self, path):
145 """Returns True if the path [and name] exists, else returns False.
176 """Returns True if the path exists, else returns False.
146
177
147 API-style wrapper for os.path.exists
178 API-style wrapper for os.path.exists
148
179
149 Parameters
180 Parameters
150 ----------
181 ----------
151 name : string
152 The name of the file you are checking.
153 path : string
182 path : string
154 The relative path to the file's directory (with '/' as separator)
183 The API path to the file (with '/' as separator)
155
184
156 Returns
185 Returns
157 -------
186 -------
@@ -159,33 +188,39 b' class FileContentsManager(ContentsManager):'
159 Whether the target exists.
188 Whether the target exists.
160 """
189 """
161 path = path.strip('/')
190 path = path.strip('/')
162 os_path = self._get_os_path(name, path=path)
191 os_path = self._get_os_path(path=path)
163 return os.path.exists(os_path)
192 return os.path.exists(os_path)
164
193
165 def _base_model(self, name, path=''):
194 def _base_model(self, path):
166 """Build the common base of a contents model"""
195 """Build the common base of a contents model"""
167 os_path = self._get_os_path(name, path)
196 os_path = self._get_os_path(path)
168 info = os.stat(os_path)
197 info = os.stat(os_path)
169 last_modified = tz.utcfromtimestamp(info.st_mtime)
198 last_modified = tz.utcfromtimestamp(info.st_mtime)
170 created = tz.utcfromtimestamp(info.st_ctime)
199 created = tz.utcfromtimestamp(info.st_ctime)
171 # Create the base model.
200 # Create the base model.
172 model = {}
201 model = {}
173 model['name'] = name
202 model['name'] = path.rsplit('/', 1)[-1]
174 model['path'] = path
203 model['path'] = path
175 model['last_modified'] = last_modified
204 model['last_modified'] = last_modified
176 model['created'] = created
205 model['created'] = created
177 model['content'] = None
206 model['content'] = None
178 model['format'] = None
207 model['format'] = None
208 model['mimetype'] = None
209 try:
210 model['writable'] = os.access(os_path, os.W_OK)
211 except OSError:
212 self.log.error("Failed to check write permissions on %s", os_path)
213 model['writable'] = False
179 return model
214 return model
180
215
181 def _dir_model(self, name, path='', content=True):
216 def _dir_model(self, path, content=True):
182 """Build a model for a directory
217 """Build a model for a directory
183
218
184 if content is requested, will include a listing of the directory
219 if content is requested, will include a listing of the directory
185 """
220 """
186 os_path = self._get_os_path(name, path)
221 os_path = self._get_os_path(path)
187
222
188 four_o_four = u'directory does not exist: %r' % os_path
223 four_o_four = u'directory does not exist: %r' % path
189
224
190 if not os.path.isdir(os_path):
225 if not os.path.isdir(os_path):
191 raise web.HTTPError(404, four_o_four)
226 raise web.HTTPError(404, four_o_four)
@@ -195,80 +230,105 b' class FileContentsManager(ContentsManager):'
195 )
230 )
196 raise web.HTTPError(404, four_o_four)
231 raise web.HTTPError(404, four_o_four)
197
232
198 if name is None:
233 model = self._base_model(path)
199 if '/' in path:
200 path, name = path.rsplit('/', 1)
201 else:
202 name = ''
203 model = self._base_model(name, path)
204 model['type'] = 'directory'
234 model['type'] = 'directory'
205 dir_path = u'{}/{}'.format(path, name)
206 if content:
235 if content:
207 model['content'] = contents = []
236 model['content'] = contents = []
208 for os_path in glob.glob(self._get_os_path('*', dir_path)):
237 os_dir = self._get_os_path(path)
209 name = os.path.basename(os_path)
238 for name in os.listdir(os_dir):
239 os_path = os.path.join(os_dir, name)
210 # skip over broken symlinks in listing
240 # skip over broken symlinks in listing
211 if not os.path.exists(os_path):
241 if not os.path.exists(os_path):
212 self.log.warn("%s doesn't exist", os_path)
242 self.log.warn("%s doesn't exist", os_path)
213 continue
243 continue
244 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
245 self.log.debug("%s not a regular file", os_path)
246 continue
214 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
247 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
215 contents.append(self.get_model(name=name, path=dir_path, content=False))
248 contents.append(self.get(
249 path='%s/%s' % (path, name),
250 content=False)
251 )
216
252
217 model['format'] = 'json'
253 model['format'] = 'json'
218
254
219 return model
255 return model
220
256
221 def _file_model(self, name, path='', content=True):
257 def _file_model(self, path, content=True, format=None):
222 """Build a model for a file
258 """Build a model for a file
223
259
224 if content is requested, include the file contents.
260 if content is requested, include the file contents.
225 UTF-8 text files will be unicode, binary files will be base64-encoded.
261
262 format:
263 If 'text', the contents will be decoded as UTF-8.
264 If 'base64', the raw bytes contents will be encoded as base64.
265 If not specified, try to decode as UTF-8, and fall back to base64
226 """
266 """
227 model = self._base_model(name, path)
267 model = self._base_model(path)
228 model['type'] = 'file'
268 model['type'] = 'file'
269
270 os_path = self._get_os_path(path)
271 model['mimetype'] = mimetypes.guess_type(os_path)[0] or 'text/plain'
272
229 if content:
273 if content:
230 os_path = self._get_os_path(name, path)
274 if not os.path.isfile(os_path):
231 with io.open(os_path, 'rb') as f:
275 # could be FIFO
276 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
277 with self.open(os_path, 'rb') as f:
232 bcontent = f.read()
278 bcontent = f.read()
233 try:
279
234 model['content'] = bcontent.decode('utf8')
280 if format != 'base64':
235 except UnicodeError as e:
281 try:
282 model['content'] = bcontent.decode('utf8')
283 except UnicodeError as e:
284 if format == 'text':
285 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
286 else:
287 model['format'] = 'text'
288
289 if model['content'] is None:
236 model['content'] = base64.encodestring(bcontent).decode('ascii')
290 model['content'] = base64.encodestring(bcontent).decode('ascii')
237 model['format'] = 'base64'
291 model['format'] = 'base64'
238 else:
292
239 model['format'] = 'text'
240 return model
293 return model
241
294
242
295
243 def _notebook_model(self, name, path='', content=True):
296 def _notebook_model(self, path, content=True):
244 """Build a notebook model
297 """Build a notebook model
245
298
246 if content is requested, the notebook content will be populated
299 if content is requested, the notebook content will be populated
247 as a JSON structure (not double-serialized)
300 as a JSON structure (not double-serialized)
248 """
301 """
249 model = self._base_model(name, path)
302 model = self._base_model(path)
250 model['type'] = 'notebook'
303 model['type'] = 'notebook'
251 if content:
304 if content:
252 os_path = self._get_os_path(name, path)
305 os_path = self._get_os_path(path)
253 with io.open(os_path, 'r', encoding='utf-8') as f:
306 with self.open(os_path, 'r', encoding='utf-8') as f:
254 try:
307 try:
255 nb = current.read(f, u'json')
308 nb = nbformat.read(f, as_version=4)
256 except Exception as e:
309 except Exception as e:
257 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
310 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
258 self.mark_trusted_cells(nb, name, path)
311 self.mark_trusted_cells(nb, path)
259 model['content'] = nb
312 model['content'] = nb
260 model['format'] = 'json'
313 model['format'] = 'json'
314 self.validate_notebook_model(model)
261 return model
315 return model
262
316
263 def get_model(self, name, path='', content=True):
317 def get(self, path, content=True, type_=None, format=None):
264 """ Takes a path and name for an entity and returns its model
318 """ Takes a path for an entity and returns its model
265
319
266 Parameters
320 Parameters
267 ----------
321 ----------
268 name : str
269 the name of the target
270 path : str
322 path : str
271 the API path that describes the relative path for the target
323 the API path that describes the relative path for the target
324 content : bool
325 Whether to include the contents in the reply
326 type_ : str, optional
327 The requested type - 'file', 'notebook', or 'directory'.
328 Will raise HTTPError 400 if the content doesn't match.
329 format : str, optional
330 The requested format for file contents. 'text' or 'base64'.
331 Ignored if this returns a notebook or directory model.
272
332
273 Returns
333 Returns
274 -------
334 -------
@@ -278,32 +338,35 b' class FileContentsManager(ContentsManager):'
278 """
338 """
279 path = path.strip('/')
339 path = path.strip('/')
280
340
281 if not self.exists(name=name, path=path):
341 if not self.exists(path):
282 raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
342 raise web.HTTPError(404, u'No such file or directory: %s' % path)
283
343
284 os_path = self._get_os_path(name, path)
344 os_path = self._get_os_path(path)
285 if os.path.isdir(os_path):
345 if os.path.isdir(os_path):
286 model = self._dir_model(name, path, content)
346 if type_ not in (None, 'directory'):
287 elif name.endswith('.ipynb'):
347 raise web.HTTPError(400,
288 model = self._notebook_model(name, path, content)
348 u'%s is a directory, not a %s' % (path, type_))
349 model = self._dir_model(path, content=content)
350 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
351 model = self._notebook_model(path, content=content)
289 else:
352 else:
290 model = self._file_model(name, path, content)
353 if type_ == 'directory':
354 raise web.HTTPError(400,
355 u'%s is not a directory')
356 model = self._file_model(path, content=content, format=format)
291 return model
357 return model
292
358
293 def _save_notebook(self, os_path, model, name='', path=''):
359 def _save_notebook(self, os_path, model, path=''):
294 """save a notebook file"""
360 """save a notebook file"""
295 # Save the notebook file
361 # Save the notebook file
296 nb = current.to_notebook_json(model['content'])
362 nb = nbformat.from_dict(model['content'])
297
363
298 self.check_and_sign(nb, name, path)
364 self.check_and_sign(nb, path)
299
365
300 if 'name' in nb['metadata']:
366 with self.atomic_writing(os_path, encoding='utf-8') as f:
301 nb['metadata']['name'] = u''
367 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
302
368
303 with atomic_writing(os_path, encoding='utf-8') as f:
369 def _save_file(self, os_path, model, path=''):
304 current.write(nb, f, u'json')
305
306 def _save_file(self, os_path, model, name='', path=''):
307 """save a non-notebook file"""
370 """save a non-notebook file"""
308 fmt = model.get('format', None)
371 fmt = model.get('format', None)
309 if fmt not in {'text', 'base64'}:
372 if fmt not in {'text', 'base64'}:
@@ -317,21 +380,22 b' class FileContentsManager(ContentsManager):'
317 bcontent = base64.decodestring(b64_bytes)
380 bcontent = base64.decodestring(b64_bytes)
318 except Exception as e:
381 except Exception as e:
319 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
382 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
320 with atomic_writing(os_path, text=False) as f:
383 with self.atomic_writing(os_path, text=False) as f:
321 f.write(bcontent)
384 f.write(bcontent)
322
385
323 def _save_directory(self, os_path, model, name='', path=''):
386 def _save_directory(self, os_path, model, path=''):
324 """create a directory"""
387 """create a directory"""
325 if is_hidden(os_path, self.root_dir):
388 if is_hidden(os_path, self.root_dir):
326 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
389 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
327 if not os.path.exists(os_path):
390 if not os.path.exists(os_path):
328 os.mkdir(os_path)
391 with self.perm_to_403():
392 os.mkdir(os_path)
329 elif not os.path.isdir(os_path):
393 elif not os.path.isdir(os_path):
330 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
394 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
331 else:
395 else:
332 self.log.debug("Directory %r already exists", os_path)
396 self.log.debug("Directory %r already exists", os_path)
333
397
334 def save(self, model, name='', path=''):
398 def save(self, model, path=''):
335 """Save the file model and return the model with no content."""
399 """Save the file model and return the model with no content."""
336 path = path.strip('/')
400 path = path.strip('/')
337
401
@@ -341,52 +405,53 b' class FileContentsManager(ContentsManager):'
341 raise web.HTTPError(400, u'No file content provided')
405 raise web.HTTPError(400, u'No file content provided')
342
406
343 # One checkpoint should always exist
407 # One checkpoint should always exist
344 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
408 if self.file_exists(path) and not self.list_checkpoints(path):
345 self.create_checkpoint(name, path)
409 self.create_checkpoint(path)
346
347 new_path = model.get('path', path).strip('/')
348 new_name = model.get('name', name)
349
410
350 if path != new_path or name != new_name:
411 os_path = self._get_os_path(path)
351 self.rename(name, path, new_name, new_path)
352
353 os_path = self._get_os_path(new_name, new_path)
354 self.log.debug("Saving %s", os_path)
412 self.log.debug("Saving %s", os_path)
355 try:
413 try:
356 if model['type'] == 'notebook':
414 if model['type'] == 'notebook':
357 self._save_notebook(os_path, model, new_name, new_path)
415 self._save_notebook(os_path, model, path)
358 elif model['type'] == 'file':
416 elif model['type'] == 'file':
359 self._save_file(os_path, model, new_name, new_path)
417 self._save_file(os_path, model, path)
360 elif model['type'] == 'directory':
418 elif model['type'] == 'directory':
361 self._save_directory(os_path, model, new_name, new_path)
419 self._save_directory(os_path, model, path)
362 else:
420 else:
363 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
421 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
364 except web.HTTPError:
422 except web.HTTPError:
365 raise
423 raise
366 except Exception as e:
424 except Exception as e:
367 raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
425 self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
426 raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
427
428 validation_message = None
429 if model['type'] == 'notebook':
430 self.validate_notebook_model(model)
431 validation_message = model.get('message', None)
368
432
369 model = self.get_model(new_name, new_path, content=False)
433 model = self.get(path, content=False)
434 if validation_message:
435 model['message'] = validation_message
370 return model
436 return model
371
437
372 def update(self, model, name, path=''):
438 def update(self, model, path):
373 """Update the file's path and/or name
439 """Update the file's path
374
440
375 For use in PATCH requests, to enable renaming a file without
441 For use in PATCH requests, to enable renaming a file without
376 re-uploading its contents. Only used for renaming at the moment.
442 re-uploading its contents. Only used for renaming at the moment.
377 """
443 """
378 path = path.strip('/')
444 path = path.strip('/')
379 new_name = model.get('name', name)
380 new_path = model.get('path', path).strip('/')
445 new_path = model.get('path', path).strip('/')
381 if path != new_path or name != new_name:
446 if path != new_path:
382 self.rename(name, path, new_name, new_path)
447 self.rename(path, new_path)
383 model = self.get_model(new_name, new_path, content=False)
448 model = self.get(new_path, content=False)
384 return model
449 return model
385
450
386 def delete(self, name, path=''):
451 def delete(self, path):
387 """Delete file by name and path."""
452 """Delete file at path."""
388 path = path.strip('/')
453 path = path.strip('/')
389 os_path = self._get_os_path(name, path)
454 os_path = self._get_os_path(path)
390 rm = os.unlink
455 rm = os.unlink
391 if os.path.isdir(os_path):
456 if os.path.isdir(os_path):
392 listing = os.listdir(os_path)
457 listing = os.listdir(os_path)
@@ -397,71 +462,81 b' class FileContentsManager(ContentsManager):'
397 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
462 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
398
463
399 # clear checkpoints
464 # clear checkpoints
400 for checkpoint in self.list_checkpoints(name, path):
465 for checkpoint in self.list_checkpoints(path):
401 checkpoint_id = checkpoint['id']
466 checkpoint_id = checkpoint['id']
402 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
467 cp_path = self.get_checkpoint_path(checkpoint_id, path)
403 if os.path.isfile(cp_path):
468 if os.path.isfile(cp_path):
404 self.log.debug("Unlinking checkpoint %s", cp_path)
469 self.log.debug("Unlinking checkpoint %s", cp_path)
405 os.unlink(cp_path)
470 with self.perm_to_403():
471 rm(cp_path)
406
472
407 if os.path.isdir(os_path):
473 if os.path.isdir(os_path):
408 self.log.debug("Removing directory %s", os_path)
474 self.log.debug("Removing directory %s", os_path)
409 shutil.rmtree(os_path)
475 with self.perm_to_403():
476 shutil.rmtree(os_path)
410 else:
477 else:
411 self.log.debug("Unlinking file %s", os_path)
478 self.log.debug("Unlinking file %s", os_path)
412 rm(os_path)
479 with self.perm_to_403():
480 rm(os_path)
413
481
414 def rename(self, old_name, old_path, new_name, new_path):
482 def rename(self, old_path, new_path):
415 """Rename a file."""
483 """Rename a file."""
416 old_path = old_path.strip('/')
484 old_path = old_path.strip('/')
417 new_path = new_path.strip('/')
485 new_path = new_path.strip('/')
418 if new_name == old_name and new_path == old_path:
486 if new_path == old_path:
419 return
487 return
420
488
421 new_os_path = self._get_os_path(new_name, new_path)
489 new_os_path = self._get_os_path(new_path)
422 old_os_path = self._get_os_path(old_name, old_path)
490 old_os_path = self._get_os_path(old_path)
423
491
424 # Should we proceed with the move?
492 # Should we proceed with the move?
425 if os.path.isfile(new_os_path):
493 if os.path.exists(new_os_path):
426 raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path)
494 raise web.HTTPError(409, u'File already exists: %s' % new_path)
427
495
428 # Move the file
496 # Move the file
429 try:
497 try:
430 shutil.move(old_os_path, new_os_path)
498 with self.perm_to_403():
499 shutil.move(old_os_path, new_os_path)
500 except web.HTTPError:
501 raise
431 except Exception as e:
502 except Exception as e:
432 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
503 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
433
504
434 # Move the checkpoints
505 # Move the checkpoints
435 old_checkpoints = self.list_checkpoints(old_name, old_path)
506 old_checkpoints = self.list_checkpoints(old_path)
436 for cp in old_checkpoints:
507 for cp in old_checkpoints:
437 checkpoint_id = cp['id']
508 checkpoint_id = cp['id']
438 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
509 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
439 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
510 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
440 if os.path.isfile(old_cp_path):
511 if os.path.isfile(old_cp_path):
441 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
512 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
442 shutil.move(old_cp_path, new_cp_path)
513 with self.perm_to_403():
514 shutil.move(old_cp_path, new_cp_path)
443
515
444 # Checkpoint-related utilities
516 # Checkpoint-related utilities
445
517
446 def get_checkpoint_path(self, checkpoint_id, name, path=''):
518 def get_checkpoint_path(self, checkpoint_id, path):
447 """find the path to a checkpoint"""
519 """find the path to a checkpoint"""
448 path = path.strip('/')
520 path = path.strip('/')
521 parent, name = ('/' + path).rsplit('/', 1)
522 parent = parent.strip('/')
449 basename, ext = os.path.splitext(name)
523 basename, ext = os.path.splitext(name)
450 filename = u"{name}-{checkpoint_id}{ext}".format(
524 filename = u"{name}-{checkpoint_id}{ext}".format(
451 name=basename,
525 name=basename,
452 checkpoint_id=checkpoint_id,
526 checkpoint_id=checkpoint_id,
453 ext=ext,
527 ext=ext,
454 )
528 )
455 os_path = self._get_os_path(path=path)
529 os_path = self._get_os_path(path=parent)
456 cp_dir = os.path.join(os_path, self.checkpoint_dir)
530 cp_dir = os.path.join(os_path, self.checkpoint_dir)
457 ensure_dir_exists(cp_dir)
531 with self.perm_to_403():
532 ensure_dir_exists(cp_dir)
458 cp_path = os.path.join(cp_dir, filename)
533 cp_path = os.path.join(cp_dir, filename)
459 return cp_path
534 return cp_path
460
535
461 def get_checkpoint_model(self, checkpoint_id, name, path=''):
536 def get_checkpoint_model(self, checkpoint_id, path):
462 """construct the info dict for a given checkpoint"""
537 """construct the info dict for a given checkpoint"""
463 path = path.strip('/')
538 path = path.strip('/')
464 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
539 cp_path = self.get_checkpoint_path(checkpoint_id, path)
465 stats = os.stat(cp_path)
540 stats = os.stat(cp_path)
466 last_modified = tz.utcfromtimestamp(stats.st_mtime)
541 last_modified = tz.utcfromtimestamp(stats.st_mtime)
467 info = dict(
542 info = dict(
@@ -472,58 +547,62 b' class FileContentsManager(ContentsManager):'
472
547
473 # public checkpoint API
548 # public checkpoint API
474
549
475 def create_checkpoint(self, name, path=''):
550 def create_checkpoint(self, path):
476 """Create a checkpoint from the current state of a file"""
551 """Create a checkpoint from the current state of a file"""
477 path = path.strip('/')
552 path = path.strip('/')
478 src_path = self._get_os_path(name, path)
553 if not self.file_exists(path):
554 raise web.HTTPError(404)
555 src_path = self._get_os_path(path)
479 # only the one checkpoint ID:
556 # only the one checkpoint ID:
480 checkpoint_id = u"checkpoint"
557 checkpoint_id = u"checkpoint"
481 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
558 cp_path = self.get_checkpoint_path(checkpoint_id, path)
482 self.log.debug("creating checkpoint for %s", name)
559 self.log.debug("creating checkpoint for %s", path)
483 self._copy(src_path, cp_path)
560 with self.perm_to_403():
561 self._copy(src_path, cp_path)
484
562
485 # return the checkpoint info
563 # return the checkpoint info
486 return self.get_checkpoint_model(checkpoint_id, name, path)
564 return self.get_checkpoint_model(checkpoint_id, path)
487
565
488 def list_checkpoints(self, name, path=''):
566 def list_checkpoints(self, path):
489 """list the checkpoints for a given file
567 """list the checkpoints for a given file
490
568
491 This contents manager currently only supports one checkpoint per file.
569 This contents manager currently only supports one checkpoint per file.
492 """
570 """
493 path = path.strip('/')
571 path = path.strip('/')
494 checkpoint_id = "checkpoint"
572 checkpoint_id = "checkpoint"
495 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
573 os_path = self.get_checkpoint_path(checkpoint_id, path)
496 if not os.path.exists(os_path):
574 if not os.path.exists(os_path):
497 return []
575 return []
498 else:
576 else:
499 return [self.get_checkpoint_model(checkpoint_id, name, path)]
577 return [self.get_checkpoint_model(checkpoint_id, path)]
500
578
501
579
502 def restore_checkpoint(self, checkpoint_id, name, path=''):
580 def restore_checkpoint(self, checkpoint_id, path):
503 """restore a file to a checkpointed state"""
581 """restore a file to a checkpointed state"""
504 path = path.strip('/')
582 path = path.strip('/')
505 self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
583 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
506 nb_path = self._get_os_path(name, path)
584 nb_path = self._get_os_path(path)
507 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
585 cp_path = self.get_checkpoint_path(checkpoint_id, path)
508 if not os.path.isfile(cp_path):
586 if not os.path.isfile(cp_path):
509 self.log.debug("checkpoint file does not exist: %s", cp_path)
587 self.log.debug("checkpoint file does not exist: %s", cp_path)
510 raise web.HTTPError(404,
588 raise web.HTTPError(404,
511 u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
589 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
512 )
590 )
513 # ensure notebook is readable (never restore from an unreadable notebook)
591 # ensure notebook is readable (never restore from an unreadable notebook)
514 if cp_path.endswith('.ipynb'):
592 if cp_path.endswith('.ipynb'):
515 with io.open(cp_path, 'r', encoding='utf-8') as f:
593 with self.open(cp_path, 'r', encoding='utf-8') as f:
516 current.read(f, u'json')
594 nbformat.read(f, as_version=4)
517 self._copy(cp_path, nb_path)
518 self.log.debug("copying %s -> %s", cp_path, nb_path)
595 self.log.debug("copying %s -> %s", cp_path, nb_path)
596 with self.perm_to_403():
597 self._copy(cp_path, nb_path)
519
598
520 def delete_checkpoint(self, checkpoint_id, name, path=''):
599 def delete_checkpoint(self, checkpoint_id, path):
521 """delete a file's checkpoint"""
600 """delete a file's checkpoint"""
522 path = path.strip('/')
601 path = path.strip('/')
523 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
602 cp_path = self.get_checkpoint_path(checkpoint_id, path)
524 if not os.path.isfile(cp_path):
603 if not os.path.isfile(cp_path):
525 raise web.HTTPError(404,
604 raise web.HTTPError(404,
526 u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
605 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
527 )
606 )
528 self.log.debug("unlinking %s", cp_path)
607 self.log.debug("unlinking %s", cp_path)
529 os.unlink(cp_path)
608 os.unlink(cp_path)
@@ -531,6 +610,10 b' class FileContentsManager(ContentsManager):'
531 def info_string(self):
610 def info_string(self):
532 return "Serving notebooks from local directory: %s" % self.root_dir
611 return "Serving notebooks from local directory: %s" % self.root_dir
533
612
534 def get_kernel_path(self, name, path='', model=None):
613 def get_kernel_path(self, path, model=None):
535 """Return the initial working dir a kernel associated with a given notebook"""
614 """Return the initial API path of a kernel associated with a given notebook"""
536 return os.path.join(self.root_dir, path)
615 if '/' in path:
616 parent_dir = path.rsplit('/', 1)[0]
617 else:
618 parent_dir = ''
619 return parent_dir
@@ -10,9 +10,9 b' from tornado import web'
10 from IPython.html.utils import url_path_join, url_escape
10 from IPython.html.utils import url_path_join, url_escape
11 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
12
12
13 from IPython.html.base.handlers import (IPythonHandler, json_errors,
13 from IPython.html.base.handlers import (
14 file_path_regex, path_regex,
14 IPythonHandler, json_errors, path_regex,
15 file_name_regex)
15 )
16
16
17
17
18 def sort_key(model):
18 def sort_key(model):
@@ -29,38 +29,44 b' class ContentsHandler(IPythonHandler):'
29
29
30 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
30 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
31
31
32 def location_url(self, name, path):
32 def location_url(self, path):
33 """Return the full URL location of a file.
33 """Return the full URL location of a file.
34
34
35 Parameters
35 Parameters
36 ----------
36 ----------
37 name : unicode
38 The base name of the file, such as "foo.ipynb".
39 path : unicode
37 path : unicode
40 The API path of the file, such as "foo/bar".
38 The API path of the file, such as "foo/bar.txt".
41 """
39 """
42 return url_escape(url_path_join(
40 return url_escape(url_path_join(
43 self.base_url, 'api', 'contents', path, name
41 self.base_url, 'api', 'contents', path
44 ))
42 ))
45
43
46 def _finish_model(self, model, location=True):
44 def _finish_model(self, model, location=True):
47 """Finish a JSON request with a model, setting relevant headers, etc."""
45 """Finish a JSON request with a model, setting relevant headers, etc."""
48 if location:
46 if location:
49 location = self.location_url(model['name'], model['path'])
47 location = self.location_url(model['path'])
50 self.set_header('Location', location)
48 self.set_header('Location', location)
51 self.set_header('Last-Modified', model['last_modified'])
49 self.set_header('Last-Modified', model['last_modified'])
52 self.finish(json.dumps(model, default=date_default))
50 self.finish(json.dumps(model, default=date_default))
53
51
54 @web.authenticated
52 @web.authenticated
55 @json_errors
53 @json_errors
56 def get(self, path='', name=None):
54 def get(self, path=''):
57 """Return a model for a file or directory.
55 """Return a model for a file or directory.
58
56
59 A directory model contains a list of models (without content)
57 A directory model contains a list of models (without content)
60 of the files and directories it contains.
58 of the files and directories it contains.
61 """
59 """
62 path = path or ''
60 path = path or ''
63 model = self.contents_manager.get_model(name=name, path=path)
61 type_ = self.get_query_argument('type', default=None)
62 if type_ not in {None, 'directory', 'file', 'notebook'}:
63 raise web.HTTPError(400, u'Type %r is invalid' % type_)
64
65 format = self.get_query_argument('format', default=None)#
66 if format not in {None, 'text', 'base64'}:
67 raise web.HTTPError(400, u'Format %r is invalid' % format)
68
69 model = self.contents_manager.get(path=path, type_=type_, format=format)
64 if model['type'] == 'directory':
70 if model['type'] == 'directory':
65 # group listing by type, then by name (case-insensitive)
71 # group listing by type, then by name (case-insensitive)
66 # FIXME: sorting should be done in the frontends
72 # FIXME: sorting should be done in the frontends
@@ -69,112 +75,83 b' class ContentsHandler(IPythonHandler):'
69
75
70 @web.authenticated
76 @web.authenticated
71 @json_errors
77 @json_errors
72 def patch(self, path='', name=None):
78 def patch(self, path=''):
73 """PATCH renames a notebook without re-uploading content."""
79 """PATCH renames a file or directory without re-uploading content."""
74 cm = self.contents_manager
80 cm = self.contents_manager
75 if name is None:
76 raise web.HTTPError(400, u'Filename missing')
77 model = self.get_json_body()
81 model = self.get_json_body()
78 if model is None:
82 if model is None:
79 raise web.HTTPError(400, u'JSON body missing')
83 raise web.HTTPError(400, u'JSON body missing')
80 model = cm.update(model, name, path)
84 model = cm.update(model, path)
81 self._finish_model(model)
85 self._finish_model(model)
82
86
83 def _copy(self, copy_from, path, copy_to=None):
87 def _copy(self, copy_from, copy_to=None):
84 """Copy a file, optionally specifying the new name.
88 """Copy a file, optionally specifying a target directory."""
85 """
89 self.log.info(u"Copying {copy_from} to {copy_to}".format(
86 self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format(
87 copy_from=copy_from,
90 copy_from=copy_from,
88 path=path,
89 copy_to=copy_to or '',
91 copy_to=copy_to or '',
90 ))
92 ))
91 model = self.contents_manager.copy(copy_from, copy_to, path)
93 model = self.contents_manager.copy(copy_from, copy_to)
92 self.set_status(201)
94 self.set_status(201)
93 self._finish_model(model)
95 self._finish_model(model)
94
96
95 def _upload(self, model, path, name=None):
97 def _upload(self, model, path):
96 """Handle upload of a new file
98 """Handle upload of a new file to path"""
97
99 self.log.info(u"Uploading file to %s", path)
98 If name specified, create it in path/name,
100 model = self.contents_manager.new(model, path)
99 otherwise create a new untitled file in path.
100 """
101 self.log.info(u"Uploading file to %s/%s", path, name or '')
102 if name:
103 model['name'] = name
104
105 model = self.contents_manager.create_file(model, path)
106 self.set_status(201)
101 self.set_status(201)
107 self._finish_model(model)
102 self._finish_model(model)
108
103
109 def _create_empty_file(self, path, name=None, ext='.ipynb'):
104 def _new_untitled(self, path, type='', ext=''):
110 """Create an empty file in path
105 """Create a new, empty untitled entity"""
111
106 self.log.info(u"Creating new %s in %s", type or 'file', path)
112 If name specified, create it in path/name.
107 model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)
113 """
114 self.log.info(u"Creating new file in %s/%s", path, name or '')
115 model = {}
116 if name:
117 model['name'] = name
118 model = self.contents_manager.create_file(model, path=path, ext=ext)
119 self.set_status(201)
108 self.set_status(201)
120 self._finish_model(model)
109 self._finish_model(model)
121
110
122 def _save(self, model, path, name):
111 def _save(self, model, path):
123 """Save an existing file."""
112 """Save an existing file."""
124 self.log.info(u"Saving file at %s/%s", path, name)
113 self.log.info(u"Saving file at %s", path)
125 model = self.contents_manager.save(model, name, path)
114 model = self.contents_manager.save(model, path)
126 if model['path'] != path.strip('/') or model['name'] != name:
115 self._finish_model(model)
127 # a rename happened, set Location header
128 location = True
129 else:
130 location = False
131 self._finish_model(model, location)
132
116
133 @web.authenticated
117 @web.authenticated
134 @json_errors
118 @json_errors
135 def post(self, path='', name=None):
119 def post(self, path=''):
136 """Create a new file or directory in the specified path.
120 """Create a new file in the specified path.
137
121
138 POST creates new files or directories. The server always decides on the name.
122 POST creates new files. The server always decides on the name.
139
123
140 POST /api/contents/path
124 POST /api/contents/path
141 New untitled notebook in path. If content specified, upload a
125 New untitled, empty file or directory.
142 notebook, otherwise start empty.
143 POST /api/contents/path
126 POST /api/contents/path
144 with body {"copy_from" : "OtherNotebook.ipynb"}
127 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
145 New copy of OtherNotebook in path
128 New copy of OtherNotebook in path
146 """
129 """
147
130
148 if name is not None:
149 path = u'{}/{}'.format(path, name)
150
151 cm = self.contents_manager
131 cm = self.contents_manager
152
132
153 if cm.file_exists(path):
133 if cm.file_exists(path):
154 raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.")
134 raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
155
135
156 if not cm.path_exists(path):
136 if not cm.dir_exists(path):
157 raise web.HTTPError(404, "No such directory: %s" % path)
137 raise web.HTTPError(404, "No such directory: %s" % path)
158
138
159 model = self.get_json_body()
139 model = self.get_json_body()
160
140
161 if model is not None:
141 if model is not None:
162 copy_from = model.get('copy_from')
142 copy_from = model.get('copy_from')
163 ext = model.get('ext', '.ipynb')
143 ext = model.get('ext', '')
164 if model.get('content') is not None:
144 type = model.get('type', '')
165 if copy_from:
145 if copy_from:
166 raise web.HTTPError(400, "Can't upload and copy at the same time.")
167 self._upload(model, path)
168 elif copy_from:
169 self._copy(copy_from, path)
146 self._copy(copy_from, path)
170 else:
147 else:
171 self._create_empty_file(path, ext=ext)
148 self._new_untitled(path, type=type, ext=ext)
172 else:
149 else:
173 self._create_empty_file(path)
150 self._new_untitled(path)
174
151
175 @web.authenticated
152 @web.authenticated
176 @json_errors
153 @json_errors
177 def put(self, path='', name=None):
154 def put(self, path=''):
178 """Saves the file in the location specified by name and path.
155 """Saves the file in the location specified by name and path.
179
156
180 PUT is very similar to POST, but the requester specifies the name,
157 PUT is very similar to POST, but the requester specifies the name,
@@ -184,39 +161,25 b' class ContentsHandler(IPythonHandler):'
184 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
161 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
185 in `content` key of JSON request body. If content is not specified,
162 in `content` key of JSON request body. If content is not specified,
186 create a new empty notebook.
163 create a new empty notebook.
187 PUT /api/contents/path/Name.ipynb
188 with JSON body::
189
190 {
191 "copy_from" : "[path/to/]OtherNotebook.ipynb"
192 }
193
194 Copy OtherNotebook to Name
195 """
164 """
196 if name is None:
197 raise web.HTTPError(400, "name must be specified with PUT.")
198
199 model = self.get_json_body()
165 model = self.get_json_body()
200 if model:
166 if model:
201 copy_from = model.get('copy_from')
167 if model.get('copy_from'):
202 if copy_from:
168 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
203 if model.get('content'):
169 if self.contents_manager.file_exists(path):
204 raise web.HTTPError(400, "Can't upload and copy at the same time.")
170 self._save(model, path)
205 self._copy(copy_from, path, name)
206 elif self.contents_manager.file_exists(name, path):
207 self._save(model, path, name)
208 else:
171 else:
209 self._upload(model, path, name)
172 self._upload(model, path)
210 else:
173 else:
211 self._create_empty_file(path, name)
174 self._new_untitled(path)
212
175
213 @web.authenticated
176 @web.authenticated
214 @json_errors
177 @json_errors
215 def delete(self, path='', name=None):
178 def delete(self, path=''):
216 """delete a file in the given path"""
179 """delete a file in the given path"""
217 cm = self.contents_manager
180 cm = self.contents_manager
218 self.log.warn('delete %s:%s', path, name)
181 self.log.warn('delete %s', path)
219 cm.delete(name, path)
182 cm.delete(path)
220 self.set_status(204)
183 self.set_status(204)
221 self.finish()
184 self.finish()
222
185
@@ -227,22 +190,22 b' class CheckpointsHandler(IPythonHandler):'
227
190
228 @web.authenticated
191 @web.authenticated
229 @json_errors
192 @json_errors
230 def get(self, path='', name=None):
193 def get(self, path=''):
231 """get lists checkpoints for a file"""
194 """get lists checkpoints for a file"""
232 cm = self.contents_manager
195 cm = self.contents_manager
233 checkpoints = cm.list_checkpoints(name, path)
196 checkpoints = cm.list_checkpoints(path)
234 data = json.dumps(checkpoints, default=date_default)
197 data = json.dumps(checkpoints, default=date_default)
235 self.finish(data)
198 self.finish(data)
236
199
237 @web.authenticated
200 @web.authenticated
238 @json_errors
201 @json_errors
239 def post(self, path='', name=None):
202 def post(self, path=''):
240 """post creates a new checkpoint"""
203 """post creates a new checkpoint"""
241 cm = self.contents_manager
204 cm = self.contents_manager
242 checkpoint = cm.create_checkpoint(name, path)
205 checkpoint = cm.create_checkpoint(path)
243 data = json.dumps(checkpoint, default=date_default)
206 data = json.dumps(checkpoint, default=date_default)
244 location = url_path_join(self.base_url, 'api/contents',
207 location = url_path_join(self.base_url, 'api/contents',
245 path, name, 'checkpoints', checkpoint['id'])
208 path, 'checkpoints', checkpoint['id'])
246 self.set_header('Location', url_escape(location))
209 self.set_header('Location', url_escape(location))
247 self.set_status(201)
210 self.set_status(201)
248 self.finish(data)
211 self.finish(data)
@@ -254,22 +217,38 b' class ModifyCheckpointsHandler(IPythonHandler):'
254
217
255 @web.authenticated
218 @web.authenticated
256 @json_errors
219 @json_errors
257 def post(self, path, name, checkpoint_id):
220 def post(self, path, checkpoint_id):
258 """post restores a file from a checkpoint"""
221 """post restores a file from a checkpoint"""
259 cm = self.contents_manager
222 cm = self.contents_manager
260 cm.restore_checkpoint(checkpoint_id, name, path)
223 cm.restore_checkpoint(checkpoint_id, path)
261 self.set_status(204)
224 self.set_status(204)
262 self.finish()
225 self.finish()
263
226
264 @web.authenticated
227 @web.authenticated
265 @json_errors
228 @json_errors
266 def delete(self, path, name, checkpoint_id):
229 def delete(self, path, checkpoint_id):
267 """delete clears a checkpoint for a given file"""
230 """delete clears a checkpoint for a given file"""
268 cm = self.contents_manager
231 cm = self.contents_manager
269 cm.delete_checkpoint(checkpoint_id, name, path)
232 cm.delete_checkpoint(checkpoint_id, path)
270 self.set_status(204)
233 self.set_status(204)
271 self.finish()
234 self.finish()
272
235
236
237 class NotebooksRedirectHandler(IPythonHandler):
238 """Redirect /api/notebooks to /api/contents"""
239 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
240
241 def get(self, path):
242 self.log.warn("/api/notebooks is deprecated, use /api/contents")
243 self.redirect(url_path_join(
244 self.base_url,
245 'api/contents',
246 path
247 ))
248
249 put = patch = post = delete = get
250
251
273 #-----------------------------------------------------------------------------
252 #-----------------------------------------------------------------------------
274 # URL to handler mappings
253 # URL to handler mappings
275 #-----------------------------------------------------------------------------
254 #-----------------------------------------------------------------------------
@@ -278,9 +257,9 b' class ModifyCheckpointsHandler(IPythonHandler):'
278 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
257 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
279
258
280 default_handlers = [
259 default_handlers = [
281 (r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler),
260 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
282 (r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex),
261 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
283 ModifyCheckpointsHandler),
262 ModifyCheckpointsHandler),
284 (r"/api/contents%s" % file_path_regex, ContentsHandler),
285 (r"/api/contents%s" % path_regex, ContentsHandler),
263 (r"/api/contents%s" % path_regex, ContentsHandler),
264 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
286 ]
265 ]
@@ -5,14 +5,18 b''
5
5
6 from fnmatch import fnmatch
6 from fnmatch import fnmatch
7 import itertools
7 import itertools
8 import json
8 import os
9 import os
10 import re
9
11
10 from tornado.web import HTTPError
12 from tornado.web import HTTPError
11
13
12 from IPython.config.configurable import LoggingConfigurable
14 from IPython.config.configurable import LoggingConfigurable
13 from IPython.nbformat import current, sign
15 from IPython.nbformat import sign, validate, ValidationError
16 from IPython.nbformat.v4 import new_notebook
14 from IPython.utils.traitlets import Instance, Unicode, List
17 from IPython.utils.traitlets import Instance, Unicode, List
15
18
19 copy_pat = re.compile(r'\-Copy\d*\.')
16
20
17 class ContentsManager(LoggingConfigurable):
21 class ContentsManager(LoggingConfigurable):
18 """Base class for serving files and directories.
22 """Base class for serving files and directories.
@@ -31,14 +35,6 b' class ContentsManager(LoggingConfigurable):'
31 - if unspecified, path defaults to '',
35 - if unspecified, path defaults to '',
32 indicating the root path.
36 indicating the root path.
33
37
34 name is also unicode, and refers to a specfic target:
35
36 - unicode, not url-escaped
37 - must not contain '/'
38 - It refers to an individual filename
39 - It may refer to a directory name,
40 in the case of listing or creating directories.
41
42 """
38 """
43
39
44 notary = Instance(sign.NotebookNotary)
40 notary = Instance(sign.NotebookNotary)
@@ -67,7 +63,7 b' class ContentsManager(LoggingConfigurable):'
67 # ContentsManager API part 1: methods that must be
63 # ContentsManager API part 1: methods that must be
68 # implemented in subclasses.
64 # implemented in subclasses.
69
65
70 def path_exists(self, path):
66 def dir_exists(self, path):
71 """Does the API-style path (directory) actually exist?
67 """Does the API-style path (directory) actually exist?
72
68
73 Like os.path.isdir
69 Like os.path.isdir
@@ -103,8 +99,8 b' class ContentsManager(LoggingConfigurable):'
103 """
99 """
104 raise NotImplementedError
100 raise NotImplementedError
105
101
106 def file_exists(self, name, path=''):
102 def file_exists(self, path=''):
107 """Does a file exist at the given name and path?
103 """Does a file exist at the given path?
108
104
109 Like os.path.isfile
105 Like os.path.isfile
110
106
@@ -124,15 +120,13 b' class ContentsManager(LoggingConfigurable):'
124 """
120 """
125 raise NotImplementedError('must be implemented in a subclass')
121 raise NotImplementedError('must be implemented in a subclass')
126
122
127 def exists(self, name, path=''):
123 def exists(self, path):
128 """Does a file or directory exist at the given name and path?
124 """Does a file or directory exist at the given path?
129
125
130 Like os.path.exists
126 Like os.path.exists
131
127
132 Parameters
128 Parameters
133 ----------
129 ----------
134 name : string
135 The name of the file you are checking.
136 path : string
130 path : string
137 The relative path to the file's directory (with '/' as separator)
131 The relative path to the file's directory (with '/' as separator)
138
132
@@ -141,17 +135,17 b' class ContentsManager(LoggingConfigurable):'
141 exists : bool
135 exists : bool
142 Whether the target exists.
136 Whether the target exists.
143 """
137 """
144 return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name))
138 return self.file_exists(path) or self.dir_exists(path)
145
139
146 def get_model(self, name, path='', content=True):
140 def get(self, path, content=True, type_=None, format=None):
147 """Get the model of a file or directory with or without content."""
141 """Get the model of a file or directory with or without content."""
148 raise NotImplementedError('must be implemented in a subclass')
142 raise NotImplementedError('must be implemented in a subclass')
149
143
150 def save(self, model, name, path=''):
144 def save(self, model, path):
151 """Save the file or directory and return the model with no content."""
145 """Save the file or directory and return the model with no content."""
152 raise NotImplementedError('must be implemented in a subclass')
146 raise NotImplementedError('must be implemented in a subclass')
153
147
154 def update(self, model, name, path=''):
148 def update(self, model, path):
155 """Update the file or directory and return the model with no content.
149 """Update the file or directory and return the model with no content.
156
150
157 For use in PATCH requests, to enable renaming a file without
151 For use in PATCH requests, to enable renaming a file without
@@ -159,26 +153,26 b' class ContentsManager(LoggingConfigurable):'
159 """
153 """
160 raise NotImplementedError('must be implemented in a subclass')
154 raise NotImplementedError('must be implemented in a subclass')
161
155
162 def delete(self, name, path=''):
156 def delete(self, path):
163 """Delete file or directory by name and path."""
157 """Delete file or directory by path."""
164 raise NotImplementedError('must be implemented in a subclass')
158 raise NotImplementedError('must be implemented in a subclass')
165
159
166 def create_checkpoint(self, name, path=''):
160 def create_checkpoint(self, path):
167 """Create a checkpoint of the current state of a file
161 """Create a checkpoint of the current state of a file
168
162
169 Returns a checkpoint_id for the new checkpoint.
163 Returns a checkpoint_id for the new checkpoint.
170 """
164 """
171 raise NotImplementedError("must be implemented in a subclass")
165 raise NotImplementedError("must be implemented in a subclass")
172
166
173 def list_checkpoints(self, name, path=''):
167 def list_checkpoints(self, path):
174 """Return a list of checkpoints for a given file"""
168 """Return a list of checkpoints for a given file"""
175 return []
169 return []
176
170
177 def restore_checkpoint(self, checkpoint_id, name, path=''):
171 def restore_checkpoint(self, checkpoint_id, path):
178 """Restore a file from one of its checkpoints"""
172 """Restore a file from one of its checkpoints"""
179 raise NotImplementedError("must be implemented in a subclass")
173 raise NotImplementedError("must be implemented in a subclass")
180
174
181 def delete_checkpoint(self, checkpoint_id, name, path=''):
175 def delete_checkpoint(self, checkpoint_id, path):
182 """delete a checkpoint for a file"""
176 """delete a checkpoint for a file"""
183 raise NotImplementedError("must be implemented in a subclass")
177 raise NotImplementedError("must be implemented in a subclass")
184
178
@@ -188,11 +182,19 b' class ContentsManager(LoggingConfigurable):'
188 def info_string(self):
182 def info_string(self):
189 return "Serving contents"
183 return "Serving contents"
190
184
191 def get_kernel_path(self, name, path='', model=None):
185 def get_kernel_path(self, path, model=None):
192 """ Return the path to start kernel in """
186 """Return the API path for the kernel
193 return path
187
188 KernelManagers can turn this value into a filesystem path,
189 or ignore it altogether.
194
190
195 def increment_filename(self, filename, path=''):
191 The default value here will start kernels in the directory of the
192 notebook server. FileContentsManager overrides this to use the
193 directory containing the notebook.
194 """
195 return ''
196
197 def increment_filename(self, filename, path='', insert=''):
196 """Increment a filename until it is unique.
198 """Increment a filename until it is unique.
197
199
198 Parameters
200 Parameters
@@ -210,87 +212,140 b' class ContentsManager(LoggingConfigurable):'
210 path = path.strip('/')
212 path = path.strip('/')
211 basename, ext = os.path.splitext(filename)
213 basename, ext = os.path.splitext(filename)
212 for i in itertools.count():
214 for i in itertools.count():
213 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
215 if i:
214 ext=ext)
216 insert_i = '{}{}'.format(insert, i)
215 if not self.file_exists(name, path):
217 else:
218 insert_i = ''
219 name = u'{basename}{insert}{ext}'.format(basename=basename,
220 insert=insert_i, ext=ext)
221 if not self.exists(u'{}/{}'.format(path, name)):
216 break
222 break
217 return name
223 return name
218
224
219 def create_file(self, model=None, path='', ext='.ipynb'):
225 def validate_notebook_model(self, model):
220 """Create a new file or directory and return its model with no content."""
226 """Add failed-validation message to model"""
227 try:
228 validate(model['content'])
229 except ValidationError as e:
230 model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
231 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
232 )
233 return model
234
235 def new_untitled(self, path='', type='', ext=''):
236 """Create a new untitled file or directory in path
237
238 path must be a directory
239
240 File extension can be specified.
241
242 Use `new` to create files with a fully specified path (including filename).
243 """
244 path = path.strip('/')
245 if not self.dir_exists(path):
246 raise HTTPError(404, 'No such directory: %s' % path)
247
248 model = {}
249 if type:
250 model['type'] = type
251
252 if ext == '.ipynb':
253 model.setdefault('type', 'notebook')
254 else:
255 model.setdefault('type', 'file')
256
257 insert = ''
258 if model['type'] == 'directory':
259 untitled = self.untitled_directory
260 insert = ' '
261 elif model['type'] == 'notebook':
262 untitled = self.untitled_notebook
263 ext = '.ipynb'
264 elif model['type'] == 'file':
265 untitled = self.untitled_file
266 else:
267 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
268
269 name = self.increment_filename(untitled + ext, path, insert=insert)
270 path = u'{0}/{1}'.format(path, name)
271 return self.new(model, path)
272
273 def new(self, model=None, path=''):
274 """Create a new file or directory and return its model with no content.
275
276 To create a new untitled entity in a directory, use `new_untitled`.
277 """
221 path = path.strip('/')
278 path = path.strip('/')
222 if model is None:
279 if model is None:
223 model = {}
280 model = {}
224 if 'content' not in model and model.get('type', None) != 'directory':
281
225 if ext == '.ipynb':
282 if path.endswith('.ipynb'):
226 metadata = current.new_metadata(name=u'')
283 model.setdefault('type', 'notebook')
227 model['content'] = current.new_notebook(metadata=metadata)
284 else:
228 model['type'] = 'notebook'
285 model.setdefault('type', 'file')
286
287 # no content, not a directory, so fill out new-file model
288 if 'content' not in model and model['type'] != 'directory':
289 if model['type'] == 'notebook':
290 model['content'] = new_notebook()
229 model['format'] = 'json'
291 model['format'] = 'json'
230 else:
292 else:
231 model['content'] = ''
293 model['content'] = ''
232 model['type'] = 'file'
294 model['type'] = 'file'
233 model['format'] = 'text'
295 model['format'] = 'text'
234 if 'name' not in model:
296
235 if model['type'] == 'directory':
297 model = self.save(model, path)
236 untitled = self.untitled_directory
237 elif model['type'] == 'notebook':
238 untitled = self.untitled_notebook
239 elif model['type'] == 'file':
240 untitled = self.untitled_file
241 else:
242 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
243 model['name'] = self.increment_filename(untitled + ext, path)
244
245 model['path'] = path
246 model = self.save(model, model['name'], model['path'])
247 return model
298 return model
248
299
249 def copy(self, from_name, to_name=None, path=''):
300 def copy(self, from_path, to_path=None):
250 """Copy an existing file and return its new model.
301 """Copy an existing file and return its new model.
251
302
252 If to_name not specified, increment `from_name-Copy#.ext`.
303 If to_path not specified, it will be the parent directory of from_path.
304 If to_path is a directory, filename will increment `from_path-Copy#.ext`.
253
305
254 copy_from can be a full path to a file,
306 from_path must be a full path to a file.
255 or just a base name. If a base name, `path` is used.
256 """
307 """
257 path = path.strip('/')
308 path = from_path.strip('/')
258 if '/' in from_name:
309 if '/' in path:
259 from_path, from_name = from_name.rsplit('/', 1)
310 from_dir, from_name = path.rsplit('/', 1)
260 else:
311 else:
261 from_path = path
312 from_dir = ''
262 model = self.get_model(from_name, from_path)
313 from_name = path
314
315 model = self.get(path)
316 model.pop('path', None)
317 model.pop('name', None)
263 if model['type'] == 'directory':
318 if model['type'] == 'directory':
264 raise HTTPError(400, "Can't copy directories")
319 raise HTTPError(400, "Can't copy directories")
265 if not to_name:
320
266 base, ext = os.path.splitext(from_name)
321 if not to_path:
267 copy_name = u'{0}-Copy{1}'.format(base, ext)
322 to_path = from_dir
268 to_name = self.increment_filename(copy_name, path)
323 if self.dir_exists(to_path):
269 model['name'] = to_name
324 name = copy_pat.sub(u'.', from_name)
270 model['path'] = path
325 to_name = self.increment_filename(name, to_path, insert='-Copy')
271 model = self.save(model, to_name, path)
326 to_path = u'{0}/{1}'.format(to_path, to_name)
327
328 model = self.save(model, to_path)
272 return model
329 return model
273
330
274 def log_info(self):
331 def log_info(self):
275 self.log.info(self.info_string())
332 self.log.info(self.info_string())
276
333
277 def trust_notebook(self, name, path=''):
334 def trust_notebook(self, path):
278 """Explicitly trust a notebook
335 """Explicitly trust a notebook
279
336
280 Parameters
337 Parameters
281 ----------
338 ----------
282 name : string
283 The filename of the notebook
284 path : string
339 path : string
285 The notebook's directory
340 The path of a notebook
286 """
341 """
287 model = self.get_model(name, path)
342 model = self.get(path)
288 nb = model['content']
343 nb = model['content']
289 self.log.warn("Trusting notebook %s/%s", path, name)
344 self.log.warn("Trusting notebook %s", path)
290 self.notary.mark_cells(nb, True)
345 self.notary.mark_cells(nb, True)
291 self.save(model, name, path)
346 self.save(model, path)
292
347
293 def check_and_sign(self, nb, name='', path=''):
348 def check_and_sign(self, nb, path=''):
294 """Check for trusted cells, and sign the notebook.
349 """Check for trusted cells, and sign the notebook.
295
350
296 Called as a part of saving notebooks.
351 Called as a part of saving notebooks.
@@ -298,18 +353,16 b' class ContentsManager(LoggingConfigurable):'
298 Parameters
353 Parameters
299 ----------
354 ----------
300 nb : dict
355 nb : dict
301 The notebook object (in nbformat.current format)
356 The notebook dict
302 name : string
303 The filename of the notebook (for logging)
304 path : string
357 path : string
305 The notebook's directory (for logging)
358 The notebook's path (for logging)
306 """
359 """
307 if self.notary.check_cells(nb):
360 if self.notary.check_cells(nb):
308 self.notary.sign(nb)
361 self.notary.sign(nb)
309 else:
362 else:
310 self.log.warn("Saving untrusted notebook %s/%s", path, name)
363 self.log.warn("Saving untrusted notebook %s", path)
311
364
312 def mark_trusted_cells(self, nb, name='', path=''):
365 def mark_trusted_cells(self, nb, path=''):
313 """Mark cells as trusted if the notebook signature matches.
366 """Mark cells as trusted if the notebook signature matches.
314
367
315 Called as a part of loading notebooks.
368 Called as a part of loading notebooks.
@@ -317,15 +370,13 b' class ContentsManager(LoggingConfigurable):'
317 Parameters
370 Parameters
318 ----------
371 ----------
319 nb : dict
372 nb : dict
320 The notebook object (in nbformat.current format)
373 The notebook object (in current nbformat)
321 name : string
322 The filename of the notebook (for logging)
323 path : string
374 path : string
324 The notebook's directory (for logging)
375 The notebook's path (for logging)
325 """
376 """
326 trusted = self.notary.check_signature(nb)
377 trusted = self.notary.check_signature(nb)
327 if not trusted:
378 if not trusted:
328 self.log.warn("Notebook %s/%s is not trusted", path, name)
379 self.log.warn("Notebook %s is not trusted", path)
329 self.notary.mark_cells(nb, trusted)
380 self.notary.mark_cells(nb, trusted)
330
381
331 def should_list(self, name):
382 def should_list(self, name):
@@ -14,9 +14,10 b' import requests'
14
14
15 from IPython.html.utils import url_path_join, url_escape
15 from IPython.html.utils import url_path_join, url_escape
16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 from IPython.nbformat import current
17 from IPython.nbformat import read, write, from_dict
18 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 from IPython.nbformat.v4 import (
19 new_heading_cell, to_notebook_json)
19 new_notebook, new_markdown_cell,
20 )
20 from IPython.nbformat import v2
21 from IPython.nbformat import v2
21 from IPython.utils import py3compat
22 from IPython.utils import py3compat
22 from IPython.utils.data import uniq_stable
23 from IPython.utils.data import uniq_stable
@@ -34,10 +35,10 b' class API(object):'
34 def __init__(self, base_url):
35 def __init__(self, base_url):
35 self.base_url = base_url
36 self.base_url = base_url
36
37
37 def _req(self, verb, path, body=None):
38 def _req(self, verb, path, body=None, params=None):
38 response = requests.request(verb,
39 response = requests.request(verb,
39 url_path_join(self.base_url, 'api/contents', path),
40 url_path_join(self.base_url, 'api/contents', path),
40 data=body,
41 data=body, params=params,
41 )
42 )
42 response.raise_for_status()
43 response.raise_for_status()
43 return response
44 return response
@@ -45,56 +46,64 b' class API(object):'
45 def list(self, path='/'):
46 def list(self, path='/'):
46 return self._req('GET', path)
47 return self._req('GET', path)
47
48
48 def read(self, name, path='/'):
49 def read(self, path, type_=None, format=None):
49 return self._req('GET', url_path_join(path, name))
50 params = {}
51 if type_ is not None:
52 params['type'] = type_
53 if format is not None:
54 params['format'] = format
55 return self._req('GET', path, params=params)
50
56
51 def create_untitled(self, path='/', ext=None):
57 def create_untitled(self, path='/', ext='.ipynb'):
52 body = None
58 body = None
53 if ext:
59 if ext:
54 body = json.dumps({'ext': ext})
60 body = json.dumps({'ext': ext})
55 return self._req('POST', path, body)
61 return self._req('POST', path, body)
56
62
57 def upload_untitled(self, body, path='/'):
63 def mkdir_untitled(self, path='/'):
58 return self._req('POST', path, body)
64 return self._req('POST', path, json.dumps({'type': 'directory'}))
59
65
60 def copy_untitled(self, copy_from, path='/'):
66 def copy(self, copy_from, path='/'):
61 body = json.dumps({'copy_from':copy_from})
67 body = json.dumps({'copy_from':copy_from})
62 return self._req('POST', path, body)
68 return self._req('POST', path, body)
63
69
64 def create(self, name, path='/'):
70 def create(self, path='/'):
65 return self._req('PUT', url_path_join(path, name))
71 return self._req('PUT', path)
72
73 def upload(self, path, body):
74 return self._req('PUT', path, body)
66
75
67 def upload(self, name, body, path='/'):
76 def mkdir_untitled(self, path='/'):
68 return self._req('PUT', url_path_join(path, name), body)
77 return self._req('POST', path, json.dumps({'type': 'directory'}))
69
78
70 def mkdir(self, name, path='/'):
79 def mkdir(self, path='/'):
71 return self._req('PUT', url_path_join(path, name), json.dumps({'type': 'directory'}))
80 return self._req('PUT', path, json.dumps({'type': 'directory'}))
72
81
73 def copy(self, copy_from, copy_to, path='/'):
82 def copy_put(self, copy_from, path='/'):
74 body = json.dumps({'copy_from':copy_from})
83 body = json.dumps({'copy_from':copy_from})
75 return self._req('PUT', url_path_join(path, copy_to), body)
84 return self._req('PUT', path, body)
76
85
77 def save(self, name, body, path='/'):
86 def save(self, path, body):
78 return self._req('PUT', url_path_join(path, name), body)
87 return self._req('PUT', path, body)
79
88
80 def delete(self, name, path='/'):
89 def delete(self, path='/'):
81 return self._req('DELETE', url_path_join(path, name))
90 return self._req('DELETE', path)
82
91
83 def rename(self, name, path, new_name):
92 def rename(self, path, new_path):
84 body = json.dumps({'name': new_name})
93 body = json.dumps({'path': new_path})
85 return self._req('PATCH', url_path_join(path, name), body)
94 return self._req('PATCH', path, body)
86
95
87 def get_checkpoints(self, name, path):
96 def get_checkpoints(self, path):
88 return self._req('GET', url_path_join(path, name, 'checkpoints'))
97 return self._req('GET', url_path_join(path, 'checkpoints'))
89
98
90 def new_checkpoint(self, name, path):
99 def new_checkpoint(self, path):
91 return self._req('POST', url_path_join(path, name, 'checkpoints'))
100 return self._req('POST', url_path_join(path, 'checkpoints'))
92
101
93 def restore_checkpoint(self, name, path, checkpoint_id):
102 def restore_checkpoint(self, path, checkpoint_id):
94 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
103 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
95
104
96 def delete_checkpoint(self, name, path, checkpoint_id):
105 def delete_checkpoint(self, path, checkpoint_id):
97 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
106 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
98
107
99 class APITest(NotebookTestBase):
108 class APITest(NotebookTestBase):
100 """Test the kernels web service API"""
109 """Test the kernels web service API"""
@@ -130,8 +139,6 b' class APITest(NotebookTestBase):'
130 self.blob = os.urandom(100)
139 self.blob = os.urandom(100)
131 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
140 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
132
141
133
134
135 for d in (self.dirs + self.hidden_dirs):
142 for d in (self.dirs + self.hidden_dirs):
136 d.replace('/', os.sep)
143 d.replace('/', os.sep)
137 if not os.path.isdir(pjoin(nbdir, d)):
144 if not os.path.isdir(pjoin(nbdir, d)):
@@ -142,8 +149,8 b' class APITest(NotebookTestBase):'
142 # create a notebook
149 # create a notebook
143 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
150 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
144 encoding='utf-8') as f:
151 encoding='utf-8') as f:
145 nb = new_notebook(name=name)
152 nb = new_notebook()
146 write(nb, f, format='ipynb')
153 write(nb, f, version=4)
147
154
148 # create a text file
155 # create a text file
149 with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w',
156 with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w',
@@ -177,12 +184,12 b' class APITest(NotebookTestBase):'
177 nbs = notebooks_only(self.api.list(u'/unicodé/').json())
184 nbs = notebooks_only(self.api.list(u'/unicodé/').json())
178 self.assertEqual(len(nbs), 1)
185 self.assertEqual(len(nbs), 1)
179 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
186 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
180 self.assertEqual(nbs[0]['path'], u'unicodé')
187 self.assertEqual(nbs[0]['path'], u'unicodé/innonascii.ipynb')
181
188
182 nbs = notebooks_only(self.api.list('/foo/bar/').json())
189 nbs = notebooks_only(self.api.list('/foo/bar/').json())
183 self.assertEqual(len(nbs), 1)
190 self.assertEqual(len(nbs), 1)
184 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
191 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
185 self.assertEqual(nbs[0]['path'], 'foo/bar')
192 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
186
193
187 nbs = notebooks_only(self.api.list('foo').json())
194 nbs = notebooks_only(self.api.list('foo').json())
188 self.assertEqual(len(nbs), 4)
195 self.assertEqual(len(nbs), 4)
@@ -197,8 +204,11 b' class APITest(NotebookTestBase):'
197 self.assertEqual(nbnames, expected)
204 self.assertEqual(nbnames, expected)
198
205
199 def test_list_dirs(self):
206 def test_list_dirs(self):
207 print(self.api.list().json())
200 dirs = dirs_only(self.api.list().json())
208 dirs = dirs_only(self.api.list().json())
201 dir_names = {normalize('NFC', d['name']) for d in dirs}
209 dir_names = {normalize('NFC', d['name']) for d in dirs}
210 print(dir_names)
211 print(self.top_level_dirs)
202 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
212 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
203
213
204 def test_list_nonexistant_dir(self):
214 def test_list_nonexistant_dir(self):
@@ -207,8 +217,10 b' class APITest(NotebookTestBase):'
207
217
208 def test_get_nb_contents(self):
218 def test_get_nb_contents(self):
209 for d, name in self.dirs_nbs:
219 for d, name in self.dirs_nbs:
210 nb = self.api.read('%s.ipynb' % name, d+'/').json()
220 path = url_path_join(d, name + '.ipynb')
221 nb = self.api.read(path).json()
211 self.assertEqual(nb['name'], u'%s.ipynb' % name)
222 self.assertEqual(nb['name'], u'%s.ipynb' % name)
223 self.assertEqual(nb['path'], path)
212 self.assertEqual(nb['type'], 'notebook')
224 self.assertEqual(nb['type'], 'notebook')
213 self.assertIn('content', nb)
225 self.assertIn('content', nb)
214 self.assertEqual(nb['format'], 'json')
226 self.assertEqual(nb['format'], 'json')
@@ -219,12 +231,14 b' class APITest(NotebookTestBase):'
219 def test_get_contents_no_such_file(self):
231 def test_get_contents_no_such_file(self):
220 # Name that doesn't exist - should be a 404
232 # Name that doesn't exist - should be a 404
221 with assert_http_error(404):
233 with assert_http_error(404):
222 self.api.read('q.ipynb', 'foo')
234 self.api.read('foo/q.ipynb')
223
235
224 def test_get_text_file_contents(self):
236 def test_get_text_file_contents(self):
225 for d, name in self.dirs_nbs:
237 for d, name in self.dirs_nbs:
226 model = self.api.read(u'%s.txt' % name, d+'/').json()
238 path = url_path_join(d, name + '.txt')
239 model = self.api.read(path).json()
227 self.assertEqual(model['name'], u'%s.txt' % name)
240 self.assertEqual(model['name'], u'%s.txt' % name)
241 self.assertEqual(model['path'], path)
228 self.assertIn('content', model)
242 self.assertIn('content', model)
229 self.assertEqual(model['format'], 'text')
243 self.assertEqual(model['format'], 'text')
230 self.assertEqual(model['type'], 'file')
244 self.assertEqual(model['type'], 'file')
@@ -232,12 +246,18 b' class APITest(NotebookTestBase):'
232
246
233 # Name that doesn't exist - should be a 404
247 # Name that doesn't exist - should be a 404
234 with assert_http_error(404):
248 with assert_http_error(404):
235 self.api.read('q.txt', 'foo')
249 self.api.read('foo/q.txt')
250
251 # Specifying format=text should fail on a non-UTF-8 file
252 with assert_http_error(400):
253 self.api.read('foo/bar/baz.blob', type_='file', format='text')
236
254
237 def test_get_binary_file_contents(self):
255 def test_get_binary_file_contents(self):
238 for d, name in self.dirs_nbs:
256 for d, name in self.dirs_nbs:
239 model = self.api.read(u'%s.blob' % name, d+'/').json()
257 path = url_path_join(d, name + '.blob')
258 model = self.api.read(path).json()
240 self.assertEqual(model['name'], u'%s.blob' % name)
259 self.assertEqual(model['name'], u'%s.blob' % name)
260 self.assertEqual(model['path'], path)
241 self.assertIn('content', model)
261 self.assertIn('content', model)
242 self.assertEqual(model['format'], 'base64')
262 self.assertEqual(model['format'], 'base64')
243 self.assertEqual(model['type'], 'file')
263 self.assertEqual(model['type'], 'file')
@@ -246,66 +266,78 b' class APITest(NotebookTestBase):'
246
266
247 # Name that doesn't exist - should be a 404
267 # Name that doesn't exist - should be a 404
248 with assert_http_error(404):
268 with assert_http_error(404):
249 self.api.read('q.txt', 'foo')
269 self.api.read('foo/q.txt')
250
270
251 def _check_created(self, resp, name, path, type='notebook'):
271 def test_get_bad_type(self):
272 with assert_http_error(400):
273 self.api.read(u'unicodé', type_='file') # this is a directory
274
275 with assert_http_error(400):
276 self.api.read(u'unicodé/innonascii.ipynb', type_='directory')
277
278 def _check_created(self, resp, path, type='notebook'):
252 self.assertEqual(resp.status_code, 201)
279 self.assertEqual(resp.status_code, 201)
253 location_header = py3compat.str_to_unicode(resp.headers['Location'])
280 location_header = py3compat.str_to_unicode(resp.headers['Location'])
254 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name)))
281 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
255 rjson = resp.json()
282 rjson = resp.json()
256 self.assertEqual(rjson['name'], name)
283 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
257 self.assertEqual(rjson['path'], path)
284 self.assertEqual(rjson['path'], path)
258 self.assertEqual(rjson['type'], type)
285 self.assertEqual(rjson['type'], type)
259 isright = os.path.isdir if type == 'directory' else os.path.isfile
286 isright = os.path.isdir if type == 'directory' else os.path.isfile
260 assert isright(pjoin(
287 assert isright(pjoin(
261 self.notebook_dir.name,
288 self.notebook_dir.name,
262 path.replace('/', os.sep),
289 path.replace('/', os.sep),
263 name,
264 ))
290 ))
265
291
266 def test_create_untitled(self):
292 def test_create_untitled(self):
267 resp = self.api.create_untitled(path=u'å b')
293 resp = self.api.create_untitled(path=u'å b')
268 self._check_created(resp, 'Untitled0.ipynb', u'å b')
294 self._check_created(resp, u'å b/Untitled.ipynb')
269
295
270 # Second time
296 # Second time
271 resp = self.api.create_untitled(path=u'å b')
297 resp = self.api.create_untitled(path=u'å b')
272 self._check_created(resp, 'Untitled1.ipynb', u'å b')
298 self._check_created(resp, u'å b/Untitled1.ipynb')
273
299
274 # And two directories down
300 # And two directories down
275 resp = self.api.create_untitled(path='foo/bar')
301 resp = self.api.create_untitled(path='foo/bar')
276 self._check_created(resp, 'Untitled0.ipynb', 'foo/bar')
302 self._check_created(resp, 'foo/bar/Untitled.ipynb')
277
303
278 def test_create_untitled_txt(self):
304 def test_create_untitled_txt(self):
279 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
305 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
280 self._check_created(resp, 'untitled0.txt', 'foo/bar', type='file')
306 self._check_created(resp, 'foo/bar/untitled.txt', type='file')
281
307
282 resp = self.api.read(path='foo/bar', name='untitled0.txt')
308 resp = self.api.read(path='foo/bar/untitled.txt')
283 model = resp.json()
309 model = resp.json()
284 self.assertEqual(model['type'], 'file')
310 self.assertEqual(model['type'], 'file')
285 self.assertEqual(model['format'], 'text')
311 self.assertEqual(model['format'], 'text')
286 self.assertEqual(model['content'], '')
312 self.assertEqual(model['content'], '')
287
313
288 def test_upload_untitled(self):
289 nb = new_notebook(name='Upload test')
290 nbmodel = {'content': nb, 'type': 'notebook'}
291 resp = self.api.upload_untitled(path=u'å b',
292 body=json.dumps(nbmodel))
293 self._check_created(resp, 'Untitled0.ipynb', u'å b')
294
295 def test_upload(self):
314 def test_upload(self):
296 nb = new_notebook(name=u'ignored')
315 nb = new_notebook()
297 nbmodel = {'content': nb, 'type': 'notebook'}
316 nbmodel = {'content': nb, 'type': 'notebook'}
298 resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
317 path = u'å b/Upload tést.ipynb'
299 body=json.dumps(nbmodel))
318 resp = self.api.upload(path, body=json.dumps(nbmodel))
300 self._check_created(resp, u'Upload tést.ipynb', u'å b')
319 self._check_created(resp, path)
320
321 def test_mkdir_untitled(self):
322 resp = self.api.mkdir_untitled(path=u'å b')
323 self._check_created(resp, u'å b/Untitled Folder', type='directory')
324
325 # Second time
326 resp = self.api.mkdir_untitled(path=u'å b')
327 self._check_created(resp, u'å b/Untitled Folder 1', type='directory')
328
329 # And two directories down
330 resp = self.api.mkdir_untitled(path='foo/bar')
331 self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
301
332
302 def test_mkdir(self):
333 def test_mkdir(self):
303 resp = self.api.mkdir(u'New ∂ir', path=u'å b')
334 path = u'å b/New ∂ir'
304 self._check_created(resp, u'New ∂ir', u'å b', type='directory')
335 resp = self.api.mkdir(path)
336 self._check_created(resp, path, type='directory')
305
337
306 def test_mkdir_hidden_400(self):
338 def test_mkdir_hidden_400(self):
307 with assert_http_error(400):
339 with assert_http_error(400):
308 resp = self.api.mkdir(u'.hidden', path=u'å b')
340 resp = self.api.mkdir(u'å b/.hidden')
309
341
310 def test_upload_txt(self):
342 def test_upload_txt(self):
311 body = u'ünicode téxt'
343 body = u'ünicode téxt'
@@ -314,11 +346,11 b' class APITest(NotebookTestBase):'
314 'format' : 'text',
346 'format' : 'text',
315 'type' : 'file',
347 'type' : 'file',
316 }
348 }
317 resp = self.api.upload(u'Upload tést.txt', path=u'å b',
349 path = u'å b/Upload tést.txt'
318 body=json.dumps(model))
350 resp = self.api.upload(path, body=json.dumps(model))
319
351
320 # check roundtrip
352 # check roundtrip
321 resp = self.api.read(path=u'å b', name=u'Upload tést.txt')
353 resp = self.api.read(path)
322 model = resp.json()
354 model = resp.json()
323 self.assertEqual(model['type'], 'file')
355 self.assertEqual(model['type'], 'file')
324 self.assertEqual(model['format'], 'text')
356 self.assertEqual(model['format'], 'text')
@@ -332,13 +364,14 b' class APITest(NotebookTestBase):'
332 'format' : 'base64',
364 'format' : 'base64',
333 'type' : 'file',
365 'type' : 'file',
334 }
366 }
335 resp = self.api.upload(u'Upload tést.blob', path=u'å b',
367 path = u'å b/Upload tést.blob'
336 body=json.dumps(model))
368 resp = self.api.upload(path, body=json.dumps(model))
337
369
338 # check roundtrip
370 # check roundtrip
339 resp = self.api.read(path=u'å b', name=u'Upload tést.blob')
371 resp = self.api.read(path)
340 model = resp.json()
372 model = resp.json()
341 self.assertEqual(model['type'], 'file')
373 self.assertEqual(model['type'], 'file')
374 self.assertEqual(model['path'], path)
342 self.assertEqual(model['format'], 'base64')
375 self.assertEqual(model['format'], 'base64')
343 decoded = base64.decodestring(model['content'].encode('ascii'))
376 decoded = base64.decodestring(model['content'].encode('ascii'))
344 self.assertEqual(decoded, body)
377 self.assertEqual(decoded, body)
@@ -349,46 +382,62 b' class APITest(NotebookTestBase):'
349 nb.worksheets.append(ws)
382 nb.worksheets.append(ws)
350 ws.cells.append(v2.new_code_cell(input='print("hi")'))
383 ws.cells.append(v2.new_code_cell(input='print("hi")'))
351 nbmodel = {'content': nb, 'type': 'notebook'}
384 nbmodel = {'content': nb, 'type': 'notebook'}
352 resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
385 path = u'å b/Upload tést.ipynb'
353 body=json.dumps(nbmodel))
386 resp = self.api.upload(path, body=json.dumps(nbmodel))
354 self._check_created(resp, u'Upload tést.ipynb', u'å b')
387 self._check_created(resp, path)
355 resp = self.api.read(u'Upload tést.ipynb', u'å b')
388 resp = self.api.read(path)
356 data = resp.json()
389 data = resp.json()
357 self.assertEqual(data['content']['nbformat'], current.nbformat)
390 self.assertEqual(data['content']['nbformat'], 4)
358 self.assertEqual(data['content']['orig_nbformat'], 2)
359
360 def test_copy_untitled(self):
361 resp = self.api.copy_untitled(u'ç d.ipynb', path=u'å b')
362 self._check_created(resp, u'ç d-Copy0.ipynb', u'å b')
363
391
364 def test_copy(self):
392 def test_copy(self):
365 resp = self.api.copy(u'ç d.ipynb', u'cøpy.ipynb', path=u'å b')
393 resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
366 self._check_created(resp, u'cøpy.ipynb', u'å b')
394 self._check_created(resp, u'å b/ç d-Copy1.ipynb')
367
395
396 resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
397 self._check_created(resp, u'å b/ç d-Copy2.ipynb')
398
399 def test_copy_copy(self):
400 resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
401 self._check_created(resp, u'å b/ç d-Copy1.ipynb')
402
403 resp = self.api.copy(u'å b/ç d-Copy1.ipynb', u'å b')
404 self._check_created(resp, u'å b/ç d-Copy2.ipynb')
405
368 def test_copy_path(self):
406 def test_copy_path(self):
369 resp = self.api.copy(u'foo/a.ipynb', u'cøpyfoo.ipynb', path=u'å b')
407 resp = self.api.copy(u'foo/a.ipynb', u'å b')
370 self._check_created(resp, u'cøpyfoo.ipynb', u'å b')
408 self._check_created(resp, u'å b/a.ipynb')
409
410 resp = self.api.copy(u'foo/a.ipynb', u'å b')
411 self._check_created(resp, u'å b/a-Copy1.ipynb')
412
413 def test_copy_put_400(self):
414 with assert_http_error(400):
415 resp = self.api.copy_put(u'å b/ç d.ipynb', u'å b/cøpy.ipynb')
371
416
372 def test_copy_dir_400(self):
417 def test_copy_dir_400(self):
373 # can't copy directories
418 # can't copy directories
374 with assert_http_error(400):
419 with assert_http_error(400):
375 resp = self.api.copy(u'å b', u'å c')
420 resp = self.api.copy(u'å b', u'foo')
376
421
377 def test_delete(self):
422 def test_delete(self):
378 for d, name in self.dirs_nbs:
423 for d, name in self.dirs_nbs:
379 resp = self.api.delete('%s.ipynb' % name, d)
424 print('%r, %r' % (d, name))
425 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
380 self.assertEqual(resp.status_code, 204)
426 self.assertEqual(resp.status_code, 204)
381
427
382 for d in self.dirs + ['/']:
428 for d in self.dirs + ['/']:
383 nbs = notebooks_only(self.api.list(d).json())
429 nbs = notebooks_only(self.api.list(d).json())
384 self.assertEqual(len(nbs), 0)
430 print('------')
431 print(d)
432 print(nbs)
433 self.assertEqual(nbs, [])
385
434
386 def test_delete_dirs(self):
435 def test_delete_dirs(self):
387 # depth-first delete everything, so we don't try to delete empty directories
436 # depth-first delete everything, so we don't try to delete empty directories
388 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
437 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
389 listing = self.api.list(name).json()['content']
438 listing = self.api.list(name).json()['content']
390 for model in listing:
439 for model in listing:
391 self.api.delete(model['name'], model['path'])
440 self.api.delete(model['path'])
392 listing = self.api.list('/').json()['content']
441 listing = self.api.list('/').json()['content']
393 self.assertEqual(listing, [])
442 self.assertEqual(listing, [])
394
443
@@ -398,9 +447,10 b' class APITest(NotebookTestBase):'
398 self.api.delete(u'å b')
447 self.api.delete(u'å b')
399
448
400 def test_rename(self):
449 def test_rename(self):
401 resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb')
450 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
402 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
451 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
403 self.assertEqual(resp.json()['name'], 'z.ipynb')
452 self.assertEqual(resp.json()['name'], 'z.ipynb')
453 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
404 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
454 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
405
455
406 nbs = notebooks_only(self.api.list('foo').json())
456 nbs = notebooks_only(self.api.list('foo').json())
@@ -410,43 +460,31 b' class APITest(NotebookTestBase):'
410
460
411 def test_rename_existing(self):
461 def test_rename_existing(self):
412 with assert_http_error(409):
462 with assert_http_error(409):
413 self.api.rename('a.ipynb', 'foo', 'b.ipynb')
463 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
414
464
415 def test_save(self):
465 def test_save(self):
416 resp = self.api.read('a.ipynb', 'foo')
466 resp = self.api.read('foo/a.ipynb')
417 nbcontent = json.loads(resp.text)['content']
467 nbcontent = json.loads(resp.text)['content']
418 nb = to_notebook_json(nbcontent)
468 nb = from_dict(nbcontent)
419 ws = new_worksheet()
469 nb.cells.append(new_markdown_cell(u'Created by test ³'))
420 nb.worksheets = [ws]
421 ws.cells.append(new_heading_cell(u'Created by test ³'))
422
470
423 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
471 nbmodel= {'content': nb, 'type': 'notebook'}
424 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
472 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
425
473
426 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
474 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
427 with io.open(nbfile, 'r', encoding='utf-8') as f:
475 with io.open(nbfile, 'r', encoding='utf-8') as f:
428 newnb = read(f, format='ipynb')
476 newnb = read(f, as_version=4)
429 self.assertEqual(newnb.worksheets[0].cells[0].source,
477 self.assertEqual(newnb.cells[0].source,
430 u'Created by test ³')
478 u'Created by test ³')
431 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
479 nbcontent = self.api.read('foo/a.ipynb').json()['content']
432 newnb = to_notebook_json(nbcontent)
480 newnb = from_dict(nbcontent)
433 self.assertEqual(newnb.worksheets[0].cells[0].source,
481 self.assertEqual(newnb.cells[0].source,
434 u'Created by test ³')
482 u'Created by test ³')
435
483
436 # Save and rename
437 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'}
438 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
439 saved = resp.json()
440 self.assertEqual(saved['name'], 'a2.ipynb')
441 self.assertEqual(saved['path'], 'foo/bar')
442 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
443 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
444 with assert_http_error(404):
445 self.api.read('a.ipynb', 'foo')
446
484
447 def test_checkpoints(self):
485 def test_checkpoints(self):
448 resp = self.api.read('a.ipynb', 'foo')
486 resp = self.api.read('foo/a.ipynb')
449 r = self.api.new_checkpoint('a.ipynb', 'foo')
487 r = self.api.new_checkpoint('foo/a.ipynb')
450 self.assertEqual(r.status_code, 201)
488 self.assertEqual(r.status_code, 201)
451 cp1 = r.json()
489 cp1 = r.json()
452 self.assertEqual(set(cp1), {'id', 'last_modified'})
490 self.assertEqual(set(cp1), {'id', 'last_modified'})
@@ -454,32 +492,30 b' class APITest(NotebookTestBase):'
454
492
455 # Modify it
493 # Modify it
456 nbcontent = json.loads(resp.text)['content']
494 nbcontent = json.loads(resp.text)['content']
457 nb = to_notebook_json(nbcontent)
495 nb = from_dict(nbcontent)
458 ws = new_worksheet()
496 hcell = new_markdown_cell('Created by test')
459 nb.worksheets = [ws]
497 nb.cells.append(hcell)
460 hcell = new_heading_cell('Created by test')
461 ws.cells.append(hcell)
462 # Save
498 # Save
463 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
499 nbmodel= {'content': nb, 'type': 'notebook'}
464 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
500 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
465
501
466 # List checkpoints
502 # List checkpoints
467 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
503 cps = self.api.get_checkpoints('foo/a.ipynb').json()
468 self.assertEqual(cps, [cp1])
504 self.assertEqual(cps, [cp1])
469
505
470 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
506 nbcontent = self.api.read('foo/a.ipynb').json()['content']
471 nb = to_notebook_json(nbcontent)
507 nb = from_dict(nbcontent)
472 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
508 self.assertEqual(nb.cells[0].source, 'Created by test')
473
509
474 # Restore cp1
510 # Restore cp1
475 r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
511 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
476 self.assertEqual(r.status_code, 204)
512 self.assertEqual(r.status_code, 204)
477 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
513 nbcontent = self.api.read('foo/a.ipynb').json()['content']
478 nb = to_notebook_json(nbcontent)
514 nb = from_dict(nbcontent)
479 self.assertEqual(nb.worksheets, [])
515 self.assertEqual(nb.cells, [])
480
516
481 # Delete cp1
517 # Delete cp1
482 r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
518 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
483 self.assertEqual(r.status_code, 204)
519 self.assertEqual(r.status_code, 204)
484 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
520 cps = self.api.get_checkpoints('foo/a.ipynb').json()
485 self.assertEqual(cps, [])
521 self.assertEqual(cps, [])
@@ -9,7 +9,7 b' from tornado.web import HTTPError'
9 from unittest import TestCase
9 from unittest import TestCase
10 from tempfile import NamedTemporaryFile
10 from tempfile import NamedTemporaryFile
11
11
12 from IPython.nbformat import current
12 from IPython.nbformat import v4 as nbformat
13
13
14 from IPython.utils.tempdir import TemporaryDirectory
14 from IPython.utils.tempdir import TemporaryDirectory
15 from IPython.utils.traitlets import TraitError
15 from IPython.utils.traitlets import TraitError
@@ -42,7 +42,7 b' class TestFileContentsManager(TestCase):'
42 with TemporaryDirectory() as td:
42 with TemporaryDirectory() as td:
43 root = td
43 root = td
44 fm = FileContentsManager(root_dir=root)
44 fm = FileContentsManager(root_dir=root)
45 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
45 path = fm._get_os_path('/path/to/notebook/test.ipynb')
46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
47 fs_path = os.path.join(fm.root_dir, *rel_path_list)
47 fs_path = os.path.join(fm.root_dir, *rel_path_list)
48 self.assertEqual(path, fs_path)
48 self.assertEqual(path, fs_path)
@@ -53,7 +53,7 b' class TestFileContentsManager(TestCase):'
53 self.assertEqual(path, fs_path)
53 self.assertEqual(path, fs_path)
54
54
55 fm = FileContentsManager(root_dir=root)
55 fm = FileContentsManager(root_dir=root)
56 path = fm._get_os_path('test.ipynb', '////')
56 path = fm._get_os_path('////test.ipynb')
57 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
57 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
58 self.assertEqual(path, fs_path)
58 self.assertEqual(path, fs_path)
59
59
@@ -64,8 +64,8 b' class TestFileContentsManager(TestCase):'
64 root = td
64 root = td
65 os.mkdir(os.path.join(td, subd))
65 os.mkdir(os.path.join(td, subd))
66 fm = FileContentsManager(root_dir=root)
66 fm = FileContentsManager(root_dir=root)
67 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
67 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb')
68 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
68 cp_subdir = fm.get_checkpoint_path('cp', '/%s/test.ipynb' % subd)
69 self.assertNotEqual(cp_dir, cp_subdir)
69 self.assertNotEqual(cp_dir, cp_subdir)
70 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
70 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
71 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
71 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
@@ -95,71 +95,98 b' class TestContentsManager(TestCase):'
95 return os_path
95 return os_path
96
96
97 def add_code_cell(self, nb):
97 def add_code_cell(self, nb):
98 output = current.new_output("display_data", output_javascript="alert('hi');")
98 output = nbformat.new_output("display_data", {'application/javascript': "alert('hi');"})
99 cell = current.new_code_cell("print('hi')", outputs=[output])
99 cell = nbformat.new_code_cell("print('hi')", outputs=[output])
100 if not nb.worksheets:
100 nb.cells.append(cell)
101 nb.worksheets.append(current.new_worksheet())
102 nb.worksheets[0].cells.append(cell)
103
101
104 def new_notebook(self):
102 def new_notebook(self):
105 cm = self.contents_manager
103 cm = self.contents_manager
106 model = cm.create_file()
104 model = cm.new_untitled(type='notebook')
107 name = model['name']
105 name = model['name']
108 path = model['path']
106 path = model['path']
109
107
110 full_model = cm.get_model(name, path)
108 full_model = cm.get(path)
111 nb = full_model['content']
109 nb = full_model['content']
112 self.add_code_cell(nb)
110 self.add_code_cell(nb)
113
111
114 cm.save(full_model, name, path)
112 cm.save(full_model, path)
115 return nb, name, path
113 return nb, name, path
116
114
117 def test_create_file(self):
115 def test_new_untitled(self):
118 cm = self.contents_manager
116 cm = self.contents_manager
119 # Test in root directory
117 # Test in root directory
120 model = cm.create_file()
118 model = cm.new_untitled(type='notebook')
121 assert isinstance(model, dict)
119 assert isinstance(model, dict)
122 self.assertIn('name', model)
120 self.assertIn('name', model)
123 self.assertIn('path', model)
121 self.assertIn('path', model)
124 self.assertEqual(model['name'], 'Untitled0.ipynb')
122 self.assertIn('type', model)
125 self.assertEqual(model['path'], '')
123 self.assertEqual(model['type'], 'notebook')
124 self.assertEqual(model['name'], 'Untitled.ipynb')
125 self.assertEqual(model['path'], 'Untitled.ipynb')
126
126
127 # Test in sub-directory
127 # Test in sub-directory
128 sub_dir = '/foo/'
128 model = cm.new_untitled(type='directory')
129 self.make_dir(cm.root_dir, 'foo')
130 model = cm.create_file(None, sub_dir)
131 assert isinstance(model, dict)
129 assert isinstance(model, dict)
132 self.assertIn('name', model)
130 self.assertIn('name', model)
133 self.assertIn('path', model)
131 self.assertIn('path', model)
134 self.assertEqual(model['name'], 'Untitled0.ipynb')
132 self.assertIn('type', model)
135 self.assertEqual(model['path'], sub_dir.strip('/'))
133 self.assertEqual(model['type'], 'directory')
134 self.assertEqual(model['name'], 'Untitled Folder')
135 self.assertEqual(model['path'], 'Untitled Folder')
136 sub_dir = model['path']
137
138 model = cm.new_untitled(path=sub_dir)
139 assert isinstance(model, dict)
140 self.assertIn('name', model)
141 self.assertIn('path', model)
142 self.assertIn('type', model)
143 self.assertEqual(model['type'], 'file')
144 self.assertEqual(model['name'], 'untitled')
145 self.assertEqual(model['path'], '%s/untitled' % sub_dir)
136
146
137 def test_get(self):
147 def test_get(self):
138 cm = self.contents_manager
148 cm = self.contents_manager
139 # Create a notebook
149 # Create a notebook
140 model = cm.create_file()
150 model = cm.new_untitled(type='notebook')
141 name = model['name']
151 name = model['name']
142 path = model['path']
152 path = model['path']
143
153
144 # Check that we 'get' on the notebook we just created
154 # Check that we 'get' on the notebook we just created
145 model2 = cm.get_model(name, path)
155 model2 = cm.get(path)
146 assert isinstance(model2, dict)
156 assert isinstance(model2, dict)
147 self.assertIn('name', model2)
157 self.assertIn('name', model2)
148 self.assertIn('path', model2)
158 self.assertIn('path', model2)
149 self.assertEqual(model['name'], name)
159 self.assertEqual(model['name'], name)
150 self.assertEqual(model['path'], path)
160 self.assertEqual(model['path'], path)
151
161
162 nb_as_file = cm.get(path, content=True, type_='file')
163 self.assertEqual(nb_as_file['path'], path)
164 self.assertEqual(nb_as_file['type'], 'file')
165 self.assertEqual(nb_as_file['format'], 'text')
166 self.assertNotIsInstance(nb_as_file['content'], dict)
167
168 nb_as_bin_file = cm.get(path, content=True, type_='file', format='base64')
169 self.assertEqual(nb_as_bin_file['format'], 'base64')
170
152 # Test in sub-directory
171 # Test in sub-directory
153 sub_dir = '/foo/'
172 sub_dir = '/foo/'
154 self.make_dir(cm.root_dir, 'foo')
173 self.make_dir(cm.root_dir, 'foo')
155 model = cm.create_file(None, sub_dir)
174 model = cm.new_untitled(path=sub_dir, ext='.ipynb')
156 model2 = cm.get_model(name, sub_dir)
175 model2 = cm.get(sub_dir + name)
157 assert isinstance(model2, dict)
176 assert isinstance(model2, dict)
158 self.assertIn('name', model2)
177 self.assertIn('name', model2)
159 self.assertIn('path', model2)
178 self.assertIn('path', model2)
160 self.assertIn('content', model2)
179 self.assertIn('content', model2)
161 self.assertEqual(model2['name'], 'Untitled0.ipynb')
180 self.assertEqual(model2['name'], 'Untitled.ipynb')
162 self.assertEqual(model2['path'], sub_dir.strip('/'))
181 self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
182
183 # Test getting directory model
184 dirmodel = cm.get('foo')
185 self.assertEqual(dirmodel['type'], 'directory')
186
187 with self.assertRaises(HTTPError):
188 cm.get('foo', type_='file')
189
163
190
164 @dec.skip_win32
191 @dec.skip_win32
165 def test_bad_symlink(self):
192 def test_bad_symlink(self):
@@ -167,26 +194,27 b' class TestContentsManager(TestCase):'
167 path = 'test bad symlink'
194 path = 'test bad symlink'
168 os_path = self.make_dir(cm.root_dir, path)
195 os_path = self.make_dir(cm.root_dir, path)
169
196
170 file_model = cm.create_file(path=path, ext='.txt')
197 file_model = cm.new_untitled(path=path, ext='.txt')
171
198
172 # create a broken symlink
199 # create a broken symlink
173 os.symlink("target", os.path.join(os_path, "bad symlink"))
200 os.symlink("target", os.path.join(os_path, "bad symlink"))
174 model = cm.get_model(path)
201 model = cm.get(path)
175 self.assertEqual(model['content'], [file_model])
202 self.assertEqual(model['content'], [file_model])
176
203
177 @dec.skip_win32
204 @dec.skip_win32
178 def test_good_symlink(self):
205 def test_good_symlink(self):
179 cm = self.contents_manager
206 cm = self.contents_manager
180 path = 'test good symlink'
207 parent = 'test good symlink'
181 os_path = self.make_dir(cm.root_dir, path)
208 name = 'good symlink'
209 path = '{0}/{1}'.format(parent, name)
210 os_path = self.make_dir(cm.root_dir, parent)
182
211
183 file_model = cm.create_file(path=path, ext='.txt')
212 file_model = cm.new(path=parent + '/zfoo.txt')
184
213
185 # create a good symlink
214 # create a good symlink
186 os.symlink(file_model['name'], os.path.join(os_path, "good symlink"))
215 os.symlink(file_model['name'], os.path.join(os_path, name))
187 symlink_model = cm.get_model(name="good symlink", path=path, content=False)
216 symlink_model = cm.get(path, content=False)
188
217 dir_model = cm.get(parent)
189 dir_model = cm.get_model(path)
190 self.assertEqual(
218 self.assertEqual(
191 sorted(dir_model['content'], key=lambda x: x['name']),
219 sorted(dir_model['content'], key=lambda x: x['name']),
192 [symlink_model, file_model],
220 [symlink_model, file_model],
@@ -195,53 +223,54 b' class TestContentsManager(TestCase):'
195 def test_update(self):
223 def test_update(self):
196 cm = self.contents_manager
224 cm = self.contents_manager
197 # Create a notebook
225 # Create a notebook
198 model = cm.create_file()
226 model = cm.new_untitled(type='notebook')
199 name = model['name']
227 name = model['name']
200 path = model['path']
228 path = model['path']
201
229
202 # Change the name in the model for rename
230 # Change the name in the model for rename
203 model['name'] = 'test.ipynb'
231 model['path'] = 'test.ipynb'
204 model = cm.update(model, name, path)
232 model = cm.update(model, path)
205 assert isinstance(model, dict)
233 assert isinstance(model, dict)
206 self.assertIn('name', model)
234 self.assertIn('name', model)
207 self.assertIn('path', model)
235 self.assertIn('path', model)
208 self.assertEqual(model['name'], 'test.ipynb')
236 self.assertEqual(model['name'], 'test.ipynb')
209
237
210 # Make sure the old name is gone
238 # Make sure the old name is gone
211 self.assertRaises(HTTPError, cm.get_model, name, path)
239 self.assertRaises(HTTPError, cm.get, path)
212
240
213 # Test in sub-directory
241 # Test in sub-directory
214 # Create a directory and notebook in that directory
242 # Create a directory and notebook in that directory
215 sub_dir = '/foo/'
243 sub_dir = '/foo/'
216 self.make_dir(cm.root_dir, 'foo')
244 self.make_dir(cm.root_dir, 'foo')
217 model = cm.create_file(None, sub_dir)
245 model = cm.new_untitled(path=sub_dir, type='notebook')
218 name = model['name']
246 name = model['name']
219 path = model['path']
247 path = model['path']
220
248
221 # Change the name in the model for rename
249 # Change the name in the model for rename
222 model['name'] = 'test_in_sub.ipynb'
250 d = path.rsplit('/', 1)[0]
223 model = cm.update(model, name, path)
251 new_path = model['path'] = d + '/test_in_sub.ipynb'
252 model = cm.update(model, path)
224 assert isinstance(model, dict)
253 assert isinstance(model, dict)
225 self.assertIn('name', model)
254 self.assertIn('name', model)
226 self.assertIn('path', model)
255 self.assertIn('path', model)
227 self.assertEqual(model['name'], 'test_in_sub.ipynb')
256 self.assertEqual(model['name'], 'test_in_sub.ipynb')
228 self.assertEqual(model['path'], sub_dir.strip('/'))
257 self.assertEqual(model['path'], new_path)
229
258
230 # Make sure the old name is gone
259 # Make sure the old name is gone
231 self.assertRaises(HTTPError, cm.get_model, name, path)
260 self.assertRaises(HTTPError, cm.get, path)
232
261
233 def test_save(self):
262 def test_save(self):
234 cm = self.contents_manager
263 cm = self.contents_manager
235 # Create a notebook
264 # Create a notebook
236 model = cm.create_file()
265 model = cm.new_untitled(type='notebook')
237 name = model['name']
266 name = model['name']
238 path = model['path']
267 path = model['path']
239
268
240 # Get the model with 'content'
269 # Get the model with 'content'
241 full_model = cm.get_model(name, path)
270 full_model = cm.get(path)
242
271
243 # Save the notebook
272 # Save the notebook
244 model = cm.save(full_model, name, path)
273 model = cm.save(full_model, path)
245 assert isinstance(model, dict)
274 assert isinstance(model, dict)
246 self.assertIn('name', model)
275 self.assertIn('name', model)
247 self.assertIn('path', model)
276 self.assertIn('path', model)
@@ -252,18 +281,18 b' class TestContentsManager(TestCase):'
252 # Create a directory and notebook in that directory
281 # Create a directory and notebook in that directory
253 sub_dir = '/foo/'
282 sub_dir = '/foo/'
254 self.make_dir(cm.root_dir, 'foo')
283 self.make_dir(cm.root_dir, 'foo')
255 model = cm.create_file(None, sub_dir)
284 model = cm.new_untitled(path=sub_dir, type='notebook')
256 name = model['name']
285 name = model['name']
257 path = model['path']
286 path = model['path']
258 model = cm.get_model(name, path)
287 model = cm.get(path)
259
288
260 # Change the name in the model for rename
289 # Change the name in the model for rename
261 model = cm.save(model, name, path)
290 model = cm.save(model, path)
262 assert isinstance(model, dict)
291 assert isinstance(model, dict)
263 self.assertIn('name', model)
292 self.assertIn('name', model)
264 self.assertIn('path', model)
293 self.assertIn('path', model)
265 self.assertEqual(model['name'], 'Untitled0.ipynb')
294 self.assertEqual(model['name'], 'Untitled.ipynb')
266 self.assertEqual(model['path'], sub_dir.strip('/'))
295 self.assertEqual(model['path'], 'foo/Untitled.ipynb')
267
296
268 def test_delete(self):
297 def test_delete(self):
269 cm = self.contents_manager
298 cm = self.contents_manager
@@ -271,36 +300,42 b' class TestContentsManager(TestCase):'
271 nb, name, path = self.new_notebook()
300 nb, name, path = self.new_notebook()
272
301
273 # Delete the notebook
302 # Delete the notebook
274 cm.delete(name, path)
303 cm.delete(path)
275
304
276 # Check that a 'get' on the deleted notebook raises and error
305 # Check that a 'get' on the deleted notebook raises and error
277 self.assertRaises(HTTPError, cm.get_model, name, path)
306 self.assertRaises(HTTPError, cm.get, path)
278
307
279 def test_copy(self):
308 def test_copy(self):
280 cm = self.contents_manager
309 cm = self.contents_manager
281 path = u'å b'
310 parent = u'å b'
282 name = u'nb √.ipynb'
311 name = u'nb √.ipynb'
283 os.mkdir(os.path.join(cm.root_dir, path))
312 path = u'{0}/{1}'.format(parent, name)
284 orig = cm.create_file({'name' : name}, path=path)
313 os.mkdir(os.path.join(cm.root_dir, parent))
314 orig = cm.new(path=path)
285
315
286 # copy with unspecified name
316 # copy with unspecified name
287 copy = cm.copy(name, path=path)
317 copy = cm.copy(path)
288 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
318 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy1.ipynb'))
289
319
290 # copy with specified name
320 # copy with specified name
291 copy2 = cm.copy(name, u'copy 2.ipynb', path=path)
321 copy2 = cm.copy(path, u'å b/copy 2.ipynb')
292 self.assertEqual(copy2['name'], u'copy 2.ipynb')
322 self.assertEqual(copy2['name'], u'copy 2.ipynb')
323 self.assertEqual(copy2['path'], u'å b/copy 2.ipynb')
324 # copy with specified path
325 copy2 = cm.copy(path, u'/')
326 self.assertEqual(copy2['name'], name)
327 self.assertEqual(copy2['path'], name)
293
328
294 def test_trust_notebook(self):
329 def test_trust_notebook(self):
295 cm = self.contents_manager
330 cm = self.contents_manager
296 nb, name, path = self.new_notebook()
331 nb, name, path = self.new_notebook()
297
332
298 untrusted = cm.get_model(name, path)['content']
333 untrusted = cm.get(path)['content']
299 assert not cm.notary.check_cells(untrusted)
334 assert not cm.notary.check_cells(untrusted)
300
335
301 # print(untrusted)
336 # print(untrusted)
302 cm.trust_notebook(name, path)
337 cm.trust_notebook(path)
303 trusted = cm.get_model(name, path)['content']
338 trusted = cm.get(path)['content']
304 # print(trusted)
339 # print(trusted)
305 assert cm.notary.check_cells(trusted)
340 assert cm.notary.check_cells(trusted)
306
341
@@ -308,27 +343,27 b' class TestContentsManager(TestCase):'
308 cm = self.contents_manager
343 cm = self.contents_manager
309 nb, name, path = self.new_notebook()
344 nb, name, path = self.new_notebook()
310
345
311 cm.mark_trusted_cells(nb, name, path)
346 cm.mark_trusted_cells(nb, path)
312 for cell in nb.worksheets[0].cells:
347 for cell in nb.cells:
313 if cell.cell_type == 'code':
348 if cell.cell_type == 'code':
314 assert not cell.trusted
349 assert not cell.metadata.trusted
315
350
316 cm.trust_notebook(name, path)
351 cm.trust_notebook(path)
317 nb = cm.get_model(name, path)['content']
352 nb = cm.get(path)['content']
318 for cell in nb.worksheets[0].cells:
353 for cell in nb.cells:
319 if cell.cell_type == 'code':
354 if cell.cell_type == 'code':
320 assert cell.trusted
355 assert cell.metadata.trusted
321
356
322 def test_check_and_sign(self):
357 def test_check_and_sign(self):
323 cm = self.contents_manager
358 cm = self.contents_manager
324 nb, name, path = self.new_notebook()
359 nb, name, path = self.new_notebook()
325
360
326 cm.mark_trusted_cells(nb, name, path)
361 cm.mark_trusted_cells(nb, path)
327 cm.check_and_sign(nb, name, path)
362 cm.check_and_sign(nb, path)
328 assert not cm.notary.check_signature(nb)
363 assert not cm.notary.check_signature(nb)
329
364
330 cm.trust_notebook(name, path)
365 cm.trust_notebook(path)
331 nb = cm.get_model(name, path)['content']
366 nb = cm.get(path)['content']
332 cm.mark_trusted_cells(nb, name, path)
367 cm.mark_trusted_cells(nb, path)
333 cm.check_and_sign(nb, name, path)
368 cm.check_and_sign(nb, path)
334 assert cm.notary.check_signature(nb)
369 assert cm.notary.check_signature(nb)
@@ -5,14 +5,16 b''
5
5
6 import json
6 import json
7 import logging
7 import logging
8 from tornado import web
8 from tornado import gen, web
9 from tornado.concurrent import Future
10 from tornado.ioloop import IOLoop
9
11
10 from IPython.utils.jsonutil import date_default
12 from IPython.utils.jsonutil import date_default
11 from IPython.utils.py3compat import string_types
13 from IPython.utils.py3compat import cast_unicode
12 from IPython.html.utils import url_path_join, url_escape
14 from IPython.html.utils import url_path_join, url_escape
13
15
14 from ...base.handlers import IPythonHandler, json_errors
16 from ...base.handlers import IPythonHandler, json_errors
15 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
17 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
16
18
17 from IPython.core.release import kernel_protocol_version
19 from IPython.core.release import kernel_protocol_version
18
20
@@ -27,16 +29,16 b' class MainKernelHandler(IPythonHandler):'
27 @web.authenticated
29 @web.authenticated
28 @json_errors
30 @json_errors
29 def post(self):
31 def post(self):
32 km = self.kernel_manager
30 model = self.get_json_body()
33 model = self.get_json_body()
31 if model is None:
34 if model is None:
32 raise web.HTTPError(400, "No JSON data provided")
35 model = {
33 try:
36 'name': km.default_kernel_name
34 name = model['name']
37 }
35 except KeyError:
38 else:
36 raise web.HTTPError(400, "Missing field in JSON data: name")
39 model.setdefault('name', km.default_kernel_name)
37
40
38 km = self.kernel_manager
41 kernel_id = km.start_kernel(kernel_name=model['name'])
39 kernel_id = km.start_kernel(kernel_name=name)
40 model = km.kernel_model(kernel_id)
42 model = km.kernel_model(kernel_id)
41 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
43 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
42 self.set_header('Location', url_escape(location))
44 self.set_header('Location', url_escape(location))
@@ -84,6 +86,10 b' class KernelActionHandler(IPythonHandler):'
84
86
85 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
87 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
86
88
89 @property
90 def kernel_info_timeout(self):
91 return self.settings.get('kernel_info_timeout', 10)
92
87 def __repr__(self):
93 def __repr__(self):
88 return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
94 return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
89
95
@@ -91,17 +97,29 b' class ZMQChannelHandler(AuthenticatedZMQStreamHandler):'
91 km = self.kernel_manager
97 km = self.kernel_manager
92 meth = getattr(km, 'connect_%s' % self.channel)
98 meth = getattr(km, 'connect_%s' % self.channel)
93 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
99 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
94 # Create a kernel_info channel to query the kernel protocol version.
95 # This channel will be closed after the kernel_info reply is received.
96 self.kernel_info_channel = None
97 self.kernel_info_channel = km.connect_shell(self.kernel_id)
98 self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
99 self._request_kernel_info()
100
100
101 def _request_kernel_info(self):
101 def request_kernel_info(self):
102 """send a request for kernel_info"""
102 """send a request for kernel_info"""
103 self.log.debug("requesting kernel info")
103 km = self.kernel_manager
104 self.session.send(self.kernel_info_channel, "kernel_info_request")
104 kernel = km.get_kernel(self.kernel_id)
105 try:
106 # check for previous request
107 future = kernel._kernel_info_future
108 except AttributeError:
109 self.log.debug("Requesting kernel info from %s", self.kernel_id)
110 # Create a kernel_info channel to query the kernel protocol version.
111 # This channel will be closed after the kernel_info reply is received.
112 if self.kernel_info_channel is None:
113 self.kernel_info_channel = km.connect_shell(self.kernel_id)
114 self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
115 self.session.send(self.kernel_info_channel, "kernel_info_request")
116 # store the future on the kernel, so only one request is sent
117 kernel._kernel_info_future = self._kernel_info_future
118 else:
119 if not future.done():
120 self.log.debug("Waiting for pending kernel_info request")
121 future.add_done_callback(lambda f: self._finish_kernel_info(f.result()))
122 return self._kernel_info_future
105
123
106 def _handle_kernel_info_reply(self, msg):
124 def _handle_kernel_info_reply(self, msg):
107 """process the kernel_info_reply
125 """process the kernel_info_reply
@@ -110,35 +128,75 b' class ZMQChannelHandler(AuthenticatedZMQStreamHandler):'
110 """
128 """
111 idents,msg = self.session.feed_identities(msg)
129 idents,msg = self.session.feed_identities(msg)
112 try:
130 try:
113 msg = self.session.unserialize(msg)
131 msg = self.session.deserialize(msg)
114 except:
132 except:
115 self.log.error("Bad kernel_info reply", exc_info=True)
133 self.log.error("Bad kernel_info reply", exc_info=True)
116 self._request_kernel_info()
134 self._kernel_info_future.set_result({})
117 return
135 return
118 else:
136 else:
119 if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in msg['content']:
137 info = msg['content']
120 self.log.error("Kernel info request failed, assuming current %s", msg['content'])
138 self.log.debug("Received kernel info: %s", info)
121 else:
139 if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info:
122 protocol_version = msg['content']['protocol_version']
140 self.log.error("Kernel info request failed, assuming current %s", info)
123 if protocol_version != kernel_protocol_version:
141 info = {}
124 self.session.adapt_version = int(protocol_version.split('.')[0])
142 self._finish_kernel_info(info)
125 self.log.info("adapting kernel to %s" % protocol_version)
143
126 self.kernel_info_channel.close()
144 # close the kernel_info channel, we don't need it anymore
145 if self.kernel_info_channel:
146 self.kernel_info_channel.close()
127 self.kernel_info_channel = None
147 self.kernel_info_channel = None
128
148
149 def _finish_kernel_info(self, info):
150 """Finish handling kernel_info reply
151
152 Set up protocol adaptation, if needed,
153 and signal that connection can continue.
154 """
155 protocol_version = info.get('protocol_version', kernel_protocol_version)
156 if protocol_version != kernel_protocol_version:
157 self.session.adapt_version = int(protocol_version.split('.')[0])
158 self.log.info("Kernel %s speaks protocol %s", self.kernel_id, protocol_version)
159 if not self._kernel_info_future.done():
160 self._kernel_info_future.set_result(info)
129
161
130 def initialize(self, *args, **kwargs):
162 def initialize(self):
163 super(ZMQChannelHandler, self).initialize()
131 self.zmq_stream = None
164 self.zmq_stream = None
165 self.kernel_id = None
166 self.kernel_info_channel = None
167 self._kernel_info_future = Future()
132
168
133 def on_first_message(self, msg):
169 @gen.coroutine
134 try:
170 def pre_get(self):
135 super(ZMQChannelHandler, self).on_first_message(msg)
171 # authenticate first
136 except web.HTTPError:
172 super(ZMQChannelHandler, self).pre_get()
137 self.close()
173 # then request kernel info, waiting up to a certain time before giving up.
138 return
174 # We don't want to wait forever, because browsers don't take it well when
175 # servers never respond to websocket connection requests.
176 future = self.request_kernel_info()
177
178 def give_up():
179 """Don't wait forever for the kernel to reply"""
180 if future.done():
181 return
182 self.log.warn("Timeout waiting for kernel_info reply from %s", self.kernel_id)
183 future.set_result({})
184 loop = IOLoop.current()
185 loop.add_timeout(loop.time() + self.kernel_info_timeout, give_up)
186 # actually wait for it
187 yield future
188
189 @gen.coroutine
190 def get(self, kernel_id):
191 self.kernel_id = cast_unicode(kernel_id, 'ascii')
192 yield super(ZMQChannelHandler, self).get(kernel_id=kernel_id)
193
194 def open(self, kernel_id):
195 super(ZMQChannelHandler, self).open()
139 try:
196 try:
140 self.create_stream()
197 self.create_stream()
141 except web.HTTPError:
198 except web.HTTPError as e:
199 self.log.error("Error opening stream: %s", e)
142 # WebSockets don't response to traditional error codes so we
200 # WebSockets don't response to traditional error codes so we
143 # close the connection.
201 # close the connection.
144 if not self.stream.closed():
202 if not self.stream.closed():
@@ -154,7 +212,10 b' class ZMQChannelHandler(AuthenticatedZMQStreamHandler):'
154 self.log.info("%s closed, closing websocket.", self)
212 self.log.info("%s closed, closing websocket.", self)
155 self.close()
213 self.close()
156 return
214 return
157 msg = json.loads(msg)
215 if isinstance(msg, bytes):
216 msg = deserialize_binary_message(msg)
217 else:
218 msg = json.loads(msg)
158 self.session.send(self.zmq_stream, msg)
219 self.session.send(self.zmq_stream, msg)
159
220
160 def on_close(self):
221 def on_close(self):
@@ -1,20 +1,11 b''
1 """A kernel manager relating notebooks and kernels
1 """A MultiKernelManager for use in the notebook webserver
2
2
3 Authors:
3 - raises HTTPErrors
4
4 - creates REST API models
5 * Brian Granger
6 """
5 """
7
6
8 #-----------------------------------------------------------------------------
7 # Copyright (c) IPython Development Team.
9 # Copyright (C) 2013 The IPython Development Team
8 # Distributed under the terms of the Modified BSD License.
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
9
19 import os
10 import os
20
11
@@ -26,10 +17,6 b' from IPython.utils.traitlets import List, Unicode, TraitError'
26 from IPython.html.utils import to_os_path
17 from IPython.html.utils import to_os_path
27 from IPython.utils.py3compat import getcwd
18 from IPython.utils.py3compat import getcwd
28
19
29 #-----------------------------------------------------------------------------
30 # Classes
31 #-----------------------------------------------------------------------------
32
33
20
34 class MappingKernelManager(MultiKernelManager):
21 class MappingKernelManager(MultiKernelManager):
35 """A KernelManager that handles notebook mapping and HTTP error handling"""
22 """A KernelManager that handles notebook mapping and HTTP error handling"""
@@ -39,7 +26,13 b' class MappingKernelManager(MultiKernelManager):'
39
26
40 kernel_argv = List(Unicode)
27 kernel_argv = List(Unicode)
41
28
42 root_dir = Unicode(getcwd(), config=True)
29 root_dir = Unicode(config=True)
30
31 def _root_dir_default(self):
32 try:
33 return self.parent.notebook_dir
34 except AttributeError:
35 return getcwd()
43
36
44 def _root_dir_changed(self, name, old, new):
37 def _root_dir_changed(self, name, old, new):
45 """Do a bit of validation of the root dir."""
38 """Do a bit of validation of the root dir."""
@@ -61,14 +54,10 b' class MappingKernelManager(MultiKernelManager):'
61
54
62 def cwd_for_path(self, path):
55 def cwd_for_path(self, path):
63 """Turn API path into absolute OS path."""
56 """Turn API path into absolute OS path."""
64 # short circuit for NotebookManagers that pass in absolute paths
65 if os.path.exists(path):
66 return path
67
68 os_path = to_os_path(path, self.root_dir)
57 os_path = to_os_path(path, self.root_dir)
69 # in the case of notebooks and kernels not being on the same filesystem,
58 # in the case of notebooks and kernels not being on the same filesystem,
70 # walk up to root_dir if the paths don't exist
59 # walk up to root_dir if the paths don't exist
71 while not os.path.exists(os_path) and os_path != self.root_dir:
60 while not os.path.isdir(os_path) and os_path != self.root_dir:
72 os_path = os.path.dirname(os_path)
61 os_path = os.path.dirname(os_path)
73 return os_path
62 return os_path
74
63
@@ -89,7 +78,6 b' class MappingKernelManager(MultiKernelManager):'
89 an existing kernel is returned, but it may be checked in the future.
78 an existing kernel is returned, but it may be checked in the future.
90 """
79 """
91 if kernel_id is None:
80 if kernel_id is None:
92 kwargs['extra_arguments'] = self.kernel_argv
93 if path is not None:
81 if path is not None:
94 kwargs['cwd'] = self.cwd_for_path(path)
82 kwargs['cwd'] = self.cwd_for_path(path)
95 kernel_id = super(MappingKernelManager, self).start_kernel(
83 kernel_id = super(MappingKernelManager, self).start_kernel(
@@ -57,6 +57,19 b' class KernelAPITest(NotebookTestBase):'
57 kernels = self.kern_api.list().json()
57 kernels = self.kern_api.list().json()
58 self.assertEqual(kernels, [])
58 self.assertEqual(kernels, [])
59
59
60 def test_default_kernel(self):
61 # POST request
62 r = self.kern_api._req('POST', '')
63 kern1 = r.json()
64 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
65 self.assertEqual(r.status_code, 201)
66 self.assertIsInstance(kern1, dict)
67
68 self.assertEqual(r.headers['Content-Security-Policy'], (
69 "frame-ancestors 'self'; "
70 "report-uri /api/security/csp-report;"
71 ))
72
60 def test_main_kernel_handler(self):
73 def test_main_kernel_handler(self):
61 # POST request
74 # POST request
62 r = self.kern_api.start()
75 r = self.kern_api.start()
@@ -65,7 +78,10 b' class KernelAPITest(NotebookTestBase):'
65 self.assertEqual(r.status_code, 201)
78 self.assertEqual(r.status_code, 201)
66 self.assertIsInstance(kern1, dict)
79 self.assertIsInstance(kern1, dict)
67
80
68 self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN")
81 self.assertEqual(r.headers['Content-Security-Policy'], (
82 "frame-ancestors 'self'; "
83 "report-uri /api/security/csp-report;"
84 ))
69
85
70 # GET request
86 # GET request
71 r = self.kern_api.list()
87 r = self.kern_api.list()
@@ -19,7 +19,11 b' class MainKernelSpecHandler(IPythonHandler):'
19 ksm = self.kernel_spec_manager
19 ksm = self.kernel_spec_manager
20 results = []
20 results = []
21 for kernel_name in sorted(ksm.find_kernel_specs(), key=_pythonfirst):
21 for kernel_name in sorted(ksm.find_kernel_specs(), key=_pythonfirst):
22 d = ksm.get_kernel_spec(kernel_name).to_dict()
22 try:
23 d = ksm.get_kernel_spec(kernel_name).to_dict()
24 except Exception:
25 self.log.error("Failed to load kernel spec: '%s'", kernel_name, exc_info=True)
26 continue
23 d['name'] = kernel_name
27 d['name'] = kernel_name
24 results.append(d)
28 results.append(d)
25
29
@@ -5,6 +5,7 b' import errno'
5 import io
5 import io
6 import json
6 import json
7 import os
7 import os
8 import shutil
8
9
9 pjoin = os.path.join
10 pjoin = os.path.join
10
11
@@ -18,7 +19,6 b' from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_erro'
18 # break these tests
19 # break these tests
19 sample_kernel_json = {'argv':['cat', '{connection_file}'],
20 sample_kernel_json = {'argv':['cat', '{connection_file}'],
20 'display_name':'Test kernel',
21 'display_name':'Test kernel',
21 'language':'bash',
22 }
22 }
23
23
24 some_resource = u"The very model of a modern major general"
24 some_resource = u"The very model of a modern major general"
@@ -66,6 +66,25 b' class APITest(NotebookTestBase):'
66
66
67 self.ks_api = KernelSpecAPI(self.base_url())
67 self.ks_api = KernelSpecAPI(self.base_url())
68
68
69 def test_list_kernelspecs_bad(self):
70 """Can list kernelspecs when one is invalid"""
71 bad_kernel_dir = pjoin(self.ipython_dir.name, 'kernels', 'bad')
72 try:
73 os.makedirs(bad_kernel_dir)
74 except OSError as e:
75 if e.errno != errno.EEXIST:
76 raise
77
78 with open(pjoin(bad_kernel_dir, 'kernel.json'), 'w') as f:
79 f.write("garbage")
80
81 specs = self.ks_api.list().json()
82 assert isinstance(specs, list)
83 # 2: the sample kernelspec created in setUp, and the native Python kernel
84 self.assertGreaterEqual(len(specs), 2)
85
86 shutil.rmtree(bad_kernel_dir)
87
69 def test_list_kernelspecs(self):
88 def test_list_kernelspecs(self):
70 specs = self.ks_api.list().json()
89 specs = self.ks_api.list().json()
71 assert isinstance(specs, list)
90 assert isinstance(specs, list)
@@ -84,7 +103,7 b' class APITest(NotebookTestBase):'
84
103
85 def test_get_kernelspec(self):
104 def test_get_kernelspec(self):
86 spec = self.ks_api.kernel_spec_info('Sample').json() # Case insensitive
105 spec = self.ks_api.kernel_spec_info('Sample').json() # Case insensitive
87 self.assertEqual(spec['language'], 'bash')
106 self.assertEqual(spec['display_name'], 'Test kernel')
88
107
89 def test_get_nonexistant_kernelspec(self):
108 def test_get_nonexistant_kernelspec(self):
90 with assert_http_error(404):
109 with assert_http_error(404):
@@ -10,6 +10,7 b' from tornado import web'
10 from ...base.handlers import IPythonHandler, json_errors
10 from ...base.handlers import IPythonHandler, json_errors
11 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
12 from IPython.html.utils import url_path_join, url_escape
12 from IPython.html.utils import url_path_join, url_escape
13 from IPython.kernel.kernelspec import NoSuchKernel
13
14
14
15
15 class SessionRootHandler(IPythonHandler):
16 class SessionRootHandler(IPythonHandler):
@@ -35,23 +36,30 b' class SessionRootHandler(IPythonHandler):'
35 if model is None:
36 if model is None:
36 raise web.HTTPError(400, "No JSON data provided")
37 raise web.HTTPError(400, "No JSON data provided")
37 try:
38 try:
38 name = model['notebook']['name']
39 except KeyError:
40 raise web.HTTPError(400, "Missing field in JSON data: notebook.name")
41 try:
42 path = model['notebook']['path']
39 path = model['notebook']['path']
43 except KeyError:
40 except KeyError:
44 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
41 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
45 try:
42 try:
46 kernel_name = model['kernel']['name']
43 kernel_name = model['kernel']['name']
47 except KeyError:
44 except KeyError:
48 raise web.HTTPError(400, "Missing field in JSON data: kernel.name")
45 self.log.debug("No kernel name specified, using default kernel")
46 kernel_name = None
49
47
50 # Check to see if session exists
48 # Check to see if session exists
51 if sm.session_exists(name=name, path=path):
49 if sm.session_exists(path=path):
52 model = sm.get_session(name=name, path=path)
50 model = sm.get_session(path=path)
53 else:
51 else:
54 model = sm.create_session(name=name, path=path, kernel_name=kernel_name)
52 try:
53 model = sm.create_session(path=path, kernel_name=kernel_name)
54 except NoSuchKernel:
55 msg = ("The '%s' kernel is not available. Please pick another "
56 "suitable kernel instead, or install that kernel." % kernel_name)
57 status_msg = '%s not found' % kernel_name
58 self.log.warn('Kernel not found: %s' % kernel_name)
59 self.set_status(501)
60 self.finish(json.dumps(dict(message=msg, short_message=status_msg)))
61 return
62
55 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
63 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
56 self.set_header('Location', url_escape(location))
64 self.set_header('Location', url_escape(location))
57 self.set_status(201)
65 self.set_status(201)
@@ -80,8 +88,6 b' class SessionHandler(IPythonHandler):'
80 changes = {}
88 changes = {}
81 if 'notebook' in model:
89 if 'notebook' in model:
82 notebook = model['notebook']
90 notebook = model['notebook']
83 if 'name' in notebook:
84 changes['name'] = notebook['name']
85 if 'path' in notebook:
91 if 'path' in notebook:
86 changes['path'] = notebook['path']
92 changes['path'] = notebook['path']
87
93
@@ -94,7 +100,11 b' class SessionHandler(IPythonHandler):'
94 def delete(self, session_id):
100 def delete(self, session_id):
95 # Deletes the session with given session_id
101 # Deletes the session with given session_id
96 sm = self.session_manager
102 sm = self.session_manager
97 sm.delete_session(session_id)
103 try:
104 sm.delete_session(session_id)
105 except KeyError:
106 # the kernel was deleted but the session wasn't!
107 raise web.HTTPError(410, "Kernel deleted before session")
98 self.set_status(204)
108 self.set_status(204)
99 self.finish()
109 self.finish()
100
110
@@ -1,20 +1,7 b''
1 """A base class session manager.
1 """A base class session manager."""
2
2
3 Authors:
3 # Copyright (c) IPython Development Team.
4
4 # Distributed under the terms of the Modified BSD License.
5 * Zach Sailer
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
5
19 import uuid
6 import uuid
20 import sqlite3
7 import sqlite3
@@ -25,9 +12,6 b' from IPython.config.configurable import LoggingConfigurable'
25 from IPython.utils.py3compat import unicode_type
12 from IPython.utils.py3compat import unicode_type
26 from IPython.utils.traitlets import Instance
13 from IPython.utils.traitlets import Instance
27
14
28 #-----------------------------------------------------------------------------
29 # Classes
30 #-----------------------------------------------------------------------------
31
15
32 class SessionManager(LoggingConfigurable):
16 class SessionManager(LoggingConfigurable):
33
17
@@ -37,7 +21,7 b' class SessionManager(LoggingConfigurable):'
37 # Session database initialized below
21 # Session database initialized below
38 _cursor = None
22 _cursor = None
39 _connection = None
23 _connection = None
40 _columns = {'session_id', 'name', 'path', 'kernel_id'}
24 _columns = {'session_id', 'path', 'kernel_id'}
41
25
42 @property
26 @property
43 def cursor(self):
27 def cursor(self):
@@ -45,7 +29,7 b' class SessionManager(LoggingConfigurable):'
45 if self._cursor is None:
29 if self._cursor is None:
46 self._cursor = self.connection.cursor()
30 self._cursor = self.connection.cursor()
47 self._cursor.execute("""CREATE TABLE session
31 self._cursor.execute("""CREATE TABLE session
48 (session_id, name, path, kernel_id)""")
32 (session_id, path, kernel_id)""")
49 return self._cursor
33 return self._cursor
50
34
51 @property
35 @property
@@ -60,9 +44,9 b' class SessionManager(LoggingConfigurable):'
60 """Close connection once SessionManager closes"""
44 """Close connection once SessionManager closes"""
61 self.cursor.close()
45 self.cursor.close()
62
46
63 def session_exists(self, name, path):
47 def session_exists(self, path):
64 """Check to see if the session for a given notebook exists"""
48 """Check to see if the session for a given notebook exists"""
65 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
49 self.cursor.execute("SELECT * FROM session WHERE path=?", (path,))
66 reply = self.cursor.fetchone()
50 reply = self.cursor.fetchone()
67 if reply is None:
51 if reply is None:
68 return False
52 return False
@@ -73,17 +57,17 b' class SessionManager(LoggingConfigurable):'
73 "Create a uuid for a new session"
57 "Create a uuid for a new session"
74 return unicode_type(uuid.uuid4())
58 return unicode_type(uuid.uuid4())
75
59
76 def create_session(self, name=None, path=None, kernel_name='python'):
60 def create_session(self, path=None, kernel_name=None):
77 """Creates a session and returns its model"""
61 """Creates a session and returns its model"""
78 session_id = self.new_session_id()
62 session_id = self.new_session_id()
79 # allow nbm to specify kernels cwd
63 # allow nbm to specify kernels cwd
80 kernel_path = self.contents_manager.get_kernel_path(name=name, path=path)
64 kernel_path = self.contents_manager.get_kernel_path(path=path)
81 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
65 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
82 kernel_name=kernel_name)
66 kernel_name=kernel_name)
83 return self.save_session(session_id, name=name, path=path,
67 return self.save_session(session_id, path=path,
84 kernel_id=kernel_id)
68 kernel_id=kernel_id)
85
69
86 def save_session(self, session_id, name=None, path=None, kernel_id=None):
70 def save_session(self, session_id, path=None, kernel_id=None):
87 """Saves the items for the session with the given session_id
71 """Saves the items for the session with the given session_id
88
72
89 Given a session_id (and any other of the arguments), this method
73 Given a session_id (and any other of the arguments), this method
@@ -94,10 +78,8 b' class SessionManager(LoggingConfigurable):'
94 ----------
78 ----------
95 session_id : str
79 session_id : str
96 uuid for the session; this method must be given a session_id
80 uuid for the session; this method must be given a session_id
97 name : str
98 the .ipynb notebook name that started the session
99 path : str
81 path : str
100 the path to the named notebook
82 the path for the given notebook
101 kernel_id : str
83 kernel_id : str
102 a uuid for the kernel associated with this session
84 a uuid for the kernel associated with this session
103
85
@@ -106,8 +88,8 b' class SessionManager(LoggingConfigurable):'
106 model : dict
88 model : dict
107 a dictionary of the session model
89 a dictionary of the session model
108 """
90 """
109 self.cursor.execute("INSERT INTO session VALUES (?,?,?,?)",
91 self.cursor.execute("INSERT INTO session VALUES (?,?,?)",
110 (session_id, name, path, kernel_id)
92 (session_id, path, kernel_id)
111 )
93 )
112 return self.get_session(session_id=session_id)
94 return self.get_session(session_id=session_id)
113
95
@@ -121,7 +103,7 b' class SessionManager(LoggingConfigurable):'
121 ----------
103 ----------
122 **kwargs : keyword argument
104 **kwargs : keyword argument
123 must be given one of the keywords and values from the session database
105 must be given one of the keywords and values from the session database
124 (i.e. session_id, name, path, kernel_id)
106 (i.e. session_id, path, kernel_id)
125
107
126 Returns
108 Returns
127 -------
109 -------
@@ -198,7 +180,6 b' class SessionManager(LoggingConfigurable):'
198 model = {
180 model = {
199 'id': row['session_id'],
181 'id': row['session_id'],
200 'notebook': {
182 'notebook': {
201 'name': row['name'],
202 'path': row['path']
183 'path': row['path']
203 },
184 },
204 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
185 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
@@ -32,24 +32,24 b' class TestSessionManager(TestCase):'
32
32
33 def test_get_session(self):
33 def test_get_session(self):
34 sm = SessionManager(kernel_manager=DummyMKM())
34 sm = SessionManager(kernel_manager=DummyMKM())
35 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
35 session_id = sm.create_session(path='/path/to/test.ipynb',
36 kernel_name='bar')['id']
36 kernel_name='bar')['id']
37 model = sm.get_session(session_id=session_id)
37 model = sm.get_session(session_id=session_id)
38 expected = {'id':session_id,
38 expected = {'id':session_id,
39 'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'},
39 'notebook':{'path': u'/path/to/test.ipynb'},
40 'kernel': {'id':u'A', 'name': 'bar'}}
40 'kernel': {'id':u'A', 'name': 'bar'}}
41 self.assertEqual(model, expected)
41 self.assertEqual(model, expected)
42
42
43 def test_bad_get_session(self):
43 def test_bad_get_session(self):
44 # Should raise error if a bad key is passed to the database.
44 # Should raise error if a bad key is passed to the database.
45 sm = SessionManager(kernel_manager=DummyMKM())
45 sm = SessionManager(kernel_manager=DummyMKM())
46 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
46 session_id = sm.create_session(path='/path/to/test.ipynb',
47 kernel_name='foo')['id']
47 kernel_name='foo')['id']
48 self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
48 self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
49
49
50 def test_get_session_dead_kernel(self):
50 def test_get_session_dead_kernel(self):
51 sm = SessionManager(kernel_manager=DummyMKM())
51 sm = SessionManager(kernel_manager=DummyMKM())
52 session = sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python')
52 session = sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python')
53 # kill the kernel
53 # kill the kernel
54 sm.kernel_manager.shutdown_kernel(session['kernel']['id'])
54 sm.kernel_manager.shutdown_kernel(session['kernel']['id'])
55 with self.assertRaises(KeyError):
55 with self.assertRaises(KeyError):
@@ -61,24 +61,33 b' class TestSessionManager(TestCase):'
61 def test_list_sessions(self):
61 def test_list_sessions(self):
62 sm = SessionManager(kernel_manager=DummyMKM())
62 sm = SessionManager(kernel_manager=DummyMKM())
63 sessions = [
63 sessions = [
64 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
64 sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
65 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
65 sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
66 sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
66 sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
67 ]
67 ]
68 sessions = sm.list_sessions()
68 sessions = sm.list_sessions()
69 expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
69 expected = [
70 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
70 {
71 {'id':sessions[1]['id'], 'notebook': {'name':u'test2.ipynb',
71 'id':sessions[0]['id'],
72 'path': u'/path/to/2/'}, 'kernel':{'id':u'B', 'name':'python'}},
72 'notebook':{'path': u'/path/to/1/test1.ipynb'},
73 {'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
73 'kernel':{'id':u'A', 'name':'python'}
74 'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
74 }, {
75 'id':sessions[1]['id'],
76 'notebook': {'path': u'/path/to/2/test2.ipynb'},
77 'kernel':{'id':u'B', 'name':'python'}
78 }, {
79 'id':sessions[2]['id'],
80 'notebook':{'path': u'/path/to/3/test3.ipynb'},
81 'kernel':{'id':u'C', 'name':'python'}
82 }
83 ]
75 self.assertEqual(sessions, expected)
84 self.assertEqual(sessions, expected)
76
85
77 def test_list_sessions_dead_kernel(self):
86 def test_list_sessions_dead_kernel(self):
78 sm = SessionManager(kernel_manager=DummyMKM())
87 sm = SessionManager(kernel_manager=DummyMKM())
79 sessions = [
88 sessions = [
80 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
89 sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
81 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
90 sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
82 ]
91 ]
83 # kill one of the kernels
92 # kill one of the kernels
84 sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
93 sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
@@ -87,8 +96,7 b' class TestSessionManager(TestCase):'
87 {
96 {
88 'id': sessions[1]['id'],
97 'id': sessions[1]['id'],
89 'notebook': {
98 'notebook': {
90 'name': u'test2.ipynb',
99 'path': u'/path/to/2/test2.ipynb',
91 'path': u'/path/to/2/',
92 },
100 },
93 'kernel': {
101 'kernel': {
94 'id': u'B',
102 'id': u'B',
@@ -100,41 +108,47 b' class TestSessionManager(TestCase):'
100
108
101 def test_update_session(self):
109 def test_update_session(self):
102 sm = SessionManager(kernel_manager=DummyMKM())
110 sm = SessionManager(kernel_manager=DummyMKM())
103 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
111 session_id = sm.create_session(path='/path/to/test.ipynb',
104 kernel_name='julia')['id']
112 kernel_name='julia')['id']
105 sm.update_session(session_id, name='new_name.ipynb')
113 sm.update_session(session_id, path='/path/to/new_name.ipynb')
106 model = sm.get_session(session_id=session_id)
114 model = sm.get_session(session_id=session_id)
107 expected = {'id':session_id,
115 expected = {'id':session_id,
108 'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'},
116 'notebook':{'path': u'/path/to/new_name.ipynb'},
109 'kernel':{'id':u'A', 'name':'julia'}}
117 'kernel':{'id':u'A', 'name':'julia'}}
110 self.assertEqual(model, expected)
118 self.assertEqual(model, expected)
111
119
112 def test_bad_update_session(self):
120 def test_bad_update_session(self):
113 # try to update a session with a bad keyword ~ raise error
121 # try to update a session with a bad keyword ~ raise error
114 sm = SessionManager(kernel_manager=DummyMKM())
122 sm = SessionManager(kernel_manager=DummyMKM())
115 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
123 session_id = sm.create_session(path='/path/to/test.ipynb',
116 kernel_name='ir')['id']
124 kernel_name='ir')['id']
117 self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
125 self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
118
126
119 def test_delete_session(self):
127 def test_delete_session(self):
120 sm = SessionManager(kernel_manager=DummyMKM())
128 sm = SessionManager(kernel_manager=DummyMKM())
121 sessions = [
129 sessions = [
122 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
130 sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
123 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
131 sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
124 sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
132 sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
125 ]
133 ]
126 sm.delete_session(sessions[1]['id'])
134 sm.delete_session(sessions[1]['id'])
127 new_sessions = sm.list_sessions()
135 new_sessions = sm.list_sessions()
128 expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
136 expected = [{
129 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
137 'id': sessions[0]['id'],
130 {'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
138 'notebook': {'path': u'/path/to/1/test1.ipynb'},
131 'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
139 'kernel': {'id':u'A', 'name':'python'}
140 }, {
141 'id': sessions[2]['id'],
142 'notebook': {'path': u'/path/to/3/test3.ipynb'},
143 'kernel': {'id':u'C', 'name':'python'}
144 }
145 ]
132 self.assertEqual(new_sessions, expected)
146 self.assertEqual(new_sessions, expected)
133
147
134 def test_bad_delete_session(self):
148 def test_bad_delete_session(self):
135 # try to delete a session that doesn't exist ~ raise error
149 # try to delete a session that doesn't exist ~ raise error
136 sm = SessionManager(kernel_manager=DummyMKM())
150 sm = SessionManager(kernel_manager=DummyMKM())
137 sm.create_session(name='test.ipynb', path='/path/to/', kernel_name='python')
151 sm.create_session(path='/path/to/test.ipynb', kernel_name='python')
138 self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
152 self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
139 self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
153 self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
140
154
@@ -11,7 +11,8 b' pjoin = os.path.join'
11
11
12 from IPython.html.utils import url_path_join
12 from IPython.html.utils import url_path_join
13 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
14 from IPython.nbformat.current import new_notebook, write
14 from IPython.nbformat.v4 import new_notebook
15 from IPython.nbformat import write
15
16
16 class SessionAPI(object):
17 class SessionAPI(object):
17 """Wrapper for notebook API calls."""
18 """Wrapper for notebook API calls."""
@@ -37,13 +38,13 b' class SessionAPI(object):'
37 def get(self, id):
38 def get(self, id):
38 return self._req('GET', id)
39 return self._req('GET', id)
39
40
40 def create(self, name, path, kernel_name='python'):
41 def create(self, path, kernel_name='python'):
41 body = json.dumps({'notebook': {'name':name, 'path':path},
42 body = json.dumps({'notebook': {'path':path},
42 'kernel': {'name': kernel_name}})
43 'kernel': {'name': kernel_name}})
43 return self._req('POST', '', body)
44 return self._req('POST', '', body)
44
45
45 def modify(self, id, name, path):
46 def modify(self, id, path):
46 body = json.dumps({'notebook': {'name':name, 'path':path}})
47 body = json.dumps({'notebook': {'path':path}})
47 return self._req('PATCH', id, body)
48 return self._req('PATCH', id, body)
48
49
49 def delete(self, id):
50 def delete(self, id):
@@ -62,8 +63,8 b' class SessionAPITest(NotebookTestBase):'
62
63
63 with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w',
64 with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w',
64 encoding='utf-8') as f:
65 encoding='utf-8') as f:
65 nb = new_notebook(name='nb1')
66 nb = new_notebook()
66 write(nb, f, format='ipynb')
67 write(nb, f, version=4)
67
68
68 self.sess_api = SessionAPI(self.base_url())
69 self.sess_api = SessionAPI(self.base_url())
69
70
@@ -77,12 +78,11 b' class SessionAPITest(NotebookTestBase):'
77 sessions = self.sess_api.list().json()
78 sessions = self.sess_api.list().json()
78 self.assertEqual(len(sessions), 0)
79 self.assertEqual(len(sessions), 0)
79
80
80 resp = self.sess_api.create('nb1.ipynb', 'foo')
81 resp = self.sess_api.create('foo/nb1.ipynb')
81 self.assertEqual(resp.status_code, 201)
82 self.assertEqual(resp.status_code, 201)
82 newsession = resp.json()
83 newsession = resp.json()
83 self.assertIn('id', newsession)
84 self.assertIn('id', newsession)
84 self.assertEqual(newsession['notebook']['name'], 'nb1.ipynb')
85 self.assertEqual(newsession['notebook']['path'], 'foo/nb1.ipynb')
85 self.assertEqual(newsession['notebook']['path'], 'foo')
86 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
86 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
87
87
88 sessions = self.sess_api.list().json()
88 sessions = self.sess_api.list().json()
@@ -94,7 +94,7 b' class SessionAPITest(NotebookTestBase):'
94 self.assertEqual(got, newsession)
94 self.assertEqual(got, newsession)
95
95
96 def test_delete(self):
96 def test_delete(self):
97 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
97 newsession = self.sess_api.create('foo/nb1.ipynb').json()
98 sid = newsession['id']
98 sid = newsession['id']
99
99
100 resp = self.sess_api.delete(sid)
100 resp = self.sess_api.delete(sid)
@@ -107,10 +107,9 b' class SessionAPITest(NotebookTestBase):'
107 self.sess_api.get(sid)
107 self.sess_api.get(sid)
108
108
109 def test_modify(self):
109 def test_modify(self):
110 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
110 newsession = self.sess_api.create('foo/nb1.ipynb').json()
111 sid = newsession['id']
111 sid = newsession['id']
112
112
113 changed = self.sess_api.modify(sid, 'nb2.ipynb', '').json()
113 changed = self.sess_api.modify(sid, 'nb2.ipynb').json()
114 self.assertEqual(changed['id'], sid)
114 self.assertEqual(changed['id'], sid)
115 self.assertEqual(changed['notebook']['name'], 'nb2.ipynb')
115 self.assertEqual(changed['notebook']['path'], 'nb2.ipynb')
116 self.assertEqual(changed['notebook']['path'], '')
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -4,7 +4,8 b''
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 ], function(IPython, $) {
7 'codemirror/lib/codemirror',
8 ], function(IPython, $, CodeMirror) {
8 "use strict";
9 "use strict";
9
10
10 var modal = function (options) {
11 var modal = function (options) {
@@ -90,6 +91,17 b' define(['
90 return modal.modal(options);
91 return modal.modal(options);
91 };
92 };
92
93
94 var kernel_modal = function (options) {
95 /**
96 * only one kernel dialog should be open at a time -- but
97 * other modal dialogs can still be open
98 */
99 $('.kernel-modal').modal('hide');
100 var dialog = modal(options);
101 dialog.addClass('kernel-modal');
102 return dialog;
103 };
104
93 var edit_metadata = function (options) {
105 var edit_metadata = function (options) {
94 options.name = options.name || "Cell";
106 options.name = options.name || "Cell";
95 var error_div = $('<div/>').css('color', 'red');
107 var error_div = $('<div/>').css('color', 'red');
@@ -130,7 +142,9 b' define(['
130 buttons: {
142 buttons: {
131 OK: { class : "btn-primary",
143 OK: { class : "btn-primary",
132 click: function() {
144 click: function() {
133 // validate json and set it
145 /**
146 * validate json and set it
147 */
134 var new_md;
148 var new_md;
135 try {
149 try {
136 new_md = JSON.parse(editor.getValue());
150 new_md = JSON.parse(editor.getValue());
@@ -153,6 +167,7 b' define(['
153
167
154 var dialog = {
168 var dialog = {
155 modal : modal,
169 modal : modal,
170 kernel_modal : kernel_modal,
156 edit_metadata : edit_metadata,
171 edit_metadata : edit_metadata,
157 };
172 };
158
173
@@ -1,22 +1,33 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3 /**
4 *
5 *
6 * @module keyboard
7 * @namespace keyboard
8 * @class ShortcutManager
9 */
3
10
4 define([
11 define([
5 'base/js/namespace',
12 'base/js/namespace',
6 'jquery',
13 'jquery',
7 'base/js/utils',
14 'base/js/utils',
8 ], function(IPython, $, utils) {
15 'underscore',
16 ], function(IPython, $, utils, _) {
9 "use strict";
17 "use strict";
10
18
11
19
12 // Setup global keycodes and inverse keycodes.
20 /**
21 * Setup global keycodes and inverse keycodes.
22 *
23 * See http://unixpapa.com/js/key.html for a complete description. The short of
24 * it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
25 * and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
26 * but have minor differences.
27 **/
13
28
14 // See http://unixpapa.com/js/key.html for a complete description. The short of
29 // These apply to Firefox, (Webkit and IE)
15 // it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
30 // This does work **only** on US keyboard.
16 // and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
17 // but have minor differences.
18
19 // These apply to Firefox, (Webkit and IE)
20 var _keycodes = {
31 var _keycodes = {
21 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
32 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
22 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
33 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
@@ -77,13 +88,32 b' define(['
77 };
88 };
78
89
79 var normalize_shortcut = function (shortcut) {
90 var normalize_shortcut = function (shortcut) {
80 // Put a shortcut into normalized form:
91 /**
81 // 1. Make lowercase
92 * @function _normalize_shortcut
82 // 2. Replace cmd by meta
93 * @private
83 // 3. Sort '-' separated modifiers into the order alt-ctrl-meta-shift
94 * return a dict containing the normalized shortcut and the number of time it should be pressed:
84 // 4. Normalize keys
95 *
96 * Put a shortcut into normalized form:
97 * 1. Make lowercase
98 * 2. Replace cmd by meta
99 * 3. Sort '-' separated modifiers into the order alt-ctrl-meta-shift
100 * 4. Normalize keys
101 **/
102 if (platform === 'MacOS') {
103 shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'cmd-');
104 } else {
105 shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'ctrl-');
106 }
107
85 shortcut = shortcut.toLowerCase().replace('cmd', 'meta');
108 shortcut = shortcut.toLowerCase().replace('cmd', 'meta');
86 shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
109 shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
110 shortcut = shortcut.replace(/,$/, 'comma'); // catch shortcuts using '-' key
111 if(shortcut.indexOf(',') !== -1){
112 var sht = shortcut.split(',');
113 sht = _.map(sht, normalize_shortcut);
114 return shortcut;
115 }
116 shortcut = shortcut.replace(/comma/g, ','); // catch shortcuts using '-' key
87 var values = shortcut.split("-");
117 var values = shortcut.split("-");
88 if (values.length === 1) {
118 if (values.length === 1) {
89 return normalize_key(values[0]);
119 return normalize_key(values[0]);
@@ -96,7 +126,9 b' define(['
96 };
126 };
97
127
98 var shortcut_to_event = function (shortcut, type) {
128 var shortcut_to_event = function (shortcut, type) {
99 // Convert a shortcut (shift-r) to a jQuery Event object
129 /**
130 * Convert a shortcut (shift-r) to a jQuery Event object
131 **/
100 type = type || 'keydown';
132 type = type || 'keydown';
101 shortcut = normalize_shortcut(shortcut);
133 shortcut = normalize_shortcut(shortcut);
102 shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
134 shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
@@ -111,8 +143,21 b' define(['
111 return $.Event(type, opts);
143 return $.Event(type, opts);
112 };
144 };
113
145
146 var only_modifier_event = function(event){
147 /**
148 * Return `true` if the event only contains modifiers keys.
149 * false otherwise
150 **/
151 var key = inv_keycodes[event.which];
152 return ((event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) &&
153 (key === 'alt'|| key === 'ctrl'|| key === 'meta'|| key === 'shift'));
154
155 };
156
114 var event_to_shortcut = function (event) {
157 var event_to_shortcut = function (event) {
115 // Convert a jQuery Event object to a shortcut (shift-r)
158 /**
159 * Convert a jQuery Event object to a normalized shortcut string (shift-r)
160 **/
116 var shortcut = '';
161 var shortcut = '';
117 var key = inv_keycodes[event.which];
162 var key = inv_keycodes[event.which];
118 if (event.altKey && key !== 'alt') {shortcut += 'alt-';}
163 if (event.altKey && key !== 'alt') {shortcut += 'alt-';}
@@ -125,35 +170,86 b' define(['
125
170
126 // Shortcut manager class
171 // Shortcut manager class
127
172
128 var ShortcutManager = function (delay, events) {
173 var ShortcutManager = function (delay, events, actions, env) {
174 /**
175 * A class to deal with keyboard event and shortcut
176 *
177 * @class ShortcutManager
178 * @constructor
179 */
129 this._shortcuts = {};
180 this._shortcuts = {};
130 this._counts = {};
131 this._timers = {};
132 this.delay = delay || 800; // delay in milliseconds
181 this.delay = delay || 800; // delay in milliseconds
133 this.events = events;
182 this.events = events;
183 this.actions = actions;
184 this.actions.extend_env(env);
185 this._queue = [];
186 this._cleartimeout = null;
187 Object.seal(this);
188 };
189
190 ShortcutManager.prototype.clearsoon = function(){
191 /**
192 * Clear the pending shortcut soon, and cancel previous clearing
193 * that might be registered.
194 **/
195 var that = this;
196 clearTimeout(this._cleartimeout);
197 this._cleartimeout = setTimeout(function(){that.clearqueue();}, this.delay);
198 };
199
200
201 ShortcutManager.prototype.clearqueue = function(){
202 /**
203 * clear the pending shortcut sequence now.
204 **/
205 this._queue = [];
206 clearTimeout(this._cleartimeout);
207 };
208
209
210 var flatten_shorttree = function(tree){
211 /**
212 * Flatten a tree of shortcut sequences.
213 * use full to iterate over all the key/values of available shortcuts.
214 **/
215 var dct = {};
216 for(var key in tree){
217 var value = tree[key];
218 if(typeof(value) === 'string'){
219 dct[key] = value;
220 } else {
221 var ftree=flatten_shorttree(value);
222 for(var subkey in ftree){
223 dct[key+','+subkey] = ftree[subkey];
224 }
225 }
226 }
227 return dct;
134 };
228 };
135
229
136 ShortcutManager.prototype.help = function () {
230 ShortcutManager.prototype.help = function () {
137 var help = [];
231 var help = [];
138 for (var shortcut in this._shortcuts) {
232 var ftree = flatten_shorttree(this._shortcuts);
139 var help_string = this._shortcuts[shortcut].help;
233 for (var shortcut in ftree) {
140 var help_index = this._shortcuts[shortcut].help_index;
234 var action = this.actions.get(ftree[shortcut]);
235 var help_string = action.help||'== no help ==';
236 var help_index = action.help_index;
141 if (help_string) {
237 if (help_string) {
142 if (platform === 'MacOS') {
238 var shortstring = (action.shortstring||shortcut);
143 shortcut = shortcut.replace('meta', 'cmd');
144 }
145 help.push({
239 help.push({
146 shortcut: shortcut,
240 shortcut: shortstring,
147 help: help_string,
241 help: help_string,
148 help_index: help_index}
242 help_index: help_index}
149 );
243 );
150 }
244 }
151 }
245 }
152 help.sort(function (a, b) {
246 help.sort(function (a, b) {
153 if (a.help_index > b.help_index)
247 if (a.help_index > b.help_index){
154 return 1;
248 return 1;
155 if (a.help_index < b.help_index)
249 }
250 if (a.help_index < b.help_index){
156 return -1;
251 return -1;
252 }
157 return 0;
253 return 0;
158 });
254 });
159 return help;
255 return help;
@@ -163,19 +259,105 b' define(['
163 this._shortcuts = {};
259 this._shortcuts = {};
164 };
260 };
165
261
166 ShortcutManager.prototype.add_shortcut = function (shortcut, data, suppress_help_update) {
262 ShortcutManager.prototype.get_shortcut = function (shortcut){
167 if (typeof(data) === 'function') {
263 /**
168 data = {help: '', help_index: '', handler: data};
264 * return a node of the shortcut tree which an action name (string) if leaf,
265 * and an object with `object.subtree===true`
266 **/
267 if(typeof(shortcut) === 'string'){
268 shortcut = shortcut.split(',');
269 }
270
271 return this._get_leaf(shortcut, this._shortcuts);
272 };
273
274
275 ShortcutManager.prototype._get_leaf = function(shortcut_array, tree){
276 /**
277 * @private
278 * find a leaf/node in a subtree of the keyboard shortcut
279 *
280 **/
281 if(shortcut_array.length === 1){
282 return tree[shortcut_array[0]];
283 } else if( typeof(tree[shortcut_array[0]]) !== 'string'){
284 return this._get_leaf(shortcut_array.slice(1), tree[shortcut_array[0]]);
285 }
286 return null;
287 };
288
289 ShortcutManager.prototype.set_shortcut = function( shortcut, action_name){
290 if( typeof(action_name) !== 'string'){ throw('action is not a string', action_name);}
291 if( typeof(shortcut) === 'string'){
292 shortcut = shortcut.split(',');
293 }
294 return this._set_leaf(shortcut, action_name, this._shortcuts);
295 };
296
297 ShortcutManager.prototype._is_leaf = function(shortcut_array, tree){
298 if(shortcut_array.length === 1){
299 return(typeof(tree[shortcut_array[0]]) === 'string');
300 } else {
301 var subtree = tree[shortcut_array[0]];
302 return this._is_leaf(shortcut_array.slice(1), subtree );
169 }
303 }
170 data.help_index = data.help_index || '';
304 };
171 data.help = data.help || '';
305
172 data.count = data.count || 1;
306 ShortcutManager.prototype._remove_leaf = function(shortcut_array, tree, allow_node){
173 if (data.help_index === '') {
307 if(shortcut_array.length === 1){
174 data.help_index = 'zz';
308 var current_node = tree[shortcut_array[0]];
309 if(typeof(current_node) === 'string'){
310 delete tree[shortcut_array[0]];
311 } else {
312 throw('try to delete non-leaf');
313 }
314 } else {
315 this._remove_leaf(shortcut_array.slice(1), tree[shortcut_array[0]], allow_node);
316 if(_.keys(tree[shortcut_array[0]]).length === 0){
317 delete tree[shortcut_array[0]];
318 }
175 }
319 }
320 };
321
322 ShortcutManager.prototype._set_leaf = function(shortcut_array, action_name, tree){
323 var current_node = tree[shortcut_array[0]];
324 if(shortcut_array.length === 1){
325 if(current_node !== undefined && typeof(current_node) !== 'string'){
326 console.warn('[warning], you are overriting a long shortcut with a shorter one');
327 }
328 tree[shortcut_array[0]] = action_name;
329 return true;
330 } else {
331 if(typeof(current_node) === 'string'){
332 console.warn('you are trying to set a shortcut that will be shadowed'+
333 'by a more specific one. Aborting for :', action_name, 'the follwing '+
334 'will take precedence', current_node);
335 return false;
336 } else {
337 tree[shortcut_array[0]] = tree[shortcut_array[0]]||{};
338 }
339 this._set_leaf(shortcut_array.slice(1), action_name, tree[shortcut_array[0]]);
340 return true;
341 }
342 };
343
344 ShortcutManager.prototype.add_shortcut = function (shortcut, data, suppress_help_update) {
345 /**
346 * Add a action to be handled by shortcut manager.
347 *
348 * - `shortcut` should be a `Shortcut Sequence` of the for `Ctrl-Alt-C,Meta-X`...
349 * - `data` could be an `action name`, an `action` or a `function`.
350 * if a `function` is passed it will be converted to an anonymous `action`.
351 *
352 **/
353 var action_name = this.actions.get_name(data);
354 if (! action_name){
355 throw('does nto know how to deal with ', data);
356 }
357
176 shortcut = normalize_shortcut(shortcut);
358 shortcut = normalize_shortcut(shortcut);
177 this._counts[shortcut] = 0;
359 this.set_shortcut(shortcut, action_name);
178 this._shortcuts[shortcut] = data;
360
179 if (!suppress_help_update) {
361 if (!suppress_help_update) {
180 // update the keyboard shortcuts notebook help
362 // update the keyboard shortcuts notebook help
181 this.events.trigger('rebuild.QuickHelp');
363 this.events.trigger('rebuild.QuickHelp');
@@ -183,6 +365,11 b' define(['
183 };
365 };
184
366
185 ShortcutManager.prototype.add_shortcuts = function (data) {
367 ShortcutManager.prototype.add_shortcuts = function (data) {
368 /**
369 * Convenient methods to call `add_shortcut(key, value)` on several items
370 *
371 * data : Dict of the form {key:value, ...}
372 **/
186 for (var shortcut in data) {
373 for (var shortcut in data) {
187 this.add_shortcut(shortcut, data[shortcut], true);
374 this.add_shortcut(shortcut, data[shortcut], true);
188 }
375 }
@@ -191,55 +378,63 b' define(['
191 };
378 };
192
379
193 ShortcutManager.prototype.remove_shortcut = function (shortcut, suppress_help_update) {
380 ShortcutManager.prototype.remove_shortcut = function (shortcut, suppress_help_update) {
381 /**
382 * Remove the binding of shortcut `sortcut` with its action.
383 * throw an error if trying to remove a non-exiting shortcut
384 **/
194 shortcut = normalize_shortcut(shortcut);
385 shortcut = normalize_shortcut(shortcut);
195 delete this._counts[shortcut];
386 if( typeof(shortcut) === 'string'){
196 delete this._shortcuts[shortcut];
387 shortcut = shortcut.split(',');
388 }
389 this._remove_leaf(shortcut, this._shortcuts);
197 if (!suppress_help_update) {
390 if (!suppress_help_update) {
198 // update the keyboard shortcuts notebook help
391 // update the keyboard shortcuts notebook help
199 this.events.trigger('rebuild.QuickHelp');
392 this.events.trigger('rebuild.QuickHelp');
200 }
393 }
201 };
394 };
202
395
203 ShortcutManager.prototype.count_handler = function (shortcut, event, data) {
396
204 var that = this;
205 var c = this._counts;
206 var t = this._timers;
207 var timer = null;
208 if (c[shortcut] === data.count-1) {
209 c[shortcut] = 0;
210 timer = t[shortcut];
211 if (timer) {clearTimeout(timer); delete t[shortcut];}
212 return data.handler(event);
213 } else {
214 c[shortcut] = c[shortcut] + 1;
215 timer = setTimeout(function () {
216 c[shortcut] = 0;
217 }, that.delay);
218 t[shortcut] = timer;
219 }
220 return false;
221 };
222
397
223 ShortcutManager.prototype.call_handler = function (event) {
398 ShortcutManager.prototype.call_handler = function (event) {
399 /**
400 * Call the corresponding shortcut handler for a keyboard event
401 * @method call_handler
402 * @return {Boolean} `true|false`, `false` if no handler was found, otherwise the value return by the handler.
403 * @param event {event}
404 *
405 * given an event, call the corresponding shortcut.
406 * return false is event wan handled, true otherwise
407 * in any case returning false stop event propagation
408 **/
409
410
411 this.clearsoon();
412 if(only_modifier_event(event)){
413 return true;
414 }
224 var shortcut = event_to_shortcut(event);
415 var shortcut = event_to_shortcut(event);
225 var data = this._shortcuts[shortcut];
416 this._queue.push(shortcut);
226 if (data) {
417 var action_name = this.get_shortcut(this._queue);
227 var handler = data.handler;
418
228 if (handler) {
419 if (typeof(action_name) === 'undefined'|| action_name === null){
229 if (data.count === 1) {
420 this.clearqueue();
230 return handler(event);
421 return true;
231 } else if (data.count > 1) {
232 return this.count_handler(shortcut, event, data);
233 }
234 }
235 }
422 }
236 return true;
423
424 if (this.actions.exists(action_name)) {
425 event.preventDefault();
426 this.clearqueue();
427 return this.actions.call(action_name, event);
428 }
429
430 return false;
237 };
431 };
238
432
433
239 ShortcutManager.prototype.handles = function (event) {
434 ShortcutManager.prototype.handles = function (event) {
240 var shortcut = event_to_shortcut(event);
435 var shortcut = event_to_shortcut(event);
241 var data = this._shortcuts[shortcut];
436 var action_name = this.get_shortcut(this._queue.concat(shortcut));
242 return !( data === undefined || data.handler === undefined );
437 return (typeof(action_name) !== 'undefined');
243 };
438 };
244
439
245 var keyboard = {
440 var keyboard = {
@@ -249,10 +444,10 b' define(['
249 normalize_key : normalize_key,
444 normalize_key : normalize_key,
250 normalize_shortcut : normalize_shortcut,
445 normalize_shortcut : normalize_shortcut,
251 shortcut_to_event : shortcut_to_event,
446 shortcut_to_event : shortcut_to_event,
252 event_to_shortcut : event_to_shortcut
447 event_to_shortcut : event_to_shortcut,
253 };
448 };
254
449
255 // For backwards compatability.
450 // For backwards compatibility.
256 IPython.keyboard = keyboard;
451 IPython.keyboard = keyboard;
257
452
258 return keyboard;
453 return keyboard;
@@ -3,6 +3,7 b''
3
3
4 var IPython = IPython || {};
4 var IPython = IPython || {};
5 define([], function(){
5 define([], function(){
6 "use strict";
6 IPython.version = "3.0.0-dev";
7 IPython.version = "3.0.0-dev";
7 return IPython;
8 return IPython;
8 });
9 });
@@ -7,6 +7,13 b' define(['
7 ], function(IPython, $) {
7 ], function(IPython, $) {
8 "use strict";
8 "use strict";
9
9
10 /**
11 * Construct a NotificationWidget object.
12 *
13 * @constructor
14 * @param {string} selector - a jQuery selector string for the
15 * notification widget element
16 */
10 var NotificationWidget = function (selector) {
17 var NotificationWidget = function (selector) {
11 this.selector = selector;
18 this.selector = selector;
12 this.timeout = null;
19 this.timeout = null;
@@ -16,27 +23,41 b' define(['
16 this.style();
23 this.style();
17 }
24 }
18 this.element.hide();
25 this.element.hide();
19 var that = this;
20
21 this.inner = $('<span/>');
26 this.inner = $('<span/>');
22 this.element.append(this.inner);
27 this.element.append(this.inner);
23
24 };
28 };
25
29
30 /**
31 * Add the 'notification_widget' CSS class to the widget element.
32 *
33 * @method style
34 */
26 NotificationWidget.prototype.style = function () {
35 NotificationWidget.prototype.style = function () {
27 this.element.addClass('notification_widget');
36 this.element.addClass('notification_widget');
28 };
37 };
29
38
30 // msg : message to display
39 /**
31 // timeout : time in ms before diseapearing
40 * Set the notification widget message to display for a certain
32 //
41 * amount of time (timeout). The widget will be shown forever if
33 // if timeout <= 0
42 * timeout is <= 0 or undefined. If the widget is clicked while it
34 // click_callback : function called if user click on notification
43 * is still displayed, execute an optional callback
35 // could return false to prevent the notification to be dismissed
44 * (click_callback). If the callback returns false, it will
45 * prevent the notification from being dismissed.
46 *
47 * Options:
48 * class - CSS class name for styling
49 * icon - CSS class name for the widget icon
50 * title - HTML title attribute for the widget
51 *
52 * @method set_message
53 * @param {string} msg - The notification to display
54 * @param {integer} [timeout] - The amount of time in milliseconds to display the widget
55 * @param {function} [click_callback] - The function to run when the widget is clicked
56 * @param {Object} [options] - Additional options
57 */
36 NotificationWidget.prototype.set_message = function (msg, timeout, click_callback, options) {
58 NotificationWidget.prototype.set_message = function (msg, timeout, click_callback, options) {
37 var options = options || {};
59 options = options || {};
38 var callback = click_callback || function() {return true;};
60
39 var that = this;
40 // unbind potential previous callback
61 // unbind potential previous callback
41 this.element.unbind('click');
62 this.element.unbind('click');
42 this.inner.attr('class', options.icon);
63 this.inner.attr('class', options.icon);
@@ -47,52 +68,87 b' define(['
47 // reset previous set style
68 // reset previous set style
48 this.element.removeClass();
69 this.element.removeClass();
49 this.style();
70 this.style();
50 if (options.class){
71 if (options.class) {
51
72 this.element.addClass(options.class);
52 this.element.addClass(options.class)
53 }
73 }
74
75 // clear previous timer
54 if (this.timeout !== null) {
76 if (this.timeout !== null) {
55 clearTimeout(this.timeout);
77 clearTimeout(this.timeout);
56 this.timeout = null;
78 this.timeout = null;
57 }
79 }
58 if (timeout !== undefined && timeout >=0) {
80
81 // set the timer if a timeout is given
82 var that = this;
83 if (timeout !== undefined && timeout >= 0) {
59 this.timeout = setTimeout(function () {
84 this.timeout = setTimeout(function () {
60 that.element.fadeOut(100, function () {that.inner.text('');});
85 that.element.fadeOut(100, function () {that.inner.text('');});
86 that.element.unbind('click');
61 that.timeout = null;
87 that.timeout = null;
62 }, timeout);
88 }, timeout);
63 } else {
89 }
64 this.element.click(function() {
90
65 if( callback() !== false ) {
91 // bind the click callback if it is given
92 if (click_callback !== undefined) {
93 this.element.click(function () {
94 if (click_callback() !== false) {
66 that.element.fadeOut(100, function () {that.inner.text('');});
95 that.element.fadeOut(100, function () {that.inner.text('');});
67 that.element.unbind('click');
68 }
96 }
69 if (that.timeout !== undefined) {
97 that.element.unbind('click');
70 that.timeout = undefined;
98 if (that.timeout !== null) {
71 clearTimeout(that.timeout);
99 clearTimeout(that.timeout);
100 that.timeout = null;
72 }
101 }
73 });
102 });
74 }
103 }
75 };
104 };
76
105
77
106 /**
107 * Display an information message (styled with the 'info'
108 * class). Arguments are the same as in set_message. Default
109 * timeout is 3500 milliseconds.
110 *
111 * @method info
112 */
78 NotificationWidget.prototype.info = function (msg, timeout, click_callback, options) {
113 NotificationWidget.prototype.info = function (msg, timeout, click_callback, options) {
79 var options = options || {};
114 options = options || {};
80 options.class = options.class +' info';
115 options.class = options.class + ' info';
81 var timeout = timeout || 3500;
116 timeout = timeout || 3500;
82 this.set_message(msg, timeout, click_callback, options);
117 this.set_message(msg, timeout, click_callback, options);
83 }
118 };
119
120 /**
121 * Display a warning message (styled with the 'warning'
122 * class). Arguments are the same as in set_message. Messages are
123 * sticky by default.
124 *
125 * @method warning
126 */
84 NotificationWidget.prototype.warning = function (msg, timeout, click_callback, options) {
127 NotificationWidget.prototype.warning = function (msg, timeout, click_callback, options) {
85 var options = options || {};
128 options = options || {};
86 options.class = options.class +' warning';
129 options.class = options.class + ' warning';
87 this.set_message(msg, timeout, click_callback, options);
130 this.set_message(msg, timeout, click_callback, options);
88 }
131 };
132
133 /**
134 * Display a danger message (styled with the 'danger'
135 * class). Arguments are the same as in set_message. Messages are
136 * sticky by default.
137 *
138 * @method danger
139 */
89 NotificationWidget.prototype.danger = function (msg, timeout, click_callback, options) {
140 NotificationWidget.prototype.danger = function (msg, timeout, click_callback, options) {
90 var options = options || {};
141 options = options || {};
91 options.class = options.class +' danger';
142 options.class = options.class + ' danger';
92 this.set_message(msg, timeout, click_callback, options);
143 this.set_message(msg, timeout, click_callback, options);
93 }
144 };
94
95
145
146 /**
147 * Get the text of the widget message.
148 *
149 * @method get_message
150 * @return {string} - the message text
151 */
96 NotificationWidget.prototype.get_message = function () {
152 NotificationWidget.prototype.get_message = function () {
97 return this.inner.html();
153 return this.inner.html();
98 };
154 };
@@ -15,23 +15,29 b' define(['
15 };
15 };
16
16
17 Page.prototype.show = function () {
17 Page.prototype.show = function () {
18 // The header and site divs start out hidden to prevent FLOUC.
18 /**
19 // Main scripts should call this method after styling everything.
19 * The header and site divs start out hidden to prevent FLOUC.
20 * Main scripts should call this method after styling everything.
21 */
20 this.show_header();
22 this.show_header();
21 this.show_site();
23 this.show_site();
22 };
24 };
23
25
24 Page.prototype.show_header = function () {
26 Page.prototype.show_header = function () {
25 // The header and site divs start out hidden to prevent FLOUC.
27 /**
26 // Main scripts should call this method after styling everything.
28 * The header and site divs start out hidden to prevent FLOUC.
27 // TODO: selector are hardcoded, pass as constructor argument
29 * Main scripts should call this method after styling everything.
30 * TODO: selector are hardcoded, pass as constructor argument
31 */
28 $('div#header').css('display','block');
32 $('div#header').css('display','block');
29 };
33 };
30
34
31 Page.prototype.show_site = function () {
35 Page.prototype.show_site = function () {
32 // The header and site divs start out hidden to prevent FLOUC.
36 /**
33 // Main scripts should call this method after styling everything.
37 * The header and site divs start out hidden to prevent FLOUC.
34 // TODO: selector are hardcoded, pass as constructor argument
38 * Main scripts should call this method after styling everything.
39 * TODO: selector are hardcoded, pass as constructor argument
40 */
35 $('div#site').css('display','block');
41 $('div#site').css('display','block');
36 };
42 };
37
43
@@ -18,8 +18,10 b' define(['
18 }
18 }
19
19
20 var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
20 var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
21 // add trusting data-attributes to the default sanitizeAttribs from caja
21 /**
22 // this function is mostly copied from the caja source
22 * add trusting data-attributes to the default sanitizeAttribs from caja
23 * this function is mostly copied from the caja source
24 */
23 var ATTRIBS = caja.html4.ATTRIBS;
25 var ATTRIBS = caja.html4.ATTRIBS;
24 for (var i = 0; i < attribs.length; i += 2) {
26 for (var i = 0; i < attribs.length; i += 2) {
25 var attribName = attribs[i];
27 var attribName = attribs[i];
@@ -34,9 +36,11 b' define(['
34 };
36 };
35
37
36 var sanitize_css = function (css, tagPolicy) {
38 var sanitize_css = function (css, tagPolicy) {
37 // sanitize CSS
39 /**
38 // like sanitize_html, but for CSS
40 * sanitize CSS
39 // called by sanitize_stylesheets
41 * like sanitize_html, but for CSS
42 * called by sanitize_stylesheets
43 */
40 return caja.sanitizeStylesheet(
44 return caja.sanitizeStylesheet(
41 window.location.pathname,
45 window.location.pathname,
42 css,
46 css,
@@ -51,8 +55,10 b' define(['
51 };
55 };
52
56
53 var sanitize_stylesheets = function (html, tagPolicy) {
57 var sanitize_stylesheets = function (html, tagPolicy) {
54 // sanitize just the css in style tags in a block of html
58 /**
55 // called by sanitize_html, if allow_css is true
59 * sanitize just the css in style tags in a block of html
60 * called by sanitize_html, if allow_css is true
61 */
56 var h = $("<div/>").append(html);
62 var h = $("<div/>").append(html);
57 var style_tags = h.find("style");
63 var style_tags = h.find("style");
58 if (!style_tags.length) {
64 if (!style_tags.length) {
@@ -66,9 +72,11 b' define(['
66 };
72 };
67
73
68 var sanitize_html = function (html, allow_css) {
74 var sanitize_html = function (html, allow_css) {
69 // sanitize HTML
75 /**
70 // if allow_css is true (default: false), CSS is sanitized as well.
76 * sanitize HTML
71 // otherwise, CSS elements and attributes are simply removed.
77 * if allow_css is true (default: false), CSS is sanitized as well.
78 * otherwise, CSS elements and attributes are simply removed.
79 */
72 var html4 = caja.html4;
80 var html4 = caja.html4;
73
81
74 if (allow_css) {
82 if (allow_css) {
@@ -4,7 +4,8 b''
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 ], function(IPython, $){
7 'codemirror/lib/codemirror',
8 ], function(IPython, $, CodeMirror){
8 "use strict";
9 "use strict";
9
10
10 IPython.load_extensions = function () {
11 IPython.load_extensions = function () {
@@ -153,7 +154,9 b' define(['
153
154
154
155
155 var uuid = function () {
156 var uuid = function () {
156 // http://www.ietf.org/rfc/rfc4122.txt
157 /**
158 * http://www.ietf.org/rfc/rfc4122.txt
159 */
157 var s = [];
160 var s = [];
158 var hexDigits = "0123456789ABCDEF";
161 var hexDigits = "0123456789ABCDEF";
159 for (var i = 0; i < 32; i++) {
162 for (var i = 0; i < 32; i++) {
@@ -271,11 +274,11 b' define(['
271 } else {
274 } else {
272 line = "background-color: ";
275 line = "background-color: ";
273 }
276 }
274 line = line + "rgb(" + r + "," + g + "," + b + ");"
277 line = line + "rgb(" + r + "," + g + "," + b + ");";
275 if ( !attrs["style"] ) {
278 if ( !attrs.style ) {
276 attrs["style"] = line;
279 attrs.style = line;
277 } else {
280 } else {
278 attrs["style"] += " " + line;
281 attrs.style += " " + line;
279 }
282 }
280 }
283 }
281 }
284 }
@@ -284,27 +287,36 b' define(['
284 function ansispan(str) {
287 function ansispan(str) {
285 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
288 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
286 // regular ansi escapes (using the table above)
289 // regular ansi escapes (using the table above)
290 var is_open = false;
287 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
291 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
288 if (!pattern) {
292 if (!pattern) {
289 // [(01|22|39|)m close spans
293 // [(01|22|39|)m close spans
290 return "</span>";
294 if (is_open) {
291 }
295 is_open = false;
292 // consume sequence of color escapes
296 return "</span>";
293 var numbers = pattern.match(/\d+/g);
297 } else {
294 var attrs = {};
298 return "";
295 while (numbers.length > 0) {
299 }
296 _process_numbers(attrs, numbers);
300 } else {
297 }
301 is_open = true;
298
302
299 var span = "<span ";
303 // consume sequence of color escapes
300 for (var attr in attrs) {
304 var numbers = pattern.match(/\d+/g);
301 var value = attrs[attr];
305 var attrs = {};
302 span = span + " " + attr + '="' + attrs[attr] + '"';
306 while (numbers.length > 0) {
307 _process_numbers(attrs, numbers);
308 }
309
310 var span = "<span ";
311 for (var attr in attrs) {
312 var value = attrs[attr];
313 span = span + " " + attr + '="' + attrs[attr] + '"';
314 }
315 return span + ">";
303 }
316 }
304 return span + ">";
305 });
317 });
306 };
318 }
307
319
308 // Transform ANSI color escape codes into HTML <span> tags with css
320 // Transform ANSI color escape codes into HTML <span> tags with css
309 // classes listed in the above ansi_colormap object. The actual color used
321 // classes listed in the above ansi_colormap object. The actual color used
310 // are set in the css file.
322 // are set in the css file.
@@ -345,7 +357,9 b' define(['
345 }
357 }
346
358
347 var points_to_pixels = function (points) {
359 var points_to_pixels = function (points) {
348 // A reasonably good way of converting between points and pixels.
360 /**
361 * A reasonably good way of converting between points and pixels.
362 */
349 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
363 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
350 $(body).append(test);
364 $(body).append(test);
351 var pixel_per_point = test.width()/10000;
365 var pixel_per_point = test.width()/10000;
@@ -354,10 +368,12 b' define(['
354 };
368 };
355
369
356 var always_new = function (constructor) {
370 var always_new = function (constructor) {
357 // wrapper around contructor to avoid requiring `var a = new constructor()`
371 /**
358 // useful for passing constructors as callbacks,
372 * wrapper around contructor to avoid requiring `var a = new constructor()`
359 // not for programmer laziness.
373 * useful for passing constructors as callbacks,
360 // from http://programmers.stackexchange.com/questions/118798
374 * not for programmer laziness.
375 * from http://programmers.stackexchange.com/questions/118798
376 */
361 return function () {
377 return function () {
362 var obj = Object.create(constructor.prototype);
378 var obj = Object.create(constructor.prototype);
363 constructor.apply(obj, arguments);
379 constructor.apply(obj, arguments);
@@ -366,7 +382,9 b' define(['
366 };
382 };
367
383
368 var url_path_join = function () {
384 var url_path_join = function () {
369 // join a sequence of url components with '/'
385 /**
386 * join a sequence of url components with '/'
387 */
370 var url = '';
388 var url = '';
371 for (var i = 0; i < arguments.length; i++) {
389 for (var i = 0; i < arguments.length; i++) {
372 if (arguments[i] === '') {
390 if (arguments[i] === '') {
@@ -382,36 +400,58 b' define(['
382 return url;
400 return url;
383 };
401 };
384
402
403 var url_path_split = function (path) {
404 /**
405 * Like os.path.split for URLs.
406 * Always returns two strings, the directory path and the base filename
407 */
408
409 var idx = path.lastIndexOf('/');
410 if (idx === -1) {
411 return ['', path];
412 } else {
413 return [ path.slice(0, idx), path.slice(idx + 1) ];
414 }
415 };
416
385 var parse_url = function (url) {
417 var parse_url = function (url) {
386 // an `a` element with an href allows attr-access to the parsed segments of a URL
418 /**
387 // a = parse_url("http://localhost:8888/path/name#hash")
419 * an `a` element with an href allows attr-access to the parsed segments of a URL
388 // a.protocol = "http:"
420 * a = parse_url("http://localhost:8888/path/name#hash")
389 // a.host = "localhost:8888"
421 * a.protocol = "http:"
390 // a.hostname = "localhost"
422 * a.host = "localhost:8888"
391 // a.port = 8888
423 * a.hostname = "localhost"
392 // a.pathname = "/path/name"
424 * a.port = 8888
393 // a.hash = "#hash"
425 * a.pathname = "/path/name"
426 * a.hash = "#hash"
427 */
394 var a = document.createElement("a");
428 var a = document.createElement("a");
395 a.href = url;
429 a.href = url;
396 return a;
430 return a;
397 };
431 };
398
432
399 var encode_uri_components = function (uri) {
433 var encode_uri_components = function (uri) {
400 // encode just the components of a multi-segment uri,
434 /**
401 // leaving '/' separators
435 * encode just the components of a multi-segment uri,
436 * leaving '/' separators
437 */
402 return uri.split('/').map(encodeURIComponent).join('/');
438 return uri.split('/').map(encodeURIComponent).join('/');
403 };
439 };
404
440
405 var url_join_encode = function () {
441 var url_join_encode = function () {
406 // join a sequence of url components with '/',
442 /**
407 // encoding each component with encodeURIComponent
443 * join a sequence of url components with '/',
444 * encoding each component with encodeURIComponent
445 */
408 return encode_uri_components(url_path_join.apply(null, arguments));
446 return encode_uri_components(url_path_join.apply(null, arguments));
409 };
447 };
410
448
411
449
412 var splitext = function (filename) {
450 var splitext = function (filename) {
413 // mimic Python os.path.splitext
451 /**
414 // Returns ['base', '.ext']
452 * mimic Python os.path.splitext
453 * Returns ['base', '.ext']
454 */
415 var idx = filename.lastIndexOf('.');
455 var idx = filename.lastIndexOf('.');
416 if (idx > 0) {
456 if (idx > 0) {
417 return [filename.slice(0, idx), filename.slice(idx)];
457 return [filename.slice(0, idx), filename.slice(idx)];
@@ -422,20 +462,26 b' define(['
422
462
423
463
424 var escape_html = function (text) {
464 var escape_html = function (text) {
425 // escape text to HTML
465 /**
466 * escape text to HTML
467 */
426 return $("<div/>").text(text).html();
468 return $("<div/>").text(text).html();
427 };
469 };
428
470
429
471
430 var get_body_data = function(key) {
472 var get_body_data = function(key) {
431 // get a url-encoded item from body.data and decode it
473 /**
432 // we should never have any encoded URLs anywhere else in code
474 * get a url-encoded item from body.data and decode it
433 // until we are building an actual request
475 * we should never have any encoded URLs anywhere else in code
476 * until we are building an actual request
477 */
434 return decodeURIComponent($('body').data(key));
478 return decodeURIComponent($('body').data(key));
435 };
479 };
436
480
437 var to_absolute_cursor_pos = function (cm, cursor) {
481 var to_absolute_cursor_pos = function (cm, cursor) {
438 // get the absolute cursor position from CodeMirror's col, ch
482 /**
483 * get the absolute cursor position from CodeMirror's col, ch
484 */
439 if (!cursor) {
485 if (!cursor) {
440 cursor = cm.getCursor();
486 cursor = cm.getCursor();
441 }
487 }
@@ -447,7 +493,9 b' define(['
447 };
493 };
448
494
449 var from_absolute_cursor_pos = function (cm, cursor_pos) {
495 var from_absolute_cursor_pos = function (cm, cursor_pos) {
450 // turn absolute cursor postion into CodeMirror col, ch cursor
496 /**
497 * turn absolute cursor postion into CodeMirror col, ch cursor
498 */
451 var i, line;
499 var i, line;
452 var offset = 0;
500 var offset = 0;
453 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
501 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
@@ -495,12 +543,16 b' define(['
495 })();
543 })();
496
544
497 var is_or_has = function (a, b) {
545 var is_or_has = function (a, b) {
498 // Is b a child of a or a itself?
546 /**
547 * Is b a child of a or a itself?
548 */
499 return a.has(b).length !==0 || a.is(b);
549 return a.has(b).length !==0 || a.is(b);
500 };
550 };
501
551
502 var is_focused = function (e) {
552 var is_focused = function (e) {
503 // Is element e, or one of its children focused?
553 /**
554 * Is element e, or one of its children focused?
555 */
504 e = $(e);
556 e = $(e);
505 var target = $(document.activeElement);
557 var target = $(document.activeElement);
506 if (target.length > 0) {
558 if (target.length > 0) {
@@ -521,21 +573,198 b' define(['
521 };
573 };
522
574
523 var ajax_error_msg = function (jqXHR) {
575 var ajax_error_msg = function (jqXHR) {
524 // Return a JSON error message if there is one,
576 /**
525 // otherwise the basic HTTP status text.
577 * Return a JSON error message if there is one,
526 if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
578 * otherwise the basic HTTP status text.
579 */
580 if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
581 return jqXHR.responseJSON.traceback;
582 } else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
527 return jqXHR.responseJSON.message;
583 return jqXHR.responseJSON.message;
528 } else {
584 } else {
529 return jqXHR.statusText;
585 return jqXHR.statusText;
530 }
586 }
531 }
587 };
532 var log_ajax_error = function (jqXHR, status, error) {
588 var log_ajax_error = function (jqXHR, status, error) {
533 // log ajax failures with informative messages
589 /**
590 * log ajax failures with informative messages
591 */
534 var msg = "API request failed (" + jqXHR.status + "): ";
592 var msg = "API request failed (" + jqXHR.status + "): ";
535 console.log(jqXHR);
593 console.log(jqXHR);
536 msg += ajax_error_msg(jqXHR);
594 msg += ajax_error_msg(jqXHR);
537 console.log(msg);
595 console.log(msg);
538 };
596 };
597
598 var requireCodeMirrorMode = function (mode, callback, errback) {
599 /**
600 * load a mode with requirejs
601 */
602 if (typeof mode != "string") mode = mode.name;
603 if (CodeMirror.modes.hasOwnProperty(mode)) {
604 callback(CodeMirror.modes.mode);
605 return;
606 }
607 require([
608 // might want to use CodeMirror.modeURL here
609 ['codemirror/mode', mode, mode].join('/'),
610 ], callback, errback
611 );
612 };
613
614 /** Error type for wrapped XHR errors. */
615 var XHR_ERROR = 'XhrError';
616
617 /**
618 * Wraps an AJAX error as an Error object.
619 */
620 var wrap_ajax_error = function (jqXHR, status, error) {
621 var wrapped_error = new Error(ajax_error_msg(jqXHR));
622 wrapped_error.name = XHR_ERROR;
623 // provide xhr response
624 wrapped_error.xhr = jqXHR;
625 wrapped_error.xhr_status = status;
626 wrapped_error.xhr_error = error;
627 return wrapped_error;
628 };
629
630 var promising_ajax = function(url, settings) {
631 /**
632 * Like $.ajax, but returning an ES6 promise. success and error settings
633 * will be ignored.
634 */
635 return new Promise(function(resolve, reject) {
636 settings.success = function(data, status, jqXHR) {
637 resolve(data);
638 };
639 settings.error = function(jqXHR, status, error) {
640 log_ajax_error(jqXHR, status, error);
641 reject(wrap_ajax_error(jqXHR, status, error));
642 };
643 $.ajax(url, settings);
644 });
645 };
646
647 var WrappedError = function(message, error){
648 /**
649 * Wrappable Error class
650 *
651 * The Error class doesn't actually act on `this`. Instead it always
652 * returns a new instance of Error. Here we capture that instance so we
653 * can apply it's properties to `this`.
654 */
655 var tmp = Error.apply(this, [message]);
656
657 // Copy the properties of the error over to this.
658 var properties = Object.getOwnPropertyNames(tmp);
659 for (var i = 0; i < properties.length; i++) {
660 this[properties[i]] = tmp[properties[i]];
661 }
662
663 // Keep a stack of the original error messages.
664 if (error instanceof WrappedError) {
665 this.error_stack = error.error_stack;
666 } else {
667 this.error_stack = [error];
668 }
669 this.error_stack.push(tmp);
670
671 return this;
672 };
673
674 WrappedError.prototype = Object.create(Error.prototype, {});
675
676
677 var load_class = function(class_name, module_name, registry) {
678 /**
679 * Tries to load a class
680 *
681 * Tries to load a class from a module using require.js, if a module
682 * is specified, otherwise tries to load a class from the global
683 * registry, if the global registry is provided.
684 */
685 return new Promise(function(resolve, reject) {
686
687 // Try loading the view module using require.js
688 if (module_name) {
689 require([module_name], function(module) {
690 if (module[class_name] === undefined) {
691 reject(new Error('Class '+class_name+' not found in module '+module_name));
692 } else {
693 resolve(module[class_name]);
694 }
695 }, reject);
696 } else {
697 if (registry && registry[class_name]) {
698 resolve(registry[class_name]);
699 } else {
700 reject(new Error('Class '+class_name+' not found in registry '));
701 }
702 }
703 });
704 };
705
706 var resolve_promises_dict = function(d) {
707 /**
708 * Resolve a promiseful dictionary.
709 * Returns a single Promise.
710 */
711 var keys = Object.keys(d);
712 var values = [];
713 keys.forEach(function(key) {
714 values.push(d[key]);
715 });
716 return Promise.all(values).then(function(v) {
717 d = {};
718 for(var i=0; i<keys.length; i++) {
719 d[keys[i]] = v[i];
720 }
721 return d;
722 });
723 };
724
725 var WrappedError = function(message, error){
726 /**
727 * Wrappable Error class
728 *
729 * The Error class doesn't actually act on `this`. Instead it always
730 * returns a new instance of Error. Here we capture that instance so we
731 * can apply it's properties to `this`.
732 */
733 var tmp = Error.apply(this, [message]);
734
735 // Copy the properties of the error over to this.
736 var properties = Object.getOwnPropertyNames(tmp);
737 for (var i = 0; i < properties.length; i++) {
738 this[properties[i]] = tmp[properties[i]];
739 }
740
741 // Keep a stack of the original error messages.
742 if (error instanceof WrappedError) {
743 this.error_stack = error.error_stack;
744 } else {
745 this.error_stack = [error];
746 }
747 this.error_stack.push(tmp);
748
749 return this;
750 };
751
752 WrappedError.prototype = Object.create(Error.prototype, {});
753
754 var reject = function(message, log) {
755 /**
756 * Creates a wrappable Promise rejection function.
757 *
758 * Creates a function that returns a Promise.reject with a new WrappedError
759 * that has the provided message and wraps the original error that
760 * caused the promise to reject.
761 */
762 return function(error) {
763 var wrapped_error = new WrappedError(message, error);
764 if (log) console.error(wrapped_error);
765 return Promise.reject(wrapped_error);
766 };
767 };
539
768
540 var utils = {
769 var utils = {
541 regex_split : regex_split,
770 regex_split : regex_split,
@@ -546,6 +775,7 b' define(['
546 points_to_pixels : points_to_pixels,
775 points_to_pixels : points_to_pixels,
547 get_body_data : get_body_data,
776 get_body_data : get_body_data,
548 parse_url : parse_url,
777 parse_url : parse_url,
778 url_path_split : url_path_split,
549 url_path_join : url_path_join,
779 url_path_join : url_path_join,
550 url_join_encode : url_join_encode,
780 url_join_encode : url_join_encode,
551 encode_uri_components : encode_uri_components,
781 encode_uri_components : encode_uri_components,
@@ -561,6 +791,14 b' define(['
561 mergeopt: mergeopt,
791 mergeopt: mergeopt,
562 ajax_error_msg : ajax_error_msg,
792 ajax_error_msg : ajax_error_msg,
563 log_ajax_error : log_ajax_error,
793 log_ajax_error : log_ajax_error,
794 requireCodeMirrorMode : requireCodeMirrorMode,
795 XHR_ERROR : XHR_ERROR,
796 wrap_ajax_error : wrap_ajax_error,
797 promising_ajax : promising_ajax,
798 WrappedError: WrappedError,
799 load_class: load_class,
800 resolve_promises_dict: resolve_promises_dict,
801 reject: reject,
564 };
802 };
565
803
566 // Backwards compatability.
804 // Backwards compatability.
@@ -8,6 +8,7 b''
8 @breadcrumb-color: darken(@border_color, 30%);
8 @breadcrumb-color: darken(@border_color, 30%);
9 @blockquote-font-size: inherit;
9 @blockquote-font-size: inherit;
10 @modal-inner-padding: 15px;
10 @modal-inner-padding: 15px;
11 @grid-float-breakpoint: 540px;
11
12
12 // Disable modal slide-in from top animation.
13 // Disable modal slide-in from top animation.
13 .modal {
14 .modal {
@@ -1,1 +1,1 b''
1 Subproject commit b3909af1b61ca7a412481759fdb441ecdfb3ab66
1 Subproject commit 87ff70d96567bf055eb94161a41e7b3e6da31b23
@@ -1,44 +1,39 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 /**
5 *
6 *
7 * @module cell
8 * @namespace cell
9 * @class Cell
10 */
11
12
4 define([
13 define([
5 'base/js/namespace',
14 'base/js/namespace',
6 'jquery',
15 'jquery',
7 'base/js/utils',
16 'base/js/utils',
8 ], function(IPython, $, utils) {
17 'codemirror/lib/codemirror',
18 'codemirror/addon/edit/matchbrackets',
19 'codemirror/addon/edit/closebrackets',
20 'codemirror/addon/comment/comment'
21 ], function(IPython, $, utils, CodeMirror, cm_match, cm_closeb, cm_comment) {
9 // TODO: remove IPython dependency here
22 // TODO: remove IPython dependency here
10 "use strict";
23 "use strict";
11
24
12 // monkey patch CM to be able to syntax highlight cell magics
13 // bug reported upstream,
14 // see https://github.com/codemirror/CodeMirror/issues/670
15 if(CodeMirror.getMode(1,'text/plain').indent === undefined ){
16 CodeMirror.modes.null = function() {
17 return {token: function(stream) {stream.skipToEnd();},indent : function(){return 0;}};
18 };
19 }
20
21 CodeMirror.patchedGetMode = function(config, mode){
22 var cmmode = CodeMirror.getMode(config, mode);
23 if(cmmode.indent === null) {
24 console.log('patch mode "' , mode, '" on the fly');
25 cmmode.indent = function(){return 0;};
26 }
27 return cmmode;
28 };
29 // end monkey patching CodeMirror
30
31 var Cell = function (options) {
25 var Cell = function (options) {
32 // Constructor
26 /* Constructor
33 //
27 *
34 // The Base `Cell` class from which to inherit.
28 * The Base `Cell` class from which to inherit.
35 //
29 * @constructor
36 // Parameters:
30 * @param:
37 // options: dictionary
31 * options: dictionary
38 // Dictionary of keyword arguments.
32 * Dictionary of keyword arguments.
39 // events: $(Events) instance
33 * events: $(Events) instance
40 // config: dictionary
34 * config: dictionary
41 // keyboard_manager: KeyboardManager instance
35 * keyboard_manager: KeyboardManager instance
36 */
42 options = options || {};
37 options = options || {};
43 this.keyboard_manager = options.keyboard_manager;
38 this.keyboard_manager = options.keyboard_manager;
44 this.events = options.events;
39 this.events = options.events;
@@ -50,7 +45,20 b' define(['
50 this.selected = false;
45 this.selected = false;
51 this.rendered = false;
46 this.rendered = false;
52 this.mode = 'command';
47 this.mode = 'command';
53 this.metadata = {};
48
49 // Metadata property
50 var that = this;
51 this._metadata = {};
52 Object.defineProperty(this, 'metadata', {
53 get: function() { return that._metadata; },
54 set: function(value) {
55 that._metadata = value;
56 if (that.celltoolbar) {
57 that.celltoolbar.rebuild();
58 }
59 }
60 });
61
54 // load this from metadata later ?
62 // load this from metadata later ?
55 this.user_highlight = 'auto';
63 this.user_highlight = 'auto';
56 this.cm_config = config.cm_config;
64 this.cm_config = config.cm_config;
@@ -104,8 +112,10 b' define(['
104 };
112 };
105
113
106 Cell.prototype.init_classes = function () {
114 Cell.prototype.init_classes = function () {
107 // Call after this.element exists to initialize the css classes
115 /**
108 // related to selected, rendered and mode.
116 * Call after this.element exists to initialize the css classes
117 * related to selected, rendered and mode.
118 */
109 if (this.selected) {
119 if (this.selected) {
110 this.element.addClass('selected');
120 this.element.addClass('selected');
111 } else {
121 } else {
@@ -157,6 +167,16 b' define(['
157 that.events.trigger('command_mode.Cell', {cell: that});
167 that.events.trigger('command_mode.Cell', {cell: that});
158 });
168 });
159 }
169 }
170
171 this.element.dblclick(function () {
172 if (that.selected === false) {
173 this.events.trigger('select.Cell', {'cell':that});
174 }
175 var cont = that.unrender();
176 if (cont) {
177 that.focus_editor();
178 }
179 });
160 };
180 };
161
181
162 /**
182 /**
@@ -174,9 +194,22 b' define(['
174 Cell.prototype.handle_codemirror_keyevent = function (editor, event) {
194 Cell.prototype.handle_codemirror_keyevent = function (editor, event) {
175 var shortcuts = this.keyboard_manager.edit_shortcuts;
195 var shortcuts = this.keyboard_manager.edit_shortcuts;
176
196
197 var cur = editor.getCursor();
198 if((cur.line !== 0 || cur.ch !==0) && event.keyCode === 38){
199 event._ipkmIgnore = true;
200 }
201 var nLastLine = editor.lastLine();
202 if ((event.keyCode === 40) &&
203 ((cur.line !== nLastLine) ||
204 (cur.ch !== editor.getLineHandle(nLastLine).text.length))
205 ) {
206 event._ipkmIgnore = true;
207 }
177 // if this is an edit_shortcuts shortcut, the global keyboard/shortcut
208 // if this is an edit_shortcuts shortcut, the global keyboard/shortcut
178 // manager will handle it
209 // manager will handle it
179 if (shortcuts.handles(event)) { return true; }
210 if (shortcuts.handles(event)) {
211 return true;
212 }
180
213
181 return false;
214 return false;
182 };
215 };
@@ -226,6 +259,14 b' define(['
226 };
259 };
227
260
228 /**
261 /**
262 * should be overritten by subclass
263 * @method execute
264 */
265 Cell.prototype.execute = function () {
266 return;
267 };
268
269 /**
229 * handle cell level logic when a cell is rendered
270 * handle cell level logic when a cell is rendered
230 * @method render
271 * @method render
231 * @return is the action being taken
272 * @return is the action being taken
@@ -267,9 +308,6 b' define(['
267 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
308 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
268 */
309 */
269 Cell.prototype.handle_keyevent = function (editor, event) {
310 Cell.prototype.handle_keyevent = function (editor, event) {
270
271 // console.log('CM', this.mode, event.which, event.type)
272
273 if (this.mode === 'command') {
311 if (this.mode === 'command') {
274 return true;
312 return true;
275 } else if (this.mode === 'edit') {
313 } else if (this.mode === 'edit') {
@@ -360,7 +398,9 b' define(['
360 * @method refresh
398 * @method refresh
361 */
399 */
362 Cell.prototype.refresh = function () {
400 Cell.prototype.refresh = function () {
363 this.code_mirror.refresh();
401 if (this.code_mirror) {
402 this.code_mirror.refresh();
403 }
364 };
404 };
365
405
366 /**
406 /**
@@ -385,12 +425,12 b' define(['
385 **/
425 **/
386 Cell.prototype.toJSON = function () {
426 Cell.prototype.toJSON = function () {
387 var data = {};
427 var data = {};
388 data.metadata = this.metadata;
428 // deepcopy the metadata so copied cells don't share the same object
429 data.metadata = JSON.parse(JSON.stringify(this.metadata));
389 data.cell_type = this.cell_type;
430 data.cell_type = this.cell_type;
390 return data;
431 return data;
391 };
432 };
392
433
393
394 /**
434 /**
395 * should be overritten by subclass
435 * should be overritten by subclass
396 * @method fromJSON
436 * @method fromJSON
@@ -399,27 +439,39 b' define(['
399 if (data.metadata !== undefined) {
439 if (data.metadata !== undefined) {
400 this.metadata = data.metadata;
440 this.metadata = data.metadata;
401 }
441 }
402 this.celltoolbar.rebuild();
403 };
442 };
404
443
405
444
406 /**
445 /**
407 * can the cell be split into two cells
446 * can the cell be split into two cells (false if not deletable)
408 * @method is_splittable
447 * @method is_splittable
409 **/
448 **/
410 Cell.prototype.is_splittable = function () {
449 Cell.prototype.is_splittable = function () {
411 return true;
450 return this.is_deletable();
412 };
451 };
413
452
414
453
415 /**
454 /**
416 * can the cell be merged with other cells
455 * can the cell be merged with other cells (false if not deletable)
417 * @method is_mergeable
456 * @method is_mergeable
418 **/
457 **/
419 Cell.prototype.is_mergeable = function () {
458 Cell.prototype.is_mergeable = function () {
420 return true;
459 return this.is_deletable();
421 };
460 };
422
461
462 /**
463 * is the cell deletable? only false (undeletable) if
464 * metadata.deletable is explicitly false -- everything else
465 * counts as true
466 *
467 * @method is_deletable
468 **/
469 Cell.prototype.is_deletable = function () {
470 if (this.metadata.deletable === false) {
471 return false;
472 }
473 return true;
474 };
423
475
424 /**
476 /**
425 * @return {String} - the text before the cursor
477 * @return {String} - the text before the cursor
@@ -484,7 +536,10 b' define(['
484 * @param {String|object|undefined} - CodeMirror mode | 'auto'
536 * @param {String|object|undefined} - CodeMirror mode | 'auto'
485 **/
537 **/
486 Cell.prototype._auto_highlight = function (modes) {
538 Cell.prototype._auto_highlight = function (modes) {
487 //Here we handle manually selected modes
539 /**
540 *Here we handle manually selected modes
541 */
542 var that = this;
488 var mode;
543 var mode;
489 if( this.user_highlight !== undefined && this.user_highlight != 'auto' )
544 if( this.user_highlight !== undefined && this.user_highlight != 'auto' )
490 {
545 {
@@ -506,33 +561,34 b' define(['
506 return;
561 return;
507 }
562 }
508 if (mode.search('magic_') !== 0) {
563 if (mode.search('magic_') !== 0) {
509 this.code_mirror.setOption('mode', mode);
564 utils.requireCodeMirrorMode(mode, function () {
510 CodeMirror.autoLoadMode(this.code_mirror, mode);
565 that.code_mirror.setOption('mode', mode);
566 });
511 return;
567 return;
512 }
568 }
513 var open = modes[mode].open || "%%";
569 var open = modes[mode].open || "%%";
514 var close = modes[mode].close || "%%end";
570 var close = modes[mode].close || "%%end";
515 var mmode = mode;
571 var magic_mode = mode;
516 mode = mmode.substr(6);
572 mode = magic_mode.substr(6);
517 if(current_mode == mode){
573 if(current_mode == magic_mode){
518 return;
574 return;
519 }
575 }
520 CodeMirror.autoLoadMode(this.code_mirror, mode);
576 utils.requireCodeMirrorMode(mode, function () {
521 // create on the fly a mode that swhitch between
577 // create on the fly a mode that switch between
522 // plain/text and smth else otherwise `%%` is
578 // plain/text and something else, otherwise `%%` is
523 // source of some highlight issues.
579 // source of some highlight issues.
524 // we use patchedGetMode to circumvent a bug in CM
580 CodeMirror.defineMode(magic_mode, function(config) {
525 CodeMirror.defineMode(mmode , function(config) {
581 return CodeMirror.multiplexingMode(
526 return CodeMirror.multiplexingMode(
582 CodeMirror.getMode(config, 'text/plain'),
527 CodeMirror.patchedGetMode(config, 'text/plain'),
583 // always set something on close
528 // always set someting on close
584 {open: open, close: close,
529 {open: open, close: close,
585 mode: CodeMirror.getMode(config, mode),
530 mode: CodeMirror.patchedGetMode(config, mode),
586 delimStyle: "delimit"
531 delimStyle: "delimit"
587 }
532 }
588 );
533 );
589 });
590 that.code_mirror.setOption('mode', magic_mode);
534 });
591 });
535 this.code_mirror.setOption('mode', mmode);
536 return;
592 return;
537 }
593 }
538 }
594 }
@@ -550,8 +606,76 b' define(['
550 this.code_mirror.setOption('mode', default_mode);
606 this.code_mirror.setOption('mode', default_mode);
551 };
607 };
552
608
609 var UnrecognizedCell = function (options) {
610 /** Constructor for unrecognized cells */
611 Cell.apply(this, arguments);
612 this.cell_type = 'unrecognized';
613 this.celltoolbar = null;
614 this.data = {};
615
616 Object.seal(this);
617 };
618
619 UnrecognizedCell.prototype = Object.create(Cell.prototype);
620
621
622 // cannot merge or split unrecognized cells
623 UnrecognizedCell.prototype.is_mergeable = function () {
624 return false;
625 };
626
627 UnrecognizedCell.prototype.is_splittable = function () {
628 return false;
629 };
630
631 UnrecognizedCell.prototype.toJSON = function () {
632 /**
633 * deepcopy the metadata so copied cells don't share the same object
634 */
635 return JSON.parse(JSON.stringify(this.data));
636 };
637
638 UnrecognizedCell.prototype.fromJSON = function (data) {
639 this.data = data;
640 if (data.metadata !== undefined) {
641 this.metadata = data.metadata;
642 } else {
643 data.metadata = this.metadata;
644 }
645 this.element.find('.inner_cell').find("a").text("Unrecognized cell type: " + data.cell_type);
646 };
647
648 UnrecognizedCell.prototype.create_element = function () {
649 Cell.prototype.create_element.apply(this, arguments);
650 var cell = this.element = $("<div>").addClass('cell unrecognized_cell');
651 cell.attr('tabindex','2');
652
653 var prompt = $('<div/>').addClass('prompt input_prompt');
654 cell.append(prompt);
655 var inner_cell = $('<div/>').addClass('inner_cell');
656 inner_cell.append(
657 $("<a>")
658 .attr("href", "#")
659 .text("Unrecognized cell type")
660 );
661 cell.append(inner_cell);
662 this.element = cell;
663 };
664
665 UnrecognizedCell.prototype.bind_events = function () {
666 Cell.prototype.bind_events.apply(this, arguments);
667 var cell = this;
668
669 this.element.find('.inner_cell').find("a").click(function () {
670 cell.events.trigger('unrecognized_cell.Cell', {cell: cell})
671 });
672 };
673
553 // Backwards compatibility.
674 // Backwards compatibility.
554 IPython.Cell = Cell;
675 IPython.Cell = Cell;
555
676
556 return {'Cell': Cell};
677 return {
678 Cell: Cell,
679 UnrecognizedCell: UnrecognizedCell
680 };
557 });
681 });
@@ -9,17 +9,19 b' define(['
9 "use strict";
9 "use strict";
10
10
11 var CellToolbar = function (options) {
11 var CellToolbar = function (options) {
12 // Constructor
12 /**
13 //
13 * Constructor
14 // Parameters:
14 *
15 // options: dictionary
15 * Parameters:
16 // Dictionary of keyword arguments.
16 * options: dictionary
17 // events: $(Events) instance
17 * Dictionary of keyword arguments.
18 // cell: Cell instance
18 * events: $(Events) instance
19 // notebook: Notebook instance
19 * cell: Cell instance
20 //
20 * notebook: Notebook instance
21 // TODO: This leaks, when cell are deleted
21 *
22 // There is still a reference to each celltoolbars.
22 * TODO: This leaks, when cell are deleted
23 * There is still a reference to each celltoolbars.
24 */
23 CellToolbar._instances.push(this);
25 CellToolbar._instances.push(this);
24 this.notebook = options.notebook;
26 this.notebook = options.notebook;
25 this.cell = options.cell;
27 this.cell = options.cell;
@@ -114,7 +116,7 b' define(['
114 * @param name {String} name to use to refer to the callback. It is advised to use a prefix with the name
116 * @param name {String} name to use to refer to the callback. It is advised to use a prefix with the name
115 * for easier sorting and avoid collision
117 * for easier sorting and avoid collision
116 * @param callback {function(div, cell)} callback that will be called to generate the ui element
118 * @param callback {function(div, cell)} callback that will be called to generate the ui element
117 * @param [cell_types] {List of String|undefined} optional list of cell types. If present the UI element
119 * @param [cell_types] {List_of_String|undefined} optional list of cell types. If present the UI element
118 * will be added only to cells of types in the list.
120 * will be added only to cells of types in the list.
119 *
121 *
120 *
122 *
@@ -163,7 +165,7 b' define(['
163 * @method register_preset
165 * @method register_preset
164 * @param name {String} name to use to refer to the preset. It is advised to use a prefix with the name
166 * @param name {String} name to use to refer to the preset. It is advised to use a prefix with the name
165 * for easier sorting and avoid collision
167 * for easier sorting and avoid collision
166 * @param preset_list {List of String} reverse order of the button in the toolbar. Each String of the list
168 * @param preset_list {List_of_String} reverse order of the button in the toolbar. Each String of the list
167 * should correspond to a name of a registerd callback.
169 * should correspond to a name of a registerd callback.
168 *
170 *
169 * @private
171 * @private
@@ -248,9 +250,11 b' define(['
248 * @method rebuild
250 * @method rebuild
249 */
251 */
250 CellToolbar.prototype.rebuild = function(){
252 CellToolbar.prototype.rebuild = function(){
251 // strip evrything from the div
253 /**
252 // which is probably inner_element
254 * strip evrything from the div
253 // or this.element.
255 * which is probably inner_element
256 * or this.element.
257 */
254 this.inner_element.empty();
258 this.inner_element.empty();
255 this.ui_controls_list = [];
259 this.ui_controls_list = [];
256
260
@@ -288,8 +292,6 b' define(['
288 };
292 };
289
293
290
294
291 /**
292 */
293 CellToolbar.utils = {};
295 CellToolbar.utils = {};
294
296
295
297
@@ -385,7 +387,7 b' define(['
385 * @method utils.select_ui_generator
387 * @method utils.select_ui_generator
386 * @static
388 * @static
387 *
389 *
388 * @param list_list {list of sublist} List of sublist of metadata value and name in the dropdown list.
390 * @param list_list {list_of_sublist} List of sublist of metadata value and name in the dropdown list.
389 * subslit shoud contain 2 element each, first a string that woul be displayed in the dropdown list,
391 * subslit shoud contain 2 element each, first a string that woul be displayed in the dropdown list,
390 * and second the corresponding value to be passed to setter/return by getter. the corresponding value
392 * and second the corresponding value to be passed to setter/return by getter. the corresponding value
391 * should not be "undefined" or behavior can be unexpected.
393 * should not be "undefined" or behavior can be unexpected.
@@ -119,7 +119,9 b' define(['
119 width: 650,
119 width: 650,
120 modal: true,
120 modal: true,
121 close: function() {
121 close: function() {
122 //cleanup on close
122 /**
123 *cleanup on close
124 */
123 $(this).remove();
125 $(this).remove();
124 }
126 }
125 });
127 });
@@ -1,5 +1,13 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3 /**
4 *
5 *
6 * @module codecell
7 * @namespace codecell
8 * @class CodeCell
9 */
10
3
11
4 define([
12 define([
5 'base/js/namespace',
13 'base/js/namespace',
@@ -10,8 +18,12 b' define(['
10 'notebook/js/outputarea',
18 'notebook/js/outputarea',
11 'notebook/js/completer',
19 'notebook/js/completer',
12 'notebook/js/celltoolbar',
20 'notebook/js/celltoolbar',
13 ], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar) {
21 'codemirror/lib/codemirror',
22 'codemirror/mode/python/python',
23 'notebook/js/codemirror-ipython'
24 ], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar, CodeMirror, cmpython, cmip) {
14 "use strict";
25 "use strict";
26
15 var Cell = cell.Cell;
27 var Cell = cell.Cell;
16
28
17 /* local util for codemirror */
29 /* local util for codemirror */
@@ -41,21 +53,23 b' define(['
41 var keycodes = keyboard.keycodes;
53 var keycodes = keyboard.keycodes;
42
54
43 var CodeCell = function (kernel, options) {
55 var CodeCell = function (kernel, options) {
44 // Constructor
56 /**
45 //
57 * Constructor
46 // A Cell conceived to write code.
58 *
47 //
59 * A Cell conceived to write code.
48 // Parameters:
60 *
49 // kernel: Kernel instance
61 * Parameters:
50 // The kernel doesn't have to be set at creation time, in that case
62 * kernel: Kernel instance
51 // it will be null and set_kernel has to be called later.
63 * The kernel doesn't have to be set at creation time, in that case
52 // options: dictionary
64 * it will be null and set_kernel has to be called later.
53 // Dictionary of keyword arguments.
65 * options: dictionary
54 // events: $(Events) instance
66 * Dictionary of keyword arguments.
55 // config: dictionary
67 * events: $(Events) instance
56 // keyboard_manager: KeyboardManager instance
68 * config: dictionary
57 // notebook: Notebook instance
69 * keyboard_manager: KeyboardManager instance
58 // tooltip: Tooltip instance
70 * notebook: Notebook instance
71 * tooltip: Tooltip instance
72 */
59 this.kernel = kernel || null;
73 this.kernel = kernel || null;
60 this.notebook = options.notebook;
74 this.notebook = options.notebook;
61 this.collapsed = false;
75 this.collapsed = false;
@@ -68,15 +82,28 b' define(['
68 this.input_prompt_number = null;
82 this.input_prompt_number = null;
69 this.celltoolbar = null;
83 this.celltoolbar = null;
70 this.output_area = null;
84 this.output_area = null;
85 // Keep a stack of the 'active' output areas (where active means the
86 // output area that recieves output). When a user activates an output
87 // area, it gets pushed to the stack. Then, when the output area is
88 // deactivated, it's popped from the stack. When the stack is empty,
89 // the cell's output area is used.
90 this.active_output_areas = [];
91 var that = this;
92 Object.defineProperty(this, 'active_output_area', {
93 get: function() {
94 if (that.active_output_areas && that.active_output_areas.length > 0) {
95 return that.active_output_areas[that.active_output_areas.length-1];
96 } else {
97 return that.output_area;
98 }
99 },
100 });
101
71 this.last_msg_id = null;
102 this.last_msg_id = null;
72 this.completer = null;
103 this.completer = null;
73
104
74
105
75 var cm_overwrite_options = {
106 var config = utils.mergeopt(CodeCell, this.config);
76 onKeyEvent: $.proxy(this.handle_keyevent,this)
77 };
78
79 var config = utils.mergeopt(CodeCell, this.config, {cm_config: cm_overwrite_options});
80 Cell.apply(this,[{
107 Cell.apply(this,[{
81 config: config,
108 config: config,
82 keyboard_manager: options.keyboard_manager,
109 keyboard_manager: options.keyboard_manager,
@@ -84,8 +111,6 b' define(['
84
111
85 // Attributes we want to override in this subclass.
112 // Attributes we want to override in this subclass.
86 this.cell_type = "code";
113 this.cell_type = "code";
87
88 var that = this;
89 this.element.focusout(
114 this.element.focusout(
90 function() { that.auto_highlight(); }
115 function() { that.auto_highlight(); }
91 );
116 );
@@ -102,15 +127,30 b' define(['
102 },
127 },
103 mode: 'ipython',
128 mode: 'ipython',
104 theme: 'ipython',
129 theme: 'ipython',
105 matchBrackets: true,
130 matchBrackets: true
106 // don't auto-close strings because of CodeMirror #2385
107 autoCloseBrackets: "()[]{}"
108 }
131 }
109 };
132 };
110
133
111 CodeCell.msg_cells = {};
134 CodeCell.msg_cells = {};
112
135
113 CodeCell.prototype = new Cell();
136 CodeCell.prototype = Object.create(Cell.prototype);
137
138 /**
139 * @method push_output_area
140 */
141 CodeCell.prototype.push_output_area = function (output_area) {
142 this.active_output_areas.push(output_area);
143 };
144
145 /**
146 * @method pop_output_area
147 */
148 CodeCell.prototype.pop_output_area = function (output_area) {
149 var index = this.active_output_areas.lastIndexOf(output_area);
150 if (index > -1) {
151 this.active_output_areas.splice(index, 1);
152 }
153 };
114
154
115 /**
155 /**
116 * @method auto_highlight
156 * @method auto_highlight
@@ -135,6 +175,7 b' define(['
135 inner_cell.append(this.celltoolbar.element);
175 inner_cell.append(this.celltoolbar.element);
136 var input_area = $('<div/>').addClass('input_area');
176 var input_area = $('<div/>').addClass('input_area');
137 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
177 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
178 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
138 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
179 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
139 inner_cell.append(input_area);
180 inner_cell.append(input_area);
140 input.append(prompt).append(inner_cell);
181 input.append(prompt).append(inner_cell);
@@ -187,6 +228,7 b' define(['
187 * true = ignore, false = don't ignore.
228 * true = ignore, false = don't ignore.
188 * @method handle_codemirror_keyevent
229 * @method handle_codemirror_keyevent
189 */
230 */
231
190 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
232 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
191
233
192 var that = this;
234 var that = this;
@@ -220,10 +262,11 b' define(['
220 }
262 }
221 // If we closed the tooltip, don't let CM or the global handlers
263 // If we closed the tooltip, don't let CM or the global handlers
222 // handle this event.
264 // handle this event.
223 event.stop();
265 event.codemirrorIgnore = true;
266 event.preventDefault();
224 return true;
267 return true;
225 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
268 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
226 if (editor.somethingSelected()){
269 if (editor.somethingSelected() || editor.getSelections().length !== 1){
227 var anchor = editor.getCursor("anchor");
270 var anchor = editor.getCursor("anchor");
228 var head = editor.getCursor("head");
271 var head = editor.getCursor("head");
229 if( anchor.line != head.line){
272 if( anchor.line != head.line){
@@ -231,12 +274,15 b' define(['
231 }
274 }
232 }
275 }
233 this.tooltip.request(that);
276 this.tooltip.request(that);
234 event.stop();
277 event.codemirrorIgnore = true;
278 event.preventDefault();
235 return true;
279 return true;
236 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
280 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
237 // Tab completion.
281 // Tab completion.
238 this.tooltip.remove_and_cancel_tooltip();
282 this.tooltip.remove_and_cancel_tooltip();
239 if (editor.somethingSelected()) {
283
284 // completion does not work on multicursor, it might be possible though in some cases
285 if (editor.somethingSelected() || editor.getSelections().length > 1) {
240 return false;
286 return false;
241 }
287 }
242 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
288 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
@@ -245,7 +291,8 b' define(['
245 // is empty. In this case, let CodeMirror handle indentation.
291 // is empty. In this case, let CodeMirror handle indentation.
246 return false;
292 return false;
247 } else {
293 } else {
248 event.stop();
294 event.codemirrorIgnore = true;
295 event.preventDefault();
249 this.completer.startCompletion();
296 this.completer.startCompletion();
250 return true;
297 return true;
251 }
298 }
@@ -267,7 +314,12 b' define(['
267 * @method execute
314 * @method execute
268 */
315 */
269 CodeCell.prototype.execute = function () {
316 CodeCell.prototype.execute = function () {
270 this.output_area.clear_output();
317 if (!this.kernel || !this.kernel.is_connected()) {
318 console.log("Can't execute, kernel is not connected.");
319 return;
320 }
321
322 this.active_output_area.clear_output();
271
323
272 // Clear widget area
324 // Clear widget area
273 this.widget_subarea.html('');
325 this.widget_subarea.html('');
@@ -288,6 +340,8 b' define(['
288 delete CodeCell.msg_cells[old_msg_id];
340 delete CodeCell.msg_cells[old_msg_id];
289 }
341 }
290 CodeCell.msg_cells[this.last_msg_id] = this;
342 CodeCell.msg_cells[this.last_msg_id] = this;
343 this.render();
344 this.events.trigger('execute.CodeCell', {cell: this});
291 };
345 };
292
346
293 /**
347 /**
@@ -295,6 +349,7 b' define(['
295 * @method get_callbacks
349 * @method get_callbacks
296 */
350 */
297 CodeCell.prototype.get_callbacks = function () {
351 CodeCell.prototype.get_callbacks = function () {
352 var that = this;
298 return {
353 return {
299 shell : {
354 shell : {
300 reply : $.proxy(this._handle_execute_reply, this),
355 reply : $.proxy(this._handle_execute_reply, this),
@@ -304,8 +359,12 b' define(['
304 }
359 }
305 },
360 },
306 iopub : {
361 iopub : {
307 output : $.proxy(this.output_area.handle_output, this.output_area),
362 output : function() {
308 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
363 that.active_output_area.handle_output.apply(that.active_output_area, arguments);
364 },
365 clear_output : function() {
366 that.active_output_area.handle_clear_output.apply(that.active_output_area, arguments);
367 },
309 },
368 },
310 input : $.proxy(this._handle_input_request, this)
369 input : $.proxy(this._handle_input_request, this)
311 };
370 };
@@ -339,7 +398,7 b' define(['
339 * @private
398 * @private
340 */
399 */
341 CodeCell.prototype._handle_input_request = function (msg) {
400 CodeCell.prototype._handle_input_request = function (msg) {
342 this.output_area.append_raw_input(msg);
401 this.active_output_area.append_raw_input(msg);
343 };
402 };
344
403
345
404
@@ -360,11 +419,6 b' define(['
360 return cont;
419 return cont;
361 };
420 };
362
421
363 CodeCell.prototype.unrender = function () {
364 // CodeCell is always rendered
365 return false;
366 };
367
368 CodeCell.prototype.select_all = function () {
422 CodeCell.prototype.select_all = function () {
369 var start = {line: 0, ch: 0};
423 var start = {line: 0, ch: 0};
370 var nlines = this.code_mirror.lineCount();
424 var nlines = this.code_mirror.lineCount();
@@ -375,13 +429,11 b' define(['
375
429
376
430
377 CodeCell.prototype.collapse_output = function () {
431 CodeCell.prototype.collapse_output = function () {
378 this.collapsed = true;
379 this.output_area.collapse();
432 this.output_area.collapse();
380 };
433 };
381
434
382
435
383 CodeCell.prototype.expand_output = function () {
436 CodeCell.prototype.expand_output = function () {
384 this.collapsed = false;
385 this.output_area.expand();
437 this.output_area.expand();
386 this.output_area.unscroll_area();
438 this.output_area.unscroll_area();
387 };
439 };
@@ -392,7 +444,6 b' define(['
392 };
444 };
393
445
394 CodeCell.prototype.toggle_output = function () {
446 CodeCell.prototype.toggle_output = function () {
395 this.collapsed = Boolean(1 - this.collapsed);
396 this.output_area.toggle_output();
447 this.output_area.toggle_output();
397 };
448 };
398
449
@@ -403,7 +454,7 b' define(['
403
454
404 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
455 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
405 var ns;
456 var ns;
406 if (prompt_value === undefined) {
457 if (prompt_value === undefined || prompt_value === null) {
407 ns = "&nbsp;";
458 ns = "&nbsp;";
408 } else {
459 } else {
409 ns = encodeURIComponent(prompt_value);
460 ns = encodeURIComponent(prompt_value);
@@ -450,7 +501,7 b' define(['
450
501
451
502
452 CodeCell.prototype.clear_output = function (wait) {
503 CodeCell.prototype.clear_output = function (wait) {
453 this.output_area.clear_output(wait);
504 this.active_output_area.clear_output(wait);
454 this.set_input_prompt();
505 this.set_input_prompt();
455 };
506 };
456
507
@@ -460,22 +511,18 b' define(['
460 CodeCell.prototype.fromJSON = function (data) {
511 CodeCell.prototype.fromJSON = function (data) {
461 Cell.prototype.fromJSON.apply(this, arguments);
512 Cell.prototype.fromJSON.apply(this, arguments);
462 if (data.cell_type === 'code') {
513 if (data.cell_type === 'code') {
463 if (data.input !== undefined) {
514 if (data.source !== undefined) {
464 this.set_text(data.input);
515 this.set_text(data.source);
465 // make this value the starting point, so that we can only undo
516 // make this value the starting point, so that we can only undo
466 // to this state, instead of a blank cell
517 // to this state, instead of a blank cell
467 this.code_mirror.clearHistory();
518 this.code_mirror.clearHistory();
468 this.auto_highlight();
519 this.auto_highlight();
469 }
520 }
470 if (data.prompt_number !== undefined) {
521 this.set_input_prompt(data.execution_count);
471 this.set_input_prompt(data.prompt_number);
522 this.output_area.trusted = data.metadata.trusted || false;
472 } else {
473 this.set_input_prompt();
474 }
475 this.output_area.trusted = data.trusted || false;
476 this.output_area.fromJSON(data.outputs);
523 this.output_area.fromJSON(data.outputs);
477 if (data.collapsed !== undefined) {
524 if (data.metadata.collapsed !== undefined) {
478 if (data.collapsed) {
525 if (data.metadata.collapsed) {
479 this.collapse_output();
526 this.collapse_output();
480 } else {
527 } else {
481 this.expand_output();
528 this.expand_output();
@@ -487,16 +534,17 b' define(['
487
534
488 CodeCell.prototype.toJSON = function () {
535 CodeCell.prototype.toJSON = function () {
489 var data = Cell.prototype.toJSON.apply(this);
536 var data = Cell.prototype.toJSON.apply(this);
490 data.input = this.get_text();
537 data.source = this.get_text();
491 // is finite protect against undefined and '*' value
538 // is finite protect against undefined and '*' value
492 if (isFinite(this.input_prompt_number)) {
539 if (isFinite(this.input_prompt_number)) {
493 data.prompt_number = this.input_prompt_number;
540 data.execution_count = this.input_prompt_number;
541 } else {
542 data.execution_count = null;
494 }
543 }
495 var outputs = this.output_area.toJSON();
544 var outputs = this.output_area.toJSON();
496 data.outputs = outputs;
545 data.outputs = outputs;
497 data.language = 'python';
546 data.metadata.trusted = this.output_area.trusted;
498 data.trusted = this.output_area.trusted;
547 data.metadata.collapsed = this.output_area.collapsed;
499 data.collapsed = this.collapsed;
500 return data;
548 return data;
501 };
549 };
502
550
@@ -3,7 +3,18 b''
3 // callback to auto-load python mode, which is more likely not the best things
3 // callback to auto-load python mode, which is more likely not the best things
4 // to do, but at least the simple one for now.
4 // to do, but at least the simple one for now.
5
5
6 CodeMirror.requireMode('python',function(){
6 (function(mod) {
7 if (typeof exports == "object" && typeof module == "object"){ // CommonJS
8 mod(require("codemirror/lib/codemirror"),
9 require("codemirror/mode/python/python")
10 );
11 } else if (typeof define == "function" && define.amd){ // AMD
12 define(["codemirror/lib/codemirror",
13 "codemirror/mode/python/python"], mod);
14 } else {// Plain browser env
15 mod(CodeMirror);
16 }
17 })(function(CodeMirror) {
7 "use strict";
18 "use strict";
8
19
9 CodeMirror.defineMode("ipython", function(conf, parserConf) {
20 CodeMirror.defineMode("ipython", function(conf, parserConf) {
@@ -1,44 +1,62 b''
1 // IPython GFM (GitHub Flavored Markdown) mode is just a slightly altered GFM
1 // IPython GFM (GitHub Flavored Markdown) mode is just a slightly altered GFM
2 // Mode with support for latex.
2 // Mode with support for latex.
3 //
3 //
4 // Latex support was supported by Codemirror GFM as of
4 // Latex support was supported by Codemirror GFM as of
5 // https://github.com/codemirror/CodeMirror/pull/567
5 // https://github.com/codemirror/CodeMirror/pull/567
6 // But was later removed in
6 // But was later removed in
7 // https://github.com/codemirror/CodeMirror/commit/d9c9f1b1ffe984aee41307f3e927f80d1f23590c
7 // https://github.com/codemirror/CodeMirror/commit/d9c9f1b1ffe984aee41307f3e927f80d1f23590c
8
8
9 CodeMirror.requireMode('gfm', function(){
9
10 CodeMirror.requireMode('stex', function(){
10 (function(mod) {
11 CodeMirror.defineMode("ipythongfm", function(config, parserConfig) {
11 if (typeof exports == "object" && typeof module == "object"){ // CommonJS
12
12 mod(require("codemirror/lib/codemirror")
13 var gfm_mode = CodeMirror.getMode(config, "gfm");
13 ,require("codemirror/addon/mode/multiplex")
14 var tex_mode = CodeMirror.getMode(config, "stex");
14 ,require("codemirror/mode/gfm/gfm")
15
15 ,require("codemirror/mode/stex/stex")
16 return CodeMirror.multiplexingMode(
16 );
17 gfm_mode,
17 } else if (typeof define == "function" && define.amd){ // AMD
18 {
18 define(["codemirror/lib/codemirror"
19 open: "$", close: "$",
19 ,"codemirror/addon/mode/multiplex"
20 mode: tex_mode,
20 ,"codemirror/mode/python/python"
21 delimStyle: "delimit"
21 ,"codemirror/mode/stex/stex"
22 },
22 ], mod);
23 {
23 } else {// Plain browser env
24 open: "$$", close: "$$",
24 mod(CodeMirror);
25 mode: tex_mode,
25 }
26 delimStyle: "delimit"
26 })( function(CodeMirror){
27 },
27 "use strict";
28 {
28
29 open: "\\(", close: "\\)",
29 CodeMirror.defineMode("ipythongfm", function(config, parserConfig) {
30 mode: tex_mode,
30
31 delimStyle: "delimit"
31 var gfm_mode = CodeMirror.getMode(config, "gfm");
32 },
32 var tex_mode = CodeMirror.getMode(config, "stex");
33 {
33
34 open: "\\[", close: "\\]",
34 return CodeMirror.multiplexingMode(
35 mode: tex_mode,
35 gfm_mode,
36 delimStyle: "delimit"
36 {
37 }
37 open: "$", close: "$",
38 // .. more multiplexed styles can follow here
38 mode: tex_mode,
39 );
39 delimStyle: "delimit"
40 }, 'gfm');
40 },
41
41 {
42 CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm");
42 // not sure this works as $$ is interpreted at (opening $, closing $, as defined just above)
43 });
43 open: "$$", close: "$$",
44 });
44 mode: tex_mode,
45 delimStyle: "delimit"
46 },
47 {
48 open: "\\(", close: "\\)",
49 mode: tex_mode,
50 delimStyle: "delimit"
51 },
52 {
53 open: "\\[", close: "\\]",
54 mode: tex_mode,
55 delimStyle: "delimit"
56 }
57 // .. more multiplexed styles can follow here
58 );
59 }, 'gfm');
60
61 CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm");
62 })
@@ -7,7 +7,8 b' define(['
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/keyboard',
8 'base/js/keyboard',
9 'notebook/js/contexthint',
9 'notebook/js/contexthint',
10 ], function(IPython, $, utils, keyboard) {
10 'codemirror/lib/codemirror',
11 ], function(IPython, $, utils, keyboard, CodeMirror) {
11 "use strict";
12 "use strict";
12
13
13 // easier key mapping
14 // easier key mapping
@@ -82,18 +83,20 b' define(['
82 this.cell = cell;
83 this.cell = cell;
83 this.editor = cell.code_mirror;
84 this.editor = cell.code_mirror;
84 var that = this;
85 var that = this;
85 events.on('status_busy.Kernel', function () {
86 events.on('kernel_busy.Kernel', function () {
86 that.skip_kernel_completion = true;
87 that.skip_kernel_completion = true;
87 });
88 });
88 events.on('status_idle.Kernel', function () {
89 events.on('kernel_idle.Kernel', function () {
89 that.skip_kernel_completion = false;
90 that.skip_kernel_completion = false;
90 });
91 });
91 };
92 };
92
93
93 Completer.prototype.startCompletion = function () {
94 Completer.prototype.startCompletion = function () {
94 // call for a 'first' completion, that will set the editor and do some
95 /**
95 // special behavior like autopicking if only one completion available.
96 * call for a 'first' completion, that will set the editor and do some
96 if (this.editor.somethingSelected()) return;
97 * special behavior like autopicking if only one completion available.
98 */
99 if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) return;
97 this.done = false;
100 this.done = false;
98 // use to get focus back on opera
101 // use to get focus back on opera
99 this.carry_on_completion(true);
102 this.carry_on_completion(true);
@@ -118,9 +121,11 b' define(['
118 * shared start
121 * shared start
119 **/
122 **/
120 Completer.prototype.carry_on_completion = function (first_invocation) {
123 Completer.prototype.carry_on_completion = function (first_invocation) {
121 // Pass true as parameter if you want the completer to autopick when
124 /**
122 // only one completion. This function is automatically reinvoked at
125 * Pass true as parameter if you want the completer to autopick when
123 // each keystroke with first_invocation = false
126 * only one completion. This function is automatically reinvoked at
127 * each keystroke with first_invocation = false
128 */
124 var cur = this.editor.getCursor();
129 var cur = this.editor.getCursor();
125 var line = this.editor.getLine(cur.line);
130 var line = this.editor.getLine(cur.line);
126 var pre_cursor = this.editor.getRange({
131 var pre_cursor = this.editor.getRange({
@@ -142,7 +147,7 b' define(['
142 }
147 }
143
148
144 // We want a single cursor position.
149 // We want a single cursor position.
145 if (this.editor.somethingSelected()) {
150 if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) {
146 return;
151 return;
147 }
152 }
148
153
@@ -163,8 +168,10 b' define(['
163 };
168 };
164
169
165 Completer.prototype.finish_completing = function (msg) {
170 Completer.prototype.finish_completing = function (msg) {
166 // let's build a function that wrap all that stuff into what is needed
171 /**
167 // for the new completer:
172 * let's build a function that wrap all that stuff into what is needed
173 * for the new completer:
174 */
168 var content = msg.content;
175 var content = msg.content;
169 var start = content.cursor_start;
176 var start = content.cursor_start;
170 var end = content.cursor_end;
177 var end = content.cursor_end;
@@ -316,11 +323,15 b' define(['
316
323
317 // Enter
324 // Enter
318 if (code == keycodes.enter) {
325 if (code == keycodes.enter) {
319 CodeMirror.e_stop(event);
326 event.codemirrorIgnore = true;
327 event._ipkmIgnore = true;
328 event.preventDefault();
320 this.pick();
329 this.pick();
321 // Escape or backspace
330 // Escape or backspace
322 } else if (code == keycodes.esc || code == keycodes.backspace) {
331 } else if (code == keycodes.esc || code == keycodes.backspace) {
323 CodeMirror.e_stop(event);
332 event.codemirrorIgnore = true;
333 event._ipkmIgnore = true;
334 event.preventDefault();
324 this.close();
335 this.close();
325 } else if (code == keycodes.tab) {
336 } else if (code == keycodes.tab) {
326 //all the fastforwarding operation,
337 //all the fastforwarding operation,
@@ -339,7 +350,9 b' define(['
339 } else if (code == keycodes.up || code == keycodes.down) {
350 } else if (code == keycodes.up || code == keycodes.down) {
340 // need to do that to be able to move the arrow
351 // need to do that to be able to move the arrow
341 // when on the first or last line ofo a code cell
352 // when on the first or last line ofo a code cell
342 CodeMirror.e_stop(event);
353 event.codemirrorIgnore = true;
354 event._ipkmIgnore = true;
355 event.preventDefault();
343
356
344 var options = this.sel.find('option');
357 var options = this.sel.find('option');
345 var index = this.sel[0].selectedIndex;
358 var index = this.sel[0].selectedIndex;
@@ -352,7 +365,7 b' define(['
352 index = Math.min(Math.max(index, 0), options.length-1);
365 index = Math.min(Math.max(index, 0), options.length-1);
353 this.sel[0].selectedIndex = index;
366 this.sel[0].selectedIndex = index;
354 } else if (code == keycodes.pageup || code == keycodes.pagedown) {
367 } else if (code == keycodes.pageup || code == keycodes.pagedown) {
355 CodeMirror.e_stop(event);
368 event._ipkmIgnore = true;
356
369
357 var options = this.sel.find('option');
370 var options = this.sel.find('option');
358 var index = this.sel[0].selectedIndex;
371 var index = this.sel[0].selectedIndex;
@@ -369,11 +382,13 b' define(['
369 };
382 };
370
383
371 Completer.prototype.keypress = function (event) {
384 Completer.prototype.keypress = function (event) {
372 // FIXME: This is a band-aid.
385 /**
373 // on keypress, trigger insertion of a single character.
386 * FIXME: This is a band-aid.
374 // This simulates the old behavior of completion as you type,
387 * on keypress, trigger insertion of a single character.
375 // before events were disconnected and CodeMirror stopped
388 * This simulates the old behavior of completion as you type,
376 // receiving events while the completer is focused.
389 * before events were disconnected and CodeMirror stopped
390 * receiving events while the completer is focused.
391 */
377
392
378 var that = this;
393 var that = this;
379 var code = event.keyCode;
394 var code = event.keyCode;
@@ -1,6 +1,15 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 /**
5 *
6 *
7 * @module config
8 * @namespace config
9 * @class Config
10 */
11
12
4 define([], function() {
13 define([], function() {
5 "use strict";
14 "use strict";
6
15
@@ -2,7 +2,7 b''
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 // highly adapted for codemiror jshint
4 // highly adapted for codemiror jshint
5 define([], function() {
5 define(['codemirror/lib/codemirror'], function(CodeMirror) {
6 "use strict";
6 "use strict";
7
7
8 var forEach = function(arr, f) {
8 var forEach = function(arr, f) {
@@ -12,7 +12,7 b' define(['
12 this.selector = selector;
12 this.selector = selector;
13 this.notebook = notebook;
13 this.notebook = notebook;
14 this.events = notebook.events;
14 this.events = notebook.events;
15 this.current_selection = notebook.default_kernel_name;
15 this.current_selection = null;
16 this.kernelspecs = {};
16 this.kernelspecs = {};
17 if (this.selector !== undefined) {
17 if (this.selector !== undefined) {
18 this.element = $(selector);
18 this.element = $(selector);
@@ -76,12 +76,12 b' define(['
76 that.element.find("#current_kernel_spec").find('.kernel_name').text(data.display_name);
76 that.element.find("#current_kernel_spec").find('.kernel_name').text(data.display_name);
77 });
77 });
78
78
79 this.events.on('started.Session', function(events, session) {
79 this.events.on('kernel_created.Session', function(event, data) {
80 if (session.kernel_name !== that.current_selection) {
80 if (data.kernel.name !== that.current_selection) {
81 // If we created a 'python' session, we only know if it's Python
81 // If we created a 'python' session, we only know if it's Python
82 // 3 or 2 on the server's reply, so we fire the event again to
82 // 3 or 2 on the server's reply, so we fire the event again to
83 // set things up.
83 // set things up.
84 var ks = that.kernelspecs[session.kernel_name];
84 var ks = that.kernelspecs[data.kernel.name];
85 that.events.trigger('spec_changed.Kernel', ks);
85 that.events.trigger('spec_changed.Kernel', ks);
86 }
86 }
87 });
87 });
This diff has been collapsed as it changes many lines, (554 lines changed) Show them Hide them
@@ -1,5 +1,12 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3 /**
4 *
5 *
6 * @module keyboardmanager
7 * @namespace keyboardmanager
8 * @class KeyboardManager
9 */
3
10
4 define([
11 define([
5 'base/js/namespace',
12 'base/js/namespace',
@@ -9,491 +16,138 b' define(['
9 ], function(IPython, $, utils, keyboard) {
16 ], function(IPython, $, utils, keyboard) {
10 "use strict";
17 "use strict";
11
18
12 var browser = utils.browser[0];
13 var platform = utils.platform;
14
15 // Main keyboard manager for the notebook
19 // Main keyboard manager for the notebook
16 var keycodes = keyboard.keycodes;
20 var keycodes = keyboard.keycodes;
17
21
18 var KeyboardManager = function (options) {
22 var KeyboardManager = function (options) {
19 // Constructor
23 /**
20 //
24 * A class to deal with keyboard event and shortcut
21 // Parameters:
25 *
22 // options: dictionary
26 * @class KeyboardManager
23 // Dictionary of keyword arguments.
27 * @constructor
24 // events: $(Events) instance
28 * @param options {dict} Dictionary of keyword arguments :
25 // pager: Pager instance
29 * @param options.events {$(Events)} instance
30 * @param options.pager: {Pager} pager instance
31 */
26 this.mode = 'command';
32 this.mode = 'command';
27 this.enabled = true;
33 this.enabled = true;
28 this.pager = options.pager;
34 this.pager = options.pager;
29 this.quick_help = undefined;
35 this.quick_help = undefined;
30 this.notebook = undefined;
36 this.notebook = undefined;
37 this.last_mode = undefined;
31 this.bind_events();
38 this.bind_events();
32 this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events);
39 this.env = {pager:this.pager};
40 this.actions = options.actions;
41 this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env );
33 this.command_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
42 this.command_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
34 this.command_shortcuts.add_shortcuts(this.get_default_command_shortcuts());
43 this.command_shortcuts.add_shortcuts(this.get_default_command_shortcuts());
35 this.edit_shortcuts = new keyboard.ShortcutManager(undefined, options.events);
44 this.edit_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env);
36 this.edit_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
45 this.edit_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
37 this.edit_shortcuts.add_shortcuts(this.get_default_edit_shortcuts());
46 this.edit_shortcuts.add_shortcuts(this.get_default_edit_shortcuts());
47 Object.seal(this);
38 };
48 };
39
49
50
51
52
53 /**
54 * Return a dict of common shortcut
55 * @method get_default_common_shortcuts
56 *
57 * @example Example of returned shortcut
58 * ```
59 * 'shortcut-key': 'action-name'
60 * // a string representing the shortcut as dash separated value.
61 * // e.g. 'shift' , 'shift-enter', 'cmd-t'
62 *```
63 */
40 KeyboardManager.prototype.get_default_common_shortcuts = function() {
64 KeyboardManager.prototype.get_default_common_shortcuts = function() {
41 var that = this;
65 return {
42 var shortcuts = {
66 'shift' : 'ipython.ignore',
43 'shift' : {
67 'shift-enter' : 'ipython.run-select-next',
44 help : '',
68 'ctrl-enter' : 'ipython.execute-in-place',
45 help_index : '',
69 'alt-enter' : 'ipython.execute-and-insert-after',
46 handler : function (event) {
70 // cmd on mac, ctrl otherwise
47 // ignore shift keydown
71 'cmdtrl-s' : 'ipython.save-notebook',
48 return true;
49 }
50 },
51 'shift-enter' : {
52 help : 'run cell, select below',
53 help_index : 'ba',
54 handler : function (event) {
55 that.notebook.execute_cell_and_select_below();
56 return false;
57 }
58 },
59 'ctrl-enter' : {
60 help : 'run cell',
61 help_index : 'bb',
62 handler : function (event) {
63 that.notebook.execute_cell();
64 return false;
65 }
66 },
67 'alt-enter' : {
68 help : 'run cell, insert below',
69 help_index : 'bc',
70 handler : function (event) {
71 that.notebook.execute_cell_and_insert_below();
72 return false;
73 }
74 }
75 };
72 };
76
77 if (platform === 'MacOS') {
78 shortcuts['cmd-s'] =
79 {
80 help : 'save notebook',
81 help_index : 'fb',
82 handler : function (event) {
83 that.notebook.save_checkpoint();
84 event.preventDefault();
85 return false;
86 }
87 };
88 } else {
89 shortcuts['ctrl-s'] =
90 {
91 help : 'save notebook',
92 help_index : 'fb',
93 handler : function (event) {
94 that.notebook.save_checkpoint();
95 event.preventDefault();
96 return false;
97 }
98 };
99 }
100 return shortcuts;
101 };
73 };
102
74
103 KeyboardManager.prototype.get_default_edit_shortcuts = function() {
75 KeyboardManager.prototype.get_default_edit_shortcuts = function() {
104 var that = this;
105 return {
76 return {
106 'esc' : {
77 'esc' : 'ipython.go-to-command-mode',
107 help : 'command mode',
78 'ctrl-m' : 'ipython.go-to-command-mode',
108 help_index : 'aa',
79 'up' : 'ipython.move-cursor-up-or-previous-cell',
109 handler : function (event) {
80 'down' : 'ipython.move-cursor-down-or-next-cell',
110 that.notebook.command_mode();
81 'ctrl-shift--' : 'ipython.split-cell-at-cursor',
111 return false;
82 'ctrl-shift-subtract' : 'ipython.split-cell-at-cursor'
112 }
113 },
114 'ctrl-m' : {
115 help : 'command mode',
116 help_index : 'ab',
117 handler : function (event) {
118 that.notebook.command_mode();
119 return false;
120 }
121 },
122 'up' : {
123 help : '',
124 help_index : '',
125 handler : function (event) {
126 var index = that.notebook.get_selected_index();
127 var cell = that.notebook.get_cell(index);
128 if (cell && cell.at_top() && index !== 0) {
129 event.preventDefault();
130 that.notebook.command_mode();
131 that.notebook.select_prev();
132 that.notebook.edit_mode();
133 var cm = that.notebook.get_selected_cell().code_mirror;
134 cm.setCursor(cm.lastLine(), 0);
135 return false;
136 } else if (cell) {
137 var cm = cell.code_mirror;
138 cm.execCommand('goLineUp');
139 return false;
140 }
141 }
142 },
143 'down' : {
144 help : '',
145 help_index : '',
146 handler : function (event) {
147 var index = that.notebook.get_selected_index();
148 var cell = that.notebook.get_cell(index);
149 if (cell.at_bottom() && index !== (that.notebook.ncells()-1)) {
150 event.preventDefault();
151 that.notebook.command_mode();
152 that.notebook.select_next();
153 that.notebook.edit_mode();
154 var cm = that.notebook.get_selected_cell().code_mirror;
155 cm.setCursor(0, 0);
156 return false;
157 } else {
158 var cm = cell.code_mirror;
159 cm.execCommand('goLineDown');
160 return false;
161 }
162 }
163 },
164 'ctrl-shift--' : {
165 help : 'split cell',
166 help_index : 'ea',
167 handler : function (event) {
168 that.notebook.split_cell();
169 return false;
170 }
171 },
172 'ctrl-shift-subtract' : {
173 help : '',
174 help_index : 'eb',
175 handler : function (event) {
176 that.notebook.split_cell();
177 return false;
178 }
179 },
180 };
83 };
181 };
84 };
182
85
183 KeyboardManager.prototype.get_default_command_shortcuts = function() {
86 KeyboardManager.prototype.get_default_command_shortcuts = function() {
184 var that = this;
185 return {
87 return {
186 'space': {
88 'shift-space': 'ipython.scroll-up',
187 help: "Scroll down",
89 'shift-v' : 'ipython.paste-cell-before',
188 handler: function(event) {
90 'shift-m' : 'ipython.merge-selected-cell-with-cell-after',
189 return that.notebook.scroll_manager.scroll(1);
91 'shift-o' : 'ipython.toggle-output-scrolling-selected-cell',
190 },
92 'ctrl-j' : 'ipython.move-selected-cell-down',
191 },
93 'ctrl-k' : 'ipython.move-selected-cell-up',
192 'shift-space': {
94 'enter' : 'ipython.enter-edit-mode',
193 help: "Scroll up",
95 'space' : 'ipython.scroll-down',
194 handler: function(event) {
96 'down' : 'ipython.select-next-cell',
195 return that.notebook.scroll_manager.scroll(-1);
97 'i,i' : 'ipython.interrupt-kernel',
196 },
98 '0,0' : 'ipython.restart-kernel',
197 },
99 'd,d' : 'ipython.delete-cell',
198 'enter' : {
100 'esc': 'ipython.close-pager',
199 help : 'edit mode',
101 'up' : 'ipython.select-previous-cell',
200 help_index : 'aa',
102 'k' : 'ipython.select-previous-cell',
201 handler : function (event) {
103 'j' : 'ipython.select-next-cell',
202 that.notebook.edit_mode();
104 'x' : 'ipython.cut-selected-cell',
203 return false;
105 'c' : 'ipython.copy-selected-cell',
204 }
106 'v' : 'ipython.paste-cell-after',
205 },
107 'a' : 'ipython.insert-cell-before',
206 'up' : {
108 'b' : 'ipython.insert-cell-after',
207 help : 'select previous cell',
109 'y' : 'ipython.change-selected-cell-to-code-cell',
208 help_index : 'da',
110 'm' : 'ipython.change-selected-cell-to-markdown-cell',
209 handler : function (event) {
111 'r' : 'ipython.change-selected-cell-to-raw-cell',
210 var index = that.notebook.get_selected_index();
112 '1' : 'ipython.change-selected-cell-to-heading-1',
211 if (index !== 0 && index !== null) {
113 '2' : 'ipython.change-selected-cell-to-heading-2',
212 that.notebook.select_prev();
114 '3' : 'ipython.change-selected-cell-to-heading-3',
213 that.notebook.focus_cell();
115 '4' : 'ipython.change-selected-cell-to-heading-4',
214 }
116 '5' : 'ipython.change-selected-cell-to-heading-5',
215 return false;
117 '6' : 'ipython.change-selected-cell-to-heading-6',
216 }
118 'o' : 'ipython.toggle-output-visibility-selected-cell',
217 },
119 's' : 'ipython.save-notebook',
218 'down' : {
120 'l' : 'ipython.toggle-line-number-selected-cell',
219 help : 'select next cell',
121 'h' : 'ipython.show-keyboard-shortcut-help-dialog',
220 help_index : 'db',
122 'z' : 'ipython.undo-last-cell-deletion',
221 handler : function (event) {
123 'q' : 'ipython.close-pager',
222 var index = that.notebook.get_selected_index();
223 if (index !== (that.notebook.ncells()-1) && index !== null) {
224 that.notebook.select_next();
225 that.notebook.focus_cell();
226 }
227 return false;
228 }
229 },
230 'k' : {
231 help : 'select previous cell',
232 help_index : 'dc',
233 handler : function (event) {
234 var index = that.notebook.get_selected_index();
235 if (index !== 0 && index !== null) {
236 that.notebook.select_prev();
237 that.notebook.focus_cell();
238 }
239 return false;
240 }
241 },
242 'j' : {
243 help : 'select next cell',
244 help_index : 'dd',
245 handler : function (event) {
246 var index = that.notebook.get_selected_index();
247 if (index !== (that.notebook.ncells()-1) && index !== null) {
248 that.notebook.select_next();
249 that.notebook.focus_cell();
250 }
251 return false;
252 }
253 },
254 'x' : {
255 help : 'cut cell',
256 help_index : 'ee',
257 handler : function (event) {
258 that.notebook.cut_cell();
259 return false;
260 }
261 },
262 'c' : {
263 help : 'copy cell',
264 help_index : 'ef',
265 handler : function (event) {
266 that.notebook.copy_cell();
267 return false;
268 }
269 },
270 'shift-v' : {
271 help : 'paste cell above',
272 help_index : 'eg',
273 handler : function (event) {
274 that.notebook.paste_cell_above();
275 return false;
276 }
277 },
278 'v' : {
279 help : 'paste cell below',
280 help_index : 'eh',
281 handler : function (event) {
282 that.notebook.paste_cell_below();
283 return false;
284 }
285 },
286 'd' : {
287 help : 'delete cell (press twice)',
288 help_index : 'ej',
289 count: 2,
290 handler : function (event) {
291 that.notebook.delete_cell();
292 return false;
293 }
294 },
295 'a' : {
296 help : 'insert cell above',
297 help_index : 'ec',
298 handler : function (event) {
299 that.notebook.insert_cell_above();
300 that.notebook.select_prev();
301 that.notebook.focus_cell();
302 return false;
303 }
304 },
305 'b' : {
306 help : 'insert cell below',
307 help_index : 'ed',
308 handler : function (event) {
309 that.notebook.insert_cell_below();
310 that.notebook.select_next();
311 that.notebook.focus_cell();
312 return false;
313 }
314 },
315 'y' : {
316 help : 'to code',
317 help_index : 'ca',
318 handler : function (event) {
319 that.notebook.to_code();
320 return false;
321 }
322 },
323 'm' : {
324 help : 'to markdown',
325 help_index : 'cb',
326 handler : function (event) {
327 that.notebook.to_markdown();
328 return false;
329 }
330 },
331 'r' : {
332 help : 'to raw',
333 help_index : 'cc',
334 handler : function (event) {
335 that.notebook.to_raw();
336 return false;
337 }
338 },
339 '1' : {
340 help : 'to heading 1',
341 help_index : 'cd',
342 handler : function (event) {
343 that.notebook.to_heading(undefined, 1);
344 return false;
345 }
346 },
347 '2' : {
348 help : 'to heading 2',
349 help_index : 'ce',
350 handler : function (event) {
351 that.notebook.to_heading(undefined, 2);
352 return false;
353 }
354 },
355 '3' : {
356 help : 'to heading 3',
357 help_index : 'cf',
358 handler : function (event) {
359 that.notebook.to_heading(undefined, 3);
360 return false;
361 }
362 },
363 '4' : {
364 help : 'to heading 4',
365 help_index : 'cg',
366 handler : function (event) {
367 that.notebook.to_heading(undefined, 4);
368 return false;
369 }
370 },
371 '5' : {
372 help : 'to heading 5',
373 help_index : 'ch',
374 handler : function (event) {
375 that.notebook.to_heading(undefined, 5);
376 return false;
377 }
378 },
379 '6' : {
380 help : 'to heading 6',
381 help_index : 'ci',
382 handler : function (event) {
383 that.notebook.to_heading(undefined, 6);
384 return false;
385 }
386 },
387 'o' : {
388 help : 'toggle output',
389 help_index : 'gb',
390 handler : function (event) {
391 that.notebook.toggle_output();
392 return false;
393 }
394 },
395 'shift-o' : {
396 help : 'toggle output scrolling',
397 help_index : 'gc',
398 handler : function (event) {
399 that.notebook.toggle_output_scroll();
400 return false;
401 }
402 },
403 's' : {
404 help : 'save notebook',
405 help_index : 'fa',
406 handler : function (event) {
407 that.notebook.save_checkpoint();
408 return false;
409 }
410 },
411 'ctrl-j' : {
412 help : 'move cell down',
413 help_index : 'eb',
414 handler : function (event) {
415 that.notebook.move_cell_down();
416 return false;
417 }
418 },
419 'ctrl-k' : {
420 help : 'move cell up',
421 help_index : 'ea',
422 handler : function (event) {
423 that.notebook.move_cell_up();
424 return false;
425 }
426 },
427 'l' : {
428 help : 'toggle line numbers',
429 help_index : 'ga',
430 handler : function (event) {
431 that.notebook.cell_toggle_line_numbers();
432 return false;
433 }
434 },
435 'i' : {
436 help : 'interrupt kernel (press twice)',
437 help_index : 'ha',
438 count: 2,
439 handler : function (event) {
440 that.notebook.kernel.interrupt();
441 return false;
442 }
443 },
444 '0' : {
445 help : 'restart kernel (press twice)',
446 help_index : 'hb',
447 count: 2,
448 handler : function (event) {
449 that.notebook.restart_kernel();
450 return false;
451 }
452 },
453 'h' : {
454 help : 'keyboard shortcuts',
455 help_index : 'ge',
456 handler : function (event) {
457 that.quick_help.show_keyboard_shortcuts();
458 return false;
459 }
460 },
461 'z' : {
462 help : 'undo last delete',
463 help_index : 'ei',
464 handler : function (event) {
465 that.notebook.undelete_cell();
466 return false;
467 }
468 },
469 'shift-m' : {
470 help : 'merge cell below',
471 help_index : 'ek',
472 handler : function (event) {
473 that.notebook.merge_cell_below();
474 return false;
475 }
476 },
477 'q' : {
478 help : 'close pager',
479 help_index : 'gd',
480 handler : function (event) {
481 that.pager.collapse();
482 return false;
483 }
484 },
485 };
124 };
486 };
125 };
487
126
488 KeyboardManager.prototype.bind_events = function () {
127 KeyboardManager.prototype.bind_events = function () {
489 var that = this;
128 var that = this;
490 $(document).keydown(function (event) {
129 $(document).keydown(function (event) {
130 if(event._ipkmIgnore===true||(event.originalEvent||{})._ipkmIgnore===true){
131 return false;
132 }
491 return that.handle_keydown(event);
133 return that.handle_keydown(event);
492 });
134 });
493 };
135 };
494
136
137 KeyboardManager.prototype.set_notebook = function (notebook) {
138 this.notebook = notebook;
139 this.actions.extend_env({notebook:notebook});
140 };
141
142 KeyboardManager.prototype.set_quickhelp = function (notebook) {
143 this.actions.extend_env({quick_help:notebook});
144 };
145
146
495 KeyboardManager.prototype.handle_keydown = function (event) {
147 KeyboardManager.prototype.handle_keydown = function (event) {
496 var notebook = this.notebook;
148 /**
149 * returning false from this will stop event propagation
150 **/
497
151
498 if (event.which === keycodes.esc) {
152 if (event.which === keycodes.esc) {
499 // Intercept escape at highest level to avoid closing
153 // Intercept escape at highest level to avoid closing
@@ -503,8 +157,7 b' define(['
503
157
504 if (!this.enabled) {
158 if (!this.enabled) {
505 if (event.which === keycodes.esc) {
159 if (event.which === keycodes.esc) {
506 // ESC
160 this.notebook.command_mode();
507 notebook.command_mode();
508 return false;
161 return false;
509 }
162 }
510 return true;
163 return true;
@@ -571,7 +224,8 b' define(['
571 });
224 });
572 };
225 };
573
226
574 // For backwards compatability.
227
228 // For backwards compatibility.
575 IPython.KeyboardManager = KeyboardManager;
229 IPython.KeyboardManager = KeyboardManager;
576
230
577 return {'KeyboardManager': KeyboardManager};
231 return {'KeyboardManager': KeyboardManager};
@@ -5,6 +5,8 b' require(['
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'notebook/js/notebook',
7 'notebook/js/notebook',
8 'contents',
9 'services/config',
8 'base/js/utils',
10 'base/js/utils',
9 'base/js/page',
11 'base/js/page',
10 'notebook/js/layoutmanager',
12 'notebook/js/layoutmanager',
@@ -16,15 +18,20 b' require(['
16 'notebook/js/menubar',
18 'notebook/js/menubar',
17 'notebook/js/notificationarea',
19 'notebook/js/notificationarea',
18 'notebook/js/savewidget',
20 'notebook/js/savewidget',
21 'notebook/js/actions',
19 'notebook/js/keyboardmanager',
22 'notebook/js/keyboardmanager',
20 'notebook/js/config',
23 'notebook/js/config',
21 'notebook/js/kernelselector',
24 'notebook/js/kernelselector',
22 // only loaded, not used:
25 'codemirror/lib/codemirror',
23 'custom/custom',
26 'notebook/js/about',
27 // only loaded, not used, please keep sure this is loaded last
28 'custom/custom'
24 ], function(
29 ], function(
25 IPython,
30 IPython,
26 $,
31 $,
27 notebook,
32 notebook,
33 contents,
34 configmod,
28 utils,
35 utils,
29 page,
36 page,
30 layoutmanager,
37 layoutmanager,
@@ -35,16 +42,24 b' require(['
35 quickhelp,
42 quickhelp,
36 menubar,
43 menubar,
37 notificationarea,
44 notificationarea,
38 savewidget,
45 savewidget,
46 actions,
39 keyboardmanager,
47 keyboardmanager,
40 config,
48 config,
41 kernelselector
49 kernelselector,
50 CodeMirror,
51 about,
52 // please keep sure that even if not used, this is loaded last
53 custom
42 ) {
54 ) {
43 "use strict";
55 "use strict";
44
56
57 // compat with old IPython, remove for IPython > 3.0
58 window.CodeMirror = CodeMirror;
59
45 var common_options = {
60 var common_options = {
61 ws_url : utils.get_body_data("wsUrl"),
46 base_url : utils.get_body_data("baseUrl"),
62 base_url : utils.get_body_data("baseUrl"),
47 ws_url : IPython.utils.get_body_data("wsUrl"),
48 notebook_path : utils.get_body_data("notebookPath"),
63 notebook_path : utils.get_body_data("notebookPath"),
49 notebook_name : utils.get_body_data('notebookName')
64 notebook_name : utils.get_body_data('notebookName')
50 };
65 };
@@ -55,34 +70,46 b' require(['
55 var pager = new pager.Pager('div#pager', 'div#pager_splitter', {
70 var pager = new pager.Pager('div#pager', 'div#pager_splitter', {
56 layout_manager: layout_manager,
71 layout_manager: layout_manager,
57 events: events});
72 events: events});
73 var acts = new actions.init();
58 var keyboard_manager = new keyboardmanager.KeyboardManager({
74 var keyboard_manager = new keyboardmanager.KeyboardManager({
59 pager: pager,
75 pager: pager,
60 events: events});
76 events: events,
77 actions: acts });
61 var save_widget = new savewidget.SaveWidget('span#save_widget', {
78 var save_widget = new savewidget.SaveWidget('span#save_widget', {
62 events: events,
79 events: events,
63 keyboard_manager: keyboard_manager});
80 keyboard_manager: keyboard_manager});
81 var contents = new contents.Contents($.extend({
82 events: events},
83 common_options));
84 var config_section = new configmod.ConfigSection('notebook', common_options);
85 config_section.load();
64 var notebook = new notebook.Notebook('div#notebook', $.extend({
86 var notebook = new notebook.Notebook('div#notebook', $.extend({
65 events: events,
87 events: events,
66 keyboard_manager: keyboard_manager,
88 keyboard_manager: keyboard_manager,
67 save_widget: save_widget,
89 save_widget: save_widget,
90 contents: contents,
68 config: user_config},
91 config: user_config},
69 common_options));
92 common_options));
70 var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options);
93 var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options);
71 var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', {
94 var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', {
72 notebook: notebook,
95 notebook: notebook,
73 events: events});
96 events: events,
97 actions: acts});
74 var quick_help = new quickhelp.QuickHelp({
98 var quick_help = new quickhelp.QuickHelp({
75 keyboard_manager: keyboard_manager,
99 keyboard_manager: keyboard_manager,
76 events: events,
100 events: events,
77 notebook: notebook});
101 notebook: notebook});
102 keyboard_manager.set_notebook(notebook);
103 keyboard_manager.set_quickhelp(quick_help);
78 var menubar = new menubar.MenuBar('#menubar', $.extend({
104 var menubar = new menubar.MenuBar('#menubar', $.extend({
79 notebook: notebook,
105 notebook: notebook,
106 contents: contents,
80 layout_manager: layout_manager,
107 layout_manager: layout_manager,
81 events: events,
108 events: events,
82 save_widget: save_widget,
109 save_widget: save_widget,
83 quick_help: quick_help},
110 quick_help: quick_help},
84 common_options));
111 common_options));
85 var notification_area = new notificationarea.NotificationArea(
112 var notification_area = new notificationarea.NotebookNotificationArea(
86 '#notification_area', {
113 '#notification_area', {
87 events: events,
114 events: events,
88 save_widget: save_widget,
115 save_widget: save_widget,
@@ -122,6 +149,7 b' require(['
122 IPython.page = page;
149 IPython.page = page;
123 IPython.layout_manager = layout_manager;
150 IPython.layout_manager = layout_manager;
124 IPython.notebook = notebook;
151 IPython.notebook = notebook;
152 IPython.contents = contents;
125 IPython.pager = pager;
153 IPython.pager = pager;
126 IPython.quick_help = quick_help;
154 IPython.quick_help = quick_help;
127 IPython.login_widget = login_widget;
155 IPython.login_widget = login_widget;
@@ -134,6 +162,13 b' require(['
134 IPython.tooltip = notebook.tooltip;
162 IPython.tooltip = notebook.tooltip;
135
163
136 events.trigger('app_initialized.NotebookApp');
164 events.trigger('app_initialized.NotebookApp');
137 notebook.load_notebook(common_options.notebook_name, common_options.notebook_path);
165 config_section.loaded.then(function() {
166 if (config_section.data.load_extensions) {
167 var nbextension_paths = Object.getOwnPropertyNames(
168 config_section.data.load_extensions);
169 IPython.load_extensions.apply(this, nbextension_paths);
170 }
171 });
172 notebook.load_notebook(common_options.notebook_path);
138
173
139 });
174 });
@@ -10,14 +10,16 b' define(['
10 "use strict";
10 "use strict";
11
11
12 var MainToolBar = function (selector, options) {
12 var MainToolBar = function (selector, options) {
13 // Constructor
13 /**
14 //
14 * Constructor
15 // Parameters:
15 *
16 // selector: string
16 * Parameters:
17 // options: dictionary
17 * selector: string
18 // Dictionary of keyword arguments.
18 * options: dictionary
19 // events: $(Events) instance
19 * Dictionary of keyword arguments.
20 // notebook: Notebook instance
20 * events: $(Events) instance
21 * notebook: Notebook instance
22 */
21 toolbar.ToolBar.apply(this, arguments);
23 toolbar.ToolBar.apply(this, arguments);
22 this.events = options.events;
24 this.events = options.events;
23 this.notebook = options.notebook;
25 this.notebook = options.notebook;
@@ -27,7 +29,7 b' define(['
27 this.bind_events();
29 this.bind_events();
28 };
30 };
29
31
30 MainToolBar.prototype = new toolbar.ToolBar();
32 MainToolBar.prototype = Object.create(toolbar.ToolBar.prototype);
31
33
32 MainToolBar.prototype.construct = function () {
34 MainToolBar.prototype.construct = function () {
33 var that = this;
35 var that = this;
@@ -108,7 +110,9 b' define(['
108 label : 'Run Cell',
110 label : 'Run Cell',
109 icon : 'fa-play',
111 icon : 'fa-play',
110 callback : function () {
112 callback : function () {
111 // emulate default shift-enter behavior
113 /**
114 * emulate default shift-enter behavior
115 */
112 that.notebook.execute_cell_and_select_below();
116 that.notebook.execute_cell_and_select_below();
113 }
117 }
114 },
118 },
@@ -117,7 +121,7 b' define(['
117 label : 'Interrupt',
121 label : 'Interrupt',
118 icon : 'fa-stop',
122 icon : 'fa-stop',
119 callback : function () {
123 callback : function () {
120 that.notebook.session.interrupt_kernel();
124 that.notebook.kernel.interrupt();
121 }
125 }
122 },
126 },
123 {
127 {
@@ -139,12 +143,7 b' define(['
139 .append($('<option/>').attr('value','code').text('Code'))
143 .append($('<option/>').attr('value','code').text('Code'))
140 .append($('<option/>').attr('value','markdown').text('Markdown'))
144 .append($('<option/>').attr('value','markdown').text('Markdown'))
141 .append($('<option/>').attr('value','raw').text('Raw NBConvert'))
145 .append($('<option/>').attr('value','raw').text('Raw NBConvert'))
142 .append($('<option/>').attr('value','heading1').text('Heading 1'))
146 .append($('<option/>').attr('value','heading').text('Heading'))
143 .append($('<option/>').attr('value','heading2').text('Heading 2'))
144 .append($('<option/>').attr('value','heading3').text('Heading 3'))
145 .append($('<option/>').attr('value','heading4').text('Heading 4'))
146 .append($('<option/>').attr('value','heading5').text('Heading 5'))
147 .append($('<option/>').attr('value','heading6').text('Heading 6'))
148 );
147 );
149 };
148 };
150
149
@@ -190,24 +189,23 b' define(['
190
189
191 this.element.find('#cell_type').change(function () {
190 this.element.find('#cell_type').change(function () {
192 var cell_type = $(this).val();
191 var cell_type = $(this).val();
193 if (cell_type === 'code') {
192 switch (cell_type) {
193 case 'code':
194 that.notebook.to_code();
194 that.notebook.to_code();
195 } else if (cell_type === 'markdown') {
195 break;
196 case 'markdown':
196 that.notebook.to_markdown();
197 that.notebook.to_markdown();
197 } else if (cell_type === 'raw') {
198 break;
199 case 'raw':
198 that.notebook.to_raw();
200 that.notebook.to_raw();
199 } else if (cell_type === 'heading1') {
201 break;
200 that.notebook.to_heading(undefined, 1);
202 case 'heading':
201 } else if (cell_type === 'heading2') {
203 that.notebook._warn_heading();
202 that.notebook.to_heading(undefined, 2);
204 that.notebook.to_heading();
203 } else if (cell_type === 'heading3') {
205 that.element.find('#cell_type').val("markdown");
204 that.notebook.to_heading(undefined, 3);
206 break;
205 } else if (cell_type === 'heading4') {
207 default:
206 that.notebook.to_heading(undefined, 4);
208 console.log("unrecognized cell type:", cell_type);
207 } else if (cell_type === 'heading5') {
208 that.notebook.to_heading(undefined, 5);
209 } else if (cell_type === 'heading6') {
210 that.notebook.to_heading(undefined, 6);
211 }
209 }
212 });
210 });
213 this.events.on('selected_cell_type_changed.Notebook', function (event, data) {
211 this.events.on('selected_cell_type_changed.Notebook', function (event, data) {
@@ -2,36 +2,41 b''
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
6 'jquery',
5 'jquery',
6 'base/js/namespace',
7 'base/js/dialog',
7 'base/js/utils',
8 'base/js/utils',
8 'notebook/js/tour',
9 'notebook/js/tour',
9 'bootstrap',
10 'bootstrap',
10 'moment',
11 'moment',
11 ], function(IPython, $, utils, tour, bootstrap, moment) {
12 ], function($, IPython, dialog, utils, tour, bootstrap, moment) {
12 "use strict";
13 "use strict";
13
14
14 var MenuBar = function (selector, options) {
15 var MenuBar = function (selector, options) {
15 // Constructor
16 /**
16 //
17 * Constructor
17 // A MenuBar Class to generate the menubar of IPython notebook
18 *
18 //
19 * A MenuBar Class to generate the menubar of IPython notebook
19 // Parameters:
20 *
20 // selector: string
21 * Parameters:
21 // options: dictionary
22 * selector: string
22 // Dictionary of keyword arguments.
23 * options: dictionary
23 // notebook: Notebook instance
24 * Dictionary of keyword arguments.
24 // layout_manager: LayoutManager instance
25 * notebook: Notebook instance
25 // events: $(Events) instance
26 * contents: ContentManager instance
26 // save_widget: SaveWidget instance
27 * layout_manager: LayoutManager instance
27 // quick_help: QuickHelp instance
28 * events: $(Events) instance
28 // base_url : string
29 * save_widget: SaveWidget instance
29 // notebook_path : string
30 * quick_help: QuickHelp instance
30 // notebook_name : string
31 * base_url : string
32 * notebook_path : string
33 * notebook_name : string
34 */
31 options = options || {};
35 options = options || {};
32 this.base_url = options.base_url || utils.get_body_data("baseUrl");
36 this.base_url = options.base_url || utils.get_body_data("baseUrl");
33 this.selector = selector;
37 this.selector = selector;
34 this.notebook = options.notebook;
38 this.notebook = options.notebook;
39 this.contents = options.contents;
35 this.layout_manager = options.layout_manager;
40 this.layout_manager = options.layout_manager;
36 this.events = options.events;
41 this.events = options.events;
37 this.save_widget = options.save_widget;
42 this.save_widget = options.save_widget;
@@ -66,33 +71,52 b' define(['
66 MenuBar.prototype._nbconvert = function (format, download) {
71 MenuBar.prototype._nbconvert = function (format, download) {
67 download = download || false;
72 download = download || false;
68 var notebook_path = this.notebook.notebook_path;
73 var notebook_path = this.notebook.notebook_path;
69 var notebook_name = this.notebook.notebook_name;
70 if (this.notebook.dirty) {
71 this.notebook.save_notebook({async : false});
72 }
73 var url = utils.url_join_encode(
74 var url = utils.url_join_encode(
74 this.base_url,
75 this.base_url,
75 'nbconvert',
76 'nbconvert',
76 format,
77 format,
77 notebook_path,
78 notebook_path
78 notebook_name
79 ) + "?download=" + download.toString();
79 ) + "?download=" + download.toString();
80
80
81 window.open(url);
81 var w = window.open()
82 if (this.notebook.dirty) {
83 this.notebook.save_notebook().then(function() {
84 w.location = url;
85 });
86 } else {
87 w.location = url;
88 }
82 };
89 };
83
90
84 MenuBar.prototype.bind_events = function () {
91 MenuBar.prototype.bind_events = function () {
85 // File
92 /**
93 * File
94 */
86 var that = this;
95 var that = this;
87 this.element.find('#new_notebook').click(function () {
96 this.element.find('#new_notebook').click(function () {
88 that.notebook.new_notebook();
97 var w = window.open();
98 // Create a new notebook in the same path as the current
99 // notebook's path.
100 var parent = utils.url_path_split(that.notebook.notebook_path)[0];
101 that.contents.new_untitled(parent, {type: "notebook"}).then(
102 function (data) {
103 w.location = utils.url_join_encode(
104 that.base_url, 'notebooks', data.path
105 );
106 },
107 function(error) {
108 w.close();
109 dialog.modal({
110 title : 'Creating Notebook Failed',
111 body : "The error was: " + error.message,
112 buttons : {'OK' : {'class' : 'btn-primary'}}
113 });
114 }
115 );
89 });
116 });
90 this.element.find('#open_notebook').click(function () {
117 this.element.find('#open_notebook').click(function () {
91 window.open(utils.url_join_encode(
118 var parent = utils.url_path_split(that.notebook.notebook_path)[0];
92 that.notebook.base_url,
119 window.open(utils.url_join_encode(that.base_url, 'tree', parent));
93 'tree',
94 that.notebook.notebook_path
95 ));
96 });
120 });
97 this.element.find('#copy_notebook').click(function () {
121 this.element.find('#copy_notebook').click(function () {
98 that.notebook.copy_notebook();
122 that.notebook.copy_notebook();
@@ -101,28 +125,18 b' define(['
101 this.element.find('#download_ipynb').click(function () {
125 this.element.find('#download_ipynb').click(function () {
102 var base_url = that.notebook.base_url;
126 var base_url = that.notebook.base_url;
103 var notebook_path = that.notebook.notebook_path;
127 var notebook_path = that.notebook.notebook_path;
104 var notebook_name = that.notebook.notebook_name;
105 if (that.notebook.dirty) {
128 if (that.notebook.dirty) {
106 that.notebook.save_notebook({async : false});
129 that.notebook.save_notebook({async : false});
107 }
130 }
108
131
109 var url = utils.url_join_encode(
132 var url = utils.url_join_encode(base_url, 'files', notebook_path);
110 base_url,
133 window.open(url + '?download=1');
111 'files',
112 notebook_path,
113 notebook_name
114 );
115 window.location.assign(url);
116 });
134 });
117
135
118 this.element.find('#print_preview').click(function () {
136 this.element.find('#print_preview').click(function () {
119 that._nbconvert('html', false);
137 that._nbconvert('html', false);
120 });
138 });
121
139
122 this.element.find('#download_py').click(function () {
123 that._nbconvert('python', true);
124 });
125
126 this.element.find('#download_html').click(function () {
140 this.element.find('#download_html').click(function () {
127 that._nbconvert('html', true);
141 that._nbconvert('html', true);
128 });
142 });
@@ -159,7 +173,9 b' define(['
159 });
173 });
160 this.element.find('#kill_and_exit').click(function () {
174 this.element.find('#kill_and_exit').click(function () {
161 var close_window = function () {
175 var close_window = function () {
162 // allow closing of new tabs in Chromium, impossible in FF
176 /**
177 * allow closing of new tabs in Chromium, impossible in FF
178 */
163 window.open('', '_self', '');
179 window.open('', '_self', '');
164 window.close();
180 window.close();
165 };
181 };
@@ -246,24 +262,6 b' define(['
246 this.element.find('#to_raw').click(function () {
262 this.element.find('#to_raw').click(function () {
247 that.notebook.to_raw();
263 that.notebook.to_raw();
248 });
264 });
249 this.element.find('#to_heading1').click(function () {
250 that.notebook.to_heading(undefined, 1);
251 });
252 this.element.find('#to_heading2').click(function () {
253 that.notebook.to_heading(undefined, 2);
254 });
255 this.element.find('#to_heading3').click(function () {
256 that.notebook.to_heading(undefined, 3);
257 });
258 this.element.find('#to_heading4').click(function () {
259 that.notebook.to_heading(undefined, 4);
260 });
261 this.element.find('#to_heading5').click(function () {
262 that.notebook.to_heading(undefined, 5);
263 });
264 this.element.find('#to_heading6').click(function () {
265 that.notebook.to_heading(undefined, 6);
266 });
267
265
268 this.element.find('#toggle_current_output').click(function () {
266 this.element.find('#toggle_current_output').click(function () {
269 that.notebook.toggle_output();
267 that.notebook.toggle_output();
@@ -287,11 +285,14 b' define(['
287
285
288 // Kernel
286 // Kernel
289 this.element.find('#int_kernel').click(function () {
287 this.element.find('#int_kernel').click(function () {
290 that.notebook.session.interrupt_kernel();
288 that.notebook.kernel.interrupt();
291 });
289 });
292 this.element.find('#restart_kernel').click(function () {
290 this.element.find('#restart_kernel').click(function () {
293 that.notebook.restart_kernel();
291 that.notebook.restart_kernel();
294 });
292 });
293 this.element.find('#reconnect_kernel').click(function () {
294 that.notebook.kernel.reconnect();
295 });
295 // Help
296 // Help
296 if (this.tour) {
297 if (this.tour) {
297 this.element.find('#notebook_tour').click(function () {
298 this.element.find('#notebook_tour').click(function () {
@@ -313,6 +314,16 b' define(['
313 this.events.on('checkpoint_created.Notebook', function (event, data) {
314 this.events.on('checkpoint_created.Notebook', function (event, data) {
314 that.update_restore_checkpoint(that.notebook.checkpoints);
315 that.update_restore_checkpoint(that.notebook.checkpoints);
315 });
316 });
317
318 this.events.on('notebook_loaded.Notebook', function() {
319 var langinfo = that.notebook.metadata.language_info || {};
320 that.update_nbconvert_script(langinfo);
321 });
322
323 this.events.on('kernel_ready.Kernel', function(event, data) {
324 var langinfo = data.kernel.info_reply.language_info || {};
325 that.update_nbconvert_script(langinfo);
326 });
316 };
327 };
317
328
318 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
329 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
@@ -345,6 +356,33 b' define(['
345 );
356 );
346 });
357 });
347 };
358 };
359
360 MenuBar.prototype.update_nbconvert_script = function(langinfo) {
361 /**
362 * Set the 'Download as foo' menu option for the relevant language.
363 */
364 var el = this.element.find('#download_script');
365 var that = this;
366
367 // Set menu entry text to e.g. "Python (.py)"
368 var langname = (langinfo.name || 'Script')
369 langname = langname.charAt(0).toUpperCase()+langname.substr(1) // Capitalise
370 el.find('a').text(langname + ' ('+(langinfo.file_extension || 'txt')+')');
371
372 // Unregister any previously registered handlers
373 el.off('click');
374 if (langinfo.nbconvert_exporter) {
375 // Metadata specifies a specific exporter, e.g. 'python'
376 el.click(function() {
377 that._nbconvert(langinfo.nbconvert_exporter, true);
378 });
379 } else {
380 // Use generic 'script' exporter
381 el.click(function() {
382 that._nbconvert('script', true);
383 });
384 }
385 };
348
386
349 // Backwards compatability.
387 // Backwards compatability.
350 IPython.MenuBar = MenuBar;
388 IPython.MenuBar = MenuBar;
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from IPython/html/static/services/kernels/js/comm.js to IPython/html/static/services/kernels/comm.js
NO CONTENT: file renamed from IPython/html/static/services/kernels/js/comm.js to IPython/html/static/services/kernels/comm.js
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from IPython/html/fabfile.py to IPython/html/tasks.py
NO CONTENT: file renamed from IPython/html/fabfile.py to IPython/html/tasks.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from IPython/nbformat/convert.py to IPython/nbformat/converter.py
NO CONTENT: file renamed from IPython/nbformat/convert.py to IPython/nbformat/converter.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from IPython/nbformat/v4/v4.withref.json to IPython/nbformat/v3/nbformat.v3.schema.json
NO CONTENT: file renamed from IPython/nbformat/v4/v4.withref.json to IPython/nbformat/v3/nbformat.v3.schema.json
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now