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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 |
|
1 | NO CONTENT: new file 100644, binary diff hidden |
|
1 | 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 |
|
1 | 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 |
|
1 | 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 |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
@@ -5,6 +5,7 b' _build' | |||
|
5 | 5 | docs/man/*.gz |
|
6 | 6 | docs/source/api/generated |
|
7 | 7 | docs/source/config/options |
|
8 | docs/source/interactive/magics-generated.txt | |
|
8 | 9 | docs/gh-pages |
|
9 | 10 | IPython/html/notebook/static/mathjax |
|
10 | 11 | IPython/html/static/style/*.map |
@@ -16,3 +17,6 b' __pycache__' | |||
|
16 | 17 | .ipynb_checkpoints |
|
17 | 18 | .tox |
|
18 | 19 | .DS_Store |
|
20 | \#*# | |
|
21 | .#* | |
|
22 | .coverage |
@@ -11,14 +11,15 b' before_install:' | |||
|
11 | 11 | # workaround for https://github.com/travis-ci/travis-cookbooks/issues/155 |
|
12 | 12 | - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm |
|
13 | 13 | # Pierre Carrier's PPA for PhantomJS and CasperJS |
|
14 |
- |
|
|
15 | - time sudo apt-get update | |
|
16 | - time sudo apt-get install pandoc casperjs libzmq3-dev | |
|
17 | # pin tornado < 4 for js tests while phantom is on super old webkit | |
|
18 | - if [[ $GROUP == 'js' ]]; then pip install 'tornado<4'; fi | |
|
19 | - time pip install -f https://nipy.bic.berkeley.edu/wheelhouse/travis jinja2 sphinx pygments tornado requests mock pyzmq jsonschema jsonpointer mistune | |
|
14 | - sudo add-apt-repository -y ppa:pcarrier/ppa | |
|
15 | # Needed to get recent version of pandoc in ubntu 12.04 | |
|
16 | - sudo add-apt-repository -y ppa:marutter/c2d4u | |
|
17 | - sudo apt-get update | |
|
18 | - sudo apt-get install pandoc casperjs libzmq3-dev | |
|
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 | 21 | install: |
|
21 | - time python setup.py install -q | |
|
22 | - pip install -f travis-wheels/wheelhouse file://$PWD#egg=ipython[all] | |
|
22 | 23 | script: |
|
23 | 24 | - cd /tmp && iptest $GROUP |
|
24 | 25 |
@@ -5,13 +5,11 b' FROM ubuntu:14.04' | |||
|
5 | 5 | |
|
6 | 6 | MAINTAINER IPython Project <ipython-dev@scipy.org> |
|
7 | 7 | |
|
8 | # Make sure apt is up to date | |
|
9 | RUN apt-get update | |
|
10 | RUN apt-get upgrade -y | |
|
8 | ENV DEBIAN_FRONTEND noninteractive | |
|
11 | 9 | |
|
12 | 10 | # Not essential, but wise to set the lang |
|
13 | 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 | 13 | ENV LANGUAGE en_US.UTF-8 |
|
16 | 14 | ENV LANG en_US.UTF-8 |
|
17 | 15 | ENV LC_ALL en_US.UTF-8 |
@@ -20,14 +18,32 b' RUN locale-gen en_US.UTF-8' | |||
|
20 | 18 | RUN dpkg-reconfigure locales |
|
21 | 19 | |
|
22 | 20 | # Python binary dependencies, developer tools |
|
23 | RUN apt-get install -y -q build-essential make gcc zlib1g-dev git && \ | |
|
24 | apt-get install -y -q python python-dev python-pip python3-dev python3-pip && \ | |
|
25 | apt-get install -y -q libzmq3-dev sqlite3 libsqlite3-dev pandoc libcurl4-openssl-dev nodejs nodejs-legacy npm | |
|
21 | RUN apt-get update && apt-get install -y -q \ | |
|
22 | build-essential \ | |
|
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 | 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 | 48 | RUN mkdir -p /srv/ |
|
33 | 49 | WORKDIR /srv/ |
@@ -37,10 +53,14 b' RUN chmod -R +rX /srv/ipython' | |||
|
37 | 53 | |
|
38 | 54 | # .[all] only works with -e, so use file://path#egg |
|
39 | 55 | # Can't use -e because ipython2 and ipython3 will clobber each other |
|
40 |
RUN pip2 install |
|
|
41 |
RUN pip3 install |
|
|
56 | RUN pip2 install file:///srv/ipython#egg=ipython[all] | |
|
57 | RUN pip3 install file:///srv/ipython#egg=ipython[all] | |
|
42 | 58 | |
|
43 | 59 | # install kernels |
|
44 | 60 | RUN python2 -m IPython kernelspec install-self --system |
|
45 | 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 | 7 | from __future__ import print_function |
|
8 | 8 | |
|
9 | import json | |
|
9 | 10 | import logging |
|
10 | 11 | import os |
|
11 | 12 | import re |
@@ -123,7 +124,16 b' class Application(SingletonConfigurable):' | |||
|
123 | 124 | |
|
124 | 125 | # A sequence of Configurable subclasses whose config=True attributes will |
|
125 | 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 | 138 | # The version string of this application. |
|
129 | 139 | version = Unicode(u'0.0') |
@@ -256,7 +266,7 b' class Application(SingletonConfigurable):' | |||
|
256 | 266 | |
|
257 | 267 | lines = [] |
|
258 | 268 | classdict = {} |
|
259 | for cls in self.classes: | |
|
269 | for cls in self._help_classes: | |
|
260 | 270 | # include all parents (up to, but excluding Configurable) in available names |
|
261 | 271 | for c in cls.mro()[:-3]: |
|
262 | 272 | classdict[c.__name__] = c |
@@ -331,7 +341,8 b' class Application(SingletonConfigurable):' | |||
|
331 | 341 | self.print_options() |
|
332 | 342 | |
|
333 | 343 | if classes: |
|
334 | if self.classes: | |
|
344 | help_classes = self._help_classes | |
|
345 | if help_classes: | |
|
335 | 346 | print("Class parameters") |
|
336 | 347 | print("----------------") |
|
337 | 348 | print() |
@@ -339,7 +350,7 b' class Application(SingletonConfigurable):' | |||
|
339 | 350 | print(p) |
|
340 | 351 | print() |
|
341 | 352 | |
|
342 |
for cls in |
|
|
353 | for cls in help_classes: | |
|
343 | 354 | cls.class_print_help() |
|
344 | 355 | print() |
|
345 | 356 | else: |
@@ -412,7 +423,7 b' class Application(SingletonConfigurable):' | |||
|
412 | 423 | # it will be a dict by parent classname of classes in our list |
|
413 | 424 | # that are descendents |
|
414 | 425 | mro_tree = defaultdict(list) |
|
415 | for cls in self.classes: | |
|
426 | for cls in self._help_classes: | |
|
416 | 427 | clsname = cls.__name__ |
|
417 | 428 | for parent in cls.mro()[1:-3]: |
|
418 | 429 | # exclude cls itself and Configurable,HasTraits,object |
@@ -491,27 +502,32 b' class Application(SingletonConfigurable):' | |||
|
491 | 502 | |
|
492 | 503 | yield each config object in turn. |
|
493 | 504 | """ |
|
494 | pyloader = PyFileConfigLoader(basefilename+'.py', path=path, log=log) | |
|
495 | jsonloader = JSONFileConfigLoader(basefilename+'.json', path=path, log=log) | |
|
496 | config = None | |
|
497 | for loader in [pyloader, jsonloader]: | |
|
498 | try: | |
|
499 | config = loader.load_config() | |
|
500 | except ConfigFileNotFound: | |
|
501 |
|
|
|
502 | except Exception: | |
|
503 | # try to get the full filename, but it will be empty in the | |
|
504 | # unlikely event that the error raised before filefind finished | |
|
505 | filename = loader.full_filename or basefilename | |
|
506 | # problem while running the file | |
|
507 |
|
|
|
508 | log.error("Exception while loading config file %s", | |
|
509 | filename, exc_info=True) | |
|
510 | else: | |
|
511 | if log: | |
|
512 | log.debug("Loaded config file: %s", loader.full_filename) | |
|
513 | if config: | |
|
514 | yield config | |
|
505 | ||
|
506 | if not isinstance(path, list): | |
|
507 | path = [path] | |
|
508 | for path in path[::-1]: | |
|
509 | # path list is in descending priority order, so load files backwards: | |
|
510 | pyloader = PyFileConfigLoader(basefilename+'.py', path=path, log=log) | |
|
511 | jsonloader = JSONFileConfigLoader(basefilename+'.json', path=path, log=log) | |
|
512 | config = None | |
|
513 | for loader in [pyloader, jsonloader]: | |
|
514 | try: | |
|
515 | config = loader.load_config() | |
|
516 | except ConfigFileNotFound: | |
|
517 | pass | |
|
518 | except Exception: | |
|
519 | # try to get the full filename, but it will be empty in the | |
|
520 | # unlikely event that the error raised before filefind finished | |
|
521 | filename = loader.full_filename or basefilename | |
|
522 | # problem while running the file | |
|
523 | if log: | |
|
524 | log.error("Exception while loading config file %s", | |
|
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 | 532 | raise StopIteration |
|
517 | 533 | |
@@ -520,8 +536,17 b' class Application(SingletonConfigurable):' | |||
|
520 | 536 | def load_config_file(self, filename, path=None): |
|
521 | 537 | """Load config files by filename and path.""" |
|
522 | 538 | filename, ext = os.path.splitext(filename) |
|
539 | loaded = [] | |
|
523 | 540 | for config in self._load_config_files(filename, path=path, log=self.log): |
|
541 | loaded.append(config) | |
|
524 | 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 | 552 | def generate_config_file(self): |
@@ -530,7 +555,7 b' class Application(SingletonConfigurable):' | |||
|
530 | 555 | lines.append('') |
|
531 | 556 | lines.append('c = get_config()') |
|
532 | 557 | lines.append('') |
|
533 | for cls in self.classes: | |
|
558 | for cls in self._config_classes: | |
|
534 | 559 | lines.append(cls.class_config_section()) |
|
535 | 560 | return '\n'.join(lines) |
|
536 | 561 |
@@ -193,7 +193,27 b' class Config(dict):' | |||
|
193 | 193 | to_update[k] = copy.deepcopy(v) |
|
194 | 194 | |
|
195 | 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 | 217 | def __contains__(self, key): |
|
198 | 218 | # allow nested contains of the form `"Section.key" in config` |
|
199 | 219 | if '.' in key: |
@@ -565,7 +585,7 b' class KeyValueConfigLoader(CommandLineConfigLoader):' | |||
|
565 | 585 | |
|
566 | 586 | |
|
567 | 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 | 589 | uargv = [] |
|
570 | 590 | if enc is None: |
|
571 | 591 | enc = DEFAULT_ENCODING |
@@ -1,27 +1,18 b'' | |||
|
1 | 1 | # coding: utf-8 |
|
2 | 2 | """ |
|
3 | 3 | Tests for IPython.config.application.Application |
|
4 | ||
|
5 | Authors: | |
|
6 | ||
|
7 | * Brian Granger | |
|
8 | 4 | """ |
|
9 | 5 | |
|
10 | #----------------------------------------------------------------------------- | |
|
11 | # Copyright (C) 2008-2011 The IPython Development Team | |
|
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 | #----------------------------------------------------------------------------- | |
|
6 | # Copyright (c) IPython Development Team. | |
|
7 | # Distributed under the terms of the Modified BSD License. | |
|
20 | 8 | |
|
21 | 9 | import logging |
|
10 | import os | |
|
22 | 11 | from io import StringIO |
|
23 | 12 | from unittest import TestCase |
|
24 | 13 | |
|
14 | pjoin = os.path.join | |
|
15 | ||
|
25 | 16 | import nose.tools as nt |
|
26 | 17 | |
|
27 | 18 | from IPython.config.configurable import Configurable |
@@ -31,13 +22,11 b' from IPython.config.application import (' | |||
|
31 | 22 | Application |
|
32 | 23 | ) |
|
33 | 24 | |
|
25 | from IPython.utils.tempdir import TemporaryDirectory | |
|
34 | 26 | from IPython.utils.traitlets import ( |
|
35 | 27 | Bool, Unicode, Integer, List, Dict |
|
36 | 28 | ) |
|
37 | 29 | |
|
38 | #----------------------------------------------------------------------------- | |
|
39 | # Code | |
|
40 | #----------------------------------------------------------------------------- | |
|
41 | 30 | |
|
42 | 31 | class Foo(Configurable): |
|
43 | 32 | |
@@ -189,5 +178,21 b' class TestApplication(TestCase):' | |||
|
189 | 178 | def test_unicode_argv(self): |
|
190 | 179 | app = MyApp() |
|
191 | 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 | 1 | # encoding: utf-8 |
|
2 | """ | |
|
3 | Tests for IPython.config.loader | |
|
4 | ||
|
5 | Authors: | |
|
6 | ||
|
7 | * Brian Granger | |
|
8 | * Fernando Perez (design help) | |
|
9 | """ | |
|
2 | """Tests for IPython.config.loader""" | |
|
10 | 3 | |
|
11 | #----------------------------------------------------------------------------- | |
|
12 | # Copyright (C) 2008 The IPython Development Team | |
|
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 | #----------------------------------------------------------------------------- | |
|
4 | # Copyright (c) IPython Development Team. | |
|
5 | # Distributed under the terms of the Modified BSD License. | |
|
21 | 6 | |
|
22 | 7 | import os |
|
23 | 8 | import pickle |
|
24 | 9 | import sys |
|
25 | import json | |
|
26 | 10 | |
|
27 | 11 | from tempfile import mkstemp |
|
28 | 12 | from unittest import TestCase |
@@ -43,10 +27,6 b' from IPython.config.loader import (' | |||
|
43 | 27 | ConfigError, |
|
44 | 28 | ) |
|
45 | 29 | |
|
46 | #----------------------------------------------------------------------------- | |
|
47 | # Actual tests | |
|
48 | #----------------------------------------------------------------------------- | |
|
49 | ||
|
50 | 30 | |
|
51 | 31 | pyfile = """ |
|
52 | 32 | c = get_config() |
@@ -117,6 +97,34 b' class TestFileCL(TestCase):' | |||
|
117 | 97 | cl = JSONFileConfigLoader(fname, log=log) |
|
118 | 98 | config = cl.load_config() |
|
119 | 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 | 129 | def test_v2raise(self): |
|
122 | 130 | fd, fname = mkstemp('.json') |
@@ -7,11 +7,6 b' refactoring of what used to be the IPython/qt/console/qtconsoleapp.py' | |||
|
7 | 7 | # Copyright (c) IPython Development Team. |
|
8 | 8 | # Distributed under the terms of the Modified BSD License. |
|
9 | 9 | |
|
10 | #----------------------------------------------------------------------------- | |
|
11 | # Imports | |
|
12 | #----------------------------------------------------------------------------- | |
|
13 | ||
|
14 | # stdlib imports | |
|
15 | 10 | import atexit |
|
16 | 11 | import os |
|
17 | 12 | import signal |
@@ -19,7 +14,6 b' import sys' | |||
|
19 | 14 | import uuid |
|
20 | 15 | |
|
21 | 16 | |
|
22 | # Local imports | |
|
23 | 17 | from IPython.config.application import boolean_flag |
|
24 | 18 | from IPython.core.profiledir import ProfileDir |
|
25 | 19 | from IPython.kernel.blocking import BlockingKernelClient |
@@ -40,18 +34,9 b' from IPython.kernel.zmq.session import Session, default_secure' | |||
|
40 | 34 | from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell |
|
41 | 35 | from IPython.kernel.connect import ConnectionFileMixin |
|
42 | 36 | |
|
43 | #----------------------------------------------------------------------------- | |
|
44 | # Network Constants | |
|
45 | #----------------------------------------------------------------------------- | |
|
46 | ||
|
47 | 37 | from IPython.utils.localinterfaces import localhost |
|
48 | 38 | |
|
49 | 39 | #----------------------------------------------------------------------------- |
|
50 | # Globals | |
|
51 | #----------------------------------------------------------------------------- | |
|
52 | ||
|
53 | ||
|
54 | #----------------------------------------------------------------------------- | |
|
55 | 40 | # Aliases and Flags |
|
56 | 41 | #----------------------------------------------------------------------------- |
|
57 | 42 | |
@@ -98,11 +83,7 b' aliases.update(app_aliases)' | |||
|
98 | 83 | # Classes |
|
99 | 84 | #----------------------------------------------------------------------------- |
|
100 | 85 | |
|
101 | #----------------------------------------------------------------------------- | |
|
102 | # IPythonConsole | |
|
103 | #----------------------------------------------------------------------------- | |
|
104 | ||
|
105 | classes = [IPKernelApp, ZMQInteractiveShell, KernelManager, ProfileDir, Session, InlineBackend] | |
|
86 | classes = [KernelManager, ProfileDir, Session] | |
|
106 | 87 | |
|
107 | 88 | class IPythonConsoleApp(ConnectionFileMixin): |
|
108 | 89 | name = 'ipython-console-mixin' |
@@ -158,8 +139,15 b' class IPythonConsoleApp(ConnectionFileMixin):' | |||
|
158 | 139 | Set to display confirmation dialog on exit. You can always use 'exit' or 'quit', |
|
159 | 140 | to force a direct exit without any confirmation.""", |
|
160 | 141 | ) |
|
161 | ||
|
162 | ||
|
142 | ||
|
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 | 151 | def build_kernel_argv(self, argv=None): |
|
164 | 152 | """build argv to be passed to kernel subprocess""" |
|
165 | 153 | if argv is None: |
@@ -303,7 +291,11 b' class IPythonConsoleApp(ConnectionFileMixin):' | |||
|
303 | 291 | self.exit(1) |
|
304 | 292 | |
|
305 | 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 | 299 | atexit.register(self.kernel_manager.cleanup_ipc_files) |
|
308 | 300 | |
|
309 | 301 | if self.sshserver: |
@@ -69,6 +69,21 b' def default_aliases():' | |||
|
69 | 69 | # things which are executable |
|
70 | 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 | 87 | else: |
|
73 | 88 | # BSD, OSX, etc. |
|
74 | 89 | ls_aliases = [('ls', 'ls -F -G'), |
@@ -7,25 +7,10 b' handling configuration and creating configurables.' | |||
|
7 | 7 | |
|
8 | 8 | The job of an :class:`Application` is to create the master configuration |
|
9 | 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 | #----------------------------------------------------------------------------- | |
|
20 | # Copyright (C) 2008 The IPython Development Team | |
|
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 | #----------------------------------------------------------------------------- | |
|
12 | # Copyright (c) IPython Development Team. | |
|
13 | # Distributed under the terms of the Modified BSD License. | |
|
29 | 14 | |
|
30 | 15 | import atexit |
|
31 | 16 | import glob |
@@ -42,14 +27,18 b' from IPython.utils.path import get_ipython_dir, get_ipython_package_dir, ensure_' | |||
|
42 | 27 | from IPython.utils import py3compat |
|
43 | 28 | from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict, Set, Instance |
|
44 | 29 | |
|
45 | #----------------------------------------------------------------------------- | |
|
46 | # Classes and functions | |
|
47 | #----------------------------------------------------------------------------- | |
|
48 | ||
|
30 | if os.name == 'nt': | |
|
31 | programdata = os.environ.get('PROGRAMDATA', None) | |
|
32 | if programdata: | |
|
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 | 43 | # aliases and flags |
|
55 | 44 | |
@@ -100,7 +89,7 b' class BaseIPythonApplication(Application):' | |||
|
100 | 89 | builtin_profile_dir = Unicode( |
|
101 | 90 | os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default') |
|
102 | 91 | ) |
|
103 | ||
|
92 | ||
|
104 | 93 | config_file_paths = List(Unicode) |
|
105 | 94 | def _config_file_paths_default(self): |
|
106 | 95 | return [py3compat.getcwd()] |
@@ -210,11 +199,12 b' class BaseIPythonApplication(Application):' | |||
|
210 | 199 | return crashhandler.crash_handler_lite(etype, evalue, tb) |
|
211 | 200 | |
|
212 | 201 | def _ipython_dir_changed(self, name, old, new): |
|
213 | str_old = py3compat.cast_bytes_py2(os.path.abspath(old), | |
|
214 | sys.getfilesystemencoding() | |
|
215 | ) | |
|
216 | if str_old in sys.path: | |
|
217 | sys.path.remove(str_old) | |
|
202 | if old is not None: | |
|
203 | str_old = py3compat.cast_bytes_py2(os.path.abspath(old), | |
|
204 | sys.getfilesystemencoding() | |
|
205 | ) | |
|
206 | if str_old in sys.path: | |
|
207 | sys.path.remove(str_old) | |
|
218 | 208 | str_path = py3compat.cast_bytes_py2(os.path.abspath(new), |
|
219 | 209 | sys.getfilesystemencoding() |
|
220 | 210 | ) |
@@ -336,6 +326,7 b' class BaseIPythonApplication(Application):' | |||
|
336 | 326 | |
|
337 | 327 | def init_config_files(self): |
|
338 | 328 | """[optionally] copy default config files into profile dir.""" |
|
329 | self.config_file_paths.extend(SYSTEM_CONFIG_DIRS) | |
|
339 | 330 | # copy config files |
|
340 | 331 | path = self.builtin_profile_dir |
|
341 | 332 | if self.copy_config_files: |
@@ -277,7 +277,7 b' class Pdb(OldPdb):' | |||
|
277 | 277 | try: |
|
278 | 278 | OldPdb.interaction(self, frame, traceback) |
|
279 | 279 | except KeyboardInterrupt: |
|
280 |
self.shell.write( |
|
|
280 | self.shell.write('\n' + self.shell.get_exception_only()) | |
|
281 | 281 | break |
|
282 | 282 | else: |
|
283 | 283 | break |
@@ -21,6 +21,7 b' from __future__ import print_function' | |||
|
21 | 21 | |
|
22 | 22 | import os |
|
23 | 23 | import struct |
|
24 | import mimetypes | |
|
24 | 25 | |
|
25 | 26 | from IPython.core.formatters import _safe_get_formatter_method |
|
26 | 27 | from IPython.utils.py3compat import (string_types, cast_bytes_py2, cast_unicode, |
@@ -781,6 +782,90 b' class Image(DisplayObject):' | |||
|
781 | 782 | def _find_ext(self, s): |
|
782 | 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 | 870 | def clear_output(wait=False): |
|
786 | 871 | """Clear the output of the current cell receiving output. |
@@ -2,25 +2,11 b'' | |||
|
2 | 2 | """Displayhook for IPython. |
|
3 | 3 | |
|
4 | 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 | #----------------------------------------------------------------------------- | |
|
14 | # Copyright (C) 2008-2011 The IPython Development Team | |
|
15 | # Copyright (C) 2001-2007 Fernando Perez <fperez@colorado.edu> | |
|
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 | #----------------------------------------------------------------------------- | |
|
7 | # Copyright (c) IPython Development Team. | |
|
8 | # Distributed under the terms of the Modified BSD License. | |
|
9 | ||
|
24 | 10 | from __future__ import print_function |
|
25 | 11 | |
|
26 | 12 | import sys |
@@ -29,13 +15,9 b' from IPython.core.formatters import _safe_get_formatter_method' | |||
|
29 | 15 | from IPython.config.configurable import Configurable |
|
30 | 16 | from IPython.utils import io |
|
31 | 17 | from IPython.utils.py3compat import builtin_mod |
|
32 | from IPython.utils.traitlets import Instance | |
|
18 | from IPython.utils.traitlets import Instance, Float | |
|
33 | 19 | from IPython.utils.warn import warn |
|
34 | 20 | |
|
35 | #----------------------------------------------------------------------------- | |
|
36 | # Main displayhook class | |
|
37 | #----------------------------------------------------------------------------- | |
|
38 | ||
|
39 | 21 | # TODO: Move the various attributes (cache_size, [others now moved]). Some |
|
40 | 22 | # of these are also attributes of InteractiveShell. They should be on ONE object |
|
41 | 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 | 32 | shell = Instance('IPython.core.interactiveshell.InteractiveShellABC') |
|
33 | cull_fraction = Float(0.2) | |
|
51 | 34 | |
|
52 | 35 | def __init__(self, shell=None, cache_size=1000, **kwargs): |
|
53 | 36 | super(DisplayHook, self).__init__(shell=shell, **kwargs) |
|
54 | ||
|
55 | 37 | cache_size_min = 3 |
|
56 | 38 | if cache_size <= 0: |
|
57 | 39 | self.do_full_cache = 0 |
@@ -168,6 +150,9 b' class DisplayHook(Configurable):' | |||
|
168 | 150 | md_dict : dict (optional) |
|
169 | 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 | 156 | # We want to print because we want to always make sure we have a |
|
172 | 157 | # newline, even if all the prompt separators are ''. This is the |
|
173 | 158 | # standard IPython behavior. |
@@ -193,13 +178,7 b' class DisplayHook(Configurable):' | |||
|
193 | 178 | # Avoid recursive reference when displaying _oh/Out |
|
194 | 179 | if result is not self.shell.user_ns['_oh']: |
|
195 | 180 | if len(self.shell.user_ns['_oh']) >= self.cache_size and self.do_full_cache: |
|
196 | warn('Output cache limit (currently '+ | |
|
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() | |
|
181 | self.cull_cache() | |
|
203 | 182 | # Don't overwrite '_' and friends if '_' is in __builtin__ (otherwise |
|
204 | 183 | # we cause buggy behavior for things like gettext). |
|
205 | 184 | |
@@ -221,6 +200,9 b' class DisplayHook(Configurable):' | |||
|
221 | 200 | |
|
222 | 201 | def log_output(self, format_dict): |
|
223 | 202 | """Log the output.""" |
|
203 | if 'text/plain' not in format_dict: | |
|
204 | # nothing to do | |
|
205 | return | |
|
224 | 206 | if self.shell.logger.log_output: |
|
225 | 207 | self.shell.logger.log_write(format_dict['text/plain'], 'output') |
|
226 | 208 | self.shell.history_manager.output_hist_reprs[self.prompt_count] = \ |
@@ -255,6 +237,21 b' class DisplayHook(Configurable):' | |||
|
255 | 237 | self.log_output(format_dict) |
|
256 | 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 | 255 | def flush(self): |
|
259 | 256 | if not self.do_full_cache: |
|
260 | 257 | raise ValueError("You shouldn't have reached the cache flush " |
@@ -63,14 +63,6 b' class EventManager(object):' | |||
|
63 | 63 | """Remove a callback from the given event.""" |
|
64 | 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 | 66 | def trigger(self, event, *args, **kwargs): |
|
75 | 67 | """Call callbacks for ``event``. |
|
76 | 68 |
@@ -5,35 +5,22 b' Inheritance diagram:' | |||
|
5 | 5 | |
|
6 | 6 | .. inheritance-diagram:: IPython.core.formatters |
|
7 | 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 | #----------------------------------------------------------------------------- | |
|
23 | # Imports | |
|
24 | #----------------------------------------------------------------------------- | |
|
10 | # Copyright (c) IPython Development Team. | |
|
11 | # Distributed under the terms of the Modified BSD License. | |
|
25 | 12 | |
|
26 | # Stdlib imports | |
|
27 | 13 | import abc |
|
28 | 14 | import inspect |
|
29 | 15 | import sys |
|
16 | import traceback | |
|
30 | 17 | import types |
|
31 | 18 | import warnings |
|
32 | 19 | |
|
33 | 20 | from IPython.external.decorator import decorator |
|
34 | 21 | |
|
35 | # Our own imports | |
|
36 | 22 | from IPython.config.configurable import Configurable |
|
23 | from IPython.core.getipython import get_ipython | |
|
37 | 24 | from IPython.lib import pretty |
|
38 | 25 | from IPython.utils.traitlets import ( |
|
39 | 26 | Bool, Dict, Integer, Unicode, CUnicode, ObjectName, List, |
@@ -223,6 +210,18 b' class DisplayFormatter(Configurable):' | |||
|
223 | 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 | 225 | class FormatterWarning(UserWarning): |
|
227 | 226 | """Warning class for errors in formatters""" |
|
228 | 227 | |
@@ -231,13 +230,16 b' def warn_format_error(method, self, *args, **kwargs):' | |||
|
231 | 230 | """decorator for warning on failed format call""" |
|
232 | 231 | try: |
|
233 | 232 | r = method(self, *args, **kwargs) |
|
234 |
except NotImplementedError |
|
|
233 | except NotImplementedError: | |
|
235 | 234 | # don't warn on NotImplementedErrors |
|
236 | 235 | return None |
|
237 |
except Exception |
|
|
238 | warnings.warn("Exception in %s formatter: %s" % (self.format_type, e), | |
|
239 | FormatterWarning, | |
|
240 | ) | |
|
236 | except Exception: | |
|
237 | exc_info = sys.exc_info() | |
|
238 | ip = get_ipython() | |
|
239 | if ip is not None: | |
|
240 | ip.showtraceback(exc_info) | |
|
241 | else: | |
|
242 | traceback.print_exception(*exc_info) | |
|
241 | 243 | return None |
|
242 | 244 | if r is None or isinstance(r, self._return_type) or \ |
|
243 | 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 | 247 | else: |
|
246 | 248 | warnings.warn( |
|
247 | 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 | 251 | FormatterWarning |
|
250 | 252 | ) |
|
251 | 253 | |
@@ -588,7 +590,14 b' class PlainTextFormatter(BaseFormatter):' | |||
|
588 | 590 | # This subclass ignores this attribute as it always need to return |
|
589 | 591 | # something. |
|
590 | 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 | 601 | # Look for a _repr_pretty_ methods to use for pretty printing. |
|
593 | 602 | print_method = ObjectName('_repr_pretty_') |
|
594 | 603 | |
@@ -672,7 +681,7 b' class PlainTextFormatter(BaseFormatter):' | |||
|
672 | 681 | def __call__(self, obj): |
|
673 | 682 | """Compute the pretty representation of the object.""" |
|
674 | 683 | if not self.pprint: |
|
675 |
return |
|
|
684 | return repr(obj) | |
|
676 | 685 | else: |
|
677 | 686 | # This uses use StringIO, as cStringIO doesn't handle unicode. |
|
678 | 687 | stream = StringIO() |
@@ -681,6 +690,7 b' class PlainTextFormatter(BaseFormatter):' | |||
|
681 | 690 | # or it will cause trouble. |
|
682 | 691 | printer = pretty.RepresentationPrinter(stream, self.verbose, |
|
683 | 692 | self.max_width, unicode_to_str(self.newline), |
|
693 | max_seq_length=self.max_seq_length, | |
|
684 | 694 | singleton_pprinters=self.singleton_printers, |
|
685 | 695 | type_pprinters=self.type_printers, |
|
686 | 696 | deferred_pprinters=self.deferred_printers) |
@@ -836,6 +846,8 b' class PDFFormatter(BaseFormatter):' | |||
|
836 | 846 | |
|
837 | 847 | print_method = ObjectName('_repr_pdf_') |
|
838 | 848 | |
|
849 | _return_type = (bytes, unicode_type) | |
|
850 | ||
|
839 | 851 | |
|
840 | 852 | FormatterABC.register(BaseFormatter) |
|
841 | 853 | FormatterABC.register(PlainTextFormatter) |
@@ -98,9 +98,24 b' def catch_corrupt_db(f, self, *a, **kw):' | |||
|
98 | 98 | # The hist_file is probably :memory: or something else. |
|
99 | 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 | 119 | """Access the history database without adding to it. |
|
105 | 120 | |
|
106 | 121 | This is intended for use by standalone history tools. IPython shells use |
@@ -544,7 +559,7 b' class HistoryManager(HistoryAccessor):' | |||
|
544 | 559 | self.input_hist_parsed[:] = [""] |
|
545 | 560 | self.input_hist_raw[:] = [""] |
|
546 | 561 | self.new_session() |
|
547 | ||
|
562 | ||
|
548 | 563 | # ------------------------------ |
|
549 | 564 | # Methods for retrieving history |
|
550 | 565 | # ------------------------------ |
@@ -20,6 +20,7 b' import ast' | |||
|
20 | 20 | import codeop |
|
21 | 21 | import re |
|
22 | 22 | import sys |
|
23 | import warnings | |
|
23 | 24 | |
|
24 | 25 | from IPython.utils.py3compat import cast_unicode |
|
25 | 26 | from IPython.core.inputtransformer import (leading_indent, |
@@ -208,6 +209,8 b' class InputSplitter(object):' | |||
|
208 | 209 | _full_dedent = False |
|
209 | 210 | # Boolean indicating whether the current block is complete |
|
210 | 211 | _is_complete = None |
|
212 | # Boolean indicating whether the current block has an unrecoverable syntax error | |
|
213 | _is_invalid = False | |
|
211 | 214 | |
|
212 | 215 | def __init__(self): |
|
213 | 216 | """Create a new InputSplitter instance. |
@@ -223,6 +226,7 b' class InputSplitter(object):' | |||
|
223 | 226 | self.source = '' |
|
224 | 227 | self.code = None |
|
225 | 228 | self._is_complete = False |
|
229 | self._is_invalid = False | |
|
226 | 230 | self._full_dedent = False |
|
227 | 231 | |
|
228 | 232 | def source_reset(self): |
@@ -232,6 +236,42 b' class InputSplitter(object):' | |||
|
232 | 236 | self.reset() |
|
233 | 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 | 275 | def push(self, lines): |
|
236 | 276 | """Push one or more lines of input. |
|
237 | 277 | |
@@ -261,6 +301,7 b' class InputSplitter(object):' | |||
|
261 | 301 | # exception is raised in compilation, we don't mislead by having |
|
262 | 302 | # inconsistent code/source attributes. |
|
263 | 303 | self.code, self._is_complete = None, None |
|
304 | self._is_invalid = False | |
|
264 | 305 | |
|
265 | 306 | # Honor termination lines properly |
|
266 | 307 | if source.endswith('\\\n'): |
@@ -268,15 +309,18 b' class InputSplitter(object):' | |||
|
268 | 309 | |
|
269 | 310 | self._update_indent(lines) |
|
270 | 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 | 315 | # Invalid syntax can produce any of a number of different errors from |
|
273 | 316 | # inside the compiler, so we have to catch them all. Syntax errors |
|
274 | 317 | # immediately produce a 'ready' block, so the invalid Python can be |
|
275 | 318 | # sent to the kernel for evaluation with possible ipython |
|
276 | 319 | # special-syntax conversion. |
|
277 | 320 | except (SyntaxError, OverflowError, ValueError, TypeError, |
|
278 | MemoryError): | |
|
321 | MemoryError, SyntaxWarning): | |
|
279 | 322 | self._is_complete = True |
|
323 | self._is_invalid = True | |
|
280 | 324 | else: |
|
281 | 325 | # Compilation didn't produce any exceptions (though it may not have |
|
282 | 326 | # given a complete code object) |
@@ -461,7 +461,7 b' def classic_prompt():' | |||
|
461 | 461 | def ipy_prompt(): |
|
462 | 462 | """Strip IPython's In [1]:/...: prompts.""" |
|
463 | 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 | 465 | return _strip_prompts(prompt_re) |
|
466 | 466 | |
|
467 | 467 |
@@ -22,6 +22,7 b' import re' | |||
|
22 | 22 | import runpy |
|
23 | 23 | import sys |
|
24 | 24 | import tempfile |
|
25 | import traceback | |
|
25 | 26 | import types |
|
26 | 27 | import subprocess |
|
27 | 28 | from io import open as io_open |
@@ -424,7 +425,7 b' class InteractiveShell(SingletonConfigurable):' | |||
|
424 | 425 | display_trap = Instance('IPython.core.display_trap.DisplayTrap') |
|
425 | 426 | extension_manager = Instance('IPython.core.extensions.ExtensionManager') |
|
426 | 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 | 429 | magics_manager = Instance('IPython.core.magic.MagicsManager') |
|
429 | 430 | |
|
430 | 431 | profile_dir = Instance('IPython.core.application.ProfileDir') |
@@ -523,7 +524,6 b' class InteractiveShell(SingletonConfigurable):' | |||
|
523 | 524 | self.init_pdb() |
|
524 | 525 | self.init_extension_manager() |
|
525 | 526 | self.init_payload() |
|
526 | self.init_comms() | |
|
527 | 527 | self.hooks.late_startup_hook() |
|
528 | 528 | self.events.trigger('shell_initialized', self) |
|
529 | 529 | atexit.register(self.atexit_operations) |
@@ -874,6 +874,8 b' class InteractiveShell(SingletonConfigurable):' | |||
|
874 | 874 | def init_events(self): |
|
875 | 875 | self.events = EventManager(self, available_events) |
|
876 | 876 | |
|
877 | self.events.register("pre_execute", self._clear_warning_registry) | |
|
878 | ||
|
877 | 879 | def register_post_execute(self, func): |
|
878 | 880 | """DEPRECATED: Use ip.events.register('post_run_cell', func) |
|
879 | 881 | |
@@ -883,6 +885,13 b' class InteractiveShell(SingletonConfigurable):' | |||
|
883 | 885 | "ip.events.register('post_run_cell', func) instead.") |
|
884 | 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 | 896 | # Things related to the "main" module |
|
888 | 897 | #------------------------------------------------------------------------- |
@@ -1778,6 +1787,15 b' class InteractiveShell(SingletonConfigurable):' | |||
|
1778 | 1787 | """ |
|
1779 | 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 | 1799 | def showtraceback(self, exc_tuple=None, filename=None, tb_offset=None, |
|
1782 | 1800 | exception_only=False): |
|
1783 | 1801 | """Display the exception that just occurred. |
@@ -1830,7 +1848,7 b' class InteractiveShell(SingletonConfigurable):' | |||
|
1830 | 1848 | self._showtraceback(etype, value, stb) |
|
1831 | 1849 | |
|
1832 | 1850 | except KeyboardInterrupt: |
|
1833 |
self.write_err( |
|
|
1851 | self.write_err('\n' + self.get_exception_only()) | |
|
1834 | 1852 | |
|
1835 | 1853 | def _showtraceback(self, etype, evalue, stb): |
|
1836 | 1854 | """Actually show a traceback. |
@@ -2344,22 +2362,38 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2344 | 2362 | if path is not None: |
|
2345 | 2363 | cmd = '"pushd %s &&"%s' % (path, cmd) |
|
2346 | 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 | 2370 | else: |
|
2349 | 2371 | cmd = py3compat.unicode_to_str(cmd) |
|
2350 | # Call the cmd using the OS shell, instead of the default /bin/sh, if set. | |
|
2351 | ec = subprocess.call(cmd, shell=True, executable=os.environ.get('SHELL', None)) | |
|
2352 | # exit code is positive for program failure, or negative for | |
|
2353 | # terminating signal number. | |
|
2354 | ||
|
2355 | # Interpret ec > 128 as signal | |
|
2356 | # Some shells (csh, fish) don't follow sh/bash conventions for exit codes | |
|
2372 | # For posix the result of the subprocess.call() below is an exit | |
|
2373 | # code, which by convention is zero for success, positive for | |
|
2374 | # program failure. Exit codes above 128 are reserved for signals, | |
|
2375 | # and the formula for converting a signal to an exit code is usually | |
|
2376 | # signal_number+128. To more easily differentiate between exit | |
|
2377 | # codes and signals, ipython uses negative numbers. For instance | |
|
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 | 2389 | if ec > 128: |
|
2358 | 2390 | ec = -(ec - 128) |
|
2359 | 2391 | |
|
2360 | 2392 | # We explicitly do NOT return the subprocess status code, because |
|
2361 | 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 | 2397 | self.user_ns['_exit_code'] = ec |
|
2364 | 2398 | |
|
2365 | 2399 | # use piped system by default, because it is better behaved |
@@ -2419,14 +2453,6 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2419 | 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 | 2456 | # Things related to the prefilter |
|
2431 | 2457 | #------------------------------------------------------------------------- |
|
2432 | 2458 | |
@@ -2565,10 +2591,16 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2565 | 2591 | silenced for zero status, as it is so common). |
|
2566 | 2592 | raise_exceptions : bool (False) |
|
2567 | 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 | 2601 | kw.setdefault('exit_ignore', False) |
|
2571 | 2602 | kw.setdefault('raise_exceptions', False) |
|
2603 | kw.setdefault('shell_futures', False) | |
|
2572 | 2604 | |
|
2573 | 2605 | fname = os.path.abspath(os.path.expanduser(fname)) |
|
2574 | 2606 | |
@@ -2587,7 +2619,10 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2587 | 2619 | |
|
2588 | 2620 | with prepended_to_syspath(dname): |
|
2589 | 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 | 2626 | except SystemExit as status: |
|
2592 | 2627 | # If the call was made with 0 or None exit status (sys.exit(0) |
|
2593 | 2628 | # or sys.exit() ), don't bother showing a traceback, as both of |
@@ -2608,7 +2643,7 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2608 | 2643 | # tb offset is 2 because we wrap execfile |
|
2609 | 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 | 2647 | """Like safe_execfile, but for .ipy or .ipynb files with IPython syntax. |
|
2613 | 2648 | |
|
2614 | 2649 | Parameters |
@@ -2616,6 +2651,11 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2616 | 2651 | fname : str |
|
2617 | 2652 | The name of the file to execute. The filename must have a |
|
2618 | 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 | 2660 | fname = os.path.abspath(os.path.expanduser(fname)) |
|
2621 | 2661 | |
@@ -2635,14 +2675,14 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2635 | 2675 | def get_cells(): |
|
2636 | 2676 | """generator for sequence of code blocks to run""" |
|
2637 | 2677 | if fname.endswith('.ipynb'): |
|
2638 |
from IPython.nbformat import |
|
|
2639 | with open(fname) as f: | |
|
2640 |
nb = |
|
|
2641 |
if not nb. |
|
|
2678 | from IPython.nbformat import read | |
|
2679 | with io_open(fname) as f: | |
|
2680 | nb = read(f, as_version=4) | |
|
2681 | if not nb.cells: | |
|
2642 | 2682 | return |
|
2643 |
for cell in nb. |
|
|
2683 | for cell in nb.cells: | |
|
2644 | 2684 | if cell.cell_type == 'code': |
|
2645 |
yield cell. |
|
|
2685 | yield cell.source | |
|
2646 | 2686 | else: |
|
2647 | 2687 | with open(fname) as f: |
|
2648 | 2688 | yield f.read() |
@@ -2654,7 +2694,7 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2654 | 2694 | # raised in user code. It would be nice if there were |
|
2655 | 2695 | # versions of run_cell that did raise, so |
|
2656 | 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 | 2698 | except: |
|
2659 | 2699 | self.showtraceback() |
|
2660 | 2700 | warn('Unknown failure executing file: <%s>' % fname) |
@@ -3072,7 +3112,15 b' class InteractiveShell(SingletonConfigurable):' | |||
|
3072 | 3112 | namespace. |
|
3073 | 3113 | """ |
|
3074 | 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 | 3124 | try: |
|
3077 | 3125 | # We have to use .vformat() here, because 'self' is a valid and common |
|
3078 | 3126 | # name, and expanding **ns for .format() would make it collide with |
@@ -1,25 +1,12 b'' | |||
|
1 | """Implementation of basic magic functions. | |
|
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 | #----------------------------------------------------------------------------- | |
|
1 | """Implementation of basic magic functions.""" | |
|
2 | ||
|
14 | 3 | from __future__ import print_function |
|
15 | 4 | |
|
16 | # Stdlib | |
|
17 | 5 | import io |
|
18 | 6 | import json |
|
19 | 7 | import sys |
|
20 | 8 | from pprint import pformat |
|
21 | 9 | |
|
22 | # Our own packages | |
|
23 | 10 | from IPython.core import magic_arguments, page |
|
24 | 11 | from IPython.core.error import UsageError |
|
25 | 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 | 17 | from IPython.utils.py3compat import unicode_type |
|
31 | 18 | from IPython.utils.warn import warn, error |
|
32 | 19 | |
|
33 | #----------------------------------------------------------------------------- | |
|
34 | # Magics class implementation | |
|
35 | #----------------------------------------------------------------------------- | |
|
36 | 20 | |
|
37 | 21 | class MagicsDisplay(object): |
|
38 | 22 | def __init__(self, magics_manager): |
@@ -362,9 +346,6 b' Currently the magic system has the following functions:""",' | |||
|
362 | 346 | Proper color support under MS Windows requires the pyreadline library. |
|
363 | 347 | You can find it at: |
|
364 | 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 | 350 | Defaulting color scheme to 'NoColor'""" |
|
370 | 351 | new_scheme = 'NoColor' |
@@ -602,13 +583,6 b' Defaulting color scheme to \'NoColor\'"""' | |||
|
602 | 583 | 'file extension will write the notebook as a Python script' |
|
603 | 584 | ) |
|
604 | 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 | 586 | 'filename', type=unicode_type, |
|
613 | 587 | help='Notebook name or filename' |
|
614 | 588 | ) |
@@ -616,41 +590,22 b' Defaulting color scheme to \'NoColor\'"""' | |||
|
616 | 590 | def notebook(self, s): |
|
617 | 591 | """Export and convert IPython notebooks. |
|
618 | 592 | |
|
619 | 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 | |
|
621 |
|
|
|
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). | |
|
593 | This function can export the current IPython history to a notebook file. | |
|
594 | For example, to export the history to "foo.ipynb" do "%notebook -e foo.ipynb". | |
|
595 | To export the history to "foo.py" do "%notebook -e foo.py". | |
|
625 | 596 | """ |
|
626 | 597 | args = magic_arguments.parse_argstring(self.notebook, s) |
|
627 | 598 | |
|
628 |
from IPython.nbformat import |
|
|
599 | from IPython.nbformat import write, v4 | |
|
629 | 600 | args.filename = unquote_filename(args.filename) |
|
630 | 601 | if args.export: |
|
631 | fname, name, format = current.parse_filename(args.filename) | |
|
632 | 602 | cells = [] |
|
633 | 603 | hist = list(self.shell.history_manager.get_range()) |
|
634 |
for session, |
|
|
635 |
cells.append( |
|
|
636 | input=input)) | |
|
637 | worksheet = current.new_worksheet(cells=cells) | |
|
638 | nb = current.new_notebook(name=name,worksheets=[worksheet]) | |
|
639 | with io.open(fname, 'w', encoding='utf-8') as f: | |
|
640 | current.write(nb, f, format); | |
|
641 | elif args.format is not None: | |
|
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) | |
|
604 | for session, execution_count, input in hist[:-1]: | |
|
605 | cells.append(v4.new_code_cell( | |
|
606 | execution_count=execution_count, | |
|
607 | source=source | |
|
608 | )) | |
|
609 | nb = v4.new_notebook(cells=cells) | |
|
610 | with io.open(args.filename, 'w', encoding='utf-8') as f: | |
|
611 | write(nb, f, version=4) |
@@ -1027,7 +1027,10 b' python-profiler package from non-free.""")' | |||
|
1027 | 1027 | worst = max(worst, worst_tuning) |
|
1028 | 1028 | # Check best timing is greater than zero to avoid a |
|
1029 | 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 | 1034 | print("The slowest run took %0.2f times longer than the " |
|
1032 | 1035 | "fastest. This could mean that an intermediate result " |
|
1033 | 1036 | "is being cached " % (worst / best)) |
@@ -1057,7 +1060,7 b' python-profiler package from non-free.""")' | |||
|
1057 | 1060 | following statement raises an error). |
|
1058 | 1061 | |
|
1059 | 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 | 1065 | Examples |
|
1063 | 1066 | -------- |
@@ -371,14 +371,52 b' class OSMagics(Magics):' | |||
|
371 | 371 | if not 'q' in opts and self.shell.user_ns['_dh']: |
|
372 | 372 | print(self.shell.user_ns['_dh'][-1]) |
|
373 | 373 | |
|
374 | ||
|
375 | 374 | @line_magic |
|
376 | 375 | def env(self, parameter_s=''): |
|
377 | 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 | 389 | return dict(os.environ) |
|
380 | 390 | |
|
381 | 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 | 420 | def pushd(self, parameter_s=''): |
|
383 | 421 | """Place the current dir on stack and change directory. |
|
384 | 422 |
@@ -40,6 +40,7 b' from IPython.utils.text import indent' | |||
|
40 | 40 | from IPython.utils.wildcard import list_namespace |
|
41 | 41 | from IPython.utils.coloransi import TermColors, ColorScheme, ColorSchemeTable |
|
42 | 42 | from IPython.utils.py3compat import cast_unicode, string_types, PY3 |
|
43 | from IPython.utils.signatures import signature | |
|
43 | 44 | |
|
44 | 45 | # builtin docstrings to ignore |
|
45 | 46 | _func_call_docstring = types.FunctionType.__call__.__doc__ |
@@ -390,7 +391,7 b' class Inspector:' | |||
|
390 | 391 | If any exception is generated, None is returned instead and the |
|
391 | 392 | exception is suppressed.""" |
|
392 | 393 | try: |
|
393 |
hdef = oname + |
|
|
394 | hdef = oname + str(signature(obj)) | |
|
394 | 395 | return cast_unicode(hdef) |
|
395 | 396 | except: |
|
396 | 397 | return None |
@@ -38,7 +38,6 b' def page(strng, start=0, screen_lines=0, pager_cmd=None):' | |||
|
38 | 38 | source='page', |
|
39 | 39 | data=data, |
|
40 | 40 | start=start, |
|
41 | screen_lines=screen_lines, | |
|
42 | 41 | ) |
|
43 | 42 | shell.payload_manager.write_payload(payload) |
|
44 | 43 |
@@ -261,6 +261,8 b' class ProfileCreate(BaseIPythonApplication):' | |||
|
261 | 261 | from IPython.terminal.ipapp import TerminalIPythonApp |
|
262 | 262 | apps = [TerminalIPythonApp] |
|
263 | 263 | for app_path in ( |
|
264 | 'IPython.kernel.zmq.kernelapp.IPKernelApp', | |
|
265 | 'IPython.terminal.console.app.ZMQTerminalIPythonApp', | |
|
264 | 266 | 'IPython.qt.console.qtconsoleapp.IPythonQtConsoleApp', |
|
265 | 267 | 'IPython.html.notebookapp.NotebookApp', |
|
266 | 268 | 'IPython.nbconvert.nbconvertapp.NbConvertApp', |
@@ -1,24 +1,9 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | """Pylab (matplotlib) support utilities. | |
|
3 | ||
|
4 | Authors | |
|
5 | ------- | |
|
6 | ||
|
7 | * Fernando Perez. | |
|
8 | * Brian Granger | |
|
9 | """ | |
|
2 | """Pylab (matplotlib) support utilities.""" | |
|
10 | 3 | from __future__ import print_function |
|
11 | 4 | |
|
12 | #----------------------------------------------------------------------------- | |
|
13 | # Copyright (C) 2009 The IPython Development Team | |
|
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 | #----------------------------------------------------------------------------- | |
|
5 | # Copyright (c) IPython Development Team. | |
|
6 | # Distributed under the terms of the Modified BSD License. | |
|
22 | 7 | |
|
23 | 8 | from io import BytesIO |
|
24 | 9 | |
@@ -34,7 +19,9 b" backends = {'tk': 'TkAgg'," | |||
|
34 | 19 | 'wx': 'WXAgg', |
|
35 | 20 | 'qt': 'Qt4Agg', # qt3 not supported |
|
36 | 21 | 'qt4': 'Qt4Agg', |
|
22 | 'qt5': 'Qt5Agg', | |
|
37 | 23 | 'osx': 'MacOSX', |
|
24 | 'nbagg': 'nbAgg', | |
|
38 | 25 | 'inline' : 'module://IPython.kernel.zmq.pylab.backend_inline'} |
|
39 | 26 | |
|
40 | 27 | # We also need a reverse backends2guis mapping that will properly choose which |
@@ -324,7 +324,7 b' class InteractiveShellApp(Configurable):' | |||
|
324 | 324 | self.log.warn("Unknown error in handling IPythonApp.exec_lines:") |
|
325 | 325 | self.shell.showtraceback() |
|
326 | 326 | |
|
327 | def _exec_file(self, fname): | |
|
327 | def _exec_file(self, fname, shell_futures=False): | |
|
328 | 328 | try: |
|
329 | 329 | full_filename = filefind(fname, [u'.', self.ipython_dir]) |
|
330 | 330 | except IOError as e: |
@@ -346,11 +346,13 b' class InteractiveShellApp(Configurable):' | |||
|
346 | 346 | with preserve_keys(self.shell.user_ns, '__file__'): |
|
347 | 347 | self.shell.user_ns['__file__'] = fname |
|
348 | 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 | 351 | else: |
|
351 | 352 | # default to python, even without extension |
|
352 | 353 | self.shell.safe_execfile(full_filename, |
|
353 |
self.shell.user_ns |
|
|
354 | self.shell.user_ns, | |
|
355 | shell_futures=shell_futures) | |
|
354 | 356 | finally: |
|
355 | 357 | sys.argv = save_argv |
|
356 | 358 | |
@@ -418,7 +420,7 b' class InteractiveShellApp(Configurable):' | |||
|
418 | 420 | elif self.file_to_run: |
|
419 | 421 | fname = self.file_to_run |
|
420 | 422 | try: |
|
421 | self._exec_file(fname) | |
|
423 | self._exec_file(fname, shell_futures=True) | |
|
422 | 424 | except: |
|
423 | 425 | self.log.warn("Error in executing file in user namespace: %s" % |
|
424 | 426 | fname) |
@@ -52,9 +52,9 b' def test_image_filename_defaults():' | |||
|
52 | 52 | nt.assert_raises(ValueError, display.Image, data='this is not an image', format='badformat', embed=True) |
|
53 | 53 | from IPython.html import DEFAULT_STATIC_FILES_PATH |
|
54 | 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 | 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 | 58 | img = display.Image(filename=imgfile) |
|
59 | 59 | nt.assert_equal('png', img.format) |
|
60 | 60 | nt.assert_is_not_none(img._repr_png_()) |
@@ -25,22 +25,8 b' class CallbackTests(unittest.TestCase):' | |||
|
25 | 25 | self.em.trigger('ping_received') |
|
26 | 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 | 28 | def test_cb_error(self): |
|
43 | 29 | cb = Mock(side_effect=ValueError) |
|
44 | 30 | self.em.register('ping_received', cb) |
|
45 | 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 | 25 | class C: |
|
26 | 26 | pass |
|
27 | 27 | |
|
28 | class BadRepr(object): | |
|
29 | def __repr__(self): | |
|
30 | raise ValueError("bad repr") | |
|
31 | ||
|
28 | 32 | class BadPretty(object): |
|
29 | 33 | _repr_pretty_ = None |
|
30 | 34 | |
@@ -234,30 +238,30 b' def test_pop_string():' | |||
|
234 | 238 | nt.assert_is(f.pop(type_str, None), None) |
|
235 | 239 | |
|
236 | 240 | |
|
237 |
def test_ |
|
|
241 | def test_error_method(): | |
|
238 | 242 | f = HTMLFormatter() |
|
239 | 243 | class BadHTML(object): |
|
240 | 244 | def _repr_html_(self): |
|
241 | return 1/0 | |
|
245 | raise ValueError("Bad HTML") | |
|
242 | 246 | bad = BadHTML() |
|
243 | 247 | with capture_output() as captured: |
|
244 | 248 | result = f(bad) |
|
245 | 249 | nt.assert_is(result, None) |
|
246 |
nt.assert_in(" |
|
|
247 |
nt.assert_in(" |
|
|
248 |
nt.assert_in(" |
|
|
250 | nt.assert_in("Traceback", captured.stdout) | |
|
251 | nt.assert_in("Bad HTML", captured.stdout) | |
|
252 | nt.assert_in("_repr_html_", captured.stdout) | |
|
249 | 253 | |
|
250 | 254 | def test_nowarn_notimplemented(): |
|
251 | 255 | f = HTMLFormatter() |
|
252 | 256 | class HTMLNotImplemented(object): |
|
253 | 257 | def _repr_html_(self): |
|
254 | 258 | raise NotImplementedError |
|
255 | return 1/0 | |
|
256 | 259 | h = HTMLNotImplemented() |
|
257 | 260 | with capture_output() as captured: |
|
258 | 261 | result = f(h) |
|
259 | 262 | nt.assert_is(result, None) |
|
260 |
nt.assert_ |
|
|
263 | nt.assert_equal("", captured.stderr) | |
|
264 | nt.assert_equal("", captured.stdout) | |
|
261 | 265 | |
|
262 | 266 | def test_warn_error_for_type(): |
|
263 | 267 | f = HTMLFormatter() |
@@ -265,11 +269,11 b' def test_warn_error_for_type():' | |||
|
265 | 269 | with capture_output() as captured: |
|
266 | 270 | result = f(5) |
|
267 | 271 | nt.assert_is(result, None) |
|
268 |
nt.assert_in(" |
|
|
269 |
nt.assert_in(" |
|
|
270 |
nt.assert_in("name_error", captured.std |
|
|
272 | nt.assert_in("Traceback", captured.stdout) | |
|
273 | nt.assert_in("NameError", captured.stdout) | |
|
274 | nt.assert_in("name_error", captured.stdout) | |
|
271 | 275 | |
|
272 |
def test_ |
|
|
276 | def test_error_pretty_method(): | |
|
273 | 277 | f = PlainTextFormatter() |
|
274 | 278 | class BadPretty(object): |
|
275 | 279 | def _repr_pretty_(self): |
@@ -278,9 +282,23 b' def test_warn_error_pretty_method():' | |||
|
278 | 282 | with capture_output() as captured: |
|
279 | 283 | result = f(bad) |
|
280 | 284 | nt.assert_is(result, None) |
|
281 |
nt.assert_in(" |
|
|
282 |
nt.assert_in(" |
|
|
283 |
nt.assert_in(" |
|
|
285 | nt.assert_in("Traceback", captured.stdout) | |
|
286 | nt.assert_in("_repr_pretty_", captured.stdout) | |
|
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 | 303 | class MakePDF(object): |
|
286 | 304 | def _repr_pdf_(self): |
@@ -320,3 +338,15 b' def test_format_config():' | |||
|
320 | 338 | result = f(Config) |
|
321 | 339 | nt.assert_is(result, None) |
|
322 | 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 | 342 | isp.push(r"(1 \ ") |
|
343 | 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 | 353 | class InteractiveLoopTestCase(unittest.TestCase): |
|
346 | 354 | """Tests for an interactive loop like a python shell. |
|
347 | 355 | """ |
@@ -228,6 +228,16 b' syntax_ml = \\' | |||
|
228 | 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 | 241 | [('In [2]: a="""','a="""'), |
|
232 | 242 | (' ...: 123"""','123"""'), |
|
233 | 243 | ], |
@@ -301,7 +301,10 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
301 | 301 | assert post_explicit.called |
|
302 | 302 | finally: |
|
303 | 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 | 309 | def test_silent_noadvance(self): |
|
307 | 310 | """run_cell(silent=True) doesn't advance execution_count""" |
@@ -479,6 +482,24 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
479 | 482 | mod = ip.new_main_mod(u'%s.py' % name, name) |
|
480 | 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 | 503 | class TestSafeExecfileNonAsciiPath(unittest.TestCase): |
|
483 | 504 | |
|
484 | 505 | @onlyif_unicode_paths |
@@ -541,6 +562,16 b' class TestSystemRaw(unittest.TestCase, ExitCodeChecks):' | |||
|
541 | 562 | cmd = u'''python -c "'åäö'" ''' |
|
542 | 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 | 575 | # TODO: Exit codes are currently ignored on Windows. |
|
545 | 576 | class TestSystemPipedExitCode(unittest.TestCase, ExitCodeChecks): |
|
546 | 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 | 6 | from __future__ import absolute_import |
|
7 | 7 | |
|
8 | #----------------------------------------------------------------------------- | |
|
9 | # Imports | |
|
10 | #----------------------------------------------------------------------------- | |
|
11 | ||
|
12 | 8 | import io |
|
13 | 9 | import os |
|
14 | 10 | import sys |
@@ -23,6 +19,7 b' except ImportError:' | |||
|
23 | 19 | import nose.tools as nt |
|
24 | 20 | |
|
25 | 21 | from IPython.core import magic |
|
22 | from IPython.core.error import UsageError | |
|
26 | 23 | from IPython.core.magic import (Magics, magics_class, line_magic, |
|
27 | 24 | cell_magic, line_cell_magic, |
|
28 | 25 | register_line_magic, register_cell_magic, |
@@ -40,9 +37,6 b' if py3compat.PY3:' | |||
|
40 | 37 | else: |
|
41 | 38 | from StringIO import StringIO |
|
42 | 39 | |
|
43 | #----------------------------------------------------------------------------- | |
|
44 | # Test functions begin | |
|
45 | #----------------------------------------------------------------------------- | |
|
46 | 40 | |
|
47 | 41 | @magic.magics_class |
|
48 | 42 | class DummyMagics(magic.Magics): pass |
@@ -624,7 +618,7 b' def test_extension():' | |||
|
624 | 618 | |
|
625 | 619 | |
|
626 | 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 | 622 | class NotebookExportMagicTests(TestCase): |
|
629 | 623 | def test_notebook_export_json(self): |
|
630 | 624 | with TemporaryDirectory() as td: |
@@ -632,39 +626,36 b' class NotebookExportMagicTests(TestCase):' | |||
|
632 | 626 | _ip.ex(py3compat.u_format(u"u = {u}'héllo'")) |
|
633 | 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): | |
|
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') | |
|
630 | class TestEnv(TestCase): | |
|
648 | 631 | |
|
649 | _ip.ex(py3compat.u_format(u"u = {u}'héllo'")) | |
|
650 | _ip.magic("notebook -f py %s" % infile) | |
|
632 | def test_env(self): | |
|
633 | env = _ip.magic("env") | |
|
634 | self.assertTrue(isinstance(env, dict)) | |
|
651 | 635 | |
|
652 |
def test_ |
|
|
653 | from IPython.nbformat.v3.tests.nbexamples import nb0 | |
|
654 | from IPython.nbformat import current | |
|
655 | with TemporaryDirectory() as td: | |
|
656 | infile = os.path.join(td, "nb.py") | |
|
657 | with io.open(infile, 'w', encoding='utf-8') as f: | |
|
658 | current.write(nb0, f, 'py') | |
|
636 | def test_env_get_set_simple(self): | |
|
637 | env = _ip.magic("env var val1") | |
|
638 | self.assertEqual(env, None) | |
|
639 | self.assertEqual(os.environ['var'], 'val1') | |
|
640 | self.assertEqual(_ip.magic("env var"), 'val1') | |
|
641 | env = _ip.magic("env var=val2") | |
|
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'")) | |
|
661 | _ip.magic("notebook -f ipynb %s" % infile) | |
|
662 | _ip.magic("notebook -f json %s" % infile) | |
|
645 | def test_env_get_set_complex(self): | |
|
646 | env = _ip.magic("env var 'val1 '' 'val2") | |
|
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(): | |
|
666 | env = _ip.magic("env") | |
|
667 | assert isinstance(env, dict), type(env) | |
|
657 | def test_env_set_whitespace(self): | |
|
658 | self.assertRaises(UsageError, lambda: _ip.magic("env var A=B")) | |
|
668 | 659 | |
|
669 | 660 | |
|
670 | 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 | 7 | place the tricks needed to handle it; most other magics are much easier to test |
|
8 | 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 | 14 | from __future__ import absolute_import |
|
11 | 15 | |
|
12 | #----------------------------------------------------------------------------- | |
|
13 | # Imports | |
|
14 | #----------------------------------------------------------------------------- | |
|
15 | 16 | |
|
16 | 17 | import functools |
|
17 | 18 | import os |
@@ -32,9 +33,6 b' from IPython.utils.io import capture_output' | |||
|
32 | 33 | from IPython.utils.tempdir import TemporaryDirectory |
|
33 | 34 | from IPython.core import debugger |
|
34 | 35 | |
|
35 | #----------------------------------------------------------------------------- | |
|
36 | # Test functions begin | |
|
37 | #----------------------------------------------------------------------------- | |
|
38 | 36 | |
|
39 | 37 | def doctest_refbug(): |
|
40 | 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 | 370 | with tt.AssertNotPrints('SystemExit'): |
|
373 | 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 | 374 | def test_run_nb(self): |
|
377 | 375 | """Test %run notebook.ipynb""" |
|
378 |
from IPython.nbformat import |
|
|
379 |
nb = |
|
|
380 |
|
|
|
381 | current.new_worksheet(cells=[ | |
|
382 | current.new_text_cell("The Ultimate Question of Everything"), | |
|
383 | current.new_code_cell("answer=42") | |
|
384 | ]) | |
|
376 | from IPython.nbformat import v4, writes | |
|
377 | nb = v4.new_notebook( | |
|
378 | cells=[ | |
|
379 | v4.new_markdown_cell("The Ultimate Question of Everything"), | |
|
380 | v4.new_code_cell("answer=42") | |
|
385 | 381 | ] |
|
386 | 382 | ) |
|
387 |
src = |
|
|
383 | src = writes(nb, version=4) | |
|
388 | 384 | self.mktmp(src, ext='.ipynb') |
|
389 | 385 | |
|
390 | 386 | _ip.magic("run %s" % self.fname) |
@@ -19,6 +19,11 b' import unittest' | |||
|
19 | 19 | |
|
20 | 20 | from IPython.testing import decorators as dec |
|
21 | 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 | 28 | class TestFileToRun(unittest.TestCase, tt.TempFileMixin): |
|
24 | 29 | """Test the behavior of the file_to_run parameter.""" |
@@ -28,10 +33,7 b' class TestFileToRun(unittest.TestCase, tt.TempFileMixin):' | |||
|
28 | 33 | src = "print(__file__)\n" |
|
29 | 34 | self.mktmp(src) |
|
30 | 35 | |
|
31 | if dec.module_not_available('sqlite3'): | |
|
32 | err = 'WARNING: IPython History requires SQLite, your history will not be saved\n' | |
|
33 | else: | |
|
34 | err = None | |
|
36 | err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None | |
|
35 | 37 | tt.ipexec_validate(self.fname, self.fname, err) |
|
36 | 38 | |
|
37 | 39 | def test_ipy_script_file_attribute(self): |
@@ -39,11 +41,28 b' class TestFileToRun(unittest.TestCase, tt.TempFileMixin):' | |||
|
39 | 41 | src = "print(__file__)\n" |
|
40 | 42 | self.mktmp(src, ext='.ipy') |
|
41 | 43 | |
|
42 | if dec.module_not_available('sqlite3'): | |
|
43 | err = 'WARNING: IPython History requires SQLite, your history will not be saved\n' | |
|
44 | else: | |
|
45 | err = None | |
|
44 | err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None | |
|
46 | 45 | tt.ipexec_validate(self.fname, self.fname, err) |
|
47 | 46 | |
|
48 | # Ideally we would also test that `__file__` is not set in the | |
|
49 | # interactive namespace after running `ipython -i <file>`. | |
|
47 | # The commands option to ipexec_validate doesn't work on Windows, and it | |
|
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 | 722 | #print '*** record:',file,lnum,func,lines,index # dbg |
|
723 | 723 | if not file: |
|
724 | 724 | file = '?' |
|
725 |
elif |
|
|
726 | # Guess that filenames like <string> aren't real filenames, so | |
|
727 | # don't call abspath on them. | |
|
728 | try: | |
|
729 | file = abspath(file) | |
|
730 | except OSError: | |
|
731 | # Not sure if this can still happen: abspath now works with | |
|
732 |
|
|
|
733 | pass | |
|
725 | elif file.startswith(str("<")) and file.endswith(str(">")): | |
|
726 | # Not a real filename, no problem... | |
|
727 | pass | |
|
728 | elif not os.path.isabs(file): | |
|
729 | # Try to make the filename absolute by trying all | |
|
730 | # sys.path entries (which is also what linecache does) | |
|
731 | for dirname in sys.path: | |
|
732 | try: | |
|
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 | 742 | file = py3compat.cast_unicode(file, util_path.fs_encoding) |
|
735 | 743 | link = tpl_link % file |
|
736 | 744 | args, varargs, varkw, locals = inspect.getargvalues(frame) |
@@ -103,11 +103,6 b' MAIN FEATURES' | |||
|
103 | 103 | If you just want to see an object's docstring, type '%pdoc object' (without |
|
104 | 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 | 106 | * Completion in the local namespace, by typing TAB at the prompt. |
|
112 | 107 | |
|
113 | 108 | At any time, hitting tab will complete any available python commands or |
@@ -183,7 +183,7 b' class ModuleReloader(object):' | |||
|
183 | 183 | return top_module, top_name |
|
184 | 184 | |
|
185 | 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 | 187 | return None, None |
|
188 | 188 | |
|
189 | 189 | if module.__name__ == '__main__': |
@@ -1,37 +1,10 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | """ |
|
3 | ===================== | |
|
4 | Cython related magics | |
|
5 | ===================== | |
|
3 | The cython magic has been integrated into Cython itself, | |
|
4 | which is now released in version 0.21. | |
|
6 | 5 | |
|
7 | Magic command interface for interactive work with Cython | |
|
8 | ||
|
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. | |
|
6 | cf github `Cython` organisation, `Cython` repo, under the | |
|
7 | file `Cython/Build/IpythonMagic.py` | |
|
35 | 8 | """ |
|
36 | 9 | #----------------------------------------------------------------------------- |
|
37 | 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 | 17 | from __future__ import print_function |
|
45 | 18 | |
|
46 | import imp | |
|
47 | import io | |
|
48 | import os | |
|
49 | import re | |
|
50 | import sys | |
|
51 | import time | |
|
19 | import IPython.utils.version as version | |
|
52 | 20 | |
|
53 | 21 | try: |
|
54 | reload | |
|
55 | except NameError: # Python 3 | |
|
56 | from imp import reload | |
|
22 | import Cython | |
|
23 | except: | |
|
24 | Cython = None | |
|
57 | 25 | |
|
58 | 26 | try: |
|
59 | import hashlib | |
|
60 | except ImportError: | |
|
61 | import md5 as hashlib | |
|
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. | |
|
27 | from Cython.Build.IpythonMagic import CythonMagics | |
|
28 | except : | |
|
29 | pass | |
|
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 | 33 | def load_ipython_extension(ip): |
|
344 | 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 | 132 | z.extractall(parent) |
|
133 | 133 | |
|
134 | 134 | # it will be mathjax-MathJax-<sha>, rename to just mathjax |
|
135 | d = os.path.join(parent, topdir) | |
|
136 | 135 | os.rename(os.path.join(parent, topdir), dest) |
|
137 | 136 | |
|
138 | 137 |
@@ -57,11 +57,11 b' def commit_api(api):' | |||
|
57 | 57 | if api == QT_API_PYSIDE: |
|
58 | 58 | ID.forbid('PyQt4') |
|
59 | 59 | ID.forbid('PyQt5') |
|
60 | elif api == QT_API_PYQT: | |
|
60 | elif api == QT_API_PYQT5: | |
|
61 | 61 | ID.forbid('PySide') |
|
62 | ID.forbid('PyQt5') | |
|
63 | else: | |
|
64 | 62 | ID.forbid('PyQt4') |
|
63 | else: # There are three other possibilities, all representing PyQt4 | |
|
64 | ID.forbid('PyQt5') | |
|
65 | 65 | ID.forbid('PySide') |
|
66 | 66 | |
|
67 | 67 | |
@@ -241,7 +241,7 b' def load_qt(api_options):' | |||
|
241 | 241 | ---------- |
|
242 | 242 | api_options: List of strings |
|
243 | 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 | 246 | Returns |
|
247 | 247 | ------- |
@@ -4,10 +4,9 b'' | |||
|
4 | 4 | |
|
5 | 5 | Developers of the IPython Notebook will need to install the following tools: |
|
6 | 6 | |
|
7 | * fabric | |
|
7 | * invoke | |
|
8 | 8 | * node.js |
|
9 | 9 | * less (`npm install -g less`) |
|
10 | * bower (`npm install -g bower`) | |
|
11 | 10 | |
|
12 | 11 | ## Components |
|
13 | 12 | |
@@ -15,14 +14,13 b' We are moving to a model where our JavaScript dependencies are managed using' | |||
|
15 | 14 | [bower](http://bower.io/). These packages are installed in `static/components` |
|
16 | 15 | and committed into a separate git repo [ipython/ipython-components](ipython/ipython-components). |
|
17 | 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 | 18 | in this directory. |
|
20 | 19 | |
|
21 | 20 | ## less |
|
22 | 21 | |
|
23 | 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 ` |
|
|
25 | or `python setup.py css` from the root of the repository. | |
|
23 | our minified css files. This can be done by running `python setup.py css` from the root of the repository. | |
|
26 | 24 | If you are working frequently with `.less` files please consider installing git hooks that |
|
27 | 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 | 4 | # Packagers: modify this line if you store the notebook static files elsewhere |
|
5 | 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 | 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 | 24 | except ImportError: |
|
25 | 25 | app_log = logging.getLogger() |
|
26 | 26 | |
|
27 | import IPython | |
|
28 | from IPython.utils.sysinfo import get_sys_info | |
|
29 | ||
|
27 | 30 | from IPython.config import Application |
|
28 | 31 | from IPython.utils.path import filefind |
|
29 | 32 | from IPython.utils.py3compat import string_types |
|
30 | 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 | 38 | # Top-level handlers |
|
34 | 39 | #----------------------------------------------------------------------------- |
|
35 | 40 | non_alphanum = re.compile(r'[^A-Za-z0-9]') |
|
36 | 41 | |
|
42 | sys_info = json.dumps(get_sys_info()) | |
|
43 | ||
|
37 | 44 | class AuthenticatedHandler(web.RequestHandler): |
|
38 | 45 | """A RequestHandler with an authenticated user.""" |
|
39 | 46 | |
|
40 | 47 | def set_default_headers(self): |
|
41 | 48 | headers = self.settings.get('headers', {}) |
|
42 | 49 | |
|
43 |
if " |
|
|
44 | headers["X-Frame-Options"] = "SAMEORIGIN" | |
|
50 | if "Content-Security-Policy" not in headers: | |
|
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 | 58 | for header_name,value in headers.items() : |
|
47 | 59 | try: |
|
48 | 60 | self.set_header(header_name, value) |
|
49 | except Exception: | |
|
61 | except Exception as e: | |
|
50 | 62 | # tornado raise Exception (not a subclass) |
|
51 | 63 | # if method is unsupported (websocket and Access-Control-Allow-Origin |
|
52 | 64 | # for example, so just ignore) |
|
53 |
|
|
|
65 | self.log.debug(e) | |
|
54 | 66 | |
|
55 | 67 | def clear_login_cookie(self): |
|
56 | 68 | self.clear_cookie(self.cookie_name) |
@@ -121,6 +133,11 b' class IPythonHandler(AuthenticatedHandler):' | |||
|
121 | 133 | #--------------------------------------------------------------- |
|
122 | 134 | |
|
123 | 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 | 141 | def mathjax_url(self): |
|
125 | 142 | return self.settings.get('mathjax_url', '') |
|
126 | 143 | |
@@ -131,6 +148,12 b' class IPythonHandler(AuthenticatedHandler):' | |||
|
131 | 148 | @property |
|
132 | 149 | def ws_url(self): |
|
133 | 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 | 159 | # Manager objects |
@@ -153,9 +176,17 b' class IPythonHandler(AuthenticatedHandler):' | |||
|
153 | 176 | return self.settings['session_manager'] |
|
154 | 177 | |
|
155 | 178 | @property |
|
179 | def terminal_manager(self): | |
|
180 | return self.settings['terminal_manager'] | |
|
181 | ||
|
182 | @property | |
|
156 | 183 | def kernel_spec_manager(self): |
|
157 | 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 | 191 | # CORS |
|
161 | 192 | #--------------------------------------------------------------- |
@@ -219,6 +250,9 b' class IPythonHandler(AuthenticatedHandler):' | |||
|
219 | 250 | logged_in=self.logged_in, |
|
220 | 251 | login_available=self.login_available, |
|
221 | 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 | 258 | def get_json_body(self): |
@@ -285,12 +319,18 b' class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):' | |||
|
285 | 319 | @web.authenticated |
|
286 | 320 | def get(self, path): |
|
287 | 321 | if os.path.splitext(path)[1] == '.ipynb': |
|
288 |
name = |
|
|
322 | name = path.rsplit('/', 1)[-1] | |
|
289 | 323 | self.set_header('Content-Type', 'application/json') |
|
290 | 324 | self.set_header('Content-Disposition','attachment; filename="%s"' % name) |
|
291 | 325 | |
|
292 | 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 | 334 | def compute_etag(self): |
|
295 | 335 | return None |
|
296 | 336 | |
@@ -359,7 +399,16 b' class FileFindHandler(web.StaticFileHandler):' | |||
|
359 | 399 | # cache search results, don't search for files more than once |
|
360 | 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 | 412 | if isinstance(path, string_types): |
|
364 | 413 | path = [path] |
|
365 | 414 | |
@@ -398,43 +447,49 b' class FileFindHandler(web.StaticFileHandler):' | |||
|
398 | 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 | 458 | class TrailingSlashHandler(web.RequestHandler): |
|
402 | 459 | """Simple redirect handler that strips trailing slashes |
|
403 | 460 | |
|
404 | 461 | This should be the first, highest priority handler. |
|
405 | 462 | """ |
|
406 | 463 | |
|
407 | SUPPORTED_METHODS = ['GET'] | |
|
408 | ||
|
409 | 464 | def get(self): |
|
410 | 465 | self.redirect(self.request.uri.rstrip('/')) |
|
466 | ||
|
467 | post = put = get | |
|
411 | 468 | |
|
412 | 469 | |
|
413 | 470 | class FilesRedirectHandler(IPythonHandler): |
|
414 | 471 | """Handler for redirecting relative URLs to the /files/ handler""" |
|
415 | 472 | def get(self, path=''): |
|
416 | 473 | cm = self.contents_manager |
|
417 |
if cm. |
|
|
474 | if cm.dir_exists(path): | |
|
418 | 475 | # it's a *directory*, redirect to /tree |
|
419 | 476 | url = url_path_join(self.base_url, 'tree', path) |
|
420 | 477 | else: |
|
421 | 478 | orig_path = path |
|
422 | 479 | # otherwise, redirect to /files |
|
423 | 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 | 483 | # redirect without files/ iff it would 404 |
|
429 | 484 | # this preserves pre-2.0-style 'files/' links |
|
430 | 485 | self.log.warn("Deprecated files/ URL: %s", orig_path) |
|
431 | 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 | 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 | 493 | url = url_escape(url) |
|
439 | 494 | self.log.debug("Redirecting %s to %s", self.request.path, url) |
|
440 | 495 | self.redirect(url) |
@@ -444,11 +499,9 b' class FilesRedirectHandler(IPythonHandler):' | |||
|
444 | 499 | # URL pattern fragments for re-use |
|
445 | 500 | #----------------------------------------------------------------------------- |
|
446 | 501 | |
|
447 | path_regex = r"(?P<path>(?:/.*)*)" | |
|
448 | notebook_name_regex = r"(?P<name>[^/]+\.ipynb)" | |
|
449 | notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex) | |
|
450 | file_name_regex = r"(?P<name>[^/]+)" | |
|
451 | file_path_regex = "%s/%s" % (path_regex, file_name_regex) | |
|
502 | # path matches any number of `/foo[/bar...]` or just `/` or '' | |
|
503 | path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))" | |
|
504 | notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)" | |
|
452 | 505 | |
|
453 | 506 | #----------------------------------------------------------------------------- |
|
454 | 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 | 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 | 2 | """Tornado handlers for WebSocket <-> ZMQ sockets.""" |
|
2 | 3 | |
|
3 | 4 | # Copyright (c) IPython Development Team. |
|
4 | 5 | # Distributed under the terms of the Modified BSD License. |
|
5 | 6 | |
|
7 | import os | |
|
6 | 8 | import json |
|
9 | import struct | |
|
10 | import warnings | |
|
7 | 11 | |
|
8 | 12 | try: |
|
9 | 13 | from urllib.parse import urlparse # Py 3 |
|
10 | 14 | except ImportError: |
|
11 | 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 | 17 | import tornado |
|
20 | from tornado import ioloop | |
|
21 |
from tornado import |
|
|
22 | from tornado import websocket | |
|
18 | from tornado import gen, ioloop, web | |
|
19 | from tornado.websocket import WebSocketHandler | |
|
23 | 20 | |
|
24 | 21 | from IPython.kernel.zmq.session import Session |
|
25 | from IPython.utils.jsonutil import date_default | |
|
26 |
from IPython.utils.py3compat import |
|
|
22 | from IPython.utils.jsonutil import date_default, extract_dates | |
|
23 | from IPython.utils.py3compat import cast_unicode | |
|
27 | 24 | |
|
28 | 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 | 97 | def check_origin(self, origin): |
|
34 | 98 | """Check Origin == Host or Access-Control-Allow-Origin. |
@@ -77,23 +141,19 b' class ZMQStreamHandler(websocket.WebSocketHandler):' | |||
|
77 | 141 | def _reserialize_reply(self, msg_list): |
|
78 | 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 | 145 | self.session and then serializes the result using JSON. This method |
|
82 | 146 | should be used by self._on_zmq_reply to build messages that can |
|
83 | 147 | be sent back to the browser. |
|
84 | 148 | """ |
|
85 | 149 | idents, msg_list = self.session.feed_identities(msg_list) |
|
86 |
msg = self.session. |
|
|
87 | try: | |
|
88 | msg['header'].pop('date') | |
|
89 | except KeyError: | |
|
90 |
|
|
|
91 | try: | |
|
92 | msg['parent_header'].pop('date') | |
|
93 | except KeyError: | |
|
94 | pass | |
|
95 | msg.pop('buffers') | |
|
96 | return json.dumps(msg, default=date_default) | |
|
150 | msg = self.session.deserialize(msg_list) | |
|
151 | if msg['buffers']: | |
|
152 | buf = serialize_binary_message(msg) | |
|
153 | return buf | |
|
154 | else: | |
|
155 | smsg = json.dumps(msg, default=date_default) | |
|
156 | return cast_unicode(smsg) | |
|
97 | 157 | |
|
98 | 158 | def _on_zmq_reply(self, msg_list): |
|
99 | 159 | # Sometimes this gets triggered when the on_close method is scheduled in the |
@@ -104,18 +164,7 b' class ZMQStreamHandler(websocket.WebSocketHandler):' | |||
|
104 | 164 | except Exception: |
|
105 | 165 | self.log.critical("Malformed message: %r" % msg_list, exc_info=True) |
|
106 | 166 | else: |
|
107 | self.write_message(msg) | |
|
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 | |
|
167 | self.write_message(msg, binary=isinstance(msg, bytes)) | |
|
119 | 168 | |
|
120 | 169 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): |
|
121 | 170 | ping_callback = None |
@@ -146,18 +195,37 b' class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):' | |||
|
146 | 195 | which doesn't make sense for websockets |
|
147 | 196 | """ |
|
148 | 197 | pass |
|
149 | ||
|
150 |
def |
|
|
151 | self.kernel_id = cast_unicode(kernel_id, 'ascii') | |
|
152 | # Check to see that origin matches host directly, including ports | |
|
153 | # Tornado 4 already does CORS checking | |
|
154 | if tornado.version_info[0] < 4: | |
|
155 | if not self.check_origin(self.get_origin()): | |
|
156 | raise web.HTTPError(403) | |
|
157 | ||
|
198 | ||
|
199 | def pre_get(self): | |
|
200 | """Run before finishing the GET request | |
|
201 | ||
|
202 | Extend this method to add logic that should fire before | |
|
203 | the websocket finishes completing. | |
|
204 | """ | |
|
205 | # authenticate the request before opening the websocket | |
|
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 | 225 | self.session = Session(config=self.config) |
|
159 | self.save_on_message = self.on_message | |
|
160 | self.on_message = self.on_first_message | |
|
226 | ||
|
227 | def open(self, *args, **kwargs): | |
|
228 | self.log.debug("Opening websocket %s", self.request.path) | |
|
161 | 229 | |
|
162 | 230 | # start the pinging |
|
163 | 231 | if self.ping_interval > 0: |
@@ -187,28 +255,3 b' class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):' | |||
|
187 | 255 | |
|
188 | 256 | def on_pong(self, data): |
|
189 | 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 | 13 | IPythonHandler, FilesRedirectHandler, |
|
14 | 14 | notebook_path_regex, path_regex, |
|
15 | 15 | ) |
|
16 |
from IPython.nbformat |
|
|
16 | from IPython.nbformat import from_dict | |
|
17 | 17 | |
|
18 | 18 | from IPython.utils.py3compat import cast_bytes |
|
19 | 19 | |
@@ -43,7 +43,7 b' def respond_zip(handler, name, output, resources):' | |||
|
43 | 43 | # Prepare the zip file |
|
44 | 44 | buffer = io.BytesIO() |
|
45 | 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 | 47 | zipf.writestr(output_filename, cast_bytes(output, 'utf-8')) |
|
48 | 48 | for filename, data in output_files.items(): |
|
49 | 49 | zipf.writestr(os.path.basename(filename), data) |
@@ -76,12 +76,13 b' class NbconvertFileHandler(IPythonHandler):' | |||
|
76 | 76 | SUPPORTED_METHODS = ('GET',) |
|
77 | 77 | |
|
78 | 78 | @web.authenticated |
|
79 |
def get(self, format, path |
|
|
79 | def get(self, format, path): | |
|
80 | 80 | |
|
81 | 81 | exporter = get_exporter(format, config=self.config, log=self.log) |
|
82 | 82 | |
|
83 | 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 | 87 | self.set_header('Last-Modified', model['last_modified']) |
|
87 | 88 | |
@@ -95,7 +96,7 b' class NbconvertFileHandler(IPythonHandler):' | |||
|
95 | 96 | |
|
96 | 97 | # Force download if requested |
|
97 | 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 | 100 | self.set_header('Content-Disposition', |
|
100 | 101 | 'attachment; filename="%s"' % filename) |
|
101 | 102 | |
@@ -109,19 +110,20 b' class NbconvertFileHandler(IPythonHandler):' | |||
|
109 | 110 | class NbconvertPostHandler(IPythonHandler): |
|
110 | 111 | SUPPORTED_METHODS = ('POST',) |
|
111 | 112 | |
|
112 |
@web.authenticated |
|
|
113 | @web.authenticated | |
|
113 | 114 | def post(self, format): |
|
114 | 115 | exporter = get_exporter(format, config=self.config) |
|
115 | 116 | |
|
116 | 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 | 121 | try: |
|
120 | 122 | output, resources = exporter.from_notebook_node(nbnode) |
|
121 | 123 | except Exception as e: |
|
122 | 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 | 127 | return |
|
126 | 128 | |
|
127 | 129 | # MIME type |
@@ -10,9 +10,10 b' import requests' | |||
|
10 | 10 | |
|
11 | 11 | from IPython.html.utils import url_path_join |
|
12 | 12 | from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error |
|
13 |
from IPython.nbformat |
|
|
14 | new_heading_cell, new_code_cell, | |
|
15 | new_output) | |
|
13 | from IPython.nbformat import write | |
|
14 | from IPython.nbformat.v4 import ( | |
|
15 | new_notebook, new_markdown_cell, new_code_cell, new_output, | |
|
16 | ) | |
|
16 | 17 | |
|
17 | 18 | from IPython.testing.decorators import onlyif_cmds_exist |
|
18 | 19 | |
@@ -43,7 +44,8 b' class NbconvertAPI(object):' | |||
|
43 | 44 | |
|
44 | 45 | png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00' |
|
45 | 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 | 50 | class APITest(NotebookTestBase): |
|
49 | 51 | def setUp(self): |
@@ -52,19 +54,20 b' class APITest(NotebookTestBase):' | |||
|
52 | 54 | if not os.path.isdir(pjoin(nbdir, 'foo')): |
|
53 | 55 | os.mkdir(pjoin(nbdir, 'foo')) |
|
54 | 56 | |
|
55 |
nb = new_notebook( |
|
|
57 | nb = new_notebook() | |
|
56 | 58 | |
|
57 | ws = new_worksheet() | |
|
58 | nb.worksheets = [ws] | |
|
59 | ws.cells.append(new_heading_cell(u'Created by test ³')) | |
|
60 | cc1 = new_code_cell(input=u'print(2*6)') | |
|
61 | cc1.outputs.append(new_output(output_text=u'12', output_type='stream')) | |
|
62 | cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout')) | |
|
63 | ws.cells.append(cc1) | |
|
59 | nb.cells.append(new_markdown_cell(u'Created by test ³')) | |
|
60 | cc1 = new_code_cell(source=u'print(2*6)') | |
|
61 | cc1.outputs.append(new_output(output_type="stream", text=u'12')) | |
|
62 | cc1.outputs.append(new_output(output_type="execute_result", | |
|
63 | data={'image/png' : png_green_pixel}, | |
|
64 | execution_count=1, | |
|
65 | )) | |
|
66 | nb.cells.append(cc1) | |
|
64 | 67 | |
|
65 | 68 | with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w', |
|
66 | 69 | encoding='utf-8') as f: |
|
67 |
write(nb, f, |
|
|
70 | write(nb, f, version=4) | |
|
68 | 71 | |
|
69 | 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 | 93 | If True, always install the files, regardless of what may already be installed. |
|
94 | 94 | symlink : bool [default: False] |
|
95 | 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 | 99 | ipython_dir : str [optional] |
|
98 | 100 | The path to an IPython directory, if the default value is not desired. |
|
99 | 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 | 149 | if overwrite and os.path.exists(dest): |
|
148 | 150 | if verbose >= 1: |
|
149 | 151 | print("removing %s" % dest) |
|
150 | if os.path.isdir(dest): | |
|
152 | if os.path.isdir(dest) and not os.path.islink(dest): | |
|
151 | 153 | shutil.rmtree(dest) |
|
152 | 154 | else: |
|
153 | 155 | os.remove(dest) |
@@ -17,18 +17,16 b' from ..utils import url_escape' | |||
|
17 | 17 | class NotebookHandler(IPythonHandler): |
|
18 | 18 | |
|
19 | 19 | @web.authenticated |
|
20 |
def get(self, path |
|
|
20 | def get(self, path): | |
|
21 | 21 | """get renders the notebook template if a name is given, or |
|
22 | 22 | redirects to the '/files/' handler if the name is not given.""" |
|
23 | 23 | path = path.strip('/') |
|
24 | 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 | 26 | # a .ipynb filename was given |
|
29 |
if not cm.file_exists( |
|
|
30 |
raise web.HTTPError(404, u'Notebook does not exist: %s |
|
|
31 |
name = url_escape( |
|
|
27 | if not cm.file_exists(path): | |
|
28 | raise web.HTTPError(404, u'Notebook does not exist: %s' % path) | |
|
29 | name = url_escape(path.rsplit('/', 1)[-1]) | |
|
32 | 30 | path = url_escape(path) |
|
33 | 31 | self.write(self.render_template('notebook.html', |
|
34 | 32 | notebook_path=path, |
@@ -7,6 +7,7 b'' | |||
|
7 | 7 | from __future__ import print_function |
|
8 | 8 | |
|
9 | 9 | import base64 |
|
10 | import datetime | |
|
10 | 11 | import errno |
|
11 | 12 | import io |
|
12 | 13 | import json |
@@ -35,7 +36,7 b' from zmq.eventloop import ioloop' | |||
|
35 | 36 | ioloop.install() |
|
36 | 37 | |
|
37 | 38 | # check for tornado 3.1.0 |
|
38 |
msg = "The IPython Notebook requires tornado >= |
|
|
39 | msg = "The IPython Notebook requires tornado >= 4.0" | |
|
39 | 40 | try: |
|
40 | 41 | import tornado |
|
41 | 42 | except ImportError: |
@@ -44,14 +45,17 b' try:' | |||
|
44 | 45 | version_info = tornado.version_info |
|
45 | 46 | except AttributeError: |
|
46 | 47 | raise ImportError(msg + ", but you have < 1.1.0") |
|
47 |
if version_info < ( |
|
|
48 | if version_info < (4,0): | |
|
48 | 49 | raise ImportError(msg + ", but you have %s" % tornado.version) |
|
49 | 50 | |
|
50 | 51 | from tornado import httpserver |
|
51 | 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 | 59 | from .base.handlers import Template404 |
|
56 | 60 | from .log import log_request |
|
57 | 61 | from .services.kernels.kernelmanager import MappingKernelManager |
@@ -81,6 +85,7 b' from IPython.utils.traitlets import (' | |||
|
81 | 85 | ) |
|
82 | 86 | from IPython.utils import py3compat |
|
83 | 87 | from IPython.utils.path import filefind, get_ipython_dir |
|
88 | from IPython.utils.sysinfo import get_sys_info | |
|
84 | 89 | |
|
85 | 90 | from .utils import url_path_join |
|
86 | 91 | |
@@ -122,37 +127,43 b' def load_handlers(name):' | |||
|
122 | 127 | class NotebookWebApplication(web.Application): |
|
123 | 128 | |
|
124 | 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 | 132 | base_url, default_url, settings_overrides, jinja_env_options): |
|
127 | 133 | |
|
128 | 134 | settings = self.init_settings( |
|
129 | 135 | ipython_app, kernel_manager, contents_manager, cluster_manager, |
|
130 |
session_manager, kernel_spec_manager, log, base_url, |
|
|
131 | settings_overrides, jinja_env_options) | |
|
136 | session_manager, kernel_spec_manager, config_manager, log, base_url, | |
|
137 | default_url, settings_overrides, jinja_env_options) | |
|
132 | 138 | handlers = self.init_handlers(settings) |
|
133 | 139 | |
|
134 | 140 | super(NotebookWebApplication, self).__init__(handlers, **settings) |
|
135 | 141 | |
|
136 | 142 | def init_settings(self, ipython_app, kernel_manager, contents_manager, |
|
137 | 143 | cluster_manager, session_manager, kernel_spec_manager, |
|
144 | config_manager, | |
|
138 | 145 | log, base_url, default_url, settings_overrides, |
|
139 | 146 | jinja_env_options=None): |
|
140 | # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and | |
|
141 | # base_url will always be unicode, which will in turn | |
|
142 | # make the patterns unicode, and ultimately result in unicode | |
|
143 | # keys in kwargs to handler._execute(**kwargs) in tornado. | |
|
144 | # This enforces that base_url be ascii in that situation. | |
|
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")) | |
|
147 | ||
|
148 | _template_path = settings_overrides.get( | |
|
149 | "template_path", | |
|
150 | ipython_app.template_file_path, | |
|
151 | ) | |
|
150 | 152 | if isinstance(_template_path, str): |
|
151 | 153 | _template_path = (_template_path,) |
|
152 | 154 | template_path = [os.path.expanduser(path) for path in _template_path] |
|
153 | 155 | |
|
154 | 156 | jenv_opt = jinja_env_options if jinja_env_options else {} |
|
155 | 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 | 167 | settings = dict( |
|
157 | 168 | # basics |
|
158 | 169 | log_function=log_request, |
@@ -162,6 +173,11 b' class NotebookWebApplication(web.Application):' | |||
|
162 | 173 | static_path=ipython_app.static_file_path, |
|
163 | 174 | static_handler_class = FileFindHandler, |
|
164 | 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 | 182 | # authentication |
|
167 | 183 | cookie_secret=ipython_app.cookie_secret, |
@@ -174,6 +190,7 b' class NotebookWebApplication(web.Application):' | |||
|
174 | 190 | cluster_manager=cluster_manager, |
|
175 | 191 | session_manager=session_manager, |
|
176 | 192 | kernel_spec_manager=kernel_spec_manager, |
|
193 | config_manager=config_manager, | |
|
177 | 194 | |
|
178 | 195 | # IPython stuff |
|
179 | 196 | nbextensions_path = ipython_app.nbextensions_path, |
@@ -181,6 +198,7 b' class NotebookWebApplication(web.Application):' | |||
|
181 | 198 | mathjax_url=ipython_app.mathjax_url, |
|
182 | 199 | config=ipython_app.config, |
|
183 | 200 | jinja2_env=env, |
|
201 | terminals_available=False, # Set later if terminals are available | |
|
184 | 202 | ) |
|
185 | 203 | |
|
186 | 204 | # allow custom overrides for the tornado web app. |
@@ -188,30 +206,34 b' class NotebookWebApplication(web.Application):' | |||
|
188 | 206 | return settings |
|
189 | 207 | |
|
190 | 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 | 212 | handlers = [] |
|
193 | handlers.extend(load_handlers('base.handlers')) | |
|
194 | 213 | handlers.extend(load_handlers('tree.handlers')) |
|
195 | 214 | handlers.extend(load_handlers('auth.login')) |
|
196 | 215 | handlers.extend(load_handlers('auth.logout')) |
|
216 | handlers.extend(load_handlers('files.handlers')) | |
|
197 | 217 | handlers.extend(load_handlers('notebook.handlers')) |
|
198 | 218 | handlers.extend(load_handlers('nbconvert.handlers')) |
|
199 | 219 | handlers.extend(load_handlers('kernelspecs.handlers')) |
|
220 | handlers.extend(load_handlers('edit.handlers')) | |
|
221 | handlers.extend(load_handlers('services.config.handlers')) | |
|
200 | 222 | handlers.extend(load_handlers('services.kernels.handlers')) |
|
201 | 223 | handlers.extend(load_handlers('services.contents.handlers')) |
|
202 | 224 | handlers.extend(load_handlers('services.clusters.handlers')) |
|
203 | 225 | handlers.extend(load_handlers('services.sessions.handlers')) |
|
204 | 226 | handlers.extend(load_handlers('services.nbconvert.handlers')) |
|
205 | 227 | handlers.extend(load_handlers('services.kernelspecs.handlers')) |
|
206 | # FIXME: /files/ should be handled by the Contents service when it exists | |
|
207 | cm = settings['contents_manager'] | |
|
208 | if hasattr(cm, 'root_dir'): | |
|
209 | handlers.append( | |
|
210 | (r"/files/(.*)", AuthenticatedFileHandler, {'path' : cm.root_dir}), | |
|
211 | ) | |
|
228 | handlers.extend(load_handlers('services.security.handlers')) | |
|
212 | 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 | 237 | # set the URL that will be redirected from `/` |
|
216 | 238 | handlers.append( |
|
217 | 239 | (r'/?', web.RedirectHandler, { |
@@ -325,7 +347,7 b' class NotebookApp(BaseIPythonApplication):' | |||
|
325 | 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 | 352 | _log_formatter_cls = LogFormatter |
|
331 | 353 | |
@@ -345,11 +367,6 b' class NotebookApp(BaseIPythonApplication):' | |||
|
345 | 367 | |
|
346 | 368 | # file to be opened in the notebook server |
|
347 | 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 | 371 | # Network related information |
|
355 | 372 | |
@@ -531,7 +548,20 b' class NotebookApp(BaseIPythonApplication):' | |||
|
531 | 548 | def static_file_path(self): |
|
532 | 549 | """return extra paths + the default location""" |
|
533 | 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 | 565 | nbextensions_path = List(Unicode, config=True, |
|
536 | 566 | help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions""" |
|
537 | 567 | ) |
@@ -599,26 +629,38 b' class NotebookApp(BaseIPythonApplication):' | |||
|
599 | 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 | 637 | kernel_spec_manager = Instance(KernelSpecManager) |
|
603 | 638 | |
|
604 | 639 | def _kernel_spec_manager_default(self): |
|
605 | 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 | 653 | trust_xheaders = Bool(False, config=True, |
|
608 | 654 | help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers" |
|
609 | 655 | "sent by the upstream reverse proxy. Necessary if the proxy handles SSL") |
|
610 | 656 | ) |
|
611 | ||
|
657 | ||
|
612 | 658 | info_file = Unicode() |
|
613 | 659 | |
|
614 | 660 | def _info_file_default(self): |
|
615 | 661 | info_file = "nbserver-%s.json"%os.getpid() |
|
616 | 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 | 664 | pylab = Unicode('disabled', config=True, |
|
623 | 665 | help=""" |
|
624 | 666 | DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib. |
@@ -636,6 +678,16 b' class NotebookApp(BaseIPythonApplication):' | |||
|
636 | 678 | ) |
|
637 | 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 | 691 | def _notebook_dir_changed(self, name, old, new): |
|
640 | 692 | """Do a bit of validation of the notebook dir.""" |
|
641 | 693 | if not os.path.isabs(new): |
@@ -671,16 +723,20 b' class NotebookApp(BaseIPythonApplication):' | |||
|
671 | 723 | self.update_config(c) |
|
672 | 724 | |
|
673 | 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 | 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 | 731 | def init_configurables(self): |
|
679 | 732 | # force Session default to be secure |
|
680 | 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 | 737 | kls = import_item(self.kernel_manager_class) |
|
682 | 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 | 740 | connection_dir = self.profile_dir.security_dir, |
|
685 | 741 | ) |
|
686 | 742 | kls = import_item(self.contents_manager_class) |
@@ -693,12 +749,19 b' class NotebookApp(BaseIPythonApplication):' | |||
|
693 | 749 | self.cluster_manager = kls(parent=self, log=self.log) |
|
694 | 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 | 756 | def init_logging(self): |
|
697 | 757 | # This prevents double log messages because tornado use a root logger that |
|
698 | 758 | # self.log is a child of. The logging module dipatches log messages to a log |
|
699 | 759 | # and all of its ancenstors until propagate is set to False. |
|
700 | 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 | 765 | # hook up tornado 3's loggers to our app handlers |
|
703 | 766 | logger = logging.getLogger('tornado') |
|
704 | 767 | logger.propagate = True |
@@ -715,6 +778,7 b' class NotebookApp(BaseIPythonApplication):' | |||
|
715 | 778 | self.web_app = NotebookWebApplication( |
|
716 | 779 | self, self.kernel_manager, self.contents_manager, |
|
717 | 780 | self.cluster_manager, self.session_manager, self.kernel_spec_manager, |
|
781 | self.config_manager, | |
|
718 | 782 | self.log, self.base_url, self.default_url, self.tornado_settings, |
|
719 | 783 | self.jinja_environment_options |
|
720 | 784 | ) |
@@ -771,6 +835,14 b' class NotebookApp(BaseIPythonApplication):' | |||
|
771 | 835 | proto = 'https' if self.certfile else 'http' |
|
772 | 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 | 846 | def init_signal(self): |
|
775 | 847 | if not sys.platform.startswith('win'): |
|
776 | 848 | signal.signal(signal.SIGINT, self._handle_sigint) |
@@ -850,6 +922,7 b' class NotebookApp(BaseIPythonApplication):' | |||
|
850 | 922 | self.init_configurables() |
|
851 | 923 | self.init_components() |
|
852 | 924 | self.init_webapp() |
|
925 | self.init_terminals() | |
|
853 | 926 | self.init_signal() |
|
854 | 927 | |
|
855 | 928 | def cleanup_kernels(self): |
@@ -917,12 +990,12 b' class NotebookApp(BaseIPythonApplication):' | |||
|
917 | 990 | browser = None |
|
918 | 991 | |
|
919 | 992 | if self.file_to_run: |
|
920 |
f |
|
|
921 | if not os.path.exists(fullpath): | |
|
922 | self.log.critical("%s does not exist" % fullpath) | |
|
993 | if not os.path.exists(self.file_to_run): | |
|
994 | self.log.critical("%s does not exist" % self.file_to_run) | |
|
923 | 995 | self.exit(1) |
|
924 | ||
|
925 | uri = url_path_join('notebooks', self.file_to_run) | |
|
996 | ||
|
997 | relpath = os.path.relpath(self.file_to_run, self.notebook_dir) | |
|
998 | uri = url_path_join('notebooks', *relpath.split(os.sep)) | |
|
926 | 999 | else: |
|
927 | 1000 | uri = 'tree' |
|
928 | 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: | |
|
4 | ||
|
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 | #----------------------------------------------------------------------------- | |
|
3 | # Copyright (c) IPython Development Team. | |
|
4 | # Distributed under the terms of the Modified BSD License. | |
|
18 | 5 | |
|
19 | 6 | from tornado import web |
|
20 | from zmq.eventloop import ioloop | |
|
21 | 7 | |
|
22 | 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 | 10 | from IPython.core.profileapp import list_profiles_in |
|
25 | 11 | from IPython.core.profiledir import ProfileDir |
|
26 | 12 | from IPython.utils import py3compat |
|
27 | 13 | from IPython.utils.path import get_ipython_dir |
|
28 | 14 | |
|
29 | 15 | |
|
30 | #----------------------------------------------------------------------------- | |
|
31 | # Classes | |
|
32 | #----------------------------------------------------------------------------- | |
|
33 | ||
|
34 | ||
|
35 | ||
|
36 | ||
|
37 | 16 | class ClusterManager(LoggingConfigurable): |
|
38 | 17 | |
|
39 | 18 | profiles = Dict() |
|
40 | 19 | |
|
41 |
delay = |
|
|
20 | delay = Float(1., config=True, | |
|
42 | 21 | help="delay (in s) between starting the controller and the engines") |
|
43 | 22 | |
|
44 | 23 | loop = Instance('zmq.eventloop.ioloop.IOLoop') |
@@ -75,16 +54,24 b' class ClusterManager(LoggingConfigurable):' | |||
|
75 | 54 | def update_profiles(self): |
|
76 | 55 | """List all profiles in the ipython_dir and cwd. |
|
77 | 56 | """ |
|
57 | ||
|
58 | stale = set(self.profiles) | |
|
78 | 59 | for path in [get_ipython_dir(), py3compat.getcwd()]: |
|
79 | 60 | for profile in list_profiles_in(path): |
|
61 | if profile in stale: | |
|
62 | stale.remove(profile) | |
|
80 | 63 | pd = self.get_profile_dir(profile, path) |
|
81 | 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 | 66 | self.profiles[profile] = { |
|
84 | 67 | 'profile': profile, |
|
85 | 68 | 'profile_dir': pd, |
|
86 | 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 | 76 | def list_profiles(self): |
|
90 | 77 | self.update_profiles() |
@@ -133,11 +120,13 b' class ClusterManager(LoggingConfigurable):' | |||
|
133 | 120 | esl.stop() |
|
134 | 121 | clean_data() |
|
135 | 122 | cl.on_stop(controller_stopped) |
|
136 | ||
|
137 | dc = ioloop.DelayedCallback(lambda: cl.start(), 0, self.loop) | |
|
138 |
d |
|
|
139 | dc = ioloop.DelayedCallback(lambda: esl.start(n), 1000*self.delay, self.loop) | |
|
140 |
|
|
|
123 | loop = self.loop | |
|
124 | ||
|
125 | def start(): | |
|
126 | """start the controller, then the engines after a delay""" | |
|
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 | 131 | self.log.debug('Cluster started') |
|
143 | 132 | data['controller_launcher'] = cl |
@@ -4,27 +4,66 b'' | |||
|
4 | 4 | # Distributed under the terms of the Modified BSD License. |
|
5 | 5 | |
|
6 | 6 | import base64 |
|
7 | import errno | |
|
7 | 8 | import io |
|
8 | 9 | import os |
|
9 | import glob | |
|
10 | 10 | import shutil |
|
11 | from contextlib import contextmanager | |
|
12 | import mimetypes | |
|
11 | 13 | |
|
12 | 14 | from tornado import web |
|
13 | 15 | |
|
14 | 16 | from .manager import ContentsManager |
|
15 |
from IPython |
|
|
17 | from IPython import nbformat | |
|
16 | 18 | from IPython.utils.io import atomic_writing |
|
17 | 19 | from IPython.utils.path import ensure_dir_exists |
|
18 | 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 | 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 | 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 | 67 | save_script = Bool(False, config=True, help='DEPRECATED, IGNORED') |
|
29 | 68 | def _save_script_changed(self): |
|
30 | 69 | self.log.warn(""" |
@@ -61,27 +100,22 b' class FileContentsManager(ContentsManager):' | |||
|
61 | 100 | except OSError as e: |
|
62 | 101 | self.log.debug("copystat on %s failed", dest, exc_info=True) |
|
63 | 102 | |
|
64 |
def _get_os_path(self, |
|
|
65 |
"""Given a |
|
|
66 | path. | |
|
103 | def _get_os_path(self, path): | |
|
104 | """Given an API path, return its file system path. | |
|
67 | 105 | |
|
68 | 106 | Parameters |
|
69 | 107 | ---------- |
|
70 | name : string | |
|
71 | A filename | |
|
72 | 108 | path : string |
|
73 | 109 | The relative API path to the named file. |
|
74 | 110 | |
|
75 | 111 | Returns |
|
76 | 112 | ------- |
|
77 | 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 | 116 | return to_os_path(path, self.root_dir) |
|
83 | 117 | |
|
84 |
def |
|
|
118 | def dir_exists(self, path): | |
|
85 | 119 | """Does the API-style path refer to an extant directory? |
|
86 | 120 | |
|
87 | 121 | API-style wrapper for os.path.isdir |
@@ -112,25 +146,22 b' class FileContentsManager(ContentsManager):' | |||
|
112 | 146 | |
|
113 | 147 | Returns |
|
114 | 148 | ------- |
|
115 |
|
|
|
116 | Whether the path is hidden. | |
|
117 | ||
|
149 | hidden : bool | |
|
150 | Whether the path exists and is hidden. | |
|
118 | 151 | """ |
|
119 | 152 | path = path.strip('/') |
|
120 | 153 | os_path = self._get_os_path(path=path) |
|
121 | 154 | return is_hidden(os_path, self.root_dir) |
|
122 | 155 | |
|
123 |
def file_exists(self, |
|
|
156 | def file_exists(self, path): | |
|
124 | 157 | """Returns True if the file exists, else returns False. |
|
125 | 158 | |
|
126 | 159 | API-style wrapper for os.path.isfile |
|
127 | 160 | |
|
128 | 161 | Parameters |
|
129 | 162 | ---------- |
|
130 | name : string | |
|
131 | The name of the file you are checking. | |
|
132 | 163 | path : string |
|
133 |
The relative path to the file |
|
|
164 | The relative path to the file (with '/' as separator) | |
|
134 | 165 | |
|
135 | 166 | Returns |
|
136 | 167 | ------- |
@@ -138,20 +169,18 b' class FileContentsManager(ContentsManager):' | |||
|
138 | 169 | Whether the file exists. |
|
139 | 170 | """ |
|
140 | 171 | path = path.strip('/') |
|
141 |
|
|
|
142 |
return os.path.isfile( |
|
|
172 | os_path = self._get_os_path(path) | |
|
173 | return os.path.isfile(os_path) | |
|
143 | 174 | |
|
144 |
def exists(self, |
|
|
145 |
"""Returns True if the path |
|
|
175 | def exists(self, path): | |
|
176 | """Returns True if the path exists, else returns False. | |
|
146 | 177 | |
|
147 | 178 | API-style wrapper for os.path.exists |
|
148 | 179 | |
|
149 | 180 | Parameters |
|
150 | 181 | ---------- |
|
151 | name : string | |
|
152 | The name of the file you are checking. | |
|
153 | 182 | path : string |
|
154 |
The |
|
|
183 | The API path to the file (with '/' as separator) | |
|
155 | 184 | |
|
156 | 185 | Returns |
|
157 | 186 | ------- |
@@ -159,33 +188,39 b' class FileContentsManager(ContentsManager):' | |||
|
159 | 188 | Whether the target exists. |
|
160 | 189 | """ |
|
161 | 190 | path = path.strip('/') |
|
162 |
os_path = self._get_os_path( |
|
|
191 | os_path = self._get_os_path(path=path) | |
|
163 | 192 | return os.path.exists(os_path) |
|
164 | 193 | |
|
165 |
def _base_model(self, |
|
|
194 | def _base_model(self, path): | |
|
166 | 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 | 197 | info = os.stat(os_path) |
|
169 | 198 | last_modified = tz.utcfromtimestamp(info.st_mtime) |
|
170 | 199 | created = tz.utcfromtimestamp(info.st_ctime) |
|
171 | 200 | # Create the base model. |
|
172 | 201 | model = {} |
|
173 |
model['name'] = |
|
|
202 | model['name'] = path.rsplit('/', 1)[-1] | |
|
174 | 203 | model['path'] = path |
|
175 | 204 | model['last_modified'] = last_modified |
|
176 | 205 | model['created'] = created |
|
177 | 206 | model['content'] = None |
|
178 | 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 | 214 | return model |
|
180 | 215 | |
|
181 |
def _dir_model(self, |
|
|
216 | def _dir_model(self, path, content=True): | |
|
182 | 217 | """Build a model for a directory |
|
183 | 218 | |
|
184 | 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 | 225 | if not os.path.isdir(os_path): |
|
191 | 226 | raise web.HTTPError(404, four_o_four) |
@@ -195,80 +230,105 b' class FileContentsManager(ContentsManager):' | |||
|
195 | 230 | ) |
|
196 | 231 | raise web.HTTPError(404, four_o_four) |
|
197 | 232 | |
|
198 | if name is None: | |
|
199 | if '/' in path: | |
|
200 | path, name = path.rsplit('/', 1) | |
|
201 | else: | |
|
202 | name = '' | |
|
203 | model = self._base_model(name, path) | |
|
233 | model = self._base_model(path) | |
|
204 | 234 | model['type'] = 'directory' |
|
205 | dir_path = u'{}/{}'.format(path, name) | |
|
206 | 235 | if content: |
|
207 | 236 | model['content'] = contents = [] |
|
208 |
|
|
|
209 |
|
|
|
237 | os_dir = self._get_os_path(path) | |
|
238 | for name in os.listdir(os_dir): | |
|
239 | os_path = os.path.join(os_dir, name) | |
|
210 | 240 | # skip over broken symlinks in listing |
|
211 | 241 | if not os.path.exists(os_path): |
|
212 | 242 | self.log.warn("%s doesn't exist", os_path) |
|
213 | 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 | 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 | 253 | model['format'] = 'json' |
|
218 | 254 | |
|
219 | 255 | return model |
|
220 | 256 | |
|
221 |
def _file_model(self, |
|
|
257 | def _file_model(self, path, content=True, format=None): | |
|
222 | 258 | """Build a model for a file |
|
223 | 259 | |
|
224 | 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 | 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 | 273 | if content: |
|
230 | os_path = self._get_os_path(name, path) | |
|
231 | with io.open(os_path, 'rb') as f: | |
|
274 | if not os.path.isfile(os_path): | |
|
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 | 278 | bcontent = f.read() |
|
233 | try: | |
|
234 | model['content'] = bcontent.decode('utf8') | |
|
235 | except UnicodeError as e: | |
|
279 | ||
|
280 | if format != 'base64': | |
|
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 | 290 | model['content'] = base64.encodestring(bcontent).decode('ascii') |
|
237 | 291 | model['format'] = 'base64' |
|
238 | else: | |
|
239 | model['format'] = 'text' | |
|
292 | ||
|
240 | 293 | return model |
|
241 | 294 | |
|
242 | 295 | |
|
243 |
def _notebook_model(self, |
|
|
296 | def _notebook_model(self, path, content=True): | |
|
244 | 297 | """Build a notebook model |
|
245 | 298 | |
|
246 | 299 | if content is requested, the notebook content will be populated |
|
247 | 300 | as a JSON structure (not double-serialized) |
|
248 | 301 | """ |
|
249 |
model = self._base_model( |
|
|
302 | model = self._base_model(path) | |
|
250 | 303 | model['type'] = 'notebook' |
|
251 | 304 | if content: |
|
252 |
os_path = self._get_os_path( |
|
|
253 |
with |
|
|
305 | os_path = self._get_os_path(path) | |
|
306 | with self.open(os_path, 'r', encoding='utf-8') as f: | |
|
254 | 307 | try: |
|
255 |
nb = |
|
|
308 | nb = nbformat.read(f, as_version=4) | |
|
256 | 309 | except Exception as e: |
|
257 |
raise web.HTTPError(400, u"Unreadable Notebook: %s % |
|
|
258 |
self.mark_trusted_cells(nb, |
|
|
310 | raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e)) | |
|
311 | self.mark_trusted_cells(nb, path) | |
|
259 | 312 | model['content'] = nb |
|
260 | 313 | model['format'] = 'json' |
|
314 | self.validate_notebook_model(model) | |
|
261 | 315 | return model |
|
262 | 316 | |
|
263 |
def get |
|
|
264 |
""" Takes a path |
|
|
317 | def get(self, path, content=True, type_=None, format=None): | |
|
318 | """ Takes a path for an entity and returns its model | |
|
265 | 319 | |
|
266 | 320 | Parameters |
|
267 | 321 | ---------- |
|
268 | name : str | |
|
269 | the name of the target | |
|
270 | 322 | path : str |
|
271 | 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 | 333 | Returns |
|
274 | 334 | ------- |
@@ -278,32 +338,35 b' class FileContentsManager(ContentsManager):' | |||
|
278 | 338 | """ |
|
279 | 339 | path = path.strip('/') |
|
280 | 340 | |
|
281 |
if not self.exists( |
|
|
282 |
raise web.HTTPError(404, u'No such file or directory: %s |
|
|
341 | if not self.exists(path): | |
|
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 | 345 | if os.path.isdir(os_path): |
|
286 | model = self._dir_model(name, path, content) | |
|
287 | elif name.endswith('.ipynb'): | |
|
288 | model = self._notebook_model(name, path, content) | |
|
346 | if type_ not in (None, 'directory'): | |
|
347 | raise web.HTTPError(400, | |
|
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 | 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 | 357 | return model |
|
292 | 358 | |
|
293 |
def _save_notebook(self, os_path, model, |
|
|
359 | def _save_notebook(self, os_path, model, path=''): | |
|
294 | 360 | """save a notebook file""" |
|
295 | 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']: | |
|
301 | nb['metadata']['name'] = u'' | |
|
366 | with self.atomic_writing(os_path, encoding='utf-8') as f: | |
|
367 | nbformat.write(nb, f, version=nbformat.NO_CONVERT) | |
|
302 | 368 | |
|
303 | with atomic_writing(os_path, encoding='utf-8') as f: | |
|
304 | current.write(nb, f, u'json') | |
|
305 | ||
|
306 | def _save_file(self, os_path, model, name='', path=''): | |
|
369 | def _save_file(self, os_path, model, path=''): | |
|
307 | 370 | """save a non-notebook file""" |
|
308 | 371 | fmt = model.get('format', None) |
|
309 | 372 | if fmt not in {'text', 'base64'}: |
@@ -317,21 +380,22 b' class FileContentsManager(ContentsManager):' | |||
|
317 | 380 | bcontent = base64.decodestring(b64_bytes) |
|
318 | 381 | except Exception as e: |
|
319 | 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 | 384 | f.write(bcontent) |
|
322 | 385 | |
|
323 |
def _save_directory(self, os_path, model, |
|
|
386 | def _save_directory(self, os_path, model, path=''): | |
|
324 | 387 | """create a directory""" |
|
325 | 388 | if is_hidden(os_path, self.root_dir): |
|
326 | 389 | raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path) |
|
327 | 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 | 393 | elif not os.path.isdir(os_path): |
|
330 | 394 | raise web.HTTPError(400, u'Not a directory: %s' % (os_path)) |
|
331 | 395 | else: |
|
332 | 396 | self.log.debug("Directory %r already exists", os_path) |
|
333 | 397 | |
|
334 |
def save(self, model, |
|
|
398 | def save(self, model, path=''): | |
|
335 | 399 | """Save the file model and return the model with no content.""" |
|
336 | 400 | path = path.strip('/') |
|
337 | 401 | |
@@ -341,52 +405,53 b' class FileContentsManager(ContentsManager):' | |||
|
341 | 405 | raise web.HTTPError(400, u'No file content provided') |
|
342 | 406 | |
|
343 | 407 | # One checkpoint should always exist |
|
344 |
if self.file_exists( |
|
|
345 |
self.create_checkpoint( |
|
|
346 | ||
|
347 | new_path = model.get('path', path).strip('/') | |
|
348 | new_name = model.get('name', name) | |
|
408 | if self.file_exists(path) and not self.list_checkpoints(path): | |
|
409 | self.create_checkpoint(path) | |
|
349 | 410 | |
|
350 | if path != new_path or name != new_name: | |
|
351 | self.rename(name, path, new_name, new_path) | |
|
352 | ||
|
353 | os_path = self._get_os_path(new_name, new_path) | |
|
411 | os_path = self._get_os_path(path) | |
|
354 | 412 | self.log.debug("Saving %s", os_path) |
|
355 | 413 | try: |
|
356 | 414 | if model['type'] == 'notebook': |
|
357 |
self._save_notebook(os_path, model, |
|
|
415 | self._save_notebook(os_path, model, path) | |
|
358 | 416 | elif model['type'] == 'file': |
|
359 |
self._save_file(os_path, model, |
|
|
417 | self._save_file(os_path, model, path) | |
|
360 | 418 | elif model['type'] == 'directory': |
|
361 |
self._save_directory(os_path, model, |
|
|
419 | self._save_directory(os_path, model, path) | |
|
362 | 420 | else: |
|
363 | 421 | raise web.HTTPError(400, "Unhandled contents type: %s" % model['type']) |
|
364 | 422 | except web.HTTPError: |
|
365 | 423 | raise |
|
366 | 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 | 436 | return model |
|
371 | 437 | |
|
372 |
def update(self, model, |
|
|
373 |
"""Update the file's path |
|
|
438 | def update(self, model, path): | |
|
439 | """Update the file's path | |
|
374 | 440 | |
|
375 | 441 | For use in PATCH requests, to enable renaming a file without |
|
376 | 442 | re-uploading its contents. Only used for renaming at the moment. |
|
377 | 443 | """ |
|
378 | 444 | path = path.strip('/') |
|
379 | new_name = model.get('name', name) | |
|
380 | 445 | new_path = model.get('path', path).strip('/') |
|
381 |
if path != new_path |
|
|
382 |
self.rename( |
|
|
383 |
model = self.get |
|
|
446 | if path != new_path: | |
|
447 | self.rename(path, new_path) | |
|
448 | model = self.get(new_path, content=False) | |
|
384 | 449 | return model |
|
385 | 450 | |
|
386 |
def delete(self, |
|
|
387 |
"""Delete file |
|
|
451 | def delete(self, path): | |
|
452 | """Delete file at path.""" | |
|
388 | 453 | path = path.strip('/') |
|
389 |
os_path = self._get_os_path( |
|
|
454 | os_path = self._get_os_path(path) | |
|
390 | 455 | rm = os.unlink |
|
391 | 456 | if os.path.isdir(os_path): |
|
392 | 457 | listing = os.listdir(os_path) |
@@ -397,71 +462,81 b' class FileContentsManager(ContentsManager):' | |||
|
397 | 462 | raise web.HTTPError(404, u'File does not exist: %s' % os_path) |
|
398 | 463 | |
|
399 | 464 | # clear checkpoints |
|
400 |
for checkpoint in self.list_checkpoints( |
|
|
465 | for checkpoint in self.list_checkpoints(path): | |
|
401 | 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 | 468 | if os.path.isfile(cp_path): |
|
404 | 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 | 473 | if os.path.isdir(os_path): |
|
408 | 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 | 477 | else: |
|
411 | 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 | 483 | """Rename a file.""" |
|
416 | 484 | old_path = old_path.strip('/') |
|
417 | 485 | new_path = new_path.strip('/') |
|
418 |
if |
|
|
486 | if new_path == old_path: | |
|
419 | 487 | return |
|
420 | 488 | |
|
421 |
new_os_path = self._get_os_path( |
|
|
422 |
old_os_path = self._get_os_path( |
|
|
489 | new_os_path = self._get_os_path(new_path) | |
|
490 | old_os_path = self._get_os_path(old_path) | |
|
423 | 491 | |
|
424 | 492 | # Should we proceed with the move? |
|
425 |
if os.path. |
|
|
426 |
raise web.HTTPError(409, u'File |
|
|
493 | if os.path.exists(new_os_path): | |
|
494 | raise web.HTTPError(409, u'File already exists: %s' % new_path) | |
|
427 | 495 | |
|
428 | 496 | # Move the file |
|
429 | 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 | 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 | 505 | # Move the checkpoints |
|
435 |
old_checkpoints = self.list_checkpoints( |
|
|
506 | old_checkpoints = self.list_checkpoints(old_path) | |
|
436 | 507 | for cp in old_checkpoints: |
|
437 | 508 | checkpoint_id = cp['id'] |
|
438 |
old_cp_path = self.get_checkpoint_path(checkpoint_id, |
|
|
439 |
new_cp_path = self.get_checkpoint_path(checkpoint_id, |
|
|
509 | old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path) | |
|
510 | new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path) | |
|
440 | 511 | if os.path.isfile(old_cp_path): |
|
441 | 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 | 516 | # Checkpoint-related utilities |
|
445 | 517 | |
|
446 |
def get_checkpoint_path(self, checkpoint_id, |
|
|
518 | def get_checkpoint_path(self, checkpoint_id, path): | |
|
447 | 519 | """find the path to a checkpoint""" |
|
448 | 520 | path = path.strip('/') |
|
521 | parent, name = ('/' + path).rsplit('/', 1) | |
|
522 | parent = parent.strip('/') | |
|
449 | 523 | basename, ext = os.path.splitext(name) |
|
450 | 524 | filename = u"{name}-{checkpoint_id}{ext}".format( |
|
451 | 525 | name=basename, |
|
452 | 526 | checkpoint_id=checkpoint_id, |
|
453 | 527 | ext=ext, |
|
454 | 528 | ) |
|
455 |
os_path = self._get_os_path(path=pat |
|
|
529 | os_path = self._get_os_path(path=parent) | |
|
456 | 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 | 533 | cp_path = os.path.join(cp_dir, filename) |
|
459 | 534 | return cp_path |
|
460 | 535 | |
|
461 |
def get_checkpoint_model(self, checkpoint_id, |
|
|
536 | def get_checkpoint_model(self, checkpoint_id, path): | |
|
462 | 537 | """construct the info dict for a given checkpoint""" |
|
463 | 538 | path = path.strip('/') |
|
464 |
cp_path = self.get_checkpoint_path(checkpoint_id, |
|
|
539 | cp_path = self.get_checkpoint_path(checkpoint_id, path) | |
|
465 | 540 | stats = os.stat(cp_path) |
|
466 | 541 | last_modified = tz.utcfromtimestamp(stats.st_mtime) |
|
467 | 542 | info = dict( |
@@ -472,58 +547,62 b' class FileContentsManager(ContentsManager):' | |||
|
472 | 547 | |
|
473 | 548 | # public checkpoint API |
|
474 | 549 | |
|
475 |
def create_checkpoint(self, |
|
|
550 | def create_checkpoint(self, path): | |
|
476 | 551 | """Create a checkpoint from the current state of a file""" |
|
477 | 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 | 556 | # only the one checkpoint ID: |
|
480 | 557 | checkpoint_id = u"checkpoint" |
|
481 |
cp_path = self.get_checkpoint_path(checkpoint_id, |
|
|
482 |
self.log.debug("creating checkpoint for %s", |
|
|
483 | self._copy(src_path, cp_path) | |
|
558 | cp_path = self.get_checkpoint_path(checkpoint_id, path) | |
|
559 | self.log.debug("creating checkpoint for %s", path) | |
|
560 | with self.perm_to_403(): | |
|
561 | self._copy(src_path, cp_path) | |
|
484 | 562 | |
|
485 | 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 | 567 | """list the checkpoints for a given file |
|
490 | 568 | |
|
491 | 569 | This contents manager currently only supports one checkpoint per file. |
|
492 | 570 | """ |
|
493 | 571 | path = path.strip('/') |
|
494 | 572 | checkpoint_id = "checkpoint" |
|
495 |
os_path = self.get_checkpoint_path(checkpoint_id, |
|
|
573 | os_path = self.get_checkpoint_path(checkpoint_id, path) | |
|
496 | 574 | if not os.path.exists(os_path): |
|
497 | 575 | return [] |
|
498 | 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 | 581 | """restore a file to a checkpointed state""" |
|
504 | 582 | path = path.strip('/') |
|
505 |
self.log.info("restoring %s from checkpoint %s", |
|
|
506 |
nb_path = self._get_os_path( |
|
|
507 |
cp_path = self.get_checkpoint_path(checkpoint_id, |
|
|
583 | self.log.info("restoring %s from checkpoint %s", path, checkpoint_id) | |
|
584 | nb_path = self._get_os_path(path) | |
|
585 | cp_path = self.get_checkpoint_path(checkpoint_id, path) | |
|
508 | 586 | if not os.path.isfile(cp_path): |
|
509 | 587 | self.log.debug("checkpoint file does not exist: %s", cp_path) |
|
510 | 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 | 591 | # ensure notebook is readable (never restore from an unreadable notebook) |
|
514 | 592 | if cp_path.endswith('.ipynb'): |
|
515 |
with |
|
|
516 |
|
|
|
517 | self._copy(cp_path, nb_path) | |
|
593 | with self.open(cp_path, 'r', encoding='utf-8') as f: | |
|
594 | nbformat.read(f, as_version=4) | |
|
518 | 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 | 600 | """delete a file's checkpoint""" |
|
522 | 601 | path = path.strip('/') |
|
523 |
cp_path = self.get_checkpoint_path(checkpoint_id, |
|
|
602 | cp_path = self.get_checkpoint_path(checkpoint_id, path) | |
|
524 | 603 | if not os.path.isfile(cp_path): |
|
525 | 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 | 607 | self.log.debug("unlinking %s", cp_path) |
|
529 | 608 | os.unlink(cp_path) |
@@ -531,6 +610,10 b' class FileContentsManager(ContentsManager):' | |||
|
531 | 610 | def info_string(self): |
|
532 | 611 | return "Serving notebooks from local directory: %s" % self.root_dir |
|
533 | 612 | |
|
534 |
def get_kernel_path(self, |
|
|
535 |
"""Return the initial |
|
|
536 | return os.path.join(self.root_dir, path) | |
|
613 | def get_kernel_path(self, path, model=None): | |
|
614 | """Return the initial API path of a kernel associated with a given notebook""" | |
|
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 | 10 | from IPython.html.utils import url_path_join, url_escape |
|
11 | 11 | from IPython.utils.jsonutil import date_default |
|
12 | 12 | |
|
13 |
from IPython.html.base.handlers import ( |
|
|
14 | file_path_regex, path_regex, | |
|
15 | file_name_regex) | |
|
13 | from IPython.html.base.handlers import ( | |
|
14 | IPythonHandler, json_errors, path_regex, | |
|
15 | ) | |
|
16 | 16 | |
|
17 | 17 | |
|
18 | 18 | def sort_key(model): |
@@ -29,38 +29,44 b' class ContentsHandler(IPythonHandler):' | |||
|
29 | 29 | |
|
30 | 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 | 33 | """Return the full URL location of a file. |
|
34 | 34 | |
|
35 | 35 | Parameters |
|
36 | 36 | ---------- |
|
37 | name : unicode | |
|
38 | The base name of the file, such as "foo.ipynb". | |
|
39 | 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 | 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 | 44 | def _finish_model(self, model, location=True): |
|
47 | 45 | """Finish a JSON request with a model, setting relevant headers, etc.""" |
|
48 | 46 | if location: |
|
49 |
location = self.location_url(model[' |
|
|
47 | location = self.location_url(model['path']) | |
|
50 | 48 | self.set_header('Location', location) |
|
51 | 49 | self.set_header('Last-Modified', model['last_modified']) |
|
52 | 50 | self.finish(json.dumps(model, default=date_default)) |
|
53 | 51 | |
|
54 | 52 | @web.authenticated |
|
55 | 53 | @json_errors |
|
56 |
def get(self, path='' |
|
|
54 | def get(self, path=''): | |
|
57 | 55 | """Return a model for a file or directory. |
|
58 | 56 | |
|
59 | 57 | A directory model contains a list of models (without content) |
|
60 | 58 | of the files and directories it contains. |
|
61 | 59 | """ |
|
62 | 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 | 70 | if model['type'] == 'directory': |
|
65 | 71 | # group listing by type, then by name (case-insensitive) |
|
66 | 72 | # FIXME: sorting should be done in the frontends |
@@ -69,112 +75,83 b' class ContentsHandler(IPythonHandler):' | |||
|
69 | 75 | |
|
70 | 76 | @web.authenticated |
|
71 | 77 | @json_errors |
|
72 |
def patch(self, path='' |
|
|
73 |
"""PATCH renames a |
|
|
78 | def patch(self, path=''): | |
|
79 | """PATCH renames a file or directory without re-uploading content.""" | |
|
74 | 80 | cm = self.contents_manager |
|
75 | if name is None: | |
|
76 | raise web.HTTPError(400, u'Filename missing') | |
|
77 | 81 | model = self.get_json_body() |
|
78 | 82 | if model is None: |
|
79 | 83 | raise web.HTTPError(400, u'JSON body missing') |
|
80 |
model = cm.update(model, |
|
|
84 | model = cm.update(model, path) | |
|
81 | 85 | self._finish_model(model) |
|
82 | 86 | |
|
83 |
def _copy(self, copy_from, |
|
|
84 |
"""Copy a file, optionally specifying |
|
|
85 | """ | |
|
86 | self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format( | |
|
87 | def _copy(self, copy_from, copy_to=None): | |
|
88 | """Copy a file, optionally specifying a target directory.""" | |
|
89 | self.log.info(u"Copying {copy_from} to {copy_to}".format( | |
|
87 | 90 | copy_from=copy_from, |
|
88 | path=path, | |
|
89 | 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 | 94 | self.set_status(201) |
|
93 | 95 | self._finish_model(model) |
|
94 | 96 | |
|
95 |
def _upload(self, model, path |
|
|
96 | """Handle upload of a new file | |
|
97 | ||
|
98 | If name specified, create it in path/name, | |
|
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) | |
|
97 | def _upload(self, model, path): | |
|
98 | """Handle upload of a new file to path""" | |
|
99 | self.log.info(u"Uploading file to %s", path) | |
|
100 | model = self.contents_manager.new(model, path) | |
|
106 | 101 | self.set_status(201) |
|
107 | 102 | self._finish_model(model) |
|
108 | ||
|
109 |
def _ |
|
|
110 |
"""Create an empty |
|
|
111 | ||
|
112 | If name specified, create it in path/name. | |
|
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) | |
|
103 | ||
|
104 | def _new_untitled(self, path, type='', ext=''): | |
|
105 | """Create a new, empty untitled entity""" | |
|
106 | self.log.info(u"Creating new %s in %s", type or 'file', path) | |
|
107 | model = self.contents_manager.new_untitled(path=path, type=type, ext=ext) | |
|
119 | 108 | self.set_status(201) |
|
120 | 109 | self._finish_model(model) |
|
121 | 110 | |
|
122 |
def _save(self, model, path |
|
|
111 | def _save(self, model, path): | |
|
123 | 112 | """Save an existing file.""" |
|
124 |
self.log.info(u"Saving file at %s |
|
|
125 |
model = self.contents_manager.save(model, |
|
|
126 | if model['path'] != path.strip('/') or model['name'] != name: | |
|
127 | # a rename happened, set Location header | |
|
128 | location = True | |
|
129 | else: | |
|
130 | location = False | |
|
131 | self._finish_model(model, location) | |
|
113 | self.log.info(u"Saving file at %s", path) | |
|
114 | model = self.contents_manager.save(model, path) | |
|
115 | self._finish_model(model) | |
|
132 | 116 | |
|
133 | 117 | @web.authenticated |
|
134 | 118 | @json_errors |
|
135 |
def post(self, path='' |
|
|
136 |
"""Create a new file |
|
|
119 | def post(self, path=''): | |
|
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 | 124 | POST /api/contents/path |
|
141 | New untitled notebook in path. If content specified, upload a | |
|
142 | notebook, otherwise start empty. | |
|
125 | New untitled, empty file or directory. | |
|
143 | 126 | POST /api/contents/path |
|
144 | with body {"copy_from" : "OtherNotebook.ipynb"} | |
|
127 | with body {"copy_from" : "/path/to/OtherNotebook.ipynb"} | |
|
145 | 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 | 131 | cm = self.contents_manager |
|
152 | 132 | |
|
153 | 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 | 137 | raise web.HTTPError(404, "No such directory: %s" % path) |
|
158 | 138 | |
|
159 | 139 | model = self.get_json_body() |
|
160 | 140 | |
|
161 | 141 | if model is not None: |
|
162 | 142 | copy_from = model.get('copy_from') |
|
163 |
ext = model.get('ext', ' |
|
|
164 |
|
|
|
165 |
|
|
|
166 | raise web.HTTPError(400, "Can't upload and copy at the same time.") | |
|
167 | self._upload(model, path) | |
|
168 | elif copy_from: | |
|
143 | ext = model.get('ext', '') | |
|
144 | type = model.get('type', '') | |
|
145 | if copy_from: | |
|
169 | 146 | self._copy(copy_from, path) |
|
170 | 147 | else: |
|
171 |
self._ |
|
|
148 | self._new_untitled(path, type=type, ext=ext) | |
|
172 | 149 | else: |
|
173 |
self._ |
|
|
150 | self._new_untitled(path) | |
|
174 | 151 | |
|
175 | 152 | @web.authenticated |
|
176 | 153 | @json_errors |
|
177 |
def put(self, path='' |
|
|
154 | def put(self, path=''): | |
|
178 | 155 | """Saves the file in the location specified by name and path. |
|
179 | 156 | |
|
180 | 157 | PUT is very similar to POST, but the requester specifies the name, |
@@ -184,39 +161,25 b' class ContentsHandler(IPythonHandler):' | |||
|
184 | 161 | Save notebook at ``path/Name.ipynb``. Notebook structure is specified |
|
185 | 162 | in `content` key of JSON request body. If content is not specified, |
|
186 | 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 | 165 | model = self.get_json_body() |
|
200 | 166 | if model: |
|
201 |
|
|
|
202 | if copy_from: | |
|
203 | if model.get('content'): | |
|
204 | raise web.HTTPError(400, "Can't upload and copy at the same time.") | |
|
205 | self._copy(copy_from, path, name) | |
|
206 | elif self.contents_manager.file_exists(name, path): | |
|
207 | self._save(model, path, name) | |
|
167 | if model.get('copy_from'): | |
|
168 | raise web.HTTPError(400, "Cannot copy with PUT, only POST") | |
|
169 | if self.contents_manager.file_exists(path): | |
|
170 | self._save(model, path) | |
|
208 | 171 | else: |
|
209 |
self._upload(model, path |
|
|
172 | self._upload(model, path) | |
|
210 | 173 | else: |
|
211 |
self._ |
|
|
174 | self._new_untitled(path) | |
|
212 | 175 | |
|
213 | 176 | @web.authenticated |
|
214 | 177 | @json_errors |
|
215 |
def delete(self, path='' |
|
|
178 | def delete(self, path=''): | |
|
216 | 179 | """delete a file in the given path""" |
|
217 | 180 | cm = self.contents_manager |
|
218 |
self.log.warn('delete %s |
|
|
219 |
cm.delete( |
|
|
181 | self.log.warn('delete %s', path) | |
|
182 | cm.delete(path) | |
|
220 | 183 | self.set_status(204) |
|
221 | 184 | self.finish() |
|
222 | 185 | |
@@ -227,22 +190,22 b' class CheckpointsHandler(IPythonHandler):' | |||
|
227 | 190 | |
|
228 | 191 | @web.authenticated |
|
229 | 192 | @json_errors |
|
230 |
def get(self, path='' |
|
|
193 | def get(self, path=''): | |
|
231 | 194 | """get lists checkpoints for a file""" |
|
232 | 195 | cm = self.contents_manager |
|
233 |
checkpoints = cm.list_checkpoints( |
|
|
196 | checkpoints = cm.list_checkpoints(path) | |
|
234 | 197 | data = json.dumps(checkpoints, default=date_default) |
|
235 | 198 | self.finish(data) |
|
236 | 199 | |
|
237 | 200 | @web.authenticated |
|
238 | 201 | @json_errors |
|
239 |
def post(self, path='' |
|
|
202 | def post(self, path=''): | |
|
240 | 203 | """post creates a new checkpoint""" |
|
241 | 204 | cm = self.contents_manager |
|
242 |
checkpoint = cm.create_checkpoint( |
|
|
205 | checkpoint = cm.create_checkpoint(path) | |
|
243 | 206 | data = json.dumps(checkpoint, default=date_default) |
|
244 | 207 | location = url_path_join(self.base_url, 'api/contents', |
|
245 |
path |
|
|
208 | path, 'checkpoints', checkpoint['id']) | |
|
246 | 209 | self.set_header('Location', url_escape(location)) |
|
247 | 210 | self.set_status(201) |
|
248 | 211 | self.finish(data) |
@@ -254,22 +217,38 b' class ModifyCheckpointsHandler(IPythonHandler):' | |||
|
254 | 217 | |
|
255 | 218 | @web.authenticated |
|
256 | 219 | @json_errors |
|
257 |
def post(self, path, |
|
|
220 | def post(self, path, checkpoint_id): | |
|
258 | 221 | """post restores a file from a checkpoint""" |
|
259 | 222 | cm = self.contents_manager |
|
260 |
cm.restore_checkpoint(checkpoint_id, |
|
|
223 | cm.restore_checkpoint(checkpoint_id, path) | |
|
261 | 224 | self.set_status(204) |
|
262 | 225 | self.finish() |
|
263 | 226 | |
|
264 | 227 | @web.authenticated |
|
265 | 228 | @json_errors |
|
266 |
def delete(self, path, |
|
|
229 | def delete(self, path, checkpoint_id): | |
|
267 | 230 | """delete clears a checkpoint for a given file""" |
|
268 | 231 | cm = self.contents_manager |
|
269 |
cm.delete_checkpoint(checkpoint_id, |
|
|
232 | cm.delete_checkpoint(checkpoint_id, path) | |
|
270 | 233 | self.set_status(204) |
|
271 | 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 | 253 | # URL to handler mappings |
|
275 | 254 | #----------------------------------------------------------------------------- |
@@ -278,9 +257,9 b' class ModifyCheckpointsHandler(IPythonHandler):' | |||
|
278 | 257 | _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)" |
|
279 | 258 | |
|
280 | 259 | default_handlers = [ |
|
281 |
(r"/api/contents%s/checkpoints" % |
|
|
282 |
(r"/api/contents%s/checkpoints/%s" % ( |
|
|
260 | (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler), | |
|
261 | (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex), | |
|
283 | 262 | ModifyCheckpointsHandler), |
|
284 | (r"/api/contents%s" % file_path_regex, ContentsHandler), | |
|
285 | 263 | (r"/api/contents%s" % path_regex, ContentsHandler), |
|
264 | (r"/api/notebooks/?(.*)", NotebooksRedirectHandler), | |
|
286 | 265 | ] |
@@ -5,14 +5,18 b'' | |||
|
5 | 5 | |
|
6 | 6 | from fnmatch import fnmatch |
|
7 | 7 | import itertools |
|
8 | import json | |
|
8 | 9 | import os |
|
10 | import re | |
|
9 | 11 | |
|
10 | 12 | from tornado.web import HTTPError |
|
11 | 13 | |
|
12 | 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 | 17 | from IPython.utils.traitlets import Instance, Unicode, List |
|
15 | 18 | |
|
19 | copy_pat = re.compile(r'\-Copy\d*\.') | |
|
16 | 20 | |
|
17 | 21 | class ContentsManager(LoggingConfigurable): |
|
18 | 22 | """Base class for serving files and directories. |
@@ -31,14 +35,6 b' class ContentsManager(LoggingConfigurable):' | |||
|
31 | 35 | - if unspecified, path defaults to '', |
|
32 | 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 | 40 | notary = Instance(sign.NotebookNotary) |
@@ -67,7 +63,7 b' class ContentsManager(LoggingConfigurable):' | |||
|
67 | 63 | # ContentsManager API part 1: methods that must be |
|
68 | 64 | # implemented in subclasses. |
|
69 | 65 | |
|
70 |
def |
|
|
66 | def dir_exists(self, path): | |
|
71 | 67 | """Does the API-style path (directory) actually exist? |
|
72 | 68 | |
|
73 | 69 | Like os.path.isdir |
@@ -103,8 +99,8 b' class ContentsManager(LoggingConfigurable):' | |||
|
103 | 99 | """ |
|
104 | 100 | raise NotImplementedError |
|
105 | 101 | |
|
106 |
def file_exists(self, |
|
|
107 |
"""Does a file exist at the given |
|
|
102 | def file_exists(self, path=''): | |
|
103 | """Does a file exist at the given path? | |
|
108 | 104 | |
|
109 | 105 | Like os.path.isfile |
|
110 | 106 | |
@@ -124,15 +120,13 b' class ContentsManager(LoggingConfigurable):' | |||
|
124 | 120 | """ |
|
125 | 121 | raise NotImplementedError('must be implemented in a subclass') |
|
126 | 122 | |
|
127 |
def exists(self, |
|
|
128 |
"""Does a file or directory exist at the given |
|
|
123 | def exists(self, path): | |
|
124 | """Does a file or directory exist at the given path? | |
|
129 | 125 | |
|
130 | 126 | Like os.path.exists |
|
131 | 127 | |
|
132 | 128 | Parameters |
|
133 | 129 | ---------- |
|
134 | name : string | |
|
135 | The name of the file you are checking. | |
|
136 | 130 | path : string |
|
137 | 131 | The relative path to the file's directory (with '/' as separator) |
|
138 | 132 | |
@@ -141,17 +135,17 b' class ContentsManager(LoggingConfigurable):' | |||
|
141 | 135 | exists : bool |
|
142 | 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 | 141 | """Get the model of a file or directory with or without content.""" |
|
148 | 142 | raise NotImplementedError('must be implemented in a subclass') |
|
149 | 143 | |
|
150 |
def save(self, model, |
|
|
144 | def save(self, model, path): | |
|
151 | 145 | """Save the file or directory and return the model with no content.""" |
|
152 | 146 | raise NotImplementedError('must be implemented in a subclass') |
|
153 | 147 | |
|
154 |
def update(self, model, |
|
|
148 | def update(self, model, path): | |
|
155 | 149 | """Update the file or directory and return the model with no content. |
|
156 | 150 | |
|
157 | 151 | For use in PATCH requests, to enable renaming a file without |
@@ -159,26 +153,26 b' class ContentsManager(LoggingConfigurable):' | |||
|
159 | 153 | """ |
|
160 | 154 | raise NotImplementedError('must be implemented in a subclass') |
|
161 | 155 | |
|
162 |
def delete(self, |
|
|
163 |
"""Delete file or directory by |
|
|
156 | def delete(self, path): | |
|
157 | """Delete file or directory by path.""" | |
|
164 | 158 | raise NotImplementedError('must be implemented in a subclass') |
|
165 | 159 | |
|
166 |
def create_checkpoint(self, |
|
|
160 | def create_checkpoint(self, path): | |
|
167 | 161 | """Create a checkpoint of the current state of a file |
|
168 | 162 | |
|
169 | 163 | Returns a checkpoint_id for the new checkpoint. |
|
170 | 164 | """ |
|
171 | 165 | raise NotImplementedError("must be implemented in a subclass") |
|
172 | 166 | |
|
173 |
def list_checkpoints(self, |
|
|
167 | def list_checkpoints(self, path): | |
|
174 | 168 | """Return a list of checkpoints for a given file""" |
|
175 | 169 | return [] |
|
176 | 170 | |
|
177 |
def restore_checkpoint(self, checkpoint_id, |
|
|
171 | def restore_checkpoint(self, checkpoint_id, path): | |
|
178 | 172 | """Restore a file from one of its checkpoints""" |
|
179 | 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 | 176 | """delete a checkpoint for a file""" |
|
183 | 177 | raise NotImplementedError("must be implemented in a subclass") |
|
184 | 178 | |
@@ -188,11 +182,19 b' class ContentsManager(LoggingConfigurable):' | |||
|
188 | 182 | def info_string(self): |
|
189 | 183 | return "Serving contents" |
|
190 | 184 | |
|
191 |
def get_kernel_path(self, |
|
|
192 |
""" |
|
|
193 | return path | |
|
185 | def get_kernel_path(self, path, model=None): | |
|
186 | """Return the API path for the kernel | |
|
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 | 198 | """Increment a filename until it is unique. |
|
197 | 199 | |
|
198 | 200 | Parameters |
@@ -210,87 +212,140 b' class ContentsManager(LoggingConfigurable):' | |||
|
210 | 212 | path = path.strip('/') |
|
211 | 213 | basename, ext = os.path.splitext(filename) |
|
212 | 214 | for i in itertools.count(): |
|
213 | name = u'{basename}{i}{ext}'.format(basename=basename, i=i, | |
|
214 | ext=ext) | |
|
215 | if not self.file_exists(name, path): | |
|
215 | if i: | |
|
216 | insert_i = '{}{}'.format(insert, i) | |
|
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 | 222 | break |
|
217 | 223 | return name |
|
218 | 224 | |
|
219 | def create_file(self, model=None, path='', ext='.ipynb'): | |
|
220 | """Create a new file or directory and return its model with no content.""" | |
|
225 | def validate_notebook_model(self, model): | |
|
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 | 278 | path = path.strip('/') |
|
222 | 279 | if model is None: |
|
223 | 280 | model = {} |
|
224 | if 'content' not in model and model.get('type', None) != 'directory': | |
|
225 |
|
|
|
226 | metadata = current.new_metadata(name=u'') | |
|
227 | model['content'] = current.new_notebook(metadata=metadata) | |
|
228 |
|
|
|
281 | ||
|
282 | if path.endswith('.ipynb'): | |
|
283 | model.setdefault('type', 'notebook') | |
|
284 | else: | |
|
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 | 291 | model['format'] = 'json' |
|
230 | 292 | else: |
|
231 | 293 | model['content'] = '' |
|
232 | 294 | model['type'] = 'file' |
|
233 | 295 | model['format'] = 'text' |
|
234 | if 'name' not in model: | |
|
235 | if model['type'] == 'directory': | |
|
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']) | |
|
296 | ||
|
297 | model = self.save(model, path) | |
|
247 | 298 | return model |
|
248 | 299 | |
|
249 |
def copy(self, from_ |
|
|
300 | def copy(self, from_path, to_path=None): | |
|
250 | 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 |
|
|
|
255 | or just a base name. If a base name, `path` is used. | |
|
306 | from_path must be a full path to a file. | |
|
256 | 307 | """ |
|
257 | path = path.strip('/') | |
|
258 |
if '/' in |
|
|
259 |
from_ |
|
|
308 | path = from_path.strip('/') | |
|
309 | if '/' in path: | |
|
310 | from_dir, from_name = path.rsplit('/', 1) | |
|
260 | 311 | else: |
|
261 |
from_ |
|
|
262 |
|
|
|
312 | from_dir = '' | |
|
313 | from_name = path | |
|
314 | ||
|
315 | model = self.get(path) | |
|
316 | model.pop('path', None) | |
|
317 | model.pop('name', None) | |
|
263 | 318 | if model['type'] == 'directory': |
|
264 | 319 | raise HTTPError(400, "Can't copy directories") |
|
265 | if not to_name: | |
|
266 | base, ext = os.path.splitext(from_name) | |
|
267 | copy_name = u'{0}-Copy{1}'.format(base, ext) | |
|
268 | to_name = self.increment_filename(copy_name, path) | |
|
269 | model['name'] = to_name | |
|
270 | model['path'] = path | |
|
271 | model = self.save(model, to_name, path) | |
|
320 | ||
|
321 | if not to_path: | |
|
322 | to_path = from_dir | |
|
323 | if self.dir_exists(to_path): | |
|
324 | name = copy_pat.sub(u'.', from_name) | |
|
325 | to_name = self.increment_filename(name, to_path, insert='-Copy') | |
|
326 | to_path = u'{0}/{1}'.format(to_path, to_name) | |
|
327 | ||
|
328 | model = self.save(model, to_path) | |
|
272 | 329 | return model |
|
273 | 330 | |
|
274 | 331 | def log_info(self): |
|
275 | 332 | self.log.info(self.info_string()) |
|
276 | 333 | |
|
277 |
def trust_notebook(self, |
|
|
334 | def trust_notebook(self, path): | |
|
278 | 335 | """Explicitly trust a notebook |
|
279 | 336 | |
|
280 | 337 | Parameters |
|
281 | 338 | ---------- |
|
282 | name : string | |
|
283 | The filename of the notebook | |
|
284 | 339 | path : string |
|
285 |
The notebook |
|
|
340 | The path of a notebook | |
|
286 | 341 | """ |
|
287 |
model = self.get |
|
|
342 | model = self.get(path) | |
|
288 | 343 | nb = model['content'] |
|
289 |
self.log.warn("Trusting notebook %s |
|
|
344 | self.log.warn("Trusting notebook %s", path) | |
|
290 | 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 | 349 | """Check for trusted cells, and sign the notebook. |
|
295 | 350 | |
|
296 | 351 | Called as a part of saving notebooks. |
@@ -298,18 +353,16 b' class ContentsManager(LoggingConfigurable):' | |||
|
298 | 353 | Parameters |
|
299 | 354 | ---------- |
|
300 | 355 | nb : dict |
|
301 |
The notebook |
|
|
302 | name : string | |
|
303 | The filename of the notebook (for logging) | |
|
356 | The notebook dict | |
|
304 | 357 | path : string |
|
305 |
The notebook's |
|
|
358 | The notebook's path (for logging) | |
|
306 | 359 | """ |
|
307 | 360 | if self.notary.check_cells(nb): |
|
308 | 361 | self.notary.sign(nb) |
|
309 | 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 | 366 | """Mark cells as trusted if the notebook signature matches. |
|
314 | 367 | |
|
315 | 368 | Called as a part of loading notebooks. |
@@ -317,15 +370,13 b' class ContentsManager(LoggingConfigurable):' | |||
|
317 | 370 | Parameters |
|
318 | 371 | ---------- |
|
319 | 372 | nb : dict |
|
320 |
The notebook object (in |
|
|
321 | name : string | |
|
322 | The filename of the notebook (for logging) | |
|
373 | The notebook object (in current nbformat) | |
|
323 | 374 | path : string |
|
324 |
The notebook's |
|
|
375 | The notebook's path (for logging) | |
|
325 | 376 | """ |
|
326 | 377 | trusted = self.notary.check_signature(nb) |
|
327 | 378 | if not trusted: |
|
328 |
self.log.warn("Notebook %s |
|
|
379 | self.log.warn("Notebook %s is not trusted", path) | |
|
329 | 380 | self.notary.mark_cells(nb, trusted) |
|
330 | 381 | |
|
331 | 382 | def should_list(self, name): |
@@ -14,9 +14,10 b' import requests' | |||
|
14 | 14 | |
|
15 | 15 | from IPython.html.utils import url_path_join, url_escape |
|
16 | 16 | from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error |
|
17 |
from IPython.nbformat import |
|
|
18 | from IPython.nbformat.current import (new_notebook, write, read, new_worksheet, | |
|
19 | new_heading_cell, to_notebook_json) | |
|
17 | from IPython.nbformat import read, write, from_dict | |
|
18 | from IPython.nbformat.v4 import ( | |
|
19 | new_notebook, new_markdown_cell, | |
|
20 | ) | |
|
20 | 21 | from IPython.nbformat import v2 |
|
21 | 22 | from IPython.utils import py3compat |
|
22 | 23 | from IPython.utils.data import uniq_stable |
@@ -34,10 +35,10 b' class API(object):' | |||
|
34 | 35 | def __init__(self, base_url): |
|
35 | 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 | 39 | response = requests.request(verb, |
|
39 | 40 | url_path_join(self.base_url, 'api/contents', path), |
|
40 | data=body, | |
|
41 | data=body, params=params, | |
|
41 | 42 | ) |
|
42 | 43 | response.raise_for_status() |
|
43 | 44 | return response |
@@ -45,56 +46,64 b' class API(object):' | |||
|
45 | 46 | def list(self, path='/'): |
|
46 | 47 | return self._req('GET', path) |
|
47 | 48 | |
|
48 |
def read(self, |
|
|
49 | return self._req('GET', url_path_join(path, name)) | |
|
49 | def read(self, path, type_=None, format=None): | |
|
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 | 58 | body = None |
|
53 | 59 | if ext: |
|
54 | 60 | body = json.dumps({'ext': ext}) |
|
55 | 61 | return self._req('POST', path, body) |
|
56 | 62 | |
|
57 |
def |
|
|
58 |
return self._req('POST', path, |
|
|
63 | def mkdir_untitled(self, path='/'): | |
|
64 | return self._req('POST', path, json.dumps({'type': 'directory'})) | |
|
59 | 65 | |
|
60 |
def copy |
|
|
66 | def copy(self, copy_from, path='/'): | |
|
61 | 67 | body = json.dumps({'copy_from':copy_from}) |
|
62 | 68 | return self._req('POST', path, body) |
|
63 | 69 | |
|
64 |
def create(self |
|
|
65 |
return self._req('PUT', |
|
|
70 | def create(self, path='/'): | |
|
71 | return self._req('PUT', path) | |
|
72 | ||
|
73 | def upload(self, path, body): | |
|
74 | return self._req('PUT', path, body) | |
|
66 | 75 | |
|
67 |
def |
|
|
68 |
return self._req('P |
|
|
76 | def mkdir_untitled(self, path='/'): | |
|
77 | return self._req('POST', path, json.dumps({'type': 'directory'})) | |
|
69 | 78 | |
|
70 |
def mkdir(self |
|
|
71 |
return self._req('PUT', |
|
|
79 | def mkdir(self, path='/'): | |
|
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 | 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, |
|
|
78 |
return self._req('PUT', |
|
|
86 | def save(self, path, body): | |
|
87 | return self._req('PUT', path, body) | |
|
79 | 88 | |
|
80 |
def delete(self |
|
|
81 |
return self._req('DELETE', |
|
|
89 | def delete(self, path='/'): | |
|
90 | return self._req('DELETE', path) | |
|
82 | 91 | |
|
83 |
def rename(self, |
|
|
84 |
body = json.dumps({' |
|
|
85 |
return self._req('PATCH', |
|
|
92 | def rename(self, path, new_path): | |
|
93 | body = json.dumps({'path': new_path}) | |
|
94 | return self._req('PATCH', path, body) | |
|
86 | 95 | |
|
87 |
def get_checkpoints(self, |
|
|
88 |
return self._req('GET', url_path_join(path, |
|
|
96 | def get_checkpoints(self, path): | |
|
97 | return self._req('GET', url_path_join(path, 'checkpoints')) | |
|
89 | 98 | |
|
90 |
def new_checkpoint(self, |
|
|
91 |
return self._req('POST', url_path_join(path, |
|
|
99 | def new_checkpoint(self, path): | |
|
100 | return self._req('POST', url_path_join(path, 'checkpoints')) | |
|
92 | 101 | |
|
93 |
def restore_checkpoint(self, |
|
|
94 |
return self._req('POST', url_path_join(path, |
|
|
102 | def restore_checkpoint(self, path, checkpoint_id): | |
|
103 | return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id)) | |
|
95 | 104 | |
|
96 |
def delete_checkpoint(self, |
|
|
97 |
return self._req('DELETE', url_path_join(path, |
|
|
105 | def delete_checkpoint(self, path, checkpoint_id): | |
|
106 | return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id)) | |
|
98 | 107 | |
|
99 | 108 | class APITest(NotebookTestBase): |
|
100 | 109 | """Test the kernels web service API""" |
@@ -130,8 +139,6 b' class APITest(NotebookTestBase):' | |||
|
130 | 139 | self.blob = os.urandom(100) |
|
131 | 140 | self.b64_blob = base64.encodestring(self.blob).decode('ascii') |
|
132 | 141 | |
|
133 | ||
|
134 | ||
|
135 | 142 | for d in (self.dirs + self.hidden_dirs): |
|
136 | 143 | d.replace('/', os.sep) |
|
137 | 144 | if not os.path.isdir(pjoin(nbdir, d)): |
@@ -142,8 +149,8 b' class APITest(NotebookTestBase):' | |||
|
142 | 149 | # create a notebook |
|
143 | 150 | with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w', |
|
144 | 151 | encoding='utf-8') as f: |
|
145 |
nb = new_notebook( |
|
|
146 |
write(nb, f, |
|
|
152 | nb = new_notebook() | |
|
153 | write(nb, f, version=4) | |
|
147 | 154 | |
|
148 | 155 | # create a text file |
|
149 | 156 | with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w', |
@@ -177,12 +184,12 b' class APITest(NotebookTestBase):' | |||
|
177 | 184 | nbs = notebooks_only(self.api.list(u'/unicodé/').json()) |
|
178 | 185 | self.assertEqual(len(nbs), 1) |
|
179 | 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 | 189 | nbs = notebooks_only(self.api.list('/foo/bar/').json()) |
|
183 | 190 | self.assertEqual(len(nbs), 1) |
|
184 | 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 | 194 | nbs = notebooks_only(self.api.list('foo').json()) |
|
188 | 195 | self.assertEqual(len(nbs), 4) |
@@ -197,8 +204,11 b' class APITest(NotebookTestBase):' | |||
|
197 | 204 | self.assertEqual(nbnames, expected) |
|
198 | 205 | |
|
199 | 206 | def test_list_dirs(self): |
|
207 | print(self.api.list().json()) | |
|
200 | 208 | dirs = dirs_only(self.api.list().json()) |
|
201 | 209 | dir_names = {normalize('NFC', d['name']) for d in dirs} |
|
210 | print(dir_names) | |
|
211 | print(self.top_level_dirs) | |
|
202 | 212 | self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs |
|
203 | 213 | |
|
204 | 214 | def test_list_nonexistant_dir(self): |
@@ -207,8 +217,10 b' class APITest(NotebookTestBase):' | |||
|
207 | 217 | |
|
208 | 218 | def test_get_nb_contents(self): |
|
209 | 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 | 222 | self.assertEqual(nb['name'], u'%s.ipynb' % name) |
|
223 | self.assertEqual(nb['path'], path) | |
|
212 | 224 | self.assertEqual(nb['type'], 'notebook') |
|
213 | 225 | self.assertIn('content', nb) |
|
214 | 226 | self.assertEqual(nb['format'], 'json') |
@@ -219,12 +231,14 b' class APITest(NotebookTestBase):' | |||
|
219 | 231 | def test_get_contents_no_such_file(self): |
|
220 | 232 | # Name that doesn't exist - should be a 404 |
|
221 | 233 | with assert_http_error(404): |
|
222 |
self.api.read('q.ipynb |
|
|
234 | self.api.read('foo/q.ipynb') | |
|
223 | 235 | |
|
224 | 236 | def test_get_text_file_contents(self): |
|
225 | 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 | 240 | self.assertEqual(model['name'], u'%s.txt' % name) |
|
241 | self.assertEqual(model['path'], path) | |
|
228 | 242 | self.assertIn('content', model) |
|
229 | 243 | self.assertEqual(model['format'], 'text') |
|
230 | 244 | self.assertEqual(model['type'], 'file') |
@@ -232,12 +246,18 b' class APITest(NotebookTestBase):' | |||
|
232 | 246 | |
|
233 | 247 | # Name that doesn't exist - should be a 404 |
|
234 | 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 | 255 | def test_get_binary_file_contents(self): |
|
238 | 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 | 259 | self.assertEqual(model['name'], u'%s.blob' % name) |
|
260 | self.assertEqual(model['path'], path) | |
|
241 | 261 | self.assertIn('content', model) |
|
242 | 262 | self.assertEqual(model['format'], 'base64') |
|
243 | 263 | self.assertEqual(model['type'], 'file') |
@@ -246,66 +266,78 b' class APITest(NotebookTestBase):' | |||
|
246 | 266 | |
|
247 | 267 | # Name that doesn't exist - should be a 404 |
|
248 | 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 | 279 | self.assertEqual(resp.status_code, 201) |
|
253 | 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 | 282 | rjson = resp.json() |
|
256 |
self.assertEqual(rjson['name'], |
|
|
283 | self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1]) | |
|
257 | 284 | self.assertEqual(rjson['path'], path) |
|
258 | 285 | self.assertEqual(rjson['type'], type) |
|
259 | 286 | isright = os.path.isdir if type == 'directory' else os.path.isfile |
|
260 | 287 | assert isright(pjoin( |
|
261 | 288 | self.notebook_dir.name, |
|
262 | 289 | path.replace('/', os.sep), |
|
263 | name, | |
|
264 | 290 | )) |
|
265 | 291 | |
|
266 | 292 | def test_create_untitled(self): |
|
267 | 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 | 296 | # Second time |
|
271 | 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 | 300 | # And two directories down |
|
275 | 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 | 304 | def test_create_untitled_txt(self): |
|
279 | 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 | 309 | model = resp.json() |
|
284 | 310 | self.assertEqual(model['type'], 'file') |
|
285 | 311 | self.assertEqual(model['format'], 'text') |
|
286 | 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 | 314 | def test_upload(self): |
|
296 |
nb = new_notebook( |
|
|
315 | nb = new_notebook() | |
|
297 | 316 | nbmodel = {'content': nb, 'type': 'notebook'} |
|
298 |
|
|
|
299 | body=json.dumps(nbmodel)) | |
|
300 |
self._check_created(resp, |
|
|
317 | path = u'å b/Upload tést.ipynb' | |
|
318 | resp = self.api.upload(path, body=json.dumps(nbmodel)) | |
|
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 | 333 | def test_mkdir(self): |
|
303 | resp = self.api.mkdir(u'New ∂ir', path=u'å b') | |
|
304 | self._check_created(resp, u'New ∂ir', u'å b', type='directory') | |
|
334 | path = u'å b/New ∂ir' | |
|
335 | resp = self.api.mkdir(path) | |
|
336 | self._check_created(resp, path, type='directory') | |
|
305 | 337 | |
|
306 | 338 | def test_mkdir_hidden_400(self): |
|
307 | 339 | with assert_http_error(400): |
|
308 |
resp = self.api.mkdir(u'.hidden |
|
|
340 | resp = self.api.mkdir(u'å b/.hidden') | |
|
309 | 341 | |
|
310 | 342 | def test_upload_txt(self): |
|
311 | 343 | body = u'ünicode téxt' |
@@ -314,11 +346,11 b' class APITest(NotebookTestBase):' | |||
|
314 | 346 | 'format' : 'text', |
|
315 | 347 | 'type' : 'file', |
|
316 | 348 | } |
|
317 |
|
|
|
318 | body=json.dumps(model)) | |
|
349 | path = u'å b/Upload tést.txt' | |
|
350 | resp = self.api.upload(path, body=json.dumps(model)) | |
|
319 | 351 | |
|
320 | 352 | # check roundtrip |
|
321 |
resp = self.api.read(path |
|
|
353 | resp = self.api.read(path) | |
|
322 | 354 | model = resp.json() |
|
323 | 355 | self.assertEqual(model['type'], 'file') |
|
324 | 356 | self.assertEqual(model['format'], 'text') |
@@ -332,13 +364,14 b' class APITest(NotebookTestBase):' | |||
|
332 | 364 | 'format' : 'base64', |
|
333 | 365 | 'type' : 'file', |
|
334 | 366 | } |
|
335 |
|
|
|
336 | body=json.dumps(model)) | |
|
367 | path = u'å b/Upload tést.blob' | |
|
368 | resp = self.api.upload(path, body=json.dumps(model)) | |
|
337 | 369 | |
|
338 | 370 | # check roundtrip |
|
339 |
resp = self.api.read(path |
|
|
371 | resp = self.api.read(path) | |
|
340 | 372 | model = resp.json() |
|
341 | 373 | self.assertEqual(model['type'], 'file') |
|
374 | self.assertEqual(model['path'], path) | |
|
342 | 375 | self.assertEqual(model['format'], 'base64') |
|
343 | 376 | decoded = base64.decodestring(model['content'].encode('ascii')) |
|
344 | 377 | self.assertEqual(decoded, body) |
@@ -349,46 +382,62 b' class APITest(NotebookTestBase):' | |||
|
349 | 382 | nb.worksheets.append(ws) |
|
350 | 383 | ws.cells.append(v2.new_code_cell(input='print("hi")')) |
|
351 | 384 | nbmodel = {'content': nb, 'type': 'notebook'} |
|
352 |
|
|
|
353 | body=json.dumps(nbmodel)) | |
|
354 |
self._check_created(resp, |
|
|
355 |
resp = self.api.read( |
|
|
385 | path = u'å b/Upload tést.ipynb' | |
|
386 | resp = self.api.upload(path, body=json.dumps(nbmodel)) | |
|
387 | self._check_created(resp, path) | |
|
388 | resp = self.api.read(path) | |
|
356 | 389 | data = resp.json() |
|
357 |
self.assertEqual(data['content']['nbformat'], |
|
|
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') | |
|
390 | self.assertEqual(data['content']['nbformat'], 4) | |
|
363 | 391 | |
|
364 | 392 | def test_copy(self): |
|
365 |
resp = self.api.copy(u'ç d.ipynb', |
|
|
366 |
self._check_created(resp, u' |
|
|
367 | ||
|
393 | resp = self.api.copy(u'å b/ç d.ipynb', u'å b') | |
|
394 | self._check_created(resp, u'å b/ç d-Copy1.ipynb') | |
|
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 | 406 | def test_copy_path(self): |
|
369 |
resp = self.api.copy(u'foo/a.ipynb', u' |
|
|
370 |
self._check_created(resp, u' |
|
|
407 | resp = self.api.copy(u'foo/a.ipynb', u'å b') | |
|
408 | self._check_created(resp, u'å b/a.ipynb') | |
|
409 | ||
|
410 | resp = self.api.copy(u'foo/a.ipynb', u'å b') | |
|
411 | self._check_created(resp, u'å b/a-Copy1.ipynb') | |
|
412 | ||
|
413 | def test_copy_put_400(self): | |
|
414 | with assert_http_error(400): | |
|
415 | resp = self.api.copy_put(u'å b/ç d.ipynb', u'å b/cøpy.ipynb') | |
|
371 | 416 | |
|
372 | 417 | def test_copy_dir_400(self): |
|
373 | 418 | # can't copy directories |
|
374 | 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 | 422 | def test_delete(self): |
|
378 | 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 | 426 | self.assertEqual(resp.status_code, 204) |
|
381 | 427 | |
|
382 | 428 | for d in self.dirs + ['/']: |
|
383 | 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 | 435 | def test_delete_dirs(self): |
|
387 | 436 | # depth-first delete everything, so we don't try to delete empty directories |
|
388 | 437 | for name in sorted(self.dirs + ['/'], key=len, reverse=True): |
|
389 | 438 | listing = self.api.list(name).json()['content'] |
|
390 | 439 | for model in listing: |
|
391 |
self.api.delete(model[' |
|
|
440 | self.api.delete(model['path']) | |
|
392 | 441 | listing = self.api.list('/').json()['content'] |
|
393 | 442 | self.assertEqual(listing, []) |
|
394 | 443 | |
@@ -398,9 +447,10 b' class APITest(NotebookTestBase):' | |||
|
398 | 447 | self.api.delete(u'å b') |
|
399 | 448 | |
|
400 | 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 | 451 | self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb') |
|
403 | 452 | self.assertEqual(resp.json()['name'], 'z.ipynb') |
|
453 | self.assertEqual(resp.json()['path'], 'foo/z.ipynb') | |
|
404 | 454 | assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb')) |
|
405 | 455 | |
|
406 | 456 | nbs = notebooks_only(self.api.list('foo').json()) |
@@ -410,43 +460,31 b' class APITest(NotebookTestBase):' | |||
|
410 | 460 | |
|
411 | 461 | def test_rename_existing(self): |
|
412 | 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 | 465 | def test_save(self): |
|
416 |
resp = self.api.read('a.ipynb |
|
|
466 | resp = self.api.read('foo/a.ipynb') | |
|
417 | 467 | nbcontent = json.loads(resp.text)['content'] |
|
418 |
nb = |
|
|
419 | ws = new_worksheet() | |
|
420 | nb.worksheets = [ws] | |
|
421 | ws.cells.append(new_heading_cell(u'Created by test ³')) | |
|
468 | nb = from_dict(nbcontent) | |
|
469 | nb.cells.append(new_markdown_cell(u'Created by test ³')) | |
|
422 | 470 | |
|
423 |
nbmodel= { |
|
|
424 |
resp = self.api.save('a.ipynb |
|
|
471 | nbmodel= {'content': nb, 'type': 'notebook'} | |
|
472 | resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel)) | |
|
425 | 473 | |
|
426 | 474 | nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb') |
|
427 | 475 | with io.open(nbfile, 'r', encoding='utf-8') as f: |
|
428 |
newnb = read(f, |
|
|
429 |
self.assertEqual(newnb |
|
|
476 | newnb = read(f, as_version=4) | |
|
477 | self.assertEqual(newnb.cells[0].source, | |
|
430 | 478 | u'Created by test ³') |
|
431 |
nbcontent = self.api.read('a.ipynb |
|
|
432 |
newnb = |
|
|
433 |
self.assertEqual(newnb |
|
|
479 | nbcontent = self.api.read('foo/a.ipynb').json()['content'] | |
|
480 | newnb = from_dict(nbcontent) | |
|
481 | self.assertEqual(newnb.cells[0].source, | |
|
434 | 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 | 485 | def test_checkpoints(self): |
|
448 |
resp = self.api.read('a.ipynb |
|
|
449 |
r = self.api.new_checkpoint('a.ipynb |
|
|
486 | resp = self.api.read('foo/a.ipynb') | |
|
487 | r = self.api.new_checkpoint('foo/a.ipynb') | |
|
450 | 488 | self.assertEqual(r.status_code, 201) |
|
451 | 489 | cp1 = r.json() |
|
452 | 490 | self.assertEqual(set(cp1), {'id', 'last_modified'}) |
@@ -454,32 +492,30 b' class APITest(NotebookTestBase):' | |||
|
454 | 492 | |
|
455 | 493 | # Modify it |
|
456 | 494 | nbcontent = json.loads(resp.text)['content'] |
|
457 |
nb = |
|
|
458 | ws = new_worksheet() | |
|
459 | nb.worksheets = [ws] | |
|
460 | hcell = new_heading_cell('Created by test') | |
|
461 | ws.cells.append(hcell) | |
|
495 | nb = from_dict(nbcontent) | |
|
496 | hcell = new_markdown_cell('Created by test') | |
|
497 | nb.cells.append(hcell) | |
|
462 | 498 | # Save |
|
463 |
nbmodel= { |
|
|
464 |
resp = self.api.save('a.ipynb |
|
|
499 | nbmodel= {'content': nb, 'type': 'notebook'} | |
|
500 | resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel)) | |
|
465 | 501 | |
|
466 | 502 | # List checkpoints |
|
467 |
cps = self.api.get_checkpoints('a.ipynb |
|
|
503 | cps = self.api.get_checkpoints('foo/a.ipynb').json() | |
|
468 | 504 | self.assertEqual(cps, [cp1]) |
|
469 | 505 | |
|
470 |
nbcontent = self.api.read('a.ipynb |
|
|
471 |
nb = |
|
|
472 |
self.assertEqual(nb |
|
|
506 | nbcontent = self.api.read('foo/a.ipynb').json()['content'] | |
|
507 | nb = from_dict(nbcontent) | |
|
508 | self.assertEqual(nb.cells[0].source, 'Created by test') | |
|
473 | 509 | |
|
474 | 510 | # Restore cp1 |
|
475 |
r = self.api.restore_checkpoint('a.ipynb |
|
|
511 | r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id']) | |
|
476 | 512 | self.assertEqual(r.status_code, 204) |
|
477 |
nbcontent = self.api.read('a.ipynb |
|
|
478 |
nb = |
|
|
479 |
self.assertEqual(nb. |
|
|
513 | nbcontent = self.api.read('foo/a.ipynb').json()['content'] | |
|
514 | nb = from_dict(nbcontent) | |
|
515 | self.assertEqual(nb.cells, []) | |
|
480 | 516 | |
|
481 | 517 | # Delete cp1 |
|
482 |
r = self.api.delete_checkpoint('a.ipynb |
|
|
518 | r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id']) | |
|
483 | 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 | 521 | self.assertEqual(cps, []) |
@@ -9,7 +9,7 b' from tornado.web import HTTPError' | |||
|
9 | 9 | from unittest import TestCase |
|
10 | 10 | from tempfile import NamedTemporaryFile |
|
11 | 11 | |
|
12 |
from IPython.nbformat import |
|
|
12 | from IPython.nbformat import v4 as nbformat | |
|
13 | 13 | |
|
14 | 14 | from IPython.utils.tempdir import TemporaryDirectory |
|
15 | 15 | from IPython.utils.traitlets import TraitError |
@@ -42,7 +42,7 b' class TestFileContentsManager(TestCase):' | |||
|
42 | 42 | with TemporaryDirectory() as td: |
|
43 | 43 | root = td |
|
44 | 44 | fm = FileContentsManager(root_dir=root) |
|
45 |
path = fm._get_os_path(' |
|
|
45 | path = fm._get_os_path('/path/to/notebook/test.ipynb') | |
|
46 | 46 | rel_path_list = '/path/to/notebook/test.ipynb'.split('/') |
|
47 | 47 | fs_path = os.path.join(fm.root_dir, *rel_path_list) |
|
48 | 48 | self.assertEqual(path, fs_path) |
@@ -53,7 +53,7 b' class TestFileContentsManager(TestCase):' | |||
|
53 | 53 | self.assertEqual(path, fs_path) |
|
54 | 54 | |
|
55 | 55 | fm = FileContentsManager(root_dir=root) |
|
56 |
path = fm._get_os_path('test.ipynb |
|
|
56 | path = fm._get_os_path('////test.ipynb') | |
|
57 | 57 | fs_path = os.path.join(fm.root_dir, 'test.ipynb') |
|
58 | 58 | self.assertEqual(path, fs_path) |
|
59 | 59 | |
@@ -64,8 +64,8 b' class TestFileContentsManager(TestCase):' | |||
|
64 | 64 | root = td |
|
65 | 65 | os.mkdir(os.path.join(td, subd)) |
|
66 | 66 | fm = FileContentsManager(root_dir=root) |
|
67 |
cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb' |
|
|
68 |
cp_subdir = fm.get_checkpoint_path('cp', ' |
|
|
67 | cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb') | |
|
68 | cp_subdir = fm.get_checkpoint_path('cp', '/%s/test.ipynb' % subd) | |
|
69 | 69 | self.assertNotEqual(cp_dir, cp_subdir) |
|
70 | 70 | self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name)) |
|
71 | 71 | self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name)) |
@@ -95,71 +95,98 b' class TestContentsManager(TestCase):' | |||
|
95 | 95 | return os_path |
|
96 | 96 | |
|
97 | 97 | def add_code_cell(self, nb): |
|
98 |
output = |
|
|
99 |
cell = |
|
|
100 | if not nb.worksheets: | |
|
101 | nb.worksheets.append(current.new_worksheet()) | |
|
102 | nb.worksheets[0].cells.append(cell) | |
|
98 | output = nbformat.new_output("display_data", {'application/javascript': "alert('hi');"}) | |
|
99 | cell = nbformat.new_code_cell("print('hi')", outputs=[output]) | |
|
100 | nb.cells.append(cell) | |
|
103 | 101 | |
|
104 | 102 | def new_notebook(self): |
|
105 | 103 | cm = self.contents_manager |
|
106 | model = cm.create_file() | |
|
104 | model = cm.new_untitled(type='notebook') | |
|
107 | 105 | name = model['name'] |
|
108 | 106 | path = model['path'] |
|
109 | 107 | |
|
110 |
full_model = cm.get |
|
|
108 | full_model = cm.get(path) | |
|
111 | 109 | nb = full_model['content'] |
|
112 | 110 | self.add_code_cell(nb) |
|
113 | 111 | |
|
114 |
cm.save(full_model, |
|
|
112 | cm.save(full_model, path) | |
|
115 | 113 | return nb, name, path |
|
116 | 114 | |
|
117 |
def test_ |
|
|
115 | def test_new_untitled(self): | |
|
118 | 116 | cm = self.contents_manager |
|
119 | 117 | # Test in root directory |
|
120 | model = cm.create_file() | |
|
118 | model = cm.new_untitled(type='notebook') | |
|
121 | 119 | assert isinstance(model, dict) |
|
122 | 120 | self.assertIn('name', model) |
|
123 | 121 | self.assertIn('path', model) |
|
124 | self.assertEqual(model['name'], 'Untitled0.ipynb') | |
|
125 |
self.assertEqual(model[' |
|
|
122 | self.assertIn('type', model) | |
|
123 | self.assertEqual(model['type'], 'notebook') | |
|
124 | self.assertEqual(model['name'], 'Untitled.ipynb') | |
|
125 | self.assertEqual(model['path'], 'Untitled.ipynb') | |
|
126 | 126 | |
|
127 | 127 | # Test in sub-directory |
|
128 | sub_dir = '/foo/' | |
|
129 | self.make_dir(cm.root_dir, 'foo') | |
|
130 | model = cm.create_file(None, sub_dir) | |
|
128 | model = cm.new_untitled(type='directory') | |
|
131 | 129 | assert isinstance(model, dict) |
|
132 | 130 | self.assertIn('name', model) |
|
133 | 131 | self.assertIn('path', model) |
|
134 | self.assertEqual(model['name'], 'Untitled0.ipynb') | |
|
135 |
self.assertEqual(model[' |
|
|
132 | self.assertIn('type', 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 | 147 | def test_get(self): |
|
138 | 148 | cm = self.contents_manager |
|
139 | 149 | # Create a notebook |
|
140 | model = cm.create_file() | |
|
150 | model = cm.new_untitled(type='notebook') | |
|
141 | 151 | name = model['name'] |
|
142 | 152 | path = model['path'] |
|
143 | 153 | |
|
144 | 154 | # Check that we 'get' on the notebook we just created |
|
145 |
model2 = cm.get |
|
|
155 | model2 = cm.get(path) | |
|
146 | 156 | assert isinstance(model2, dict) |
|
147 | 157 | self.assertIn('name', model2) |
|
148 | 158 | self.assertIn('path', model2) |
|
149 | 159 | self.assertEqual(model['name'], name) |
|
150 | 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 | 171 | # Test in sub-directory |
|
153 | 172 | sub_dir = '/foo/' |
|
154 | 173 | self.make_dir(cm.root_dir, 'foo') |
|
155 | model = cm.create_file(None, sub_dir) | |
|
156 |
model2 = cm.get |
|
|
174 | model = cm.new_untitled(path=sub_dir, ext='.ipynb') | |
|
175 | model2 = cm.get(sub_dir + name) | |
|
157 | 176 | assert isinstance(model2, dict) |
|
158 | 177 | self.assertIn('name', model2) |
|
159 | 178 | self.assertIn('path', model2) |
|
160 | 179 | self.assertIn('content', model2) |
|
161 |
self.assertEqual(model2['name'], 'Untitled |
|
|
162 | self.assertEqual(model2['path'], sub_dir.strip('/')) | |
|
180 | self.assertEqual(model2['name'], 'Untitled.ipynb') | |
|
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 | 191 | @dec.skip_win32 |
|
165 | 192 | def test_bad_symlink(self): |
@@ -167,26 +194,27 b' class TestContentsManager(TestCase):' | |||
|
167 | 194 | path = 'test bad symlink' |
|
168 | 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 | 199 | # create a broken symlink |
|
173 | 200 | os.symlink("target", os.path.join(os_path, "bad symlink")) |
|
174 |
model = cm.get |
|
|
201 | model = cm.get(path) | |
|
175 | 202 | self.assertEqual(model['content'], [file_model]) |
|
176 | 203 | |
|
177 | 204 | @dec.skip_win32 |
|
178 | 205 | def test_good_symlink(self): |
|
179 | 206 | cm = self.contents_manager |
|
180 |
pat |
|
|
181 | os_path = self.make_dir(cm.root_dir, path) | |
|
207 | parent = 'test good symlink' | |
|
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 | 214 | # create a good symlink |
|
186 |
os.symlink(file_model['name'], os.path.join(os_path, |
|
|
187 |
symlink_model = cm.get |
|
|
188 | ||
|
189 | dir_model = cm.get_model(path) | |
|
215 | os.symlink(file_model['name'], os.path.join(os_path, name)) | |
|
216 | symlink_model = cm.get(path, content=False) | |
|
217 | dir_model = cm.get(parent) | |
|
190 | 218 | self.assertEqual( |
|
191 | 219 | sorted(dir_model['content'], key=lambda x: x['name']), |
|
192 | 220 | [symlink_model, file_model], |
@@ -195,53 +223,54 b' class TestContentsManager(TestCase):' | |||
|
195 | 223 | def test_update(self): |
|
196 | 224 | cm = self.contents_manager |
|
197 | 225 | # Create a notebook |
|
198 | model = cm.create_file() | |
|
226 | model = cm.new_untitled(type='notebook') | |
|
199 | 227 | name = model['name'] |
|
200 | 228 | path = model['path'] |
|
201 | 229 | |
|
202 | 230 | # Change the name in the model for rename |
|
203 |
model[' |
|
|
204 |
model = cm.update(model, |
|
|
231 | model['path'] = 'test.ipynb' | |
|
232 | model = cm.update(model, path) | |
|
205 | 233 | assert isinstance(model, dict) |
|
206 | 234 | self.assertIn('name', model) |
|
207 | 235 | self.assertIn('path', model) |
|
208 | 236 | self.assertEqual(model['name'], 'test.ipynb') |
|
209 | 237 | |
|
210 | 238 | # Make sure the old name is gone |
|
211 |
self.assertRaises(HTTPError, cm.get |
|
|
239 | self.assertRaises(HTTPError, cm.get, path) | |
|
212 | 240 | |
|
213 | 241 | # Test in sub-directory |
|
214 | 242 | # Create a directory and notebook in that directory |
|
215 | 243 | sub_dir = '/foo/' |
|
216 | 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 | 246 | name = model['name'] |
|
219 | 247 | path = model['path'] |
|
220 | 248 | |
|
221 | 249 | # Change the name in the model for rename |
|
222 | model['name'] = 'test_in_sub.ipynb' | |
|
223 | model = cm.update(model, name, path) | |
|
250 | d = path.rsplit('/', 1)[0] | |
|
251 | new_path = model['path'] = d + '/test_in_sub.ipynb' | |
|
252 | model = cm.update(model, path) | |
|
224 | 253 | assert isinstance(model, dict) |
|
225 | 254 | self.assertIn('name', model) |
|
226 | 255 | self.assertIn('path', model) |
|
227 | 256 | self.assertEqual(model['name'], 'test_in_sub.ipynb') |
|
228 |
self.assertEqual(model['path'], |
|
|
257 | self.assertEqual(model['path'], new_path) | |
|
229 | 258 | |
|
230 | 259 | # Make sure the old name is gone |
|
231 |
self.assertRaises(HTTPError, cm.get |
|
|
260 | self.assertRaises(HTTPError, cm.get, path) | |
|
232 | 261 | |
|
233 | 262 | def test_save(self): |
|
234 | 263 | cm = self.contents_manager |
|
235 | 264 | # Create a notebook |
|
236 | model = cm.create_file() | |
|
265 | model = cm.new_untitled(type='notebook') | |
|
237 | 266 | name = model['name'] |
|
238 | 267 | path = model['path'] |
|
239 | 268 | |
|
240 | 269 | # Get the model with 'content' |
|
241 |
full_model = cm.get |
|
|
270 | full_model = cm.get(path) | |
|
242 | 271 | |
|
243 | 272 | # Save the notebook |
|
244 |
model = cm.save(full_model, |
|
|
273 | model = cm.save(full_model, path) | |
|
245 | 274 | assert isinstance(model, dict) |
|
246 | 275 | self.assertIn('name', model) |
|
247 | 276 | self.assertIn('path', model) |
@@ -252,18 +281,18 b' class TestContentsManager(TestCase):' | |||
|
252 | 281 | # Create a directory and notebook in that directory |
|
253 | 282 | sub_dir = '/foo/' |
|
254 | 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 | 285 | name = model['name'] |
|
257 | 286 | path = model['path'] |
|
258 |
model = cm.get |
|
|
287 | model = cm.get(path) | |
|
259 | 288 | |
|
260 | 289 | # Change the name in the model for rename |
|
261 |
model = cm.save(model, |
|
|
290 | model = cm.save(model, path) | |
|
262 | 291 | assert isinstance(model, dict) |
|
263 | 292 | self.assertIn('name', model) |
|
264 | 293 | self.assertIn('path', model) |
|
265 |
self.assertEqual(model['name'], 'Untitled |
|
|
266 |
self.assertEqual(model['path'], |
|
|
294 | self.assertEqual(model['name'], 'Untitled.ipynb') | |
|
295 | self.assertEqual(model['path'], 'foo/Untitled.ipynb') | |
|
267 | 296 | |
|
268 | 297 | def test_delete(self): |
|
269 | 298 | cm = self.contents_manager |
@@ -271,36 +300,42 b' class TestContentsManager(TestCase):' | |||
|
271 | 300 | nb, name, path = self.new_notebook() |
|
272 | 301 | |
|
273 | 302 | # Delete the notebook |
|
274 |
cm.delete( |
|
|
303 | cm.delete(path) | |
|
275 | 304 | |
|
276 | 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 | 308 | def test_copy(self): |
|
280 | 309 | cm = self.contents_manager |
|
281 |
pat |
|
|
310 | parent = u'å b' | |
|
282 | 311 | name = u'nb √.ipynb' |
|
283 | os.mkdir(os.path.join(cm.root_dir, path)) | |
|
284 | orig = cm.create_file({'name' : name}, path=path) | |
|
312 | path = u'{0}/{1}'.format(parent, name) | |
|
313 | os.mkdir(os.path.join(cm.root_dir, parent)) | |
|
314 | orig = cm.new(path=path) | |
|
285 | 315 | |
|
286 | 316 | # copy with unspecified name |
|
287 |
copy = cm.copy( |
|
|
288 |
self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy |
|
|
317 | copy = cm.copy(path) | |
|
318 | self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy1.ipynb')) | |
|
289 | 319 | |
|
290 | 320 | # copy with specified name |
|
291 |
copy2 = cm.copy( |
|
|
321 | copy2 = cm.copy(path, u'å b/copy 2.ipynb') | |
|
292 | 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 | 329 | def test_trust_notebook(self): |
|
295 | 330 | cm = self.contents_manager |
|
296 | 331 | nb, name, path = self.new_notebook() |
|
297 | 332 | |
|
298 |
untrusted = cm.get |
|
|
333 | untrusted = cm.get(path)['content'] | |
|
299 | 334 | assert not cm.notary.check_cells(untrusted) |
|
300 | 335 | |
|
301 | 336 | # print(untrusted) |
|
302 |
cm.trust_notebook( |
|
|
303 |
trusted = cm.get |
|
|
337 | cm.trust_notebook(path) | |
|
338 | trusted = cm.get(path)['content'] | |
|
304 | 339 | # print(trusted) |
|
305 | 340 | assert cm.notary.check_cells(trusted) |
|
306 | 341 | |
@@ -308,27 +343,27 b' class TestContentsManager(TestCase):' | |||
|
308 | 343 | cm = self.contents_manager |
|
309 | 344 | nb, name, path = self.new_notebook() |
|
310 | 345 | |
|
311 |
cm.mark_trusted_cells(nb, |
|
|
312 |
for cell in nb. |
|
|
346 | cm.mark_trusted_cells(nb, path) | |
|
347 | for cell in nb.cells: | |
|
313 | 348 | if cell.cell_type == 'code': |
|
314 | assert not cell.trusted | |
|
349 | assert not cell.metadata.trusted | |
|
315 | 350 | |
|
316 |
cm.trust_notebook( |
|
|
317 |
nb = cm.get |
|
|
318 |
for cell in nb. |
|
|
351 | cm.trust_notebook(path) | |
|
352 | nb = cm.get(path)['content'] | |
|
353 | for cell in nb.cells: | |
|
319 | 354 | if cell.cell_type == 'code': |
|
320 | assert cell.trusted | |
|
355 | assert cell.metadata.trusted | |
|
321 | 356 | |
|
322 | 357 | def test_check_and_sign(self): |
|
323 | 358 | cm = self.contents_manager |
|
324 | 359 | nb, name, path = self.new_notebook() |
|
325 | 360 | |
|
326 |
cm.mark_trusted_cells(nb, |
|
|
327 |
cm.check_and_sign(nb, |
|
|
361 | cm.mark_trusted_cells(nb, path) | |
|
362 | cm.check_and_sign(nb, path) | |
|
328 | 363 | assert not cm.notary.check_signature(nb) |
|
329 | 364 | |
|
330 |
cm.trust_notebook( |
|
|
331 |
nb = cm.get |
|
|
332 |
cm.mark_trusted_cells(nb, |
|
|
333 |
cm.check_and_sign(nb, |
|
|
365 | cm.trust_notebook(path) | |
|
366 | nb = cm.get(path)['content'] | |
|
367 | cm.mark_trusted_cells(nb, path) | |
|
368 | cm.check_and_sign(nb, path) | |
|
334 | 369 | assert cm.notary.check_signature(nb) |
@@ -5,14 +5,16 b'' | |||
|
5 | 5 | |
|
6 | 6 | import json |
|
7 | 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 | 12 | from IPython.utils.jsonutil import date_default |
|
11 |
from IPython.utils.py3compat import |
|
|
13 | from IPython.utils.py3compat import cast_unicode | |
|
12 | 14 | from IPython.html.utils import url_path_join, url_escape |
|
13 | 15 | |
|
14 | 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 | 19 | from IPython.core.release import kernel_protocol_version |
|
18 | 20 | |
@@ -27,16 +29,16 b' class MainKernelHandler(IPythonHandler):' | |||
|
27 | 29 | @web.authenticated |
|
28 | 30 | @json_errors |
|
29 | 31 | def post(self): |
|
32 | km = self.kernel_manager | |
|
30 | 33 | model = self.get_json_body() |
|
31 | 34 | if model is None: |
|
32 | raise web.HTTPError(400, "No JSON data provided") | |
|
33 | try: | |
|
34 | name = model['name'] | |
|
35 | except KeyError: | |
|
36 | raise web.HTTPError(400, "Missing field in JSON data: name") | |
|
35 | model = { | |
|
36 | 'name': km.default_kernel_name | |
|
37 | } | |
|
38 | else: | |
|
39 | model.setdefault('name', km.default_kernel_name) | |
|
37 | 40 | |
|
38 | km = self.kernel_manager | |
|
39 | kernel_id = km.start_kernel(kernel_name=name) | |
|
41 | kernel_id = km.start_kernel(kernel_name=model['name']) | |
|
40 | 42 | model = km.kernel_model(kernel_id) |
|
41 | 43 | location = url_path_join(self.base_url, 'api', 'kernels', kernel_id) |
|
42 | 44 | self.set_header('Location', url_escape(location)) |
@@ -84,6 +86,10 b' class KernelActionHandler(IPythonHandler):' | |||
|
84 | 86 | |
|
85 | 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 | 93 | def __repr__(self): |
|
88 | 94 | return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized')) |
|
89 | 95 | |
@@ -91,17 +97,29 b' class ZMQChannelHandler(AuthenticatedZMQStreamHandler):' | |||
|
91 | 97 | km = self.kernel_manager |
|
92 | 98 | meth = getattr(km, 'connect_%s' % self.channel) |
|
93 | 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 | 102 | """send a request for kernel_info""" |
|
103 | self.log.debug("requesting kernel info") | |
|
104 | self.session.send(self.kernel_info_channel, "kernel_info_request") | |
|
103 | km = self.kernel_manager | |
|
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 | 124 | def _handle_kernel_info_reply(self, msg): |
|
107 | 125 | """process the kernel_info_reply |
@@ -110,35 +128,75 b' class ZMQChannelHandler(AuthenticatedZMQStreamHandler):' | |||
|
110 | 128 | """ |
|
111 | 129 | idents,msg = self.session.feed_identities(msg) |
|
112 | 130 | try: |
|
113 |
msg = self.session. |
|
|
131 | msg = self.session.deserialize(msg) | |
|
114 | 132 | except: |
|
115 | 133 | self.log.error("Bad kernel_info reply", exc_info=True) |
|
116 |
self. |
|
|
134 | self._kernel_info_future.set_result({}) | |
|
117 | 135 | return |
|
118 | 136 | else: |
|
119 | if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in msg['content']: | |
|
120 | self.log.error("Kernel info request failed, assuming current %s", msg['content']) | |
|
121 | else: | |
|
122 | protocol_version = msg['content']['protocol_version'] | |
|
123 | if protocol_version != kernel_protocol_version: | |
|
124 | self.session.adapt_version = int(protocol_version.split('.')[0]) | |
|
125 | self.log.info("adapting kernel to %s" % protocol_version) | |
|
126 | self.kernel_info_channel.close() | |
|
137 | info = msg['content'] | |
|
138 | self.log.debug("Received kernel info: %s", info) | |
|
139 | if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info: | |
|
140 | self.log.error("Kernel info request failed, assuming current %s", info) | |
|
141 | info = {} | |
|
142 | self._finish_kernel_info(info) | |
|
143 | ||
|
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 | 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 | 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): | |
|
134 | try: | |
|
135 | super(ZMQChannelHandler, self).on_first_message(msg) | |
|
136 | except web.HTTPError: | |
|
137 | self.close() | |
|
138 | return | |
|
169 | @gen.coroutine | |
|
170 | def pre_get(self): | |
|
171 | # authenticate first | |
|
172 | super(ZMQChannelHandler, self).pre_get() | |
|
173 | # then request kernel info, waiting up to a certain time before giving up. | |
|
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 | 196 | try: |
|
140 | 197 | self.create_stream() |
|
141 | except web.HTTPError: | |
|
198 | except web.HTTPError as e: | |
|
199 | self.log.error("Error opening stream: %s", e) | |
|
142 | 200 | # WebSockets don't response to traditional error codes so we |
|
143 | 201 | # close the connection. |
|
144 | 202 | if not self.stream.closed(): |
@@ -154,7 +212,10 b' class ZMQChannelHandler(AuthenticatedZMQStreamHandler):' | |||
|
154 | 212 | self.log.info("%s closed, closing websocket.", self) |
|
155 | 213 | self.close() |
|
156 | 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 | 219 | self.session.send(self.zmq_stream, msg) |
|
159 | 220 | |
|
160 | 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: | |
|
4 | ||
|
5 | * Brian Granger | |
|
3 | - raises HTTPErrors | |
|
4 | - creates REST API models | |
|
6 | 5 | """ |
|
7 | 6 | |
|
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 | #----------------------------------------------------------------------------- | |
|
7 | # Copyright (c) IPython Development Team. | |
|
8 | # Distributed under the terms of the Modified BSD License. | |
|
18 | 9 | |
|
19 | 10 | import os |
|
20 | 11 | |
@@ -26,10 +17,6 b' from IPython.utils.traitlets import List, Unicode, TraitError' | |||
|
26 | 17 | from IPython.html.utils import to_os_path |
|
27 | 18 | from IPython.utils.py3compat import getcwd |
|
28 | 19 | |
|
29 | #----------------------------------------------------------------------------- | |
|
30 | # Classes | |
|
31 | #----------------------------------------------------------------------------- | |
|
32 | ||
|
33 | 20 | |
|
34 | 21 | class MappingKernelManager(MultiKernelManager): |
|
35 | 22 | """A KernelManager that handles notebook mapping and HTTP error handling""" |
@@ -39,7 +26,13 b' class MappingKernelManager(MultiKernelManager):' | |||
|
39 | 26 | |
|
40 | 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 | 37 | def _root_dir_changed(self, name, old, new): |
|
45 | 38 | """Do a bit of validation of the root dir.""" |
@@ -61,14 +54,10 b' class MappingKernelManager(MultiKernelManager):' | |||
|
61 | 54 | |
|
62 | 55 | def cwd_for_path(self, path): |
|
63 | 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 | 57 | os_path = to_os_path(path, self.root_dir) |
|
69 | 58 | # in the case of notebooks and kernels not being on the same filesystem, |
|
70 | 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 | 61 | os_path = os.path.dirname(os_path) |
|
73 | 62 | return os_path |
|
74 | 63 | |
@@ -89,7 +78,6 b' class MappingKernelManager(MultiKernelManager):' | |||
|
89 | 78 | an existing kernel is returned, but it may be checked in the future. |
|
90 | 79 | """ |
|
91 | 80 | if kernel_id is None: |
|
92 | kwargs['extra_arguments'] = self.kernel_argv | |
|
93 | 81 | if path is not None: |
|
94 | 82 | kwargs['cwd'] = self.cwd_for_path(path) |
|
95 | 83 | kernel_id = super(MappingKernelManager, self).start_kernel( |
@@ -57,6 +57,19 b' class KernelAPITest(NotebookTestBase):' | |||
|
57 | 57 | kernels = self.kern_api.list().json() |
|
58 | 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 | 73 | def test_main_kernel_handler(self): |
|
61 | 74 | # POST request |
|
62 | 75 | r = self.kern_api.start() |
@@ -65,7 +78,10 b' class KernelAPITest(NotebookTestBase):' | |||
|
65 | 78 | self.assertEqual(r.status_code, 201) |
|
66 | 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 | 86 | # GET request |
|
71 | 87 | r = self.kern_api.list() |
@@ -19,7 +19,11 b' class MainKernelSpecHandler(IPythonHandler):' | |||
|
19 | 19 | ksm = self.kernel_spec_manager |
|
20 | 20 | results = [] |
|
21 | 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 | 27 | d['name'] = kernel_name |
|
24 | 28 | results.append(d) |
|
25 | 29 |
@@ -5,6 +5,7 b' import errno' | |||
|
5 | 5 | import io |
|
6 | 6 | import json |
|
7 | 7 | import os |
|
8 | import shutil | |
|
8 | 9 | |
|
9 | 10 | pjoin = os.path.join |
|
10 | 11 | |
@@ -18,7 +19,6 b' from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_erro' | |||
|
18 | 19 | # break these tests |
|
19 | 20 | sample_kernel_json = {'argv':['cat', '{connection_file}'], |
|
20 | 21 | 'display_name':'Test kernel', |
|
21 | 'language':'bash', | |
|
22 | 22 | } |
|
23 | 23 | |
|
24 | 24 | some_resource = u"The very model of a modern major general" |
@@ -66,6 +66,25 b' class APITest(NotebookTestBase):' | |||
|
66 | 66 | |
|
67 | 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 | 88 | def test_list_kernelspecs(self): |
|
70 | 89 | specs = self.ks_api.list().json() |
|
71 | 90 | assert isinstance(specs, list) |
@@ -84,7 +103,7 b' class APITest(NotebookTestBase):' | |||
|
84 | 103 | |
|
85 | 104 | def test_get_kernelspec(self): |
|
86 | 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 | 108 | def test_get_nonexistant_kernelspec(self): |
|
90 | 109 | with assert_http_error(404): |
@@ -10,6 +10,7 b' from tornado import web' | |||
|
10 | 10 | from ...base.handlers import IPythonHandler, json_errors |
|
11 | 11 | from IPython.utils.jsonutil import date_default |
|
12 | 12 | from IPython.html.utils import url_path_join, url_escape |
|
13 | from IPython.kernel.kernelspec import NoSuchKernel | |
|
13 | 14 | |
|
14 | 15 | |
|
15 | 16 | class SessionRootHandler(IPythonHandler): |
@@ -35,23 +36,30 b' class SessionRootHandler(IPythonHandler):' | |||
|
35 | 36 | if model is None: |
|
36 | 37 | raise web.HTTPError(400, "No JSON data provided") |
|
37 | 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 | 39 | path = model['notebook']['path'] |
|
43 | 40 | except KeyError: |
|
44 | 41 | raise web.HTTPError(400, "Missing field in JSON data: notebook.path") |
|
45 | 42 | try: |
|
46 | 43 | kernel_name = model['kernel']['name'] |
|
47 | 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 | 48 | # Check to see if session exists |
|
51 |
if sm.session_exists( |
|
|
52 |
model = sm.get_session( |
|
|
49 | if sm.session_exists(path=path): | |
|
50 | model = sm.get_session(path=path) | |
|
53 | 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 | 63 | location = url_path_join(self.base_url, 'api', 'sessions', model['id']) |
|
56 | 64 | self.set_header('Location', url_escape(location)) |
|
57 | 65 | self.set_status(201) |
@@ -80,8 +88,6 b' class SessionHandler(IPythonHandler):' | |||
|
80 | 88 | changes = {} |
|
81 | 89 | if 'notebook' in model: |
|
82 | 90 | notebook = model['notebook'] |
|
83 | if 'name' in notebook: | |
|
84 | changes['name'] = notebook['name'] | |
|
85 | 91 | if 'path' in notebook: |
|
86 | 92 | changes['path'] = notebook['path'] |
|
87 | 93 | |
@@ -94,7 +100,11 b' class SessionHandler(IPythonHandler):' | |||
|
94 | 100 | def delete(self, session_id): |
|
95 | 101 | # Deletes the session with given session_id |
|
96 | 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 | 108 | self.set_status(204) |
|
99 | 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: | |
|
4 | ||
|
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 | #----------------------------------------------------------------------------- | |
|
3 | # Copyright (c) IPython Development Team. | |
|
4 | # Distributed under the terms of the Modified BSD License. | |
|
18 | 5 | |
|
19 | 6 | import uuid |
|
20 | 7 | import sqlite3 |
@@ -25,9 +12,6 b' from IPython.config.configurable import LoggingConfigurable' | |||
|
25 | 12 | from IPython.utils.py3compat import unicode_type |
|
26 | 13 | from IPython.utils.traitlets import Instance |
|
27 | 14 | |
|
28 | #----------------------------------------------------------------------------- | |
|
29 | # Classes | |
|
30 | #----------------------------------------------------------------------------- | |
|
31 | 15 | |
|
32 | 16 | class SessionManager(LoggingConfigurable): |
|
33 | 17 | |
@@ -37,7 +21,7 b' class SessionManager(LoggingConfigurable):' | |||
|
37 | 21 | # Session database initialized below |
|
38 | 22 | _cursor = None |
|
39 | 23 | _connection = None |
|
40 |
_columns = {'session_id', ' |
|
|
24 | _columns = {'session_id', 'path', 'kernel_id'} | |
|
41 | 25 | |
|
42 | 26 | @property |
|
43 | 27 | def cursor(self): |
@@ -45,7 +29,7 b' class SessionManager(LoggingConfigurable):' | |||
|
45 | 29 | if self._cursor is None: |
|
46 | 30 | self._cursor = self.connection.cursor() |
|
47 | 31 | self._cursor.execute("""CREATE TABLE session |
|
48 |
(session_id, |
|
|
32 | (session_id, path, kernel_id)""") | |
|
49 | 33 | return self._cursor |
|
50 | 34 | |
|
51 | 35 | @property |
@@ -60,9 +44,9 b' class SessionManager(LoggingConfigurable):' | |||
|
60 | 44 | """Close connection once SessionManager closes""" |
|
61 | 45 | self.cursor.close() |
|
62 | 46 | |
|
63 |
def session_exists(self, |
|
|
47 | def session_exists(self, path): | |
|
64 | 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 | 50 | reply = self.cursor.fetchone() |
|
67 | 51 | if reply is None: |
|
68 | 52 | return False |
@@ -73,17 +57,17 b' class SessionManager(LoggingConfigurable):' | |||
|
73 | 57 | "Create a uuid for a new session" |
|
74 | 58 | return unicode_type(uuid.uuid4()) |
|
75 | 59 | |
|
76 |
def create_session(self, |
|
|
60 | def create_session(self, path=None, kernel_name=None): | |
|
77 | 61 | """Creates a session and returns its model""" |
|
78 | 62 | session_id = self.new_session_id() |
|
79 | 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 | 65 | kernel_id = self.kernel_manager.start_kernel(path=kernel_path, |
|
82 | 66 | kernel_name=kernel_name) |
|
83 |
return self.save_session(session_id, |
|
|
67 | return self.save_session(session_id, path=path, | |
|
84 | 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 | 71 | """Saves the items for the session with the given session_id |
|
88 | 72 | |
|
89 | 73 | Given a session_id (and any other of the arguments), this method |
@@ -94,10 +78,8 b' class SessionManager(LoggingConfigurable):' | |||
|
94 | 78 | ---------- |
|
95 | 79 | session_id : str |
|
96 | 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 | 81 | path : str |
|
100 |
the path |
|
|
82 | the path for the given notebook | |
|
101 | 83 | kernel_id : str |
|
102 | 84 | a uuid for the kernel associated with this session |
|
103 | 85 | |
@@ -106,8 +88,8 b' class SessionManager(LoggingConfigurable):' | |||
|
106 | 88 | model : dict |
|
107 | 89 | a dictionary of the session model |
|
108 | 90 | """ |
|
109 |
self.cursor.execute("INSERT INTO session VALUES (?,?,? |
|
|
110 |
(session_id |
|
|
91 | self.cursor.execute("INSERT INTO session VALUES (?,?,?)", | |
|
92 | (session_id, path, kernel_id) | |
|
111 | 93 | ) |
|
112 | 94 | return self.get_session(session_id=session_id) |
|
113 | 95 | |
@@ -121,7 +103,7 b' class SessionManager(LoggingConfigurable):' | |||
|
121 | 103 | ---------- |
|
122 | 104 | **kwargs : keyword argument |
|
123 | 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 | 108 | Returns |
|
127 | 109 | ------- |
@@ -198,7 +180,6 b' class SessionManager(LoggingConfigurable):' | |||
|
198 | 180 | model = { |
|
199 | 181 | 'id': row['session_id'], |
|
200 | 182 | 'notebook': { |
|
201 | 'name': row['name'], | |
|
202 | 183 | 'path': row['path'] |
|
203 | 184 | }, |
|
204 | 185 | 'kernel': self.kernel_manager.kernel_model(row['kernel_id']) |
@@ -32,24 +32,24 b' class TestSessionManager(TestCase):' | |||
|
32 | 32 | |
|
33 | 33 | def test_get_session(self): |
|
34 | 34 | sm = SessionManager(kernel_manager=DummyMKM()) |
|
35 |
session_id = sm.create_session( |
|
|
35 | session_id = sm.create_session(path='/path/to/test.ipynb', | |
|
36 | 36 | kernel_name='bar')['id'] |
|
37 | 37 | model = sm.get_session(session_id=session_id) |
|
38 | 38 | expected = {'id':session_id, |
|
39 |
'notebook':{' |
|
|
39 | 'notebook':{'path': u'/path/to/test.ipynb'}, | |
|
40 | 40 | 'kernel': {'id':u'A', 'name': 'bar'}} |
|
41 | 41 | self.assertEqual(model, expected) |
|
42 | 42 | |
|
43 | 43 | def test_bad_get_session(self): |
|
44 | 44 | # Should raise error if a bad key is passed to the database. |
|
45 | 45 | sm = SessionManager(kernel_manager=DummyMKM()) |
|
46 |
session_id = sm.create_session( |
|
|
46 | session_id = sm.create_session(path='/path/to/test.ipynb', | |
|
47 | 47 | kernel_name='foo')['id'] |
|
48 | 48 | self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword |
|
49 | 49 | |
|
50 | 50 | def test_get_session_dead_kernel(self): |
|
51 | 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 | 53 | # kill the kernel |
|
54 | 54 | sm.kernel_manager.shutdown_kernel(session['kernel']['id']) |
|
55 | 55 | with self.assertRaises(KeyError): |
@@ -61,24 +61,33 b' class TestSessionManager(TestCase):' | |||
|
61 | 61 | def test_list_sessions(self): |
|
62 | 62 | sm = SessionManager(kernel_manager=DummyMKM()) |
|
63 | 63 | sessions = [ |
|
64 |
sm.create_session( |
|
|
65 |
sm.create_session( |
|
|
66 |
sm.create_session( |
|
|
64 | sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'), | |
|
65 | sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'), | |
|
66 | sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'), | |
|
67 | 67 | ] |
|
68 | 68 | sessions = sm.list_sessions() |
|
69 | expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb', | |
|
70 | 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}}, | |
|
71 |
|
|
|
72 | 'path': u'/path/to/2/'}, 'kernel':{'id':u'B', 'name':'python'}}, | |
|
73 | {'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb', | |
|
74 | 'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}] | |
|
69 | expected = [ | |
|
70 | { | |
|
71 | 'id':sessions[0]['id'], | |
|
72 | 'notebook':{'path': u'/path/to/1/test1.ipynb'}, | |
|
73 | 'kernel':{'id':u'A', '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 | 84 | self.assertEqual(sessions, expected) |
|
76 | 85 | |
|
77 | 86 | def test_list_sessions_dead_kernel(self): |
|
78 | 87 | sm = SessionManager(kernel_manager=DummyMKM()) |
|
79 | 88 | sessions = [ |
|
80 |
sm.create_session( |
|
|
81 |
sm.create_session( |
|
|
89 | sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'), | |
|
90 | sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'), | |
|
82 | 91 | ] |
|
83 | 92 | # kill one of the kernels |
|
84 | 93 | sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id']) |
@@ -87,8 +96,7 b' class TestSessionManager(TestCase):' | |||
|
87 | 96 | { |
|
88 | 97 | 'id': sessions[1]['id'], |
|
89 | 98 | 'notebook': { |
|
90 |
' |
|
|
91 | 'path': u'/path/to/2/', | |
|
99 | 'path': u'/path/to/2/test2.ipynb', | |
|
92 | 100 | }, |
|
93 | 101 | 'kernel': { |
|
94 | 102 | 'id': u'B', |
@@ -100,41 +108,47 b' class TestSessionManager(TestCase):' | |||
|
100 | 108 | |
|
101 | 109 | def test_update_session(self): |
|
102 | 110 | sm = SessionManager(kernel_manager=DummyMKM()) |
|
103 |
session_id = sm.create_session( |
|
|
111 | session_id = sm.create_session(path='/path/to/test.ipynb', | |
|
104 | 112 | kernel_name='julia')['id'] |
|
105 |
sm.update_session(session_id, |
|
|
113 | sm.update_session(session_id, path='/path/to/new_name.ipynb') | |
|
106 | 114 | model = sm.get_session(session_id=session_id) |
|
107 | 115 | expected = {'id':session_id, |
|
108 |
'notebook':{' |
|
|
116 | 'notebook':{'path': u'/path/to/new_name.ipynb'}, | |
|
109 | 117 | 'kernel':{'id':u'A', 'name':'julia'}} |
|
110 | 118 | self.assertEqual(model, expected) |
|
111 | 119 | |
|
112 | 120 | def test_bad_update_session(self): |
|
113 | 121 | # try to update a session with a bad keyword ~ raise error |
|
114 | 122 | sm = SessionManager(kernel_manager=DummyMKM()) |
|
115 |
session_id = sm.create_session( |
|
|
123 | session_id = sm.create_session(path='/path/to/test.ipynb', | |
|
116 | 124 | kernel_name='ir')['id'] |
|
117 | 125 | self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword |
|
118 | 126 | |
|
119 | 127 | def test_delete_session(self): |
|
120 | 128 | sm = SessionManager(kernel_manager=DummyMKM()) |
|
121 | 129 | sessions = [ |
|
122 |
sm.create_session( |
|
|
123 |
sm.create_session( |
|
|
124 |
sm.create_session( |
|
|
130 | sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'), | |
|
131 | sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'), | |
|
132 | sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'), | |
|
125 | 133 | ] |
|
126 | 134 | sm.delete_session(sessions[1]['id']) |
|
127 | 135 | new_sessions = sm.list_sessions() |
|
128 | expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb', | |
|
129 | 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}}, | |
|
130 |
|
|
|
131 |
|
|
|
136 | expected = [{ | |
|
137 | 'id': sessions[0]['id'], | |
|
138 | 'notebook': {'path': u'/path/to/1/test1.ipynb'}, | |
|
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 | 146 | self.assertEqual(new_sessions, expected) |
|
133 | 147 | |
|
134 | 148 | def test_bad_delete_session(self): |
|
135 | 149 | # try to delete a session that doesn't exist ~ raise error |
|
136 | 150 | sm = SessionManager(kernel_manager=DummyMKM()) |
|
137 |
sm.create_session( |
|
|
151 | sm.create_session(path='/path/to/test.ipynb', kernel_name='python') | |
|
138 | 152 | self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword |
|
139 | 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 | 12 | from IPython.html.utils import url_path_join |
|
13 | 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 | 17 | class SessionAPI(object): |
|
17 | 18 | """Wrapper for notebook API calls.""" |
@@ -37,13 +38,13 b' class SessionAPI(object):' | |||
|
37 | 38 | def get(self, id): |
|
38 | 39 | return self._req('GET', id) |
|
39 | 40 | |
|
40 |
def create(self |
|
|
41 |
body = json.dumps({'notebook': {' |
|
|
41 | def create(self, path, kernel_name='python'): | |
|
42 | body = json.dumps({'notebook': {'path':path}, | |
|
42 | 43 | 'kernel': {'name': kernel_name}}) |
|
43 | 44 | return self._req('POST', '', body) |
|
44 | 45 | |
|
45 |
def modify(self, id, |
|
|
46 |
body = json.dumps({'notebook': {' |
|
|
46 | def modify(self, id, path): | |
|
47 | body = json.dumps({'notebook': {'path':path}}) | |
|
47 | 48 | return self._req('PATCH', id, body) |
|
48 | 49 | |
|
49 | 50 | def delete(self, id): |
@@ -62,8 +63,8 b' class SessionAPITest(NotebookTestBase):' | |||
|
62 | 63 | |
|
63 | 64 | with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w', |
|
64 | 65 | encoding='utf-8') as f: |
|
65 |
nb = new_notebook( |
|
|
66 |
write(nb, f, |
|
|
66 | nb = new_notebook() | |
|
67 | write(nb, f, version=4) | |
|
67 | 68 | |
|
68 | 69 | self.sess_api = SessionAPI(self.base_url()) |
|
69 | 70 | |
@@ -77,12 +78,11 b' class SessionAPITest(NotebookTestBase):' | |||
|
77 | 78 | sessions = self.sess_api.list().json() |
|
78 | 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 | 82 | self.assertEqual(resp.status_code, 201) |
|
82 | 83 | newsession = resp.json() |
|
83 | 84 | self.assertIn('id', newsession) |
|
84 |
self.assertEqual(newsession['notebook'][' |
|
|
85 | self.assertEqual(newsession['notebook']['path'], 'foo') | |
|
85 | self.assertEqual(newsession['notebook']['path'], 'foo/nb1.ipynb') | |
|
86 | 86 | self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id'])) |
|
87 | 87 | |
|
88 | 88 | sessions = self.sess_api.list().json() |
@@ -94,7 +94,7 b' class SessionAPITest(NotebookTestBase):' | |||
|
94 | 94 | self.assertEqual(got, newsession) |
|
95 | 95 | |
|
96 | 96 | def test_delete(self): |
|
97 |
newsession = self.sess_api.create('nb1.ipynb |
|
|
97 | newsession = self.sess_api.create('foo/nb1.ipynb').json() | |
|
98 | 98 | sid = newsession['id'] |
|
99 | 99 | |
|
100 | 100 | resp = self.sess_api.delete(sid) |
@@ -107,10 +107,9 b' class SessionAPITest(NotebookTestBase):' | |||
|
107 | 107 | self.sess_api.get(sid) |
|
108 | 108 | |
|
109 | 109 | def test_modify(self): |
|
110 |
newsession = self.sess_api.create('nb1.ipynb |
|
|
110 | newsession = self.sess_api.create('foo/nb1.ipynb').json() | |
|
111 | 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 | 114 | self.assertEqual(changed['id'], sid) |
|
115 |
self.assertEqual(changed['notebook'][' |
|
|
116 | self.assertEqual(changed['notebook']['path'], '') | |
|
115 | self.assertEqual(changed['notebook']['path'], 'nb2.ipynb') |
|
1 | NO CONTENT: modified file, binary diff hidden |
@@ -4,7 +4,8 b'' | |||
|
4 | 4 | define([ |
|
5 | 5 | 'base/js/namespace', |
|
6 | 6 | 'jquery', |
|
7 | ], function(IPython, $) { | |
|
7 | 'codemirror/lib/codemirror', | |
|
8 | ], function(IPython, $, CodeMirror) { | |
|
8 | 9 | "use strict"; |
|
9 | 10 | |
|
10 | 11 | var modal = function (options) { |
@@ -90,6 +91,17 b' define([' | |||
|
90 | 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 | 105 | var edit_metadata = function (options) { |
|
94 | 106 | options.name = options.name || "Cell"; |
|
95 | 107 | var error_div = $('<div/>').css('color', 'red'); |
@@ -130,7 +142,9 b' define([' | |||
|
130 | 142 | buttons: { |
|
131 | 143 | OK: { class : "btn-primary", |
|
132 | 144 | click: function() { |
|
133 |
/ |
|
|
145 | /** | |
|
146 | * validate json and set it | |
|
147 | */ | |
|
134 | 148 | var new_md; |
|
135 | 149 | try { |
|
136 | 150 | new_md = JSON.parse(editor.getValue()); |
@@ -153,6 +167,7 b' define([' | |||
|
153 | 167 | |
|
154 | 168 | var dialog = { |
|
155 | 169 | modal : modal, |
|
170 | kernel_modal : kernel_modal, | |
|
156 | 171 | edit_metadata : edit_metadata, |
|
157 | 172 | }; |
|
158 | 173 |
@@ -1,22 +1,33 b'' | |||
|
1 | 1 | // Copyright (c) IPython Development Team. |
|
2 | 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 | 11 | define([ |
|
5 | 12 | 'base/js/namespace', |
|
6 | 13 | 'jquery', |
|
7 | 14 | 'base/js/utils', |
|
8 | ], function(IPython, $, utils) { | |
|
15 | 'underscore', | |
|
16 | ], function(IPython, $, utils, _) { | |
|
9 | 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 | |
|
15 | // it is that there are different keycode sets. Firefox uses the "Mozilla keycodes" | |
|
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) | |
|
29 | // These apply to Firefox, (Webkit and IE) | |
|
30 | // This does work **only** on US keyboard. | |
|
20 | 31 | var _keycodes = { |
|
21 | 32 | 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73, |
|
22 | 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 | 90 | var normalize_shortcut = function (shortcut) { |
|
80 | // Put a shortcut into normalized form: | |
|
81 | // 1. Make lowercase | |
|
82 | // 2. Replace cmd by meta | |
|
83 | // 3. Sort '-' separated modifiers into the order alt-ctrl-meta-shift | |
|
84 | // 4. Normalize keys | |
|
91 | /** | |
|
92 | * @function _normalize_shortcut | |
|
93 | * @private | |
|
94 | * return a dict containing the normalized shortcut and the number of time it should be pressed: | |
|
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 | 108 | shortcut = shortcut.toLowerCase().replace('cmd', 'meta'); |
|
86 | 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 | 117 | var values = shortcut.split("-"); |
|
88 | 118 | if (values.length === 1) { |
|
89 | 119 | return normalize_key(values[0]); |
@@ -96,7 +126,9 b' define([' | |||
|
96 | 126 | }; |
|
97 | 127 | |
|
98 | 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 | 132 | type = type || 'keydown'; |
|
101 | 133 | shortcut = normalize_shortcut(shortcut); |
|
102 | 134 | shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key |
@@ -111,8 +143,21 b' define([' | |||
|
111 | 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 | 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 | 161 | var shortcut = ''; |
|
117 | 162 | var key = inv_keycodes[event.which]; |
|
118 | 163 | if (event.altKey && key !== 'alt') {shortcut += 'alt-';} |
@@ -125,35 +170,86 b' define([' | |||
|
125 | 170 | |
|
126 | 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 | 180 | this._shortcuts = {}; |
|
130 | this._counts = {}; | |
|
131 | this._timers = {}; | |
|
132 | 181 | this.delay = delay || 800; // delay in milliseconds |
|
133 | 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 | 230 | ShortcutManager.prototype.help = function () { |
|
137 | 231 | var help = []; |
|
138 |
|
|
|
139 | var help_string = this._shortcuts[shortcut].help; | |
|
140 |
var |
|
|
232 | var ftree = flatten_shorttree(this._shortcuts); | |
|
233 | for (var shortcut in ftree) { | |
|
234 | var action = this.actions.get(ftree[shortcut]); | |
|
235 | var help_string = action.help||'== no help =='; | |
|
236 | var help_index = action.help_index; | |
|
141 | 237 | if (help_string) { |
|
142 | if (platform === 'MacOS') { | |
|
143 | shortcut = shortcut.replace('meta', 'cmd'); | |
|
144 | } | |
|
238 | var shortstring = (action.shortstring||shortcut); | |
|
145 | 239 | help.push({ |
|
146 |
shortcut: short |
|
|
240 | shortcut: shortstring, | |
|
147 | 241 | help: help_string, |
|
148 | 242 | help_index: help_index} |
|
149 | 243 | ); |
|
150 | 244 | } |
|
151 | 245 | } |
|
152 | 246 | help.sort(function (a, b) { |
|
153 | if (a.help_index > b.help_index) | |
|
247 | if (a.help_index > b.help_index){ | |
|
154 | 248 | return 1; |
|
155 | if (a.help_index < b.help_index) | |
|
249 | } | |
|
250 | if (a.help_index < b.help_index){ | |
|
156 | 251 | return -1; |
|
252 | } | |
|
157 | 253 | return 0; |
|
158 | 254 | }); |
|
159 | 255 | return help; |
@@ -163,19 +259,105 b' define([' | |||
|
163 | 259 | this._shortcuts = {}; |
|
164 | 260 | }; |
|
165 | 261 | |
|
166 |
ShortcutManager.prototype. |
|
|
167 | if (typeof(data) === 'function') { | |
|
168 | data = {help: '', help_index: '', handler: data}; | |
|
262 | ShortcutManager.prototype.get_shortcut = function (shortcut){ | |
|
263 | /** | |
|
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 || ''; | |
|
171 | data.help = data.help || ''; | |
|
172 | data.count = data.count || 1; | |
|
173 | if (data.help_index === '') { | |
|
174 | data.help_index = 'zz'; | |
|
304 | }; | |
|
305 | ||
|
306 | ShortcutManager.prototype._remove_leaf = function(shortcut_array, tree, allow_node){ | |
|
307 | if(shortcut_array.length === 1){ | |
|
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 | 358 | shortcut = normalize_shortcut(shortcut); |
|
177 | this._counts[shortcut] = 0; | |
|
178 | this._shortcuts[shortcut] = data; | |
|
359 | this.set_shortcut(shortcut, action_name); | |
|
360 | ||
|
179 | 361 | if (!suppress_help_update) { |
|
180 | 362 | // update the keyboard shortcuts notebook help |
|
181 | 363 | this.events.trigger('rebuild.QuickHelp'); |
@@ -183,6 +365,11 b' define([' | |||
|
183 | 365 | }; |
|
184 | 366 | |
|
185 | 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 | 373 | for (var shortcut in data) { |
|
187 | 374 | this.add_shortcut(shortcut, data[shortcut], true); |
|
188 | 375 | } |
@@ -191,55 +378,63 b' define([' | |||
|
191 | 378 | }; |
|
192 | 379 | |
|
193 | 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 | 385 | shortcut = normalize_shortcut(shortcut); |
|
195 | delete this._counts[shortcut]; | |
|
196 |
|
|
|
386 | if( typeof(shortcut) === 'string'){ | |
|
387 | shortcut = shortcut.split(','); | |
|
388 | } | |
|
389 | this._remove_leaf(shortcut, this._shortcuts); | |
|
197 | 390 | if (!suppress_help_update) { |
|
198 | 391 | // update the keyboard shortcuts notebook help |
|
199 | 392 | this.events.trigger('rebuild.QuickHelp'); |
|
200 | 393 | } |
|
201 | 394 | }; |
|
202 | 395 | |
|
203 | ShortcutManager.prototype.count_handler = function (shortcut, event, data) { | |
|
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 | }; | |
|
396 | ||
|
222 | 397 | |
|
223 | 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 | 415 | var shortcut = event_to_shortcut(event); |
|
225 | var data = this._shortcuts[shortcut]; | |
|
226 | if (data) { | |
|
227 | var handler = data.handler; | |
|
228 | if (handler) { | |
|
229 | if (data.count === 1) { | |
|
230 |
|
|
|
231 | } else if (data.count > 1) { | |
|
232 | return this.count_handler(shortcut, event, data); | |
|
233 | } | |
|
234 | } | |
|
416 | this._queue.push(shortcut); | |
|
417 | var action_name = this.get_shortcut(this._queue); | |
|
418 | ||
|
419 | if (typeof(action_name) === 'undefined'|| action_name === null){ | |
|
420 | this.clearqueue(); | |
|
421 | return true; | |
|
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 | 434 | ShortcutManager.prototype.handles = function (event) { |
|
240 | 435 | var shortcut = event_to_shortcut(event); |
|
241 |
var |
|
|
242 | return !( data === undefined || data.handler === undefined ); | |
|
436 | var action_name = this.get_shortcut(this._queue.concat(shortcut)); | |
|
437 | return (typeof(action_name) !== 'undefined'); | |
|
243 | 438 | }; |
|
244 | 439 | |
|
245 | 440 | var keyboard = { |
@@ -249,10 +444,10 b' define([' | |||
|
249 | 444 | normalize_key : normalize_key, |
|
250 | 445 | normalize_shortcut : normalize_shortcut, |
|
251 | 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 | 451 | IPython.keyboard = keyboard; |
|
257 | 452 | |
|
258 | 453 | return keyboard; |
@@ -3,6 +3,7 b'' | |||
|
3 | 3 | |
|
4 | 4 | var IPython = IPython || {}; |
|
5 | 5 | define([], function(){ |
|
6 | "use strict"; | |
|
6 | 7 | IPython.version = "3.0.0-dev"; |
|
7 | 8 | return IPython; |
|
8 | 9 | }); |
@@ -7,6 +7,13 b' define([' | |||
|
7 | 7 | ], function(IPython, $) { |
|
8 | 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 | 17 | var NotificationWidget = function (selector) { |
|
11 | 18 | this.selector = selector; |
|
12 | 19 | this.timeout = null; |
@@ -16,27 +23,41 b' define([' | |||
|
16 | 23 | this.style(); |
|
17 | 24 | } |
|
18 | 25 | this.element.hide(); |
|
19 | var that = this; | |
|
20 | ||
|
21 | 26 | this.inner = $('<span/>'); |
|
22 | 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 | 35 | NotificationWidget.prototype.style = function () { |
|
27 | 36 | this.element.addClass('notification_widget'); |
|
28 | 37 | }; |
|
29 | 38 | |
|
30 | // msg : message to display | |
|
31 | // timeout : time in ms before diseapearing | |
|
32 | // | |
|
33 | // if timeout <= 0 | |
|
34 | // click_callback : function called if user click on notification | |
|
35 | // could return false to prevent the notification to be dismissed | |
|
39 | /** | |
|
40 | * Set the notification widget message to display for a certain | |
|
41 | * amount of time (timeout). The widget will be shown forever if | |
|
42 | * timeout is <= 0 or undefined. If the widget is clicked while it | |
|
43 | * is still displayed, execute an optional callback | |
|
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 | 58 | NotificationWidget.prototype.set_message = function (msg, timeout, click_callback, options) { |
|
37 |
|
|
|
38 | var callback = click_callback || function() {return true;}; | |
|
39 | var that = this; | |
|
59 | options = options || {}; | |
|
60 | ||
|
40 | 61 | // unbind potential previous callback |
|
41 | 62 | this.element.unbind('click'); |
|
42 | 63 | this.inner.attr('class', options.icon); |
@@ -47,52 +68,87 b' define([' | |||
|
47 | 68 | // reset previous set style |
|
48 | 69 | this.element.removeClass(); |
|
49 | 70 | this.style(); |
|
50 | if (options.class){ | |
|
51 | ||
|
52 | this.element.addClass(options.class) | |
|
71 | if (options.class) { | |
|
72 | this.element.addClass(options.class); | |
|
53 | 73 | } |
|
74 | ||
|
75 | // clear previous timer | |
|
54 | 76 | if (this.timeout !== null) { |
|
55 | 77 | clearTimeout(this.timeout); |
|
56 | 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 | 84 | this.timeout = setTimeout(function () { |
|
60 | 85 | that.element.fadeOut(100, function () {that.inner.text('');}); |
|
86 | that.element.unbind('click'); | |
|
61 | 87 | that.timeout = null; |
|
62 | 88 | }, timeout); |
|
63 |
} |
|
|
64 | this.element.click(function() { | |
|
65 | if( callback() !== false ) { | |
|
89 | } | |
|
90 | ||
|
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 | 95 | that.element.fadeOut(100, function () {that.inner.text('');}); |
|
67 | that.element.unbind('click'); | |
|
68 | 96 | } |
|
69 |
|
|
|
70 |
|
|
|
97 | that.element.unbind('click'); | |
|
98 | if (that.timeout !== null) { | |
|
71 | 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 | 113 | NotificationWidget.prototype.info = function (msg, timeout, click_callback, options) { |
|
79 |
|
|
|
80 | options.class = options.class +' info'; | |
|
81 |
|
|
|
114 | options = options || {}; | |
|
115 | options.class = options.class + ' info'; | |
|
116 | timeout = timeout || 3500; | |
|
82 | 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 | 127 | NotificationWidget.prototype.warning = function (msg, timeout, click_callback, options) { |
|
85 |
|
|
|
86 | options.class = options.class +' warning'; | |
|
128 | options = options || {}; | |
|
129 | options.class = options.class + ' warning'; | |
|
87 | 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 | 140 | NotificationWidget.prototype.danger = function (msg, timeout, click_callback, options) { |
|
90 |
|
|
|
91 | options.class = options.class +' danger'; | |
|
141 | options = options || {}; | |
|
142 | options.class = options.class + ' danger'; | |
|
92 | 143 | this.set_message(msg, timeout, click_callback, options); |
|
93 | } | |
|
94 | ||
|
144 | }; | |
|
95 | 145 | |
|
146 | /** | |
|
147 | * Get the text of the widget message. | |
|
148 | * | |
|
149 | * @method get_message | |
|
150 | * @return {string} - the message text | |
|
151 | */ | |
|
96 | 152 | NotificationWidget.prototype.get_message = function () { |
|
97 | 153 | return this.inner.html(); |
|
98 | 154 | }; |
@@ -15,23 +15,29 b' define([' | |||
|
15 | 15 | }; |
|
16 | 16 | |
|
17 | 17 | Page.prototype.show = function () { |
|
18 | // The header and site divs start out hidden to prevent FLOUC. | |
|
19 | // Main scripts should call this method after styling everything. | |
|
18 | /** | |
|
19 | * The header and site divs start out hidden to prevent FLOUC. | |
|
20 | * Main scripts should call this method after styling everything. | |
|
21 | */ | |
|
20 | 22 | this.show_header(); |
|
21 | 23 | this.show_site(); |
|
22 | 24 | }; |
|
23 | 25 | |
|
24 | 26 | Page.prototype.show_header = function () { |
|
25 | // The header and site divs start out hidden to prevent FLOUC. | |
|
26 | // Main scripts should call this method after styling everything. | |
|
27 | // TODO: selector are hardcoded, pass as constructor argument | |
|
27 | /** | |
|
28 | * The header and site divs start out hidden to prevent FLOUC. | |
|
29 | * Main scripts should call this method after styling everything. | |
|
30 | * TODO: selector are hardcoded, pass as constructor argument | |
|
31 | */ | |
|
28 | 32 | $('div#header').css('display','block'); |
|
29 | 33 | }; |
|
30 | 34 | |
|
31 | 35 | Page.prototype.show_site = function () { |
|
32 | // The header and site divs start out hidden to prevent FLOUC. | |
|
33 | // Main scripts should call this method after styling everything. | |
|
34 | // TODO: selector are hardcoded, pass as constructor argument | |
|
36 | /** | |
|
37 | * The header and site divs start out hidden to prevent FLOUC. | |
|
38 | * Main scripts should call this method after styling everything. | |
|
39 | * TODO: selector are hardcoded, pass as constructor argument | |
|
40 | */ | |
|
35 | 41 | $('div#site').css('display','block'); |
|
36 | 42 | }; |
|
37 | 43 |
@@ -18,8 +18,10 b' define([' | |||
|
18 | 18 | } |
|
19 | 19 | |
|
20 | 20 | var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) { |
|
21 | // add trusting data-attributes to the default sanitizeAttribs from caja | |
|
22 | // this function is mostly copied from the caja source | |
|
21 | /** | |
|
22 | * add trusting data-attributes to the default sanitizeAttribs from caja | |
|
23 | * this function is mostly copied from the caja source | |
|
24 | */ | |
|
23 | 25 | var ATTRIBS = caja.html4.ATTRIBS; |
|
24 | 26 | for (var i = 0; i < attribs.length; i += 2) { |
|
25 | 27 | var attribName = attribs[i]; |
@@ -34,9 +36,11 b' define([' | |||
|
34 | 36 | }; |
|
35 | 37 | |
|
36 | 38 | var sanitize_css = function (css, tagPolicy) { |
|
37 | // sanitize CSS | |
|
38 |
|
|
|
39 | // called by sanitize_stylesheets | |
|
39 | /** | |
|
40 | * sanitize CSS | |
|
41 | * like sanitize_html, but for CSS | |
|
42 | * called by sanitize_stylesheets | |
|
43 | */ | |
|
40 | 44 | return caja.sanitizeStylesheet( |
|
41 | 45 | window.location.pathname, |
|
42 | 46 | css, |
@@ -51,8 +55,10 b' define([' | |||
|
51 | 55 | }; |
|
52 | 56 | |
|
53 | 57 | var sanitize_stylesheets = function (html, tagPolicy) { |
|
54 | // sanitize just the css in style tags in a block of html | |
|
55 | // called by sanitize_html, if allow_css is true | |
|
58 | /** | |
|
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 | 62 | var h = $("<div/>").append(html); |
|
57 | 63 | var style_tags = h.find("style"); |
|
58 | 64 | if (!style_tags.length) { |
@@ -66,9 +72,11 b' define([' | |||
|
66 | 72 | }; |
|
67 | 73 | |
|
68 | 74 | var sanitize_html = function (html, allow_css) { |
|
69 | // sanitize HTML | |
|
70 | // if allow_css is true (default: false), CSS is sanitized as well. | |
|
71 | // otherwise, CSS elements and attributes are simply removed. | |
|
75 | /** | |
|
76 | * sanitize HTML | |
|
77 | * if allow_css is true (default: false), CSS is sanitized as well. | |
|
78 | * otherwise, CSS elements and attributes are simply removed. | |
|
79 | */ | |
|
72 | 80 | var html4 = caja.html4; |
|
73 | 81 | |
|
74 | 82 | if (allow_css) { |
@@ -4,7 +4,8 b'' | |||
|
4 | 4 | define([ |
|
5 | 5 | 'base/js/namespace', |
|
6 | 6 | 'jquery', |
|
7 | ], function(IPython, $){ | |
|
7 | 'codemirror/lib/codemirror', | |
|
8 | ], function(IPython, $, CodeMirror){ | |
|
8 | 9 | "use strict"; |
|
9 | 10 | |
|
10 | 11 | IPython.load_extensions = function () { |
@@ -153,7 +154,9 b' define([' | |||
|
153 | 154 | |
|
154 | 155 | |
|
155 | 156 | var uuid = function () { |
|
156 | // http://www.ietf.org/rfc/rfc4122.txt | |
|
157 | /** | |
|
158 | * http://www.ietf.org/rfc/rfc4122.txt | |
|
159 | */ | |
|
157 | 160 | var s = []; |
|
158 | 161 | var hexDigits = "0123456789ABCDEF"; |
|
159 | 162 | for (var i = 0; i < 32; i++) { |
@@ -271,11 +274,11 b' define([' | |||
|
271 | 274 | } else { |
|
272 | 275 | line = "background-color: "; |
|
273 | 276 | } |
|
274 | line = line + "rgb(" + r + "," + g + "," + b + ");" | |
|
275 |
if ( !attrs |
|
|
276 |
attrs |
|
|
277 | line = line + "rgb(" + r + "," + g + "," + b + ");"; | |
|
278 | if ( !attrs.style ) { | |
|
279 | attrs.style = line; | |
|
277 | 280 | } else { |
|
278 |
attrs |
|
|
281 | attrs.style += " " + line; | |
|
279 | 282 | } |
|
280 | 283 | } |
|
281 | 284 | } |
@@ -284,27 +287,36 b' define([' | |||
|
284 | 287 | function ansispan(str) { |
|
285 | 288 | // ansispan function adapted from github.com/mmalecki/ansispan (MIT License) |
|
286 | 289 | // regular ansi escapes (using the table above) |
|
290 | var is_open = false; | |
|
287 | 291 | return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) { |
|
288 | 292 | if (!pattern) { |
|
289 | 293 | // [(01|22|39|)m close spans |
|
290 |
|
|
|
291 | } | |
|
292 | // consume sequence of color escapes | |
|
293 | var numbers = pattern.match(/\d+/g); | |
|
294 | var attrs = {}; | |
|
295 | while (numbers.length > 0) { | |
|
296 | _process_numbers(attrs, numbers); | |
|
297 | } | |
|
298 | ||
|
299 | var span = "<span "; | |
|
300 | for (var attr in attrs) { | |
|
301 |
var |
|
|
302 | span = span + " " + attr + '="' + attrs[attr] + '"'; | |
|
294 | if (is_open) { | |
|
295 | is_open = false; | |
|
296 | return "</span>"; | |
|
297 | } else { | |
|
298 | return ""; | |
|
299 | } | |
|
300 | } else { | |
|
301 | is_open = true; | |
|
302 | ||
|
303 | // consume sequence of color escapes | |
|
304 | var numbers = pattern.match(/\d+/g); | |
|
305 | var attrs = {}; | |
|
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 |
} |
|
|
307 | ||
|
318 | } | |
|
319 | ||
|
308 | 320 | // Transform ANSI color escape codes into HTML <span> tags with css |
|
309 | 321 | // classes listed in the above ansi_colormap object. The actual color used |
|
310 | 322 | // are set in the css file. |
@@ -345,7 +357,9 b' define([' | |||
|
345 | 357 | } |
|
346 | 358 | |
|
347 | 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 | 363 | var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>'); |
|
350 | 364 | $(body).append(test); |
|
351 | 365 | var pixel_per_point = test.width()/10000; |
@@ -354,10 +368,12 b' define([' | |||
|
354 | 368 | }; |
|
355 | 369 | |
|
356 | 370 | var always_new = function (constructor) { |
|
357 | // wrapper around contructor to avoid requiring `var a = new constructor()` | |
|
358 | // useful for passing constructors as callbacks, | |
|
359 | // not for programmer laziness. | |
|
360 | // from http://programmers.stackexchange.com/questions/118798 | |
|
371 | /** | |
|
372 | * wrapper around contructor to avoid requiring `var a = new constructor()` | |
|
373 | * useful for passing constructors as callbacks, | |
|
374 | * not for programmer laziness. | |
|
375 | * from http://programmers.stackexchange.com/questions/118798 | |
|
376 | */ | |
|
361 | 377 | return function () { |
|
362 | 378 | var obj = Object.create(constructor.prototype); |
|
363 | 379 | constructor.apply(obj, arguments); |
@@ -366,7 +382,9 b' define([' | |||
|
366 | 382 | }; |
|
367 | 383 | |
|
368 | 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 | 388 | var url = ''; |
|
371 | 389 | for (var i = 0; i < arguments.length; i++) { |
|
372 | 390 | if (arguments[i] === '') { |
@@ -382,36 +400,58 b' define([' | |||
|
382 | 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 | 417 | var parse_url = function (url) { |
|
386 | // an `a` element with an href allows attr-access to the parsed segments of a URL | |
|
387 | // a = parse_url("http://localhost:8888/path/name#hash") | |
|
388 | // a.protocol = "http:" | |
|
389 | // a.host = "localhost:8888" | |
|
390 |
|
|
|
391 | // a.port = 8888 | |
|
392 | // a.pathname = "/path/name" | |
|
393 | // a.hash = "#hash" | |
|
418 | /** | |
|
419 | * an `a` element with an href allows attr-access to the parsed segments of a URL | |
|
420 | * a = parse_url("http://localhost:8888/path/name#hash") | |
|
421 | * a.protocol = "http:" | |
|
422 | * a.host = "localhost:8888" | |
|
423 | * a.hostname = "localhost" | |
|
424 | * a.port = 8888 | |
|
425 | * a.pathname = "/path/name" | |
|
426 | * a.hash = "#hash" | |
|
427 | */ | |
|
394 | 428 | var a = document.createElement("a"); |
|
395 | 429 | a.href = url; |
|
396 | 430 | return a; |
|
397 | 431 | }; |
|
398 | 432 | |
|
399 | 433 | var encode_uri_components = function (uri) { |
|
400 | // encode just the components of a multi-segment uri, | |
|
401 | // leaving '/' separators | |
|
434 | /** | |
|
435 | * encode just the components of a multi-segment uri, | |
|
436 | * leaving '/' separators | |
|
437 | */ | |
|
402 | 438 | return uri.split('/').map(encodeURIComponent).join('/'); |
|
403 | 439 | }; |
|
404 | 440 | |
|
405 | 441 | var url_join_encode = function () { |
|
406 | // join a sequence of url components with '/', | |
|
407 | // encoding each component with encodeURIComponent | |
|
442 | /** | |
|
443 | * join a sequence of url components with '/', | |
|
444 | * encoding each component with encodeURIComponent | |
|
445 | */ | |
|
408 | 446 | return encode_uri_components(url_path_join.apply(null, arguments)); |
|
409 | 447 | }; |
|
410 | 448 | |
|
411 | 449 | |
|
412 | 450 | var splitext = function (filename) { |
|
413 | // mimic Python os.path.splitext | |
|
414 | // Returns ['base', '.ext'] | |
|
451 | /** | |
|
452 | * mimic Python os.path.splitext | |
|
453 | * Returns ['base', '.ext'] | |
|
454 | */ | |
|
415 | 455 | var idx = filename.lastIndexOf('.'); |
|
416 | 456 | if (idx > 0) { |
|
417 | 457 | return [filename.slice(0, idx), filename.slice(idx)]; |
@@ -422,20 +462,26 b' define([' | |||
|
422 | 462 | |
|
423 | 463 | |
|
424 | 464 | var escape_html = function (text) { |
|
425 | // escape text to HTML | |
|
465 | /** | |
|
466 | * escape text to HTML | |
|
467 | */ | |
|
426 | 468 | return $("<div/>").text(text).html(); |
|
427 | 469 | }; |
|
428 | 470 | |
|
429 | 471 | |
|
430 | 472 | var get_body_data = function(key) { |
|
431 | // get a url-encoded item from body.data and decode it | |
|
432 | // we should never have any encoded URLs anywhere else in code | |
|
433 | // until we are building an actual request | |
|
473 | /** | |
|
474 | * get a url-encoded item from body.data and decode it | |
|
475 | * we should never have any encoded URLs anywhere else in code | |
|
476 | * until we are building an actual request | |
|
477 | */ | |
|
434 | 478 | return decodeURIComponent($('body').data(key)); |
|
435 | 479 | }; |
|
436 | 480 | |
|
437 | 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 | 485 | if (!cursor) { |
|
440 | 486 | cursor = cm.getCursor(); |
|
441 | 487 | } |
@@ -447,7 +493,9 b' define([' | |||
|
447 | 493 | }; |
|
448 | 494 | |
|
449 | 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 | 499 | var i, line; |
|
452 | 500 | var offset = 0; |
|
453 | 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 | 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 | 549 | return a.has(b).length !==0 || a.is(b); |
|
500 | 550 | }; |
|
501 | 551 | |
|
502 | 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 | 556 | e = $(e); |
|
505 | 557 | var target = $(document.activeElement); |
|
506 | 558 | if (target.length > 0) { |
@@ -521,21 +573,198 b' define([' | |||
|
521 | 573 | }; |
|
522 | 574 | |
|
523 | 575 | var ajax_error_msg = function (jqXHR) { |
|
524 | // Return a JSON error message if there is one, | |
|
525 | // otherwise the basic HTTP status text. | |
|
526 | if (jqXHR.responseJSON && jqXHR.responseJSON.message) { | |
|
576 | /** | |
|
577 | * Return a JSON error message if there is one, | |
|
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 | 583 | return jqXHR.responseJSON.message; |
|
528 | 584 | } else { |
|
529 | 585 | return jqXHR.statusText; |
|
530 | 586 | } |
|
531 | } | |
|
587 | }; | |
|
532 | 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 | 592 | var msg = "API request failed (" + jqXHR.status + "): "; |
|
535 | 593 | console.log(jqXHR); |
|
536 | 594 | msg += ajax_error_msg(jqXHR); |
|
537 | 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 | 769 | var utils = { |
|
541 | 770 | regex_split : regex_split, |
@@ -546,6 +775,7 b' define([' | |||
|
546 | 775 | points_to_pixels : points_to_pixels, |
|
547 | 776 | get_body_data : get_body_data, |
|
548 | 777 | parse_url : parse_url, |
|
778 | url_path_split : url_path_split, | |
|
549 | 779 | url_path_join : url_path_join, |
|
550 | 780 | url_join_encode : url_join_encode, |
|
551 | 781 | encode_uri_components : encode_uri_components, |
@@ -561,6 +791,14 b' define([' | |||
|
561 | 791 | mergeopt: mergeopt, |
|
562 | 792 | ajax_error_msg : ajax_error_msg, |
|
563 | 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 | 804 | // Backwards compatability. |
@@ -8,6 +8,7 b'' | |||
|
8 | 8 | @breadcrumb-color: darken(@border_color, 30%); |
|
9 | 9 | @blockquote-font-size: inherit; |
|
10 | 10 | @modal-inner-padding: 15px; |
|
11 | @grid-float-breakpoint: 540px; | |
|
11 | 12 | |
|
12 | 13 | // Disable modal slide-in from top animation. |
|
13 | 14 | .modal { |
@@ -1,1 +1,1 b'' | |||
|
1 | Subproject commit b3909af1b61ca7a412481759fdb441ecdfb3ab66 | |
|
1 | Subproject commit 87ff70d96567bf055eb94161a41e7b3e6da31b23 |
@@ -1,44 +1,39 b'' | |||
|
1 | 1 | // Copyright (c) IPython Development Team. |
|
2 | 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 | 13 | define([ |
|
5 | 14 | 'base/js/namespace', |
|
6 | 15 | 'jquery', |
|
7 | 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 | 22 | // TODO: remove IPython dependency here |
|
10 | 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 | 25 | var Cell = function (options) { |
|
32 |
/ |
|
|
33 |
|
|
|
34 |
|
|
|
35 | // | |
|
36 |
|
|
|
37 |
|
|
|
38 |
|
|
|
39 |
|
|
|
40 |
|
|
|
41 |
|
|
|
26 | /* Constructor | |
|
27 | * | |
|
28 | * The Base `Cell` class from which to inherit. | |
|
29 | * @constructor | |
|
30 | * @param: | |
|
31 | * options: dictionary | |
|
32 | * Dictionary of keyword arguments. | |
|
33 | * events: $(Events) instance | |
|
34 | * config: dictionary | |
|
35 | * keyboard_manager: KeyboardManager instance | |
|
36 | */ | |
|
42 | 37 | options = options || {}; |
|
43 | 38 | this.keyboard_manager = options.keyboard_manager; |
|
44 | 39 | this.events = options.events; |
@@ -50,7 +45,20 b' define([' | |||
|
50 | 45 | this.selected = false; |
|
51 | 46 | this.rendered = false; |
|
52 | 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 | 62 | // load this from metadata later ? |
|
55 | 63 | this.user_highlight = 'auto'; |
|
56 | 64 | this.cm_config = config.cm_config; |
@@ -104,8 +112,10 b' define([' | |||
|
104 | 112 | }; |
|
105 | 113 | |
|
106 | 114 | Cell.prototype.init_classes = function () { |
|
107 | // Call after this.element exists to initialize the css classes | |
|
108 | // related to selected, rendered and mode. | |
|
115 | /** | |
|
116 | * Call after this.element exists to initialize the css classes | |
|
117 | * related to selected, rendered and mode. | |
|
118 | */ | |
|
109 | 119 | if (this.selected) { |
|
110 | 120 | this.element.addClass('selected'); |
|
111 | 121 | } else { |
@@ -157,6 +167,16 b' define([' | |||
|
157 | 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 | 194 | Cell.prototype.handle_codemirror_keyevent = function (editor, event) { |
|
175 | 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 | 208 | // if this is an edit_shortcuts shortcut, the global keyboard/shortcut |
|
178 | 209 | // manager will handle it |
|
179 |
if (shortcuts.handles(event)) { |
|
|
210 | if (shortcuts.handles(event)) { | |
|
211 | return true; | |
|
212 | } | |
|
180 | 213 | |
|
181 | 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 | 270 | * handle cell level logic when a cell is rendered |
|
230 | 271 | * @method render |
|
231 | 272 | * @return is the action being taken |
@@ -267,9 +308,6 b' define([' | |||
|
267 | 308 | * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise |
|
268 | 309 | */ |
|
269 | 310 | Cell.prototype.handle_keyevent = function (editor, event) { |
|
270 | ||
|
271 | // console.log('CM', this.mode, event.which, event.type) | |
|
272 | ||
|
273 | 311 | if (this.mode === 'command') { |
|
274 | 312 | return true; |
|
275 | 313 | } else if (this.mode === 'edit') { |
@@ -360,7 +398,9 b' define([' | |||
|
360 | 398 | * @method refresh |
|
361 | 399 | */ |
|
362 | 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 | 426 | Cell.prototype.toJSON = function () { |
|
387 | 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 | 430 | data.cell_type = this.cell_type; |
|
390 | 431 | return data; |
|
391 | 432 | }; |
|
392 | 433 | |
|
393 | ||
|
394 | 434 | /** |
|
395 | 435 | * should be overritten by subclass |
|
396 | 436 | * @method fromJSON |
@@ -399,27 +439,39 b' define([' | |||
|
399 | 439 | if (data.metadata !== undefined) { |
|
400 | 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 | 447 | * @method is_splittable |
|
409 | 448 | **/ |
|
410 | 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 | 456 | * @method is_mergeable |
|
418 | 457 | **/ |
|
419 | 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 | 477 | * @return {String} - the text before the cursor |
@@ -484,7 +536,10 b' define([' | |||
|
484 | 536 | * @param {String|object|undefined} - CodeMirror mode | 'auto' |
|
485 | 537 | **/ |
|
486 | 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 | 543 | var mode; |
|
489 | 544 | if( this.user_highlight !== undefined && this.user_highlight != 'auto' ) |
|
490 | 545 | { |
@@ -506,33 +561,34 b' define([' | |||
|
506 | 561 | return; |
|
507 | 562 | } |
|
508 | 563 | if (mode.search('magic_') !== 0) { |
|
509 |
t |
|
|
510 |
|
|
|
564 | utils.requireCodeMirrorMode(mode, function () { | |
|
565 | that.code_mirror.setOption('mode', mode); | |
|
566 | }); | |
|
511 | 567 | return; |
|
512 | 568 | } |
|
513 | 569 | var open = modes[mode].open || "%%"; |
|
514 | 570 | var close = modes[mode].close || "%%end"; |
|
515 | var mmode = mode; | |
|
516 | mode = mmode.substr(6); | |
|
517 | if(current_mode == mode){ | |
|
571 | var magic_mode = mode; | |
|
572 | mode = magic_mode.substr(6); | |
|
573 | if(current_mode == magic_mode){ | |
|
518 | 574 | return; |
|
519 | 575 | } |
|
520 | CodeMirror.autoLoadMode(this.code_mirror, mode); | |
|
521 |
// create on the fly a mode that sw |
|
|
522 | // plain/text and smth else otherwise `%%` is | |
|
523 | // source of some highlight issues. | |
|
524 | // we use patchedGetMode to circumvent a bug in CM | |
|
525 | CodeMirror.defineMode(mmode , function(config) { | |
|
526 |
|
|
|
527 | CodeMirror.patchedGetMode(config, 'text/plain'), | |
|
528 |
|
|
|
529 | {open: open, close: close, | |
|
530 | mode: CodeMirror.patchedGetMode(config, mode), | |
|
531 |
|
|
|
532 |
|
|
|
533 | ); | |
|
576 | utils.requireCodeMirrorMode(mode, function () { | |
|
577 | // create on the fly a mode that switch between | |
|
578 | // plain/text and something else, otherwise `%%` is | |
|
579 | // source of some highlight issues. | |
|
580 | CodeMirror.defineMode(magic_mode, function(config) { | |
|
581 | return CodeMirror.multiplexingMode( | |
|
582 | CodeMirror.getMode(config, 'text/plain'), | |
|
583 | // always set something on close | |
|
584 | {open: open, close: close, | |
|
585 | mode: CodeMirror.getMode(config, mode), | |
|
586 | delimStyle: "delimit" | |
|
587 | } | |
|
588 | ); | |
|
589 | }); | |
|
590 | that.code_mirror.setOption('mode', magic_mode); | |
|
534 | 591 | }); |
|
535 | this.code_mirror.setOption('mode', mmode); | |
|
536 | 592 | return; |
|
537 | 593 | } |
|
538 | 594 | } |
@@ -550,8 +606,76 b' define([' | |||
|
550 | 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 | 674 | // Backwards compatibility. |
|
554 | 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 | 9 | "use strict"; |
|
10 | 10 | |
|
11 | 11 | var CellToolbar = function (options) { |
|
12 | // Constructor | |
|
13 | // | |
|
14 | // Parameters: | |
|
15 | // options: dictionary | |
|
16 | // Dictionary of keyword arguments. | |
|
17 | // events: $(Events) instance | |
|
18 |
|
|
|
19 |
|
|
|
20 | // | |
|
21 | // TODO: This leaks, when cell are deleted | |
|
22 | // There is still a reference to each celltoolbars. | |
|
12 | /** | |
|
13 | * Constructor | |
|
14 | * | |
|
15 | * Parameters: | |
|
16 | * options: dictionary | |
|
17 | * Dictionary of keyword arguments. | |
|
18 | * events: $(Events) instance | |
|
19 | * cell: Cell instance | |
|
20 | * notebook: Notebook instance | |
|
21 | * | |
|
22 | * TODO: This leaks, when cell are deleted | |
|
23 | * There is still a reference to each celltoolbars. | |
|
24 | */ | |
|
23 | 25 | CellToolbar._instances.push(this); |
|
24 | 26 | this.notebook = options.notebook; |
|
25 | 27 | this.cell = options.cell; |
@@ -114,7 +116,7 b' define([' | |||
|
114 | 116 | * @param name {String} name to use to refer to the callback. It is advised to use a prefix with the name |
|
115 | 117 | * for easier sorting and avoid collision |
|
116 | 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 | 120 | * will be added only to cells of types in the list. |
|
119 | 121 | * |
|
120 | 122 | * |
@@ -163,7 +165,7 b' define([' | |||
|
163 | 165 | * @method register_preset |
|
164 | 166 | * @param name {String} name to use to refer to the preset. It is advised to use a prefix with the name |
|
165 | 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 | 169 | * should correspond to a name of a registerd callback. |
|
168 | 170 | * |
|
169 | 171 | * @private |
@@ -248,9 +250,11 b' define([' | |||
|
248 | 250 | * @method rebuild |
|
249 | 251 | */ |
|
250 | 252 | CellToolbar.prototype.rebuild = function(){ |
|
251 | // strip evrything from the div | |
|
252 | // which is probably inner_element | |
|
253 | // or this.element. | |
|
253 | /** | |
|
254 | * strip evrything from the div | |
|
255 | * which is probably inner_element | |
|
256 | * or this.element. | |
|
257 | */ | |
|
254 | 258 | this.inner_element.empty(); |
|
255 | 259 | this.ui_controls_list = []; |
|
256 | 260 | |
@@ -288,8 +292,6 b' define([' | |||
|
288 | 292 | }; |
|
289 | 293 | |
|
290 | 294 | |
|
291 | /** | |
|
292 | */ | |
|
293 | 295 | CellToolbar.utils = {}; |
|
294 | 296 | |
|
295 | 297 | |
@@ -385,7 +387,7 b' define([' | |||
|
385 | 387 | * @method utils.select_ui_generator |
|
386 | 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 | 391 | * subslit shoud contain 2 element each, first a string that woul be displayed in the dropdown list, |
|
390 | 392 | * and second the corresponding value to be passed to setter/return by getter. the corresponding value |
|
391 | 393 | * should not be "undefined" or behavior can be unexpected. |
@@ -119,7 +119,9 b' define([' | |||
|
119 | 119 | width: 650, |
|
120 | 120 | modal: true, |
|
121 | 121 | close: function() { |
|
122 |
/ |
|
|
122 | /** | |
|
123 | *cleanup on close | |
|
124 | */ | |
|
123 | 125 | $(this).remove(); |
|
124 | 126 | } |
|
125 | 127 | }); |
@@ -1,5 +1,13 b'' | |||
|
1 | 1 | // Copyright (c) IPython Development Team. |
|
2 | 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 | 12 | define([ |
|
5 | 13 | 'base/js/namespace', |
@@ -10,8 +18,12 b' define([' | |||
|
10 | 18 | 'notebook/js/outputarea', |
|
11 | 19 | 'notebook/js/completer', |
|
12 | 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 | 25 | "use strict"; |
|
26 | ||
|
15 | 27 | var Cell = cell.Cell; |
|
16 | 28 | |
|
17 | 29 | /* local util for codemirror */ |
@@ -41,21 +53,23 b' define([' | |||
|
41 | 53 | var keycodes = keyboard.keycodes; |
|
42 | 54 | |
|
43 | 55 | var CodeCell = function (kernel, options) { |
|
44 | // Constructor | |
|
45 | // | |
|
46 | // A Cell conceived to write code. | |
|
47 | // | |
|
48 | // Parameters: | |
|
49 | // kernel: Kernel instance | |
|
50 | // The kernel doesn't have to be set at creation time, in that case | |
|
51 | // it will be null and set_kernel has to be called later. | |
|
52 | // options: dictionary | |
|
53 | // Dictionary of keyword arguments. | |
|
54 | // events: $(Events) instance | |
|
55 | // config: dictionary | |
|
56 | // keyboard_manager: KeyboardManager instance | |
|
57 |
|
|
|
58 |
|
|
|
56 | /** | |
|
57 | * Constructor | |
|
58 | * | |
|
59 | * A Cell conceived to write code. | |
|
60 | * | |
|
61 | * Parameters: | |
|
62 | * kernel: Kernel instance | |
|
63 | * The kernel doesn't have to be set at creation time, in that case | |
|
64 | * it will be null and set_kernel has to be called later. | |
|
65 | * options: dictionary | |
|
66 | * Dictionary of keyword arguments. | |
|
67 | * events: $(Events) instance | |
|
68 | * config: dictionary | |
|
69 | * keyboard_manager: KeyboardManager instance | |
|
70 | * notebook: Notebook instance | |
|
71 | * tooltip: Tooltip instance | |
|
72 | */ | |
|
59 | 73 | this.kernel = kernel || null; |
|
60 | 74 | this.notebook = options.notebook; |
|
61 | 75 | this.collapsed = false; |
@@ -68,15 +82,28 b' define([' | |||
|
68 | 82 | this.input_prompt_number = null; |
|
69 | 83 | this.celltoolbar = null; |
|
70 | 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 | 102 | this.last_msg_id = null; |
|
72 | 103 | this.completer = null; |
|
73 | 104 | |
|
74 | 105 | |
|
75 | var cm_overwrite_options = { | |
|
76 | onKeyEvent: $.proxy(this.handle_keyevent,this) | |
|
77 | }; | |
|
78 | ||
|
79 | var config = utils.mergeopt(CodeCell, this.config, {cm_config: cm_overwrite_options}); | |
|
106 | var config = utils.mergeopt(CodeCell, this.config); | |
|
80 | 107 | Cell.apply(this,[{ |
|
81 | 108 | config: config, |
|
82 | 109 | keyboard_manager: options.keyboard_manager, |
@@ -84,8 +111,6 b' define([' | |||
|
84 | 111 | |
|
85 | 112 | // Attributes we want to override in this subclass. |
|
86 | 113 | this.cell_type = "code"; |
|
87 | ||
|
88 | var that = this; | |
|
89 | 114 | this.element.focusout( |
|
90 | 115 | function() { that.auto_highlight(); } |
|
91 | 116 | ); |
@@ -102,15 +127,30 b' define([' | |||
|
102 | 127 | }, |
|
103 | 128 | mode: 'ipython', |
|
104 | 129 | theme: 'ipython', |
|
105 |
matchBrackets: true |
|
|
106 | // don't auto-close strings because of CodeMirror #2385 | |
|
107 | autoCloseBrackets: "()[]{}" | |
|
130 | matchBrackets: true | |
|
108 | 131 | } |
|
109 | 132 | }; |
|
110 | 133 | |
|
111 | 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 | 156 | * @method auto_highlight |
@@ -135,6 +175,7 b' define([' | |||
|
135 | 175 | inner_cell.append(this.celltoolbar.element); |
|
136 | 176 | var input_area = $('<div/>').addClass('input_area'); |
|
137 | 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 | 179 | $(this.code_mirror.getInputField()).attr("spellcheck", "false"); |
|
139 | 180 | inner_cell.append(input_area); |
|
140 | 181 | input.append(prompt).append(inner_cell); |
@@ -187,6 +228,7 b' define([' | |||
|
187 | 228 | * true = ignore, false = don't ignore. |
|
188 | 229 | * @method handle_codemirror_keyevent |
|
189 | 230 | */ |
|
231 | ||
|
190 | 232 | CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) { |
|
191 | 233 | |
|
192 | 234 | var that = this; |
@@ -220,10 +262,11 b' define([' | |||
|
220 | 262 | } |
|
221 | 263 | // If we closed the tooltip, don't let CM or the global handlers |
|
222 | 264 | // handle this event. |
|
223 | event.stop(); | |
|
265 | event.codemirrorIgnore = true; | |
|
266 | event.preventDefault(); | |
|
224 | 267 | return true; |
|
225 | 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 | 270 | var anchor = editor.getCursor("anchor"); |
|
228 | 271 | var head = editor.getCursor("head"); |
|
229 | 272 | if( anchor.line != head.line){ |
@@ -231,12 +274,15 b' define([' | |||
|
231 | 274 | } |
|
232 | 275 | } |
|
233 | 276 | this.tooltip.request(that); |
|
234 |
event. |
|
|
277 | event.codemirrorIgnore = true; | |
|
278 | event.preventDefault(); | |
|
235 | 279 | return true; |
|
236 | 280 | } else if (event.keyCode === keycodes.tab && event.type == 'keydown') { |
|
237 | 281 | // Tab completion. |
|
238 | 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 | 286 | return false; |
|
241 | 287 | } |
|
242 | 288 | var pre_cursor = editor.getRange({line:cur.line,ch:0},cur); |
@@ -245,7 +291,8 b' define([' | |||
|
245 | 291 | // is empty. In this case, let CodeMirror handle indentation. |
|
246 | 292 | return false; |
|
247 | 293 | } else { |
|
248 |
event. |
|
|
294 | event.codemirrorIgnore = true; | |
|
295 | event.preventDefault(); | |
|
249 | 296 | this.completer.startCompletion(); |
|
250 | 297 | return true; |
|
251 | 298 | } |
@@ -267,7 +314,12 b' define([' | |||
|
267 | 314 | * @method execute |
|
268 | 315 | */ |
|
269 | 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 | 324 | // Clear widget area |
|
273 | 325 | this.widget_subarea.html(''); |
@@ -288,6 +340,8 b' define([' | |||
|
288 | 340 | delete CodeCell.msg_cells[old_msg_id]; |
|
289 | 341 | } |
|
290 | 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 | 349 | * @method get_callbacks |
|
296 | 350 | */ |
|
297 | 351 | CodeCell.prototype.get_callbacks = function () { |
|
352 | var that = this; | |
|
298 | 353 | return { |
|
299 | 354 | shell : { |
|
300 | 355 | reply : $.proxy(this._handle_execute_reply, this), |
@@ -304,8 +359,12 b' define([' | |||
|
304 | 359 | } |
|
305 | 360 | }, |
|
306 | 361 | iopub : { |
|
307 | output : $.proxy(this.output_area.handle_output, this.output_area), | |
|
308 |
|
|
|
362 | output : function() { | |
|
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 | 369 | input : $.proxy(this._handle_input_request, this) |
|
311 | 370 | }; |
@@ -339,7 +398,7 b' define([' | |||
|
339 | 398 | * @private |
|
340 | 399 | */ |
|
341 | 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 | 419 | return cont; |
|
361 | 420 | }; |
|
362 | 421 | |
|
363 | CodeCell.prototype.unrender = function () { | |
|
364 | // CodeCell is always rendered | |
|
365 | return false; | |
|
366 | }; | |
|
367 | ||
|
368 | 422 | CodeCell.prototype.select_all = function () { |
|
369 | 423 | var start = {line: 0, ch: 0}; |
|
370 | 424 | var nlines = this.code_mirror.lineCount(); |
@@ -375,13 +429,11 b' define([' | |||
|
375 | 429 | |
|
376 | 430 | |
|
377 | 431 | CodeCell.prototype.collapse_output = function () { |
|
378 | this.collapsed = true; | |
|
379 | 432 | this.output_area.collapse(); |
|
380 | 433 | }; |
|
381 | 434 | |
|
382 | 435 | |
|
383 | 436 | CodeCell.prototype.expand_output = function () { |
|
384 | this.collapsed = false; | |
|
385 | 437 | this.output_area.expand(); |
|
386 | 438 | this.output_area.unscroll_area(); |
|
387 | 439 | }; |
@@ -392,7 +444,6 b' define([' | |||
|
392 | 444 | }; |
|
393 | 445 | |
|
394 | 446 | CodeCell.prototype.toggle_output = function () { |
|
395 | this.collapsed = Boolean(1 - this.collapsed); | |
|
396 | 447 | this.output_area.toggle_output(); |
|
397 | 448 | }; |
|
398 | 449 | |
@@ -403,7 +454,7 b' define([' | |||
|
403 | 454 | |
|
404 | 455 | CodeCell.input_prompt_classical = function (prompt_value, lines_number) { |
|
405 | 456 | var ns; |
|
406 | if (prompt_value === undefined) { | |
|
457 | if (prompt_value === undefined || prompt_value === null) { | |
|
407 | 458 | ns = " "; |
|
408 | 459 | } else { |
|
409 | 460 | ns = encodeURIComponent(prompt_value); |
@@ -450,7 +501,7 b' define([' | |||
|
450 | 501 | |
|
451 | 502 | |
|
452 | 503 | CodeCell.prototype.clear_output = function (wait) { |
|
453 | this.output_area.clear_output(wait); | |
|
504 | this.active_output_area.clear_output(wait); | |
|
454 | 505 | this.set_input_prompt(); |
|
455 | 506 | }; |
|
456 | 507 | |
@@ -460,22 +511,18 b' define([' | |||
|
460 | 511 | CodeCell.prototype.fromJSON = function (data) { |
|
461 | 512 | Cell.prototype.fromJSON.apply(this, arguments); |
|
462 | 513 | if (data.cell_type === 'code') { |
|
463 |
if (data. |
|
|
464 |
this.set_text(data. |
|
|
514 | if (data.source !== undefined) { | |
|
515 | this.set_text(data.source); | |
|
465 | 516 | // make this value the starting point, so that we can only undo |
|
466 | 517 | // to this state, instead of a blank cell |
|
467 | 518 | this.code_mirror.clearHistory(); |
|
468 | 519 | this.auto_highlight(); |
|
469 | 520 | } |
|
470 | if (data.prompt_number !== undefined) { | |
|
471 | this.set_input_prompt(data.prompt_number); | |
|
472 | } else { | |
|
473 | this.set_input_prompt(); | |
|
474 | } | |
|
475 | this.output_area.trusted = data.trusted || false; | |
|
521 | this.set_input_prompt(data.execution_count); | |
|
522 | this.output_area.trusted = data.metadata.trusted || false; | |
|
476 | 523 | this.output_area.fromJSON(data.outputs); |
|
477 | if (data.collapsed !== undefined) { | |
|
478 | if (data.collapsed) { | |
|
524 | if (data.metadata.collapsed !== undefined) { | |
|
525 | if (data.metadata.collapsed) { | |
|
479 | 526 | this.collapse_output(); |
|
480 | 527 | } else { |
|
481 | 528 | this.expand_output(); |
@@ -487,16 +534,17 b' define([' | |||
|
487 | 534 | |
|
488 | 535 | CodeCell.prototype.toJSON = function () { |
|
489 | 536 | var data = Cell.prototype.toJSON.apply(this); |
|
490 |
data. |
|
|
537 | data.source = this.get_text(); | |
|
491 | 538 | // is finite protect against undefined and '*' value |
|
492 | 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 | 544 | var outputs = this.output_area.toJSON(); |
|
496 | 545 | data.outputs = outputs; |
|
497 | data.language = 'python'; | |
|
498 |
data. |
|
|
499 | data.collapsed = this.collapsed; | |
|
546 | data.metadata.trusted = this.output_area.trusted; | |
|
547 | data.metadata.collapsed = this.output_area.collapsed; | |
|
500 | 548 | return data; |
|
501 | 549 | }; |
|
502 | 550 |
@@ -3,7 +3,18 b'' | |||
|
3 | 3 | // callback to auto-load python mode, which is more likely not the best things |
|
4 | 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 | 18 | "use strict"; |
|
8 | 19 | |
|
9 | 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 |
|
|
2 |
// Mode with support for latex. |
|
|
1 | // IPython GFM (GitHub Flavored Markdown) mode is just a slightly altered GFM | |
|
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 | 5 | // https://github.com/codemirror/CodeMirror/pull/567 |
|
6 | 6 | // But was later removed in |
|
7 | 7 | // https://github.com/codemirror/CodeMirror/commit/d9c9f1b1ffe984aee41307f3e927f80d1f23590c |
|
8 | 8 | |
|
9 | CodeMirror.requireMode('gfm', function(){ | |
|
10 | CodeMirror.requireMode('stex', function(){ | |
|
11 | CodeMirror.defineMode("ipythongfm", function(config, parserConfig) { | |
|
12 | ||
|
13 | var gfm_mode = CodeMirror.getMode(config, "gfm"); | |
|
14 | var tex_mode = CodeMirror.getMode(config, "stex"); | |
|
15 | ||
|
16 | return CodeMirror.multiplexingMode( | |
|
17 | gfm_mode, | |
|
18 | { | |
|
19 | open: "$", close: "$", | |
|
20 | mode: tex_mode, | |
|
21 | delimStyle: "delimit" | |
|
22 |
|
|
|
23 | { | |
|
24 | open: "$$", close: "$$", | |
|
25 | mode: tex_mode, | |
|
26 | delimStyle: "delimit" | |
|
27 | }, | |
|
28 | { | |
|
29 | open: "\\(", close: "\\)", | |
|
30 | mode: tex_mode, | |
|
31 | delimStyle: "delimit" | |
|
32 | }, | |
|
33 | { | |
|
34 | open: "\\[", close: "\\]", | |
|
35 |
|
|
|
36 | delimStyle: "delimit" | |
|
37 | } | |
|
38 | // .. more multiplexed styles can follow here | |
|
39 | ); | |
|
40 |
}, |
|
|
41 | ||
|
42 | CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm"); | |
|
43 | }); | |
|
44 | }); | |
|
9 | ||
|
10 | (function(mod) { | |
|
11 | if (typeof exports == "object" && typeof module == "object"){ // CommonJS | |
|
12 | mod(require("codemirror/lib/codemirror") | |
|
13 | ,require("codemirror/addon/mode/multiplex") | |
|
14 | ,require("codemirror/mode/gfm/gfm") | |
|
15 | ,require("codemirror/mode/stex/stex") | |
|
16 | ); | |
|
17 | } else if (typeof define == "function" && define.amd){ // AMD | |
|
18 | define(["codemirror/lib/codemirror" | |
|
19 | ,"codemirror/addon/mode/multiplex" | |
|
20 | ,"codemirror/mode/python/python" | |
|
21 | ,"codemirror/mode/stex/stex" | |
|
22 | ], mod); | |
|
23 | } else {// Plain browser env | |
|
24 | mod(CodeMirror); | |
|
25 | } | |
|
26 | })( function(CodeMirror){ | |
|
27 | "use strict"; | |
|
28 | ||
|
29 | CodeMirror.defineMode("ipythongfm", function(config, parserConfig) { | |
|
30 | ||
|
31 | var gfm_mode = CodeMirror.getMode(config, "gfm"); | |
|
32 | var tex_mode = CodeMirror.getMode(config, "stex"); | |
|
33 | ||
|
34 | return CodeMirror.multiplexingMode( | |
|
35 | gfm_mode, | |
|
36 | { | |
|
37 | open: "$", close: "$", | |
|
38 | mode: tex_mode, | |
|
39 | delimStyle: "delimit" | |
|
40 | }, | |
|
41 | { | |
|
42 | // not sure this works as $$ is interpreted at (opening $, closing $, as defined just above) | |
|
43 | open: "$$", close: "$$", | |
|
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 | 7 | 'base/js/utils', |
|
8 | 8 | 'base/js/keyboard', |
|
9 | 9 | 'notebook/js/contexthint', |
|
10 | ], function(IPython, $, utils, keyboard) { | |
|
10 | 'codemirror/lib/codemirror', | |
|
11 | ], function(IPython, $, utils, keyboard, CodeMirror) { | |
|
11 | 12 | "use strict"; |
|
12 | 13 | |
|
13 | 14 | // easier key mapping |
@@ -82,18 +83,20 b' define([' | |||
|
82 | 83 | this.cell = cell; |
|
83 | 84 | this.editor = cell.code_mirror; |
|
84 | 85 | var that = this; |
|
85 |
events.on(' |
|
|
86 | events.on('kernel_busy.Kernel', function () { | |
|
86 | 87 | that.skip_kernel_completion = true; |
|
87 | 88 | }); |
|
88 |
events.on(' |
|
|
89 | events.on('kernel_idle.Kernel', function () { | |
|
89 | 90 | that.skip_kernel_completion = false; |
|
90 | 91 | }); |
|
91 | 92 | }; |
|
92 | 93 | |
|
93 | 94 | Completer.prototype.startCompletion = function () { |
|
94 | // call for a 'first' completion, that will set the editor and do some | |
|
95 | // special behavior like autopicking if only one completion available. | |
|
96 | if (this.editor.somethingSelected()) return; | |
|
95 | /** | |
|
96 | * call for a 'first' completion, that will set the editor and do some | |
|
97 | * special behavior like autopicking if only one completion available. | |
|
98 | */ | |
|
99 | if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) return; | |
|
97 | 100 | this.done = false; |
|
98 | 101 | // use to get focus back on opera |
|
99 | 102 | this.carry_on_completion(true); |
@@ -118,9 +121,11 b' define([' | |||
|
118 | 121 | * shared start |
|
119 | 122 | **/ |
|
120 | 123 | Completer.prototype.carry_on_completion = function (first_invocation) { |
|
121 | // Pass true as parameter if you want the completer to autopick when | |
|
122 | // only one completion. This function is automatically reinvoked at | |
|
123 | // each keystroke with first_invocation = false | |
|
124 | /** | |
|
125 | * Pass true as parameter if you want the completer to autopick when | |
|
126 | * only one completion. This function is automatically reinvoked at | |
|
127 | * each keystroke with first_invocation = false | |
|
128 | */ | |
|
124 | 129 | var cur = this.editor.getCursor(); |
|
125 | 130 | var line = this.editor.getLine(cur.line); |
|
126 | 131 | var pre_cursor = this.editor.getRange({ |
@@ -142,7 +147,7 b' define([' | |||
|
142 | 147 | } |
|
143 | 148 | |
|
144 | 149 | // We want a single cursor position. |
|
145 | if (this.editor.somethingSelected()) { | |
|
150 | if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) { | |
|
146 | 151 | return; |
|
147 | 152 | } |
|
148 | 153 | |
@@ -163,8 +168,10 b' define([' | |||
|
163 | 168 | }; |
|
164 | 169 | |
|
165 | 170 | Completer.prototype.finish_completing = function (msg) { |
|
166 | // let's build a function that wrap all that stuff into what is needed | |
|
167 | // for the new completer: | |
|
171 | /** | |
|
172 | * let's build a function that wrap all that stuff into what is needed | |
|
173 | * for the new completer: | |
|
174 | */ | |
|
168 | 175 | var content = msg.content; |
|
169 | 176 | var start = content.cursor_start; |
|
170 | 177 | var end = content.cursor_end; |
@@ -316,11 +323,15 b' define([' | |||
|
316 | 323 | |
|
317 | 324 | // Enter |
|
318 | 325 | if (code == keycodes.enter) { |
|
319 |
|
|
|
326 | event.codemirrorIgnore = true; | |
|
327 | event._ipkmIgnore = true; | |
|
328 | event.preventDefault(); | |
|
320 | 329 | this.pick(); |
|
321 | 330 | // Escape or backspace |
|
322 | 331 | } else if (code == keycodes.esc || code == keycodes.backspace) { |
|
323 |
|
|
|
332 | event.codemirrorIgnore = true; | |
|
333 | event._ipkmIgnore = true; | |
|
334 | event.preventDefault(); | |
|
324 | 335 | this.close(); |
|
325 | 336 | } else if (code == keycodes.tab) { |
|
326 | 337 | //all the fastforwarding operation, |
@@ -339,7 +350,9 b' define([' | |||
|
339 | 350 | } else if (code == keycodes.up || code == keycodes.down) { |
|
340 | 351 | // need to do that to be able to move the arrow |
|
341 | 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 | 357 | var options = this.sel.find('option'); |
|
345 | 358 | var index = this.sel[0].selectedIndex; |
@@ -352,7 +365,7 b' define([' | |||
|
352 | 365 | index = Math.min(Math.max(index, 0), options.length-1); |
|
353 | 366 | this.sel[0].selectedIndex = index; |
|
354 | 367 | } else if (code == keycodes.pageup || code == keycodes.pagedown) { |
|
355 | CodeMirror.e_stop(event); | |
|
368 | event._ipkmIgnore = true; | |
|
356 | 369 | |
|
357 | 370 | var options = this.sel.find('option'); |
|
358 | 371 | var index = this.sel[0].selectedIndex; |
@@ -369,11 +382,13 b' define([' | |||
|
369 | 382 | }; |
|
370 | 383 | |
|
371 | 384 | Completer.prototype.keypress = function (event) { |
|
372 | // FIXME: This is a band-aid. | |
|
373 | // on keypress, trigger insertion of a single character. | |
|
374 | // This simulates the old behavior of completion as you type, | |
|
375 | // before events were disconnected and CodeMirror stopped | |
|
376 | // receiving events while the completer is focused. | |
|
385 | /** | |
|
386 | * FIXME: This is a band-aid. | |
|
387 | * on keypress, trigger insertion of a single character. | |
|
388 | * This simulates the old behavior of completion as you type, | |
|
389 | * before events were disconnected and CodeMirror stopped | |
|
390 | * receiving events while the completer is focused. | |
|
391 | */ | |
|
377 | 392 | |
|
378 | 393 | var that = this; |
|
379 | 394 | var code = event.keyCode; |
@@ -1,6 +1,15 b'' | |||
|
1 | 1 | // Copyright (c) IPython Development Team. |
|
2 | 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 | 13 | define([], function() { |
|
5 | 14 | "use strict"; |
|
6 | 15 |
@@ -2,7 +2,7 b'' | |||
|
2 | 2 | // Distributed under the terms of the Modified BSD License. |
|
3 | 3 | |
|
4 | 4 | // highly adapted for codemiror jshint |
|
5 | define([], function() { | |
|
5 | define(['codemirror/lib/codemirror'], function(CodeMirror) { | |
|
6 | 6 | "use strict"; |
|
7 | 7 | |
|
8 | 8 | var forEach = function(arr, f) { |
@@ -12,7 +12,7 b' define([' | |||
|
12 | 12 | this.selector = selector; |
|
13 | 13 | this.notebook = notebook; |
|
14 | 14 | this.events = notebook.events; |
|
15 |
this.current_selection = n |
|
|
15 | this.current_selection = null; | |
|
16 | 16 | this.kernelspecs = {}; |
|
17 | 17 | if (this.selector !== undefined) { |
|
18 | 18 | this.element = $(selector); |
@@ -76,12 +76,12 b' define([' | |||
|
76 | 76 | that.element.find("#current_kernel_spec").find('.kernel_name').text(data.display_name); |
|
77 | 77 | }); |
|
78 | 78 | |
|
79 |
this.events.on(' |
|
|
80 |
if ( |
|
|
79 | this.events.on('kernel_created.Session', function(event, data) { | |
|
80 | if (data.kernel.name !== that.current_selection) { | |
|
81 | 81 | // If we created a 'python' session, we only know if it's Python |
|
82 | 82 | // 3 or 2 on the server's reply, so we fire the event again to |
|
83 | 83 | // set things up. |
|
84 |
var ks = that.kernelspecs[ |
|
|
84 | var ks = that.kernelspecs[data.kernel.name]; | |
|
85 | 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 | 1 | // Copyright (c) IPython Development Team. |
|
2 | 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 | 11 | define([ |
|
5 | 12 | 'base/js/namespace', |
@@ -9,491 +16,138 b' define([' | |||
|
9 | 16 | ], function(IPython, $, utils, keyboard) { |
|
10 | 17 | "use strict"; |
|
11 | 18 | |
|
12 | var browser = utils.browser[0]; | |
|
13 | var platform = utils.platform; | |
|
14 | ||
|
15 | 19 | // Main keyboard manager for the notebook |
|
16 | 20 | var keycodes = keyboard.keycodes; |
|
17 | 21 | |
|
18 | 22 | var KeyboardManager = function (options) { |
|
19 | // Constructor | |
|
20 | // | |
|
21 | // Parameters: | |
|
22 | // options: dictionary | |
|
23 | // Dictionary of keyword arguments. | |
|
24 | // events: $(Events) instance | |
|
25 | // pager: Pager instance | |
|
23 | /** | |
|
24 | * A class to deal with keyboard event and shortcut | |
|
25 | * | |
|
26 | * @class KeyboardManager | |
|
27 | * @constructor | |
|
28 | * @param options {dict} Dictionary of keyword arguments : | |
|
29 | * @param options.events {$(Events)} instance | |
|
30 | * @param options.pager: {Pager} pager instance | |
|
31 | */ | |
|
26 | 32 | this.mode = 'command'; |
|
27 | 33 | this.enabled = true; |
|
28 | 34 | this.pager = options.pager; |
|
29 | 35 | this.quick_help = undefined; |
|
30 | 36 | this.notebook = undefined; |
|
37 | this.last_mode = undefined; | |
|
31 | 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 | 42 | this.command_shortcuts.add_shortcuts(this.get_default_common_shortcuts()); |
|
34 | 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 | 45 | this.edit_shortcuts.add_shortcuts(this.get_default_common_shortcuts()); |
|
37 | 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 | 64 | KeyboardManager.prototype.get_default_common_shortcuts = function() { |
|
41 | var that = this; | |
|
42 | var shortcuts = { | |
|
43 | 'shift' : { | |
|
44 | help : '', | |
|
45 | help_index : '', | |
|
46 | handler : function (event) { | |
|
47 | // ignore shift keydown | |
|
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 | } | |
|
65 | return { | |
|
66 | 'shift' : 'ipython.ignore', | |
|
67 | 'shift-enter' : 'ipython.run-select-next', | |
|
68 | 'ctrl-enter' : 'ipython.execute-in-place', | |
|
69 | 'alt-enter' : 'ipython.execute-and-insert-after', | |
|
70 | // cmd on mac, ctrl otherwise | |
|
71 | 'cmdtrl-s' : 'ipython.save-notebook', | |
|
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 | 75 | KeyboardManager.prototype.get_default_edit_shortcuts = function() { |
|
104 | var that = this; | |
|
105 | 76 | return { |
|
106 | 'esc' : { | |
|
107 | help : 'command mode', | |
|
108 | help_index : 'aa', | |
|
109 | handler : function (event) { | |
|
110 | that.notebook.command_mode(); | |
|
111 | return false; | |
|
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 | }, | |
|
77 | 'esc' : 'ipython.go-to-command-mode', | |
|
78 | 'ctrl-m' : 'ipython.go-to-command-mode', | |
|
79 | 'up' : 'ipython.move-cursor-up-or-previous-cell', | |
|
80 | 'down' : 'ipython.move-cursor-down-or-next-cell', | |
|
81 | 'ctrl-shift--' : 'ipython.split-cell-at-cursor', | |
|
82 | 'ctrl-shift-subtract' : 'ipython.split-cell-at-cursor' | |
|
180 | 83 | }; |
|
181 | 84 | }; |
|
182 | 85 | |
|
183 | 86 | KeyboardManager.prototype.get_default_command_shortcuts = function() { |
|
184 | var that = this; | |
|
185 | 87 | return { |
|
186 |
'space': |
|
|
187 | help: "Scroll down", | |
|
188 | handler: function(event) { | |
|
189 | return that.notebook.scroll_manager.scroll(1); | |
|
190 | }, | |
|
191 | }, | |
|
192 | 'shift-space': { | |
|
193 | help: "Scroll up", | |
|
194 | handler: function(event) { | |
|
195 | return that.notebook.scroll_manager.scroll(-1); | |
|
196 | }, | |
|
197 | }, | |
|
198 | 'enter' : { | |
|
199 | help : 'edit mode', | |
|
200 | help_index : 'aa', | |
|
201 | handler : function (event) { | |
|
202 | that.notebook.edit_mode(); | |
|
203 | return false; | |
|
204 | } | |
|
205 | }, | |
|
206 | 'up' : { | |
|
207 | help : 'select previous cell', | |
|
208 | help_index : 'da', | |
|
209 | handler : function (event) { | |
|
210 | var index = that.notebook.get_selected_index(); | |
|
211 | if (index !== 0 && index !== null) { | |
|
212 | that.notebook.select_prev(); | |
|
213 | that.notebook.focus_cell(); | |
|
214 | } | |
|
215 | return false; | |
|
216 | } | |
|
217 | }, | |
|
218 | 'down' : { | |
|
219 | help : 'select next cell', | |
|
220 | help_index : 'db', | |
|
221 | handler : function (event) { | |
|
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 | }, | |
|
88 | 'shift-space': 'ipython.scroll-up', | |
|
89 | 'shift-v' : 'ipython.paste-cell-before', | |
|
90 | 'shift-m' : 'ipython.merge-selected-cell-with-cell-after', | |
|
91 | 'shift-o' : 'ipython.toggle-output-scrolling-selected-cell', | |
|
92 | 'ctrl-j' : 'ipython.move-selected-cell-down', | |
|
93 | 'ctrl-k' : 'ipython.move-selected-cell-up', | |
|
94 | 'enter' : 'ipython.enter-edit-mode', | |
|
95 | 'space' : 'ipython.scroll-down', | |
|
96 | 'down' : 'ipython.select-next-cell', | |
|
97 | 'i,i' : 'ipython.interrupt-kernel', | |
|
98 | '0,0' : 'ipython.restart-kernel', | |
|
99 | 'd,d' : 'ipython.delete-cell', | |
|
100 | 'esc': 'ipython.close-pager', | |
|
101 | 'up' : 'ipython.select-previous-cell', | |
|
102 | 'k' : 'ipython.select-previous-cell', | |
|
103 | 'j' : 'ipython.select-next-cell', | |
|
104 | 'x' : 'ipython.cut-selected-cell', | |
|
105 | 'c' : 'ipython.copy-selected-cell', | |
|
106 | 'v' : 'ipython.paste-cell-after', | |
|
107 | 'a' : 'ipython.insert-cell-before', | |
|
108 | 'b' : 'ipython.insert-cell-after', | |
|
109 | 'y' : 'ipython.change-selected-cell-to-code-cell', | |
|
110 | 'm' : 'ipython.change-selected-cell-to-markdown-cell', | |
|
111 | 'r' : 'ipython.change-selected-cell-to-raw-cell', | |
|
112 | '1' : 'ipython.change-selected-cell-to-heading-1', | |
|
113 | '2' : 'ipython.change-selected-cell-to-heading-2', | |
|
114 | '3' : 'ipython.change-selected-cell-to-heading-3', | |
|
115 | '4' : 'ipython.change-selected-cell-to-heading-4', | |
|
116 | '5' : 'ipython.change-selected-cell-to-heading-5', | |
|
117 | '6' : 'ipython.change-selected-cell-to-heading-6', | |
|
118 | 'o' : 'ipython.toggle-output-visibility-selected-cell', | |
|
119 | 's' : 'ipython.save-notebook', | |
|
120 | 'l' : 'ipython.toggle-line-number-selected-cell', | |
|
121 | 'h' : 'ipython.show-keyboard-shortcut-help-dialog', | |
|
122 | 'z' : 'ipython.undo-last-cell-deletion', | |
|
123 | 'q' : 'ipython.close-pager', | |
|
485 | 124 | }; |
|
486 | 125 | }; |
|
487 | 126 | |
|
488 | 127 | KeyboardManager.prototype.bind_events = function () { |
|
489 | 128 | var that = this; |
|
490 | 129 | $(document).keydown(function (event) { |
|
130 | if(event._ipkmIgnore===true||(event.originalEvent||{})._ipkmIgnore===true){ | |
|
131 | return false; | |
|
132 | } | |
|
491 | 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 | 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 | 152 | if (event.which === keycodes.esc) { |
|
499 | 153 | // Intercept escape at highest level to avoid closing |
@@ -503,8 +157,7 b' define([' | |||
|
503 | 157 | |
|
504 | 158 | if (!this.enabled) { |
|
505 | 159 | if (event.which === keycodes.esc) { |
|
506 | // ESC | |
|
507 | notebook.command_mode(); | |
|
160 | this.notebook.command_mode(); | |
|
508 | 161 | return false; |
|
509 | 162 | } |
|
510 | 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 | 229 | IPython.KeyboardManager = KeyboardManager; |
|
576 | 230 | |
|
577 | 231 | return {'KeyboardManager': KeyboardManager}; |
@@ -5,6 +5,8 b' require([' | |||
|
5 | 5 | 'base/js/namespace', |
|
6 | 6 | 'jquery', |
|
7 | 7 | 'notebook/js/notebook', |
|
8 | 'contents', | |
|
9 | 'services/config', | |
|
8 | 10 | 'base/js/utils', |
|
9 | 11 | 'base/js/page', |
|
10 | 12 | 'notebook/js/layoutmanager', |
@@ -16,15 +18,20 b' require([' | |||
|
16 | 18 | 'notebook/js/menubar', |
|
17 | 19 | 'notebook/js/notificationarea', |
|
18 | 20 | 'notebook/js/savewidget', |
|
21 | 'notebook/js/actions', | |
|
19 | 22 | 'notebook/js/keyboardmanager', |
|
20 | 23 | 'notebook/js/config', |
|
21 | 24 | 'notebook/js/kernelselector', |
|
22 | // only loaded, not used: | |
|
23 | 'custom/custom', | |
|
25 | 'codemirror/lib/codemirror', | |
|
26 | 'notebook/js/about', | |
|
27 | // only loaded, not used, please keep sure this is loaded last | |
|
28 | 'custom/custom' | |
|
24 | 29 | ], function( |
|
25 | 30 | IPython, |
|
26 | 31 | $, |
|
27 | 32 | notebook, |
|
33 | contents, | |
|
34 | configmod, | |
|
28 | 35 | utils, |
|
29 | 36 | page, |
|
30 | 37 | layoutmanager, |
@@ -35,16 +42,24 b' require([' | |||
|
35 | 42 | quickhelp, |
|
36 | 43 | menubar, |
|
37 | 44 | notificationarea, |
|
38 |
savewidget, |
|
|
45 | savewidget, | |
|
46 | actions, | |
|
39 | 47 | keyboardmanager, |
|
40 | 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 | 55 | "use strict"; |
|
44 | 56 | |
|
57 | // compat with old IPython, remove for IPython > 3.0 | |
|
58 | window.CodeMirror = CodeMirror; | |
|
59 | ||
|
45 | 60 | var common_options = { |
|
61 | ws_url : utils.get_body_data("wsUrl"), | |
|
46 | 62 | base_url : utils.get_body_data("baseUrl"), |
|
47 | ws_url : IPython.utils.get_body_data("wsUrl"), | |
|
48 | 63 | notebook_path : utils.get_body_data("notebookPath"), |
|
49 | 64 | notebook_name : utils.get_body_data('notebookName') |
|
50 | 65 | }; |
@@ -55,34 +70,46 b' require([' | |||
|
55 | 70 | var pager = new pager.Pager('div#pager', 'div#pager_splitter', { |
|
56 | 71 | layout_manager: layout_manager, |
|
57 | 72 | events: events}); |
|
73 | var acts = new actions.init(); | |
|
58 | 74 | var keyboard_manager = new keyboardmanager.KeyboardManager({ |
|
59 | 75 | pager: pager, |
|
60 |
events: events |
|
|
76 | events: events, | |
|
77 | actions: acts }); | |
|
61 | 78 | var save_widget = new savewidget.SaveWidget('span#save_widget', { |
|
62 | 79 | events: events, |
|
63 | 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 | 86 | var notebook = new notebook.Notebook('div#notebook', $.extend({ |
|
65 | 87 | events: events, |
|
66 | 88 | keyboard_manager: keyboard_manager, |
|
67 | 89 | save_widget: save_widget, |
|
90 | contents: contents, | |
|
68 | 91 | config: user_config}, |
|
69 | 92 | common_options)); |
|
70 | 93 | var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options); |
|
71 | 94 | var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', { |
|
72 | 95 | notebook: notebook, |
|
73 |
events: events |
|
|
96 | events: events, | |
|
97 | actions: acts}); | |
|
74 | 98 | var quick_help = new quickhelp.QuickHelp({ |
|
75 | 99 | keyboard_manager: keyboard_manager, |
|
76 | 100 | events: events, |
|
77 | 101 | notebook: notebook}); |
|
102 | keyboard_manager.set_notebook(notebook); | |
|
103 | keyboard_manager.set_quickhelp(quick_help); | |
|
78 | 104 | var menubar = new menubar.MenuBar('#menubar', $.extend({ |
|
79 | 105 | notebook: notebook, |
|
106 | contents: contents, | |
|
80 | 107 | layout_manager: layout_manager, |
|
81 | 108 | events: events, |
|
82 | 109 | save_widget: save_widget, |
|
83 | 110 | quick_help: quick_help}, |
|
84 | 111 | common_options)); |
|
85 | var notification_area = new notificationarea.NotificationArea( | |
|
112 | var notification_area = new notificationarea.NotebookNotificationArea( | |
|
86 | 113 | '#notification_area', { |
|
87 | 114 | events: events, |
|
88 | 115 | save_widget: save_widget, |
@@ -122,6 +149,7 b' require([' | |||
|
122 | 149 | IPython.page = page; |
|
123 | 150 | IPython.layout_manager = layout_manager; |
|
124 | 151 | IPython.notebook = notebook; |
|
152 | IPython.contents = contents; | |
|
125 | 153 | IPython.pager = pager; |
|
126 | 154 | IPython.quick_help = quick_help; |
|
127 | 155 | IPython.login_widget = login_widget; |
@@ -134,6 +162,13 b' require([' | |||
|
134 | 162 | IPython.tooltip = notebook.tooltip; |
|
135 | 163 | |
|
136 | 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 | 10 | "use strict"; |
|
11 | 11 | |
|
12 | 12 | var MainToolBar = function (selector, options) { |
|
13 | // Constructor | |
|
14 | // | |
|
15 | // Parameters: | |
|
16 | // selector: string | |
|
17 | // options: dictionary | |
|
18 | // Dictionary of keyword arguments. | |
|
19 | // events: $(Events) instance | |
|
20 |
|
|
|
13 | /** | |
|
14 | * Constructor | |
|
15 | * | |
|
16 | * Parameters: | |
|
17 | * selector: string | |
|
18 | * options: dictionary | |
|
19 | * Dictionary of keyword arguments. | |
|
20 | * events: $(Events) instance | |
|
21 | * notebook: Notebook instance | |
|
22 | */ | |
|
21 | 23 | toolbar.ToolBar.apply(this, arguments); |
|
22 | 24 | this.events = options.events; |
|
23 | 25 | this.notebook = options.notebook; |
@@ -27,7 +29,7 b' define([' | |||
|
27 | 29 | this.bind_events(); |
|
28 | 30 | }; |
|
29 | 31 | |
|
30 |
MainToolBar.prototype = |
|
|
32 | MainToolBar.prototype = Object.create(toolbar.ToolBar.prototype); | |
|
31 | 33 | |
|
32 | 34 | MainToolBar.prototype.construct = function () { |
|
33 | 35 | var that = this; |
@@ -108,7 +110,9 b' define([' | |||
|
108 | 110 | label : 'Run Cell', |
|
109 | 111 | icon : 'fa-play', |
|
110 | 112 | callback : function () { |
|
111 | // emulate default shift-enter behavior | |
|
113 | /** | |
|
114 | * emulate default shift-enter behavior | |
|
115 | */ | |
|
112 | 116 | that.notebook.execute_cell_and_select_below(); |
|
113 | 117 | } |
|
114 | 118 | }, |
@@ -117,7 +121,7 b' define([' | |||
|
117 | 121 | label : 'Interrupt', |
|
118 | 122 | icon : 'fa-stop', |
|
119 | 123 | callback : function () { |
|
120 |
that.notebook. |
|
|
124 | that.notebook.kernel.interrupt(); | |
|
121 | 125 | } |
|
122 | 126 | }, |
|
123 | 127 | { |
@@ -139,12 +143,7 b' define([' | |||
|
139 | 143 | .append($('<option/>').attr('value','code').text('Code')) |
|
140 | 144 | .append($('<option/>').attr('value','markdown').text('Markdown')) |
|
141 | 145 | .append($('<option/>').attr('value','raw').text('Raw NBConvert')) |
|
142 |
.append($('<option/>').attr('value','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')) | |
|
146 | .append($('<option/>').attr('value','heading').text('Heading')) | |
|
148 | 147 | ); |
|
149 | 148 | }; |
|
150 | 149 | |
@@ -190,24 +189,23 b' define([' | |||
|
190 | 189 | |
|
191 | 190 | this.element.find('#cell_type').change(function () { |
|
192 | 191 | var cell_type = $(this).val(); |
|
193 |
|
|
|
192 | switch (cell_type) { | |
|
193 | case 'code': | |
|
194 | 194 | that.notebook.to_code(); |
|
195 | } else if (cell_type === 'markdown') { | |
|
195 | break; | |
|
196 | case 'markdown': | |
|
196 | 197 | that.notebook.to_markdown(); |
|
197 | } else if (cell_type === 'raw') { | |
|
198 | break; | |
|
199 | case 'raw': | |
|
198 | 200 | that.notebook.to_raw(); |
|
199 | } else if (cell_type === 'heading1') { | |
|
200 | that.notebook.to_heading(undefined, 1); | |
|
201 | } else if (cell_type === 'heading2') { | |
|
202 |
that.notebook.to_heading( |
|
|
203 | } else if (cell_type === 'heading3') { | |
|
204 | that.notebook.to_heading(undefined, 3); | |
|
205 | } else if (cell_type === 'heading4') { | |
|
206 | that.notebook.to_heading(undefined, 4); | |
|
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); | |
|
201 | break; | |
|
202 | case 'heading': | |
|
203 | that.notebook._warn_heading(); | |
|
204 | that.notebook.to_heading(); | |
|
205 | that.element.find('#cell_type').val("markdown"); | |
|
206 | break; | |
|
207 | default: | |
|
208 | console.log("unrecognized cell type:", cell_type); | |
|
211 | 209 | } |
|
212 | 210 | }); |
|
213 | 211 | this.events.on('selected_cell_type_changed.Notebook', function (event, data) { |
@@ -2,36 +2,41 b'' | |||
|
2 | 2 | // Distributed under the terms of the Modified BSD License. |
|
3 | 3 | |
|
4 | 4 | define([ |
|
5 | 'base/js/namespace', | |
|
6 | 5 | 'jquery', |
|
6 | 'base/js/namespace', | |
|
7 | 'base/js/dialog', | |
|
7 | 8 | 'base/js/utils', |
|
8 | 9 | 'notebook/js/tour', |
|
9 | 10 | 'bootstrap', |
|
10 | 11 | 'moment', |
|
11 |
], function(IPython, |
|
|
12 | ], function($, IPython, dialog, utils, tour, bootstrap, moment) { | |
|
12 | 13 | "use strict"; |
|
13 | 14 | |
|
14 | 15 | var MenuBar = function (selector, options) { |
|
15 | // Constructor | |
|
16 | // | |
|
17 | // A MenuBar Class to generate the menubar of IPython notebook | |
|
18 | // | |
|
19 | // Parameters: | |
|
20 | // selector: string | |
|
21 | // options: dictionary | |
|
22 | // Dictionary of keyword arguments. | |
|
23 | // notebook: Notebook instance | |
|
24 |
|
|
|
25 |
|
|
|
26 |
|
|
|
27 |
|
|
|
28 | // base_url : string | |
|
29 | // notebook_path : string | |
|
30 |
|
|
|
16 | /** | |
|
17 | * Constructor | |
|
18 | * | |
|
19 | * A MenuBar Class to generate the menubar of IPython notebook | |
|
20 | * | |
|
21 | * Parameters: | |
|
22 | * selector: string | |
|
23 | * options: dictionary | |
|
24 | * Dictionary of keyword arguments. | |
|
25 | * notebook: Notebook instance | |
|
26 | * contents: ContentManager instance | |
|
27 | * layout_manager: LayoutManager instance | |
|
28 | * events: $(Events) instance | |
|
29 | * save_widget: SaveWidget instance | |
|
30 | * quick_help: QuickHelp instance | |
|
31 | * base_url : string | |
|
32 | * notebook_path : string | |
|
33 | * notebook_name : string | |
|
34 | */ | |
|
31 | 35 | options = options || {}; |
|
32 | 36 | this.base_url = options.base_url || utils.get_body_data("baseUrl"); |
|
33 | 37 | this.selector = selector; |
|
34 | 38 | this.notebook = options.notebook; |
|
39 | this.contents = options.contents; | |
|
35 | 40 | this.layout_manager = options.layout_manager; |
|
36 | 41 | this.events = options.events; |
|
37 | 42 | this.save_widget = options.save_widget; |
@@ -66,33 +71,52 b' define([' | |||
|
66 | 71 | MenuBar.prototype._nbconvert = function (format, download) { |
|
67 | 72 | download = download || false; |
|
68 | 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 | 74 | var url = utils.url_join_encode( |
|
74 | 75 | this.base_url, |
|
75 | 76 | 'nbconvert', |
|
76 | 77 | format, |
|
77 |
notebook_path |
|
|
78 | notebook_name | |
|
78 | notebook_path | |
|
79 | 79 | ) + "?download=" + download.toString(); |
|
80 | ||
|
81 |
window.open( |
|
|
80 | ||
|
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 | 91 | MenuBar.prototype.bind_events = function () { |
|
85 |
/ |
|
|
92 | /** | |
|
93 | * File | |
|
94 | */ | |
|
86 | 95 | var that = this; |
|
87 | 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 | 117 | this.element.find('#open_notebook').click(function () { |
|
91 | window.open(utils.url_join_encode( | |
|
92 | that.notebook.base_url, | |
|
93 | 'tree', | |
|
94 | that.notebook.notebook_path | |
|
95 | )); | |
|
118 | var parent = utils.url_path_split(that.notebook.notebook_path)[0]; | |
|
119 | window.open(utils.url_join_encode(that.base_url, 'tree', parent)); | |
|
96 | 120 | }); |
|
97 | 121 | this.element.find('#copy_notebook').click(function () { |
|
98 | 122 | that.notebook.copy_notebook(); |
@@ -101,28 +125,18 b' define([' | |||
|
101 | 125 | this.element.find('#download_ipynb').click(function () { |
|
102 | 126 | var base_url = that.notebook.base_url; |
|
103 | 127 | var notebook_path = that.notebook.notebook_path; |
|
104 | var notebook_name = that.notebook.notebook_name; | |
|
105 | 128 | if (that.notebook.dirty) { |
|
106 | 129 | that.notebook.save_notebook({async : false}); |
|
107 | 130 | } |
|
108 | 131 | |
|
109 | var url = utils.url_join_encode( | |
|
110 | base_url, | |
|
111 | 'files', | |
|
112 | notebook_path, | |
|
113 | notebook_name | |
|
114 | ); | |
|
115 | window.location.assign(url); | |
|
132 | var url = utils.url_join_encode(base_url, 'files', notebook_path); | |
|
133 | window.open(url + '?download=1'); | |
|
116 | 134 | }); |
|
117 | 135 | |
|
118 | 136 | this.element.find('#print_preview').click(function () { |
|
119 | 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 | 140 | this.element.find('#download_html').click(function () { |
|
127 | 141 | that._nbconvert('html', true); |
|
128 | 142 | }); |
@@ -159,7 +173,9 b' define([' | |||
|
159 | 173 | }); |
|
160 | 174 | this.element.find('#kill_and_exit').click(function () { |
|
161 | 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 | 179 | window.open('', '_self', ''); |
|
164 | 180 | window.close(); |
|
165 | 181 | }; |
@@ -246,24 +262,6 b' define([' | |||
|
246 | 262 | this.element.find('#to_raw').click(function () { |
|
247 | 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 | 266 | this.element.find('#toggle_current_output').click(function () { |
|
269 | 267 | that.notebook.toggle_output(); |
@@ -287,11 +285,14 b' define([' | |||
|
287 | 285 | |
|
288 | 286 | // Kernel |
|
289 | 287 | this.element.find('#int_kernel').click(function () { |
|
290 |
that.notebook. |
|
|
288 | that.notebook.kernel.interrupt(); | |
|
291 | 289 | }); |
|
292 | 290 | this.element.find('#restart_kernel').click(function () { |
|
293 | 291 | that.notebook.restart_kernel(); |
|
294 | 292 | }); |
|
293 | this.element.find('#reconnect_kernel').click(function () { | |
|
294 | that.notebook.kernel.reconnect(); | |
|
295 | }); | |
|
295 | 296 | // Help |
|
296 | 297 | if (this.tour) { |
|
297 | 298 | this.element.find('#notebook_tour').click(function () { |
@@ -313,6 +314,16 b' define([' | |||
|
313 | 314 | this.events.on('checkpoint_created.Notebook', function (event, data) { |
|
314 | 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 | 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 | 387 | // Backwards compatability. |
|
350 | 388 | IPython.MenuBar = MenuBar; |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 |
|
1 | NO CONTENT: file was removed, binary diff hidden |
|
1 | NO CONTENT: file was removed |
|
1 | NO CONTENT: file was removed |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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 | |
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