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 |
- |
|
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 |
|
56 | RUN pip2 install file:///srv/ipython#egg=ipython[all] | |
41 |
RUN pip3 install |
|
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 = |
|
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 |
|
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 |
|
|
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 |
|
|
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( |
|
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 |
|
233 | except NotImplementedError: | |
235 | # don't warn on NotImplementedErrors |
|
234 | # don't warn on NotImplementedErrors | |
236 | return None |
|
235 | return None | |
237 |
except Exception |
|
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, |
|
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 |
|
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+\]: |\ |
|
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.History |
|
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( |
|
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 |
|
|
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 |
|
2678 | from IPython.nbformat import read | |
2639 | with open(fname) as f: |
|
2679 | with io_open(fname) as f: | |
2640 |
nb = |
|
2680 | nb = read(f, as_version=4) | |
2641 |
if not nb. |
|
2681 | if not nb.cells: | |
2642 | return |
|
2682 | return | |
2643 |
for cell in nb. |
|
2683 | for cell in nb.cells: | |
2644 | if cell.cell_type == 'code': |
|
2684 | if cell.cell_type == 'code': | |
2645 |
yield cell. |
|
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= |
|
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 |
|
|
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 |
|
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, |
|
604 | for session, execution_count, input in hist[:-1]: | |
635 |
cells.append( |
|
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 control |
|
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 + |
|
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/ |
|
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/ |
|
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_ |
|
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(" |
|
250 | nt.assert_in("Traceback", captured.stdout) | |
247 |
nt.assert_in(" |
|
251 | nt.assert_in("Bad HTML", captured.stdout) | |
248 |
nt.assert_in(" |
|
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_ |
|
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(" |
|
272 | nt.assert_in("Traceback", captured.stdout) | |
269 |
nt.assert_in(" |
|
273 | nt.assert_in("NameError", captured.stdout) | |
270 |
nt.assert_in("name_error", captured.std |
|
274 | nt.assert_in("name_error", captured.stdout) | |
271 |
|
275 | |||
272 |
def test_ |
|
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(" |
|
285 | nt.assert_in("Traceback", captured.stdout) | |
282 |
nt.assert_in(" |
|
286 | nt.assert_in("_repr_pretty_", captured.stdout) | |
283 |
nt.assert_in(" |
|
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. |
|
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 |
|
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_ |
|
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 |
|
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 |
|
376 | from IPython.nbformat import v4, writes | |
379 |
nb = |
|
377 | nb = v4.new_notebook( | |
380 |
|
|
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 = |
|
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 |
|
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 |
|
|
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 'pyqt |
|
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 ` |
|
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 ` |
|
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 " |
|
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 |
|
|
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 = |
|
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. |
|
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( |
|
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 |
|
487 | path = '/'.join(parts) | |
433 |
|
488 | |||
434 |
if not cm.file_exists( |
|
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 |
|
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 |
|
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 |
|
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, |
|
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. |
|
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 |
|
|
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 |
|
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 |
|
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] + |
|
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 |
|
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 |
|
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] + |
|
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, |
|
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 |
|
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( |
|
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, |
|
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 |
|
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( |
|
27 | if not cm.file_exists(path): | |
30 |
raise web.HTTPError(404, u'Notebook does not exist: %s |
|
28 | raise web.HTTPError(404, u'Notebook does not exist: %s' % path) | |
31 |
name = url_escape( |
|
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 >= |
|
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 < ( |
|
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 |
|
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, |
|
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, |
|
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 |
|
|
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, { |
|
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 |
f |
|
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, |
|
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 = |
|
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'" |
|
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 |
d |
|
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 |
|
|
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 |
|
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, |
|
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( |
|
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, |
|
103 | def _get_os_path(self, path): | |
65 |
"""Given a |
|
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 |
|
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 |
|
|
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, |
|
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 |
|
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 |
|
|
172 | os_path = self._get_os_path(path) | |
142 |
return os.path.isfile( |
|
173 | return os.path.isfile(os_path) | |
143 |
|
174 | |||
144 |
def exists(self, |
|
175 | def exists(self, path): | |
145 |
"""Returns True if the path |
|
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 |
|
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( |
|
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, |
|
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( |
|
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'] = |
|
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, |
|
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( |
|
221 | os_path = self._get_os_path(path) | |
187 |
|
222 | |||
188 |
four_o_four = u'directory does not exist: %r' % |
|
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 |
|
|
237 | os_dir = self._get_os_path(path) | |
209 |
|
|
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 |
|
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, |
|
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( |
|
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, |
|
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( |
|
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( |
|
305 | os_path = self._get_os_path(path) | |
253 |
with |
|
306 | with self.open(os_path, 'r', encoding='utf-8') as f: | |
254 | try: |
|
307 | try: | |
255 |
nb = |
|
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 % |
|
310 | raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e)) | |
258 |
self.mark_trusted_cells(nb, |
|
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 |
|
317 | def get(self, path, content=True, type_=None, format=None): | |
264 |
""" Takes a path |
|
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( |
|
341 | if not self.exists(path): | |
282 |
raise web.HTTPError(404, u'No such file or directory: %s |
|
342 | raise web.HTTPError(404, u'No such file or directory: %s' % path) | |
283 |
|
343 | |||
284 |
os_path = self._get_os_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, |
|
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 = |
|
362 | nb = nbformat.from_dict(model['content']) | |
297 |
|
363 | |||
298 |
self.check_and_sign(nb, |
|
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, |
|
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, |
|
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( |
|
408 | if self.file_exists(path) and not self.list_checkpoints(path): | |
345 |
self.create_checkpoint( |
|
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, |
|
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, |
|
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, |
|
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 |
|
|
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 |
|
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, |
|
438 | def update(self, model, path): | |
373 |
"""Update the file's path |
|
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 |
|
446 | if path != new_path: | |
382 |
self.rename( |
|
447 | self.rename(path, new_path) | |
383 |
model = self.get |
|
448 | model = self.get(new_path, content=False) | |
384 | return model |
|
449 | return model | |
385 |
|
450 | |||
386 |
def delete(self, |
|
451 | def delete(self, path): | |
387 |
"""Delete file |
|
452 | """Delete file at path.""" | |
388 | path = path.strip('/') |
|
453 | path = path.strip('/') | |
389 |
os_path = self._get_os_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( |
|
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, |
|
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, |
|
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 |
|
486 | if new_path == old_path: | |
419 | return |
|
487 | return | |
420 |
|
488 | |||
421 |
new_os_path = self._get_os_path( |
|
489 | new_os_path = self._get_os_path(new_path) | |
422 |
old_os_path = self._get_os_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. |
|
493 | if os.path.exists(new_os_path): | |
426 |
raise web.HTTPError(409, u'File |
|
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_ |
|
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( |
|
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, |
|
509 | old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path) | |
439 |
new_cp_path = self.get_checkpoint_path(checkpoint_id, |
|
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, |
|
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=pat |
|
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, |
|
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, |
|
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, |
|
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, |
|
558 | cp_path = self.get_checkpoint_path(checkpoint_id, path) | |
482 |
self.log.debug("creating checkpoint for %s", |
|
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, |
|
564 | return self.get_checkpoint_model(checkpoint_id, path) | |
487 |
|
565 | |||
488 |
def list_checkpoints(self, |
|
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, |
|
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, |
|
577 | return [self.get_checkpoint_model(checkpoint_id, path)] | |
500 |
|
578 | |||
501 |
|
579 | |||
502 |
def restore_checkpoint(self, checkpoint_id, |
|
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", |
|
583 | self.log.info("restoring %s from checkpoint %s", path, checkpoint_id) | |
506 |
nb_path = self._get_os_path( |
|
584 | nb_path = self._get_os_path(path) | |
507 |
cp_path = self.get_checkpoint_path(checkpoint_id, |
|
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 |
|
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 |
|
593 | with self.open(cp_path, 'r', encoding='utf-8') as f: | |
516 |
|
|
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, |
|
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, |
|
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 |
|
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, |
|
613 | def get_kernel_path(self, path, model=None): | |
535 |
"""Return the initial |
|
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 ( |
|
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, |
|
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 |
|
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[' |
|
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='' |
|
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='' |
|
78 | def patch(self, path=''): | |
73 |
"""PATCH renames a |
|
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, |
|
84 | model = cm.update(model, path) | |
81 | self._finish_model(model) |
|
85 | self._finish_model(model) | |
82 |
|
86 | |||
83 |
def _copy(self, copy_from, |
|
87 | def _copy(self, copy_from, copy_to=None): | |
84 |
"""Copy a file, optionally specifying |
|
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 |
|
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 |
|
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 _ |
|
104 | def _new_untitled(self, path, type='', ext=''): | |
110 |
"""Create an empty |
|
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 |
|
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 |
|
113 | self.log.info(u"Saving file at %s", path) | |
125 |
model = self.contents_manager.save(model, |
|
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='' |
|
119 | def post(self, path=''): | |
136 |
"""Create a new file |
|
120 | """Create a new file in the specified path. | |
137 |
|
121 | |||
138 |
POST creates new files |
|
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 |
|
134 | raise web.HTTPError(400, "Cannot POST to files, use PUT instead.") | |
155 |
|
135 | |||
156 |
if not cm. |
|
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', ' |
|
143 | ext = model.get('ext', '') | |
164 |
|
|
144 | type = model.get('type', '') | |
165 |
|
|
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._ |
|
148 | self._new_untitled(path, type=type, ext=ext) | |
172 | else: |
|
149 | else: | |
173 |
self._ |
|
150 | self._new_untitled(path) | |
174 |
|
151 | |||
175 | @web.authenticated |
|
152 | @web.authenticated | |
176 | @json_errors |
|
153 | @json_errors | |
177 |
def put(self, path='' |
|
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 |
|
|
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 |
|
172 | self._upload(model, path) | |
210 | else: |
|
173 | else: | |
211 |
self._ |
|
174 | self._new_untitled(path) | |
212 |
|
175 | |||
213 | @web.authenticated |
|
176 | @web.authenticated | |
214 | @json_errors |
|
177 | @json_errors | |
215 |
def delete(self, path='' |
|
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 |
|
181 | self.log.warn('delete %s', path) | |
219 |
cm.delete( |
|
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='' |
|
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( |
|
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='' |
|
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( |
|
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 |
|
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, |
|
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, |
|
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, |
|
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, |
|
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" % |
|
260 | (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler), | |
282 |
(r"/api/contents%s/checkpoints/%s" % ( |
|
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 |
|
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 |
|
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, |
|
102 | def file_exists(self, path=''): | |
107 |
"""Does a file exist at the given |
|
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, |
|
123 | def exists(self, path): | |
128 |
"""Does a file or directory exist at the given |
|
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( |
|
138 | return self.file_exists(path) or self.dir_exists(path) | |
145 |
|
139 | |||
146 |
def get |
|
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, |
|
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, |
|
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, |
|
156 | def delete(self, path): | |
163 |
"""Delete file or directory by |
|
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, |
|
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, |
|
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, |
|
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, |
|
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, |
|
185 | def get_kernel_path(self, path, model=None): | |
192 |
""" |
|
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 |
|
|
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 |
|
|
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_ |
|
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_ |
|
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 |
|
|
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 |
|
309 | if '/' in path: | |
259 |
from_ |
|
310 | from_dir, from_name = path.rsplit('/', 1) | |
260 | else: |
|
311 | else: | |
261 |
from_ |
|
312 | from_dir = '' | |
262 |
|
|
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, |
|
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 |
|
340 | The path of a notebook | |
286 | """ |
|
341 | """ | |
287 |
model = self.get |
|
342 | model = self.get(path) | |
288 | nb = model['content'] |
|
343 | nb = model['content'] | |
289 |
self.log.warn("Trusting notebook %s |
|
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, |
|
346 | self.save(model, path) | |
292 |
|
347 | |||
293 |
def check_and_sign(self, nb, |
|
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 |
|
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 |
|
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 |
|
363 | self.log.warn("Saving untrusted notebook %s", path) | |
311 |
|
364 | |||
312 |
def mark_trusted_cells(self, nb, |
|
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 |
|
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 |
|
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 |
|
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 |
|
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, |
|
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= |
|
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 |
|
63 | def mkdir_untitled(self, path='/'): | |
58 |
return self._req('POST', path, |
|
64 | return self._req('POST', path, json.dumps({'type': 'directory'})) | |
59 |
|
65 | |||
60 |
def copy |
|
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 |
|
70 | def create(self, path='/'): | |
65 |
return self._req('PUT', |
|
71 | return self._req('PUT', path) | |
|
72 | ||||
|
73 | def upload(self, path, body): | |||
|
74 | return self._req('PUT', path, body) | |||
66 |
|
75 | |||
67 |
def |
|
76 | def mkdir_untitled(self, path='/'): | |
68 |
return self._req('P |
|
77 | return self._req('POST', path, json.dumps({'type': 'directory'})) | |
69 |
|
78 | |||
70 |
def mkdir(self |
|
79 | def mkdir(self, path='/'): | |
71 |
return self._req('PUT', |
|
80 | return self._req('PUT', path, json.dumps({'type': 'directory'})) | |
72 |
|
81 | |||
73 |
def copy(self, copy_from |
|
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', |
|
84 | return self._req('PUT', path, body) | |
76 |
|
85 | |||
77 |
def save(self, |
|
86 | def save(self, path, body): | |
78 |
return self._req('PUT', |
|
87 | return self._req('PUT', path, body) | |
79 |
|
88 | |||
80 |
def delete(self |
|
89 | def delete(self, path='/'): | |
81 |
return self._req('DELETE', |
|
90 | return self._req('DELETE', path) | |
82 |
|
91 | |||
83 |
def rename(self, |
|
92 | def rename(self, path, new_path): | |
84 |
body = json.dumps({' |
|
93 | body = json.dumps({'path': new_path}) | |
85 |
return self._req('PATCH', |
|
94 | return self._req('PATCH', path, body) | |
86 |
|
95 | |||
87 |
def get_checkpoints(self, |
|
96 | def get_checkpoints(self, path): | |
88 |
return self._req('GET', url_path_join(path, |
|
97 | return self._req('GET', url_path_join(path, 'checkpoints')) | |
89 |
|
98 | |||
90 |
def new_checkpoint(self, |
|
99 | def new_checkpoint(self, path): | |
91 |
return self._req('POST', url_path_join(path, |
|
100 | return self._req('POST', url_path_join(path, 'checkpoints')) | |
92 |
|
101 | |||
93 |
def restore_checkpoint(self, |
|
102 | def restore_checkpoint(self, path, checkpoint_id): | |
94 |
return self._req('POST', url_path_join(path, |
|
103 | return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id)) | |
95 |
|
104 | |||
96 |
def delete_checkpoint(self, |
|
105 | def delete_checkpoint(self, path, checkpoint_id): | |
97 |
return self._req('DELETE', url_path_join(path, |
|
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( |
|
152 | nb = new_notebook() | |
146 |
write(nb, f, |
|
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 |
|
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 |
|
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 |
|
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 |
|
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'], |
|
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, |
|
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, |
|
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, 'Untitled |
|
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, 'untitled |
|
306 | self._check_created(resp, 'foo/bar/untitled.txt', type='file') | |
281 |
|
307 | |||
282 |
resp = self.api.read(path='foo/bar |
|
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( |
|
315 | nb = new_notebook() | |
297 | nbmodel = {'content': nb, 'type': 'notebook'} |
|
316 | nbmodel = {'content': nb, 'type': 'notebook'} | |
298 |
|
|
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, |
|
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 |
|
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 |
|
|
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 |
|
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 |
|
|
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 |
|
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 |
|
|
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, |
|
387 | self._check_created(resp, path) | |
355 |
resp = self.api.read( |
|
388 | resp = self.api.read(path) | |
356 | data = resp.json() |
|
389 | data = resp.json() | |
357 |
self.assertEqual(data['content']['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', |
|
393 | resp = self.api.copy(u'å b/ç d.ipynb', u'å b') | |
366 |
self._check_created(resp, u' |
|
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' |
|
407 | resp = self.api.copy(u'foo/a.ipynb', u'å b') | |
370 |
self._check_created(resp, u' |
|
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' |
|
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[' |
|
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 |
|
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 |
|
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 |
|
466 | resp = self.api.read('foo/a.ipynb') | |
417 | nbcontent = json.loads(resp.text)['content'] |
|
467 | nbcontent = json.loads(resp.text)['content'] | |
418 |
nb = |
|
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= { |
|
471 | nbmodel= {'content': nb, 'type': 'notebook'} | |
424 |
resp = self.api.save('a.ipynb |
|
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, |
|
476 | newnb = read(f, as_version=4) | |
429 |
self.assertEqual(newnb |
|
477 | self.assertEqual(newnb.cells[0].source, | |
430 | u'Created by test ³') |
|
478 | u'Created by test ³') | |
431 |
nbcontent = self.api.read('a.ipynb |
|
479 | nbcontent = self.api.read('foo/a.ipynb').json()['content'] | |
432 |
newnb = |
|
480 | newnb = from_dict(nbcontent) | |
433 |
self.assertEqual(newnb |
|
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 |
|
486 | resp = self.api.read('foo/a.ipynb') | |
449 |
r = self.api.new_checkpoint('a.ipynb |
|
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 = |
|
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= { |
|
499 | nbmodel= {'content': nb, 'type': 'notebook'} | |
464 |
resp = self.api.save('a.ipynb |
|
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 |
|
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 |
|
506 | nbcontent = self.api.read('foo/a.ipynb').json()['content'] | |
471 |
nb = |
|
507 | nb = from_dict(nbcontent) | |
472 |
self.assertEqual(nb |
|
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 |
|
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 |
|
513 | nbcontent = self.api.read('foo/a.ipynb').json()['content'] | |
478 |
nb = |
|
514 | nb = from_dict(nbcontent) | |
479 |
self.assertEqual(nb. |
|
515 | self.assertEqual(nb.cells, []) | |
480 |
|
516 | |||
481 | # Delete cp1 |
|
517 | # Delete cp1 | |
482 |
r = self.api.delete_checkpoint('a.ipynb |
|
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 |
|
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 |
|
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(' |
|
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', ' |
|
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 = |
|
98 | output = nbformat.new_output("display_data", {'application/javascript': "alert('hi');"}) | |
99 |
cell = |
|
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 |
|
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, |
|
112 | cm.save(full_model, path) | |
115 | return nb, name, path |
|
113 | return nb, name, path | |
116 |
|
114 | |||
117 |
def test_ |
|
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[' |
|
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[' |
|
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 |
|
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 |
|
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'], 'Untitled |
|
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. |
|
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 |
|
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 |
pat |
|
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. |
|
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, |
|
215 | os.symlink(file_model['name'], os.path.join(os_path, name)) | |
187 |
symlink_model = cm.get |
|
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[' |
|
231 | model['path'] = 'test.ipynb' | |
204 |
model = cm.update(model, |
|
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 |
|
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'], |
|
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 |
|
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 |
|
270 | full_model = cm.get(path) | |
242 |
|
271 | |||
243 | # Save the notebook |
|
272 | # Save the notebook | |
244 |
model = cm.save(full_model, |
|
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 |
|
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, |
|
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'], 'Untitled |
|
294 | self.assertEqual(model['name'], 'Untitled.ipynb') | |
266 |
self.assertEqual(model['path'], |
|
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( |
|
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 |
|
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 |
pat |
|
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( |
|
317 | copy = cm.copy(path) | |
288 |
self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy |
|
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( |
|
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 |
|
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( |
|
337 | cm.trust_notebook(path) | |
303 |
trusted = cm.get |
|
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, |
|
346 | cm.mark_trusted_cells(nb, path) | |
312 |
for cell in nb. |
|
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( |
|
351 | cm.trust_notebook(path) | |
317 |
nb = cm.get |
|
352 | nb = cm.get(path)['content'] | |
318 |
for cell in nb. |
|
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, |
|
361 | cm.mark_trusted_cells(nb, path) | |
327 |
cm.check_and_sign(nb, |
|
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( |
|
365 | cm.trust_notebook(path) | |
331 |
nb = cm.get |
|
366 | nb = cm.get(path)['content'] | |
332 |
cm.mark_trusted_cells(nb, |
|
367 | cm.mark_trusted_cells(nb, path) | |
333 |
cm.check_and_sign(nb, |
|
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 |
|
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 |
|
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. |
|
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. |
|
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 |
|
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 |
|
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( |
|
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. |
|
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[' |
|
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[' |
|
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( |
|
49 | if sm.session_exists(path=path): | |
52 |
model = sm.get_session( |
|
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', ' |
|
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, |
|
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, |
|
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 |
|
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, |
|
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( |
|
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, |
|
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 |
|
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 |
|
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 |
|
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, |
|
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( |
|
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':{' |
|
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( |
|
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( |
|
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( |
|
64 | sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'), | |
65 |
sm.create_session( |
|
65 | sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'), | |
66 |
sm.create_session( |
|
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 |
|
|
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( |
|
89 | sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'), | |
81 |
sm.create_session( |
|
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 |
' |
|
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( |
|
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, |
|
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':{' |
|
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( |
|
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( |
|
130 | sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'), | |
123 |
sm.create_session( |
|
131 | sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'), | |
124 |
sm.create_session( |
|
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 |
|
|
138 | 'notebook': {'path': u'/path/to/1/test1.ipynb'}, | |
131 |
|
|
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( |
|
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. |
|
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 |
|
41 | def create(self, path, kernel_name='python'): | |
41 |
body = json.dumps({'notebook': {' |
|
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, |
|
46 | def modify(self, id, path): | |
46 |
body = json.dumps({'notebook': {' |
|
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( |
|
66 | nb = new_notebook() | |
66 |
write(nb, f, |
|
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 |
|
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'][' |
|
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 |
|
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 |
|
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' |
|
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'][' |
|
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 |
/ |
|
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 |
|
|
232 | var ftree = flatten_shorttree(this._shortcuts); | |
139 | var help_string = this._shortcuts[shortcut].help; |
|
233 | for (var shortcut in ftree) { | |
140 |
var |
|
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: short |
|
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. |
|
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 |
|
|
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 |
|
|
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 |
|
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 compat |
|
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 |
|
|
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 |
} |
|
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 |
|
|
97 | that.element.unbind('click'); | |
70 |
|
|
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 |
|
|
114 | options = options || {}; | |
80 | options.class = options.class +' info'; |
|
115 | options.class = options.class + ' info'; | |
81 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
278 | if ( !attrs.style ) { | |
276 |
attrs |
|
279 | attrs.style = line; | |
277 | } else { |
|
280 | } else { | |
278 |
attrs |
|
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 |
|
|
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 |
|
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 |
|
|
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 |
/ |
|
26 | /* Constructor | |
33 |
|
|
27 | * | |
34 |
|
|
28 | * The Base `Cell` class from which to inherit. | |
35 | // |
|
29 | * @constructor | |
36 |
|
|
30 | * @param: | |
37 |
|
|
31 | * options: dictionary | |
38 |
|
|
32 | * Dictionary of keyword arguments. | |
39 |
|
|
33 | * events: $(Events) instance | |
40 |
|
|
34 | * config: dictionary | |
41 |
|
|
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)) { |
|
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 |
|
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 t |
|
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 t |
|
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 |
t |
|
564 | utils.requireCodeMirrorMode(mode, function () { | |
510 |
|
|
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 sw |
|
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 |
|
|
582 | CodeMirror.getMode(config, 'text/plain'), | |
527 | CodeMirror.patchedGetMode(config, 'text/plain'), |
|
583 | // always set something on close | |
528 |
|
|
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 |
|
|
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 { |
|
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 |
|
|
18 | * events: $(Events) instance | |
19 |
|
|
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 |
|
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 |
|
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 |
|
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 |
/ |
|
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 |
|
|
69 | * keyboard_manager: KeyboardManager instance | |
58 |
|
|
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 = |
|
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. |
|
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. |
|
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 |
|
|
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 = " "; |
|
458 | ns = " "; | |
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. |
|
514 | if (data.source !== undefined) { | |
464 |
this.set_text(data. |
|
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. |
|
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. |
|
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. |
|
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 |
|
|
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 |
}, |
|
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(' |
|
86 | events.on('kernel_busy.Kernel', function () { | |
86 | that.skip_kernel_completion = true; |
|
87 | that.skip_kernel_completion = true; | |
87 | }); |
|
88 | }); | |
88 |
events.on(' |
|
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 |
|
|
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 |
|
|
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 |
|
|
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 = n |
|
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(' |
|
79 | this.events.on('kernel_created.Session', function(event, data) { | |
80 |
if ( |
|
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[ |
|
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 |
|
|
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 = |
|
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. |
|
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','heading |
|
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 |
|
|
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( |
|
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, |
|
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 |
|
|
25 | * notebook: Notebook instance | |
25 |
|
|
26 | * contents: ContentManager instance | |
26 |
|
|
27 | * layout_manager: LayoutManager instance | |
27 |
|
|
28 | * events: $(Events) instance | |
28 | // base_url : string |
|
29 | * save_widget: SaveWidget instance | |
29 | // notebook_path : string |
|
30 | * quick_help: QuickHelp instance | |
30 |
|
|
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( |
|
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 |
/ |
|
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. |
|
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