##// END OF EJS Templates
Merge pull request #6866 from takluyver/nb-texteditor...
Min RK -
r19078:231dfe88 merge
parent child Browse files
Show More
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
@@ -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,74 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 // Find and load the highlighting mode
40 var modeinfo = CodeMirror.findModeByMIME(model.mimetype);
41 if (modeinfo) {
42 utils.requireCodeMirrorMode(modeinfo.mode, function() {
43 cm.setOption('mode', modeinfo.mode);
44 });
45 }
46 that.save_enabled = true;
47 },
48 function(error) {
49 cm.setValue("Error! " + error.message +
50 "\nSaving disabled.");
51 that.save_enabled = false;
52 }
53 );
54 };
55
56 Editor.prototype.save = function() {
57 if (!this.save_enabled) {
58 console.log("Not saving, save disabled");
59 return;
60 }
61 var model = {
62 path: this.file_path,
63 type: 'file',
64 format: 'text',
65 content: this.codemirror.getValue(),
66 };
67 var that = this;
68 this.contents.save(this.file_path, model).then(function() {
69 that.events.trigger("save_succeeded.TextEditor");
70 });
71 };
72
73 return {Editor: Editor};
74 });
@@ -0,0 +1,53 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 'edit/js/editor',
11 'edit/js/menubar',
12 'edit/js/notificationarea',
13 'custom/custom',
14 ], function(
15 IPython,
16 utils,
17 page,
18 events,
19 contents,
20 editor,
21 menubar,
22 notificationarea
23 ){
24 page = new page.Page();
25
26 var base_url = utils.get_body_data('baseUrl');
27 var file_path = utils.get_body_data('filePath');
28 contents = new contents.Contents({base_url: base_url});
29
30 var editor = new editor.Editor('#texteditor-container', {
31 base_url: base_url,
32 events: events,
33 contents: contents,
34 file_path: file_path,
35 });
36
37 // Make it available for debugging
38 IPython.editor = editor;
39
40 var menus = new menubar.MenuBar('#menubar', {
41 base_url: base_url,
42 editor: editor,
43 });
44
45 var notification_area = new notificationarea.EditorNotificationArea(
46 '#notification_area', {
47 events: events,
48 });
49 notification_area.init_notification_widgets();
50
51 editor.load();
52 page.show();
53 });
@@ -0,0 +1,46 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 // Constructor
14 //
15 // A MenuBar Class to generate the menubar of IPython notebook
16 //
17 // Parameters:
18 // selector: string
19 // options: dictionary
20 // Dictionary of keyword arguments.
21 // codemirror: CodeMirror instance
22 // contents: ContentManager instance
23 // events: $(Events) instance
24 // base_url : string
25 // file_path : string
26 options = options || {};
27 this.base_url = options.base_url || utils.get_body_data("baseUrl");
28 this.selector = selector;
29 this.editor = options.editor;
30
31 if (this.selector !== undefined) {
32 this.element = $(selector);
33 this.bind_events();
34 }
35 };
36
37 MenuBar.prototype.bind_events = function () {
38 // File
39 var that = this;
40 this.element.find('#save_file').click(function () {
41 that.editor.save();
42 });
43 };
44
45 return {'MenuBar': MenuBar};
46 });
@@ -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,72 b''
1 {% extends "page.html" %}
2
3 {% block title %}{{page_title}}{% endblock %}
4
5 {% block stylesheet %}
6 <link rel="stylesheet" href="{{ static_url('components/codemirror/lib/codemirror.css') }}">
7 <link rel="stylesheet" href="{{ static_url('components/codemirror/addon/dialog/dialog.css') }}">
8 <style>
9 #texteditor-container {
10 border-bottom: 1px solid #ccc;
11 }
12
13 #filename {
14 font-size: 16pt;
15 display: table;
16 padding: 0px 5px;
17 }
18 </style>
19
20 {{super()}}
21 {% endblock %}
22
23 {% block params %}
24
25 data-base-url="{{base_url}}"
26 data-file-path="{{file_path}}"
27
28 {% endblock %}
29
30 {% block header %}
31
32 <span id="filename">{{ basename }}</span>
33
34 {% endblock %}
35
36 {% block site %}
37
38 <div id="menubar-container" class="container">
39 <div id="menubar">
40 <div id="menus" class="navbar navbar-default" role="navigation">
41 <div class="container-fluid">
42 <button type="button" class="btn btn-default navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
43 <i class="fa fa-bars"></i>
44 <span class="navbar-text">Menu</span>
45 </button>
46 <ul class="nav navbar-nav navbar-right">
47 <li id="notification_area"></li>
48 </ul>
49 <div class="navbar-collapse collapse">
50 <ul class="nav navbar-nav">
51 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
52 <ul id="file_menu" class="dropdown-menu">
53 <li id="save_file"><a href="#">Save</a></li>
54 </ul>
55 </li>
56 </ul>
57 </div>
58 </div>
59 </div>
60 </div>
61 </div>
62
63 <div id="texteditor-container" class="container"></div>
64
65 {% endblock %}
66
67 {% block script %}
68
69 {{super()}}
70
71 <script src="{{ static_url("edit/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
72 {% endblock %}
@@ -1,1008 +1,1009 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import errno
11 11 import io
12 12 import json
13 13 import logging
14 14 import os
15 15 import random
16 16 import re
17 17 import select
18 18 import signal
19 19 import socket
20 20 import sys
21 21 import threading
22 22 import time
23 23 import webbrowser
24 24
25 25
26 26 # check for pyzmq 2.1.11
27 27 from IPython.utils.zmqrelated import check_for_zmq
28 28 check_for_zmq('2.1.11', 'IPython.html')
29 29
30 30 from jinja2 import Environment, FileSystemLoader
31 31
32 32 # Install the pyzmq ioloop. This has to be done before anything else from
33 33 # tornado is imported.
34 34 from zmq.eventloop import ioloop
35 35 ioloop.install()
36 36
37 37 # check for tornado 3.1.0
38 38 msg = "The IPython Notebook requires tornado >= 4.0"
39 39 try:
40 40 import tornado
41 41 except ImportError:
42 42 raise ImportError(msg)
43 43 try:
44 44 version_info = tornado.version_info
45 45 except AttributeError:
46 46 raise ImportError(msg + ", but you have < 1.1.0")
47 47 if version_info < (4,0):
48 48 raise ImportError(msg + ", but you have %s" % tornado.version)
49 49
50 50 from tornado import httpserver
51 51 from tornado import web
52 52 from tornado.log import LogFormatter, app_log, access_log, gen_log
53 53
54 54 from IPython.html import (
55 55 DEFAULT_STATIC_FILES_PATH,
56 56 DEFAULT_TEMPLATE_PATH_LIST,
57 57 )
58 58 from .base.handlers import Template404
59 59 from .log import log_request
60 60 from .services.kernels.kernelmanager import MappingKernelManager
61 61 from .services.contents.manager import ContentsManager
62 62 from .services.contents.filemanager import FileContentsManager
63 63 from .services.clusters.clustermanager import ClusterManager
64 64 from .services.sessions.sessionmanager import SessionManager
65 65
66 66 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
67 67
68 68 from IPython.config import Config
69 69 from IPython.config.application import catch_config_error, boolean_flag
70 70 from IPython.core.application import (
71 71 BaseIPythonApplication, base_flags, base_aliases,
72 72 )
73 73 from IPython.core.profiledir import ProfileDir
74 74 from IPython.kernel import KernelManager
75 75 from IPython.kernel.kernelspec import KernelSpecManager
76 76 from IPython.kernel.zmq.session import default_secure, Session
77 77 from IPython.nbformat.sign import NotebookNotary
78 78 from IPython.utils.importstring import import_item
79 79 from IPython.utils import submodule
80 80 from IPython.utils.process import check_pid
81 81 from IPython.utils.traitlets import (
82 82 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
83 83 DottedObjectName, TraitError,
84 84 )
85 85 from IPython.utils import py3compat
86 86 from IPython.utils.path import filefind, get_ipython_dir
87 87
88 88 from .utils import url_path_join
89 89
90 90 #-----------------------------------------------------------------------------
91 91 # Module globals
92 92 #-----------------------------------------------------------------------------
93 93
94 94 _examples = """
95 95 ipython notebook # start the notebook
96 96 ipython notebook --profile=sympy # use the sympy profile
97 97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
98 98 """
99 99
100 100 #-----------------------------------------------------------------------------
101 101 # Helper functions
102 102 #-----------------------------------------------------------------------------
103 103
104 104 def random_ports(port, n):
105 105 """Generate a list of n random ports near the given port.
106 106
107 107 The first 5 ports will be sequential, and the remaining n-5 will be
108 108 randomly selected in the range [port-2*n, port+2*n].
109 109 """
110 110 for i in range(min(5, n)):
111 111 yield port + i
112 112 for i in range(n-5):
113 113 yield max(1, port + random.randint(-2*n, 2*n))
114 114
115 115 def load_handlers(name):
116 116 """Load the (URL pattern, handler) tuples for each component."""
117 117 name = 'IPython.html.' + name
118 118 mod = __import__(name, fromlist=['default_handlers'])
119 119 return mod.default_handlers
120 120
121 121 #-----------------------------------------------------------------------------
122 122 # The Tornado web application
123 123 #-----------------------------------------------------------------------------
124 124
125 125 class NotebookWebApplication(web.Application):
126 126
127 127 def __init__(self, ipython_app, kernel_manager, contents_manager,
128 128 cluster_manager, session_manager, kernel_spec_manager, log,
129 129 base_url, default_url, settings_overrides, jinja_env_options):
130 130
131 131 settings = self.init_settings(
132 132 ipython_app, kernel_manager, contents_manager, cluster_manager,
133 133 session_manager, kernel_spec_manager, log, base_url, default_url,
134 134 settings_overrides, jinja_env_options)
135 135 handlers = self.init_handlers(settings)
136 136
137 137 super(NotebookWebApplication, self).__init__(handlers, **settings)
138 138
139 139 def init_settings(self, ipython_app, kernel_manager, contents_manager,
140 140 cluster_manager, session_manager, kernel_spec_manager,
141 141 log, base_url, default_url, settings_overrides,
142 142 jinja_env_options=None):
143 143
144 144 _template_path = settings_overrides.get(
145 145 "template_path",
146 146 ipython_app.template_file_path,
147 147 )
148 148 if isinstance(_template_path, str):
149 149 _template_path = (_template_path,)
150 150 template_path = [os.path.expanduser(path) for path in _template_path]
151 151
152 152 jenv_opt = jinja_env_options if jinja_env_options else {}
153 153 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
154 154 settings = dict(
155 155 # basics
156 156 log_function=log_request,
157 157 base_url=base_url,
158 158 default_url=default_url,
159 159 template_path=template_path,
160 160 static_path=ipython_app.static_file_path,
161 161 static_handler_class = FileFindHandler,
162 162 static_url_prefix = url_path_join(base_url,'/static/'),
163 163
164 164 # authentication
165 165 cookie_secret=ipython_app.cookie_secret,
166 166 login_url=url_path_join(base_url,'/login'),
167 167 password=ipython_app.password,
168 168
169 169 # managers
170 170 kernel_manager=kernel_manager,
171 171 contents_manager=contents_manager,
172 172 cluster_manager=cluster_manager,
173 173 session_manager=session_manager,
174 174 kernel_spec_manager=kernel_spec_manager,
175 175
176 176 # IPython stuff
177 177 nbextensions_path = ipython_app.nbextensions_path,
178 178 websocket_url=ipython_app.websocket_url,
179 179 mathjax_url=ipython_app.mathjax_url,
180 180 config=ipython_app.config,
181 181 jinja2_env=env,
182 182 terminals_available=False, # Set later if terminals are available
183 183 profile_dir = ipython_app.profile_dir.location,
184 184 )
185 185
186 186 # allow custom overrides for the tornado web app.
187 187 settings.update(settings_overrides)
188 188 return settings
189 189
190 190 def init_handlers(self, settings):
191 191 """Load the (URL pattern, handler) tuples for each component."""
192 192
193 193 # Order matters. The first handler to match the URL will handle the request.
194 194 handlers = []
195 195 handlers.extend(load_handlers('tree.handlers'))
196 196 handlers.extend(load_handlers('auth.login'))
197 197 handlers.extend(load_handlers('auth.logout'))
198 198 handlers.extend(load_handlers('files.handlers'))
199 199 handlers.extend(load_handlers('notebook.handlers'))
200 200 handlers.extend(load_handlers('nbconvert.handlers'))
201 201 handlers.extend(load_handlers('kernelspecs.handlers'))
202 handlers.extend(load_handlers('edit.handlers'))
202 203 handlers.extend(load_handlers('services.config.handlers'))
203 204 handlers.extend(load_handlers('services.kernels.handlers'))
204 205 handlers.extend(load_handlers('services.contents.handlers'))
205 206 handlers.extend(load_handlers('services.clusters.handlers'))
206 207 handlers.extend(load_handlers('services.sessions.handlers'))
207 208 handlers.extend(load_handlers('services.nbconvert.handlers'))
208 209 handlers.extend(load_handlers('services.kernelspecs.handlers'))
209 210 handlers.append(
210 211 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
211 212 )
212 213 # register base handlers last
213 214 handlers.extend(load_handlers('base.handlers'))
214 215 # set the URL that will be redirected from `/`
215 216 handlers.append(
216 217 (r'/?', web.RedirectHandler, {
217 218 'url' : url_path_join(settings['base_url'], settings['default_url']),
218 219 'permanent': False, # want 302, not 301
219 220 })
220 221 )
221 222 # prepend base_url onto the patterns that we match
222 223 new_handlers = []
223 224 for handler in handlers:
224 225 pattern = url_path_join(settings['base_url'], handler[0])
225 226 new_handler = tuple([pattern] + list(handler[1:]))
226 227 new_handlers.append(new_handler)
227 228 # add 404 on the end, which will catch everything that falls through
228 229 new_handlers.append((r'(.*)', Template404))
229 230 return new_handlers
230 231
231 232
232 233 class NbserverListApp(BaseIPythonApplication):
233 234
234 235 description="List currently running notebook servers in this profile."
235 236
236 237 flags = dict(
237 238 json=({'NbserverListApp': {'json': True}},
238 239 "Produce machine-readable JSON output."),
239 240 )
240 241
241 242 json = Bool(False, config=True,
242 243 help="If True, each line of output will be a JSON object with the "
243 244 "details from the server info file.")
244 245
245 246 def start(self):
246 247 if not self.json:
247 248 print("Currently running servers:")
248 249 for serverinfo in list_running_servers(self.profile):
249 250 if self.json:
250 251 print(json.dumps(serverinfo))
251 252 else:
252 253 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
253 254
254 255 #-----------------------------------------------------------------------------
255 256 # Aliases and Flags
256 257 #-----------------------------------------------------------------------------
257 258
258 259 flags = dict(base_flags)
259 260 flags['no-browser']=(
260 261 {'NotebookApp' : {'open_browser' : False}},
261 262 "Don't open the notebook in a browser after startup."
262 263 )
263 264 flags['pylab']=(
264 265 {'NotebookApp' : {'pylab' : 'warn'}},
265 266 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
266 267 )
267 268 flags['no-mathjax']=(
268 269 {'NotebookApp' : {'enable_mathjax' : False}},
269 270 """Disable MathJax
270 271
271 272 MathJax is the javascript library IPython uses to render math/LaTeX. It is
272 273 very large, so you may want to disable it if you have a slow internet
273 274 connection, or for offline use of the notebook.
274 275
275 276 When disabled, equations etc. will appear as their untransformed TeX source.
276 277 """
277 278 )
278 279
279 280 # Add notebook manager flags
280 281 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
281 282 'DEPRECATED, IGNORED',
282 283 'DEPRECATED, IGNORED'))
283 284
284 285 aliases = dict(base_aliases)
285 286
286 287 aliases.update({
287 288 'ip': 'NotebookApp.ip',
288 289 'port': 'NotebookApp.port',
289 290 'port-retries': 'NotebookApp.port_retries',
290 291 'transport': 'KernelManager.transport',
291 292 'keyfile': 'NotebookApp.keyfile',
292 293 'certfile': 'NotebookApp.certfile',
293 294 'notebook-dir': 'NotebookApp.notebook_dir',
294 295 'browser': 'NotebookApp.browser',
295 296 'pylab': 'NotebookApp.pylab',
296 297 })
297 298
298 299 #-----------------------------------------------------------------------------
299 300 # NotebookApp
300 301 #-----------------------------------------------------------------------------
301 302
302 303 class NotebookApp(BaseIPythonApplication):
303 304
304 305 name = 'ipython-notebook'
305 306
306 307 description = """
307 308 The IPython HTML Notebook.
308 309
309 310 This launches a Tornado based HTML Notebook Server that serves up an
310 311 HTML5/Javascript Notebook client.
311 312 """
312 313 examples = _examples
313 314 aliases = aliases
314 315 flags = flags
315 316
316 317 classes = [
317 318 KernelManager, ProfileDir, Session, MappingKernelManager,
318 319 ContentsManager, FileContentsManager, NotebookNotary,
319 320 ]
320 321 flags = Dict(flags)
321 322 aliases = Dict(aliases)
322 323
323 324 subcommands = dict(
324 325 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
325 326 )
326 327
327 328 ipython_kernel_argv = List(Unicode)
328 329
329 330 _log_formatter_cls = LogFormatter
330 331
331 332 def _log_level_default(self):
332 333 return logging.INFO
333 334
334 335 def _log_datefmt_default(self):
335 336 """Exclude date from default date format"""
336 337 return "%H:%M:%S"
337 338
338 339 def _log_format_default(self):
339 340 """override default log format to include time"""
340 341 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
341 342
342 343 # create requested profiles by default, if they don't exist:
343 344 auto_create = Bool(True)
344 345
345 346 # file to be opened in the notebook server
346 347 file_to_run = Unicode('', config=True)
347 348
348 349 # Network related information
349 350
350 351 allow_origin = Unicode('', config=True,
351 352 help="""Set the Access-Control-Allow-Origin header
352 353
353 354 Use '*' to allow any origin to access your server.
354 355
355 356 Takes precedence over allow_origin_pat.
356 357 """
357 358 )
358 359
359 360 allow_origin_pat = Unicode('', config=True,
360 361 help="""Use a regular expression for the Access-Control-Allow-Origin header
361 362
362 363 Requests from an origin matching the expression will get replies with:
363 364
364 365 Access-Control-Allow-Origin: origin
365 366
366 367 where `origin` is the origin of the request.
367 368
368 369 Ignored if allow_origin is set.
369 370 """
370 371 )
371 372
372 373 allow_credentials = Bool(False, config=True,
373 374 help="Set the Access-Control-Allow-Credentials: true header"
374 375 )
375 376
376 377 default_url = Unicode('/tree', config=True,
377 378 help="The default URL to redirect to from `/`"
378 379 )
379 380
380 381 ip = Unicode('localhost', config=True,
381 382 help="The IP address the notebook server will listen on."
382 383 )
383 384
384 385 def _ip_changed(self, name, old, new):
385 386 if new == u'*': self.ip = u''
386 387
387 388 port = Integer(8888, config=True,
388 389 help="The port the notebook server will listen on."
389 390 )
390 391 port_retries = Integer(50, config=True,
391 392 help="The number of additional ports to try if the specified port is not available."
392 393 )
393 394
394 395 certfile = Unicode(u'', config=True,
395 396 help="""The full path to an SSL/TLS certificate file."""
396 397 )
397 398
398 399 keyfile = Unicode(u'', config=True,
399 400 help="""The full path to a private key file for usage with SSL/TLS."""
400 401 )
401 402
402 403 cookie_secret_file = Unicode(config=True,
403 404 help="""The file where the cookie secret is stored."""
404 405 )
405 406 def _cookie_secret_file_default(self):
406 407 if self.profile_dir is None:
407 408 return ''
408 409 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
409 410
410 411 cookie_secret = Bytes(b'', config=True,
411 412 help="""The random bytes used to secure cookies.
412 413 By default this is a new random number every time you start the Notebook.
413 414 Set it to a value in a config file to enable logins to persist across server sessions.
414 415
415 416 Note: Cookie secrets should be kept private, do not share config files with
416 417 cookie_secret stored in plaintext (you can read the value from a file).
417 418 """
418 419 )
419 420 def _cookie_secret_default(self):
420 421 if os.path.exists(self.cookie_secret_file):
421 422 with io.open(self.cookie_secret_file, 'rb') as f:
422 423 return f.read()
423 424 else:
424 425 secret = base64.encodestring(os.urandom(1024))
425 426 self._write_cookie_secret_file(secret)
426 427 return secret
427 428
428 429 def _write_cookie_secret_file(self, secret):
429 430 """write my secret to my secret_file"""
430 431 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
431 432 with io.open(self.cookie_secret_file, 'wb') as f:
432 433 f.write(secret)
433 434 try:
434 435 os.chmod(self.cookie_secret_file, 0o600)
435 436 except OSError:
436 437 self.log.warn(
437 438 "Could not set permissions on %s",
438 439 self.cookie_secret_file
439 440 )
440 441
441 442 password = Unicode(u'', config=True,
442 443 help="""Hashed password to use for web authentication.
443 444
444 445 To generate, type in a python/IPython shell:
445 446
446 447 from IPython.lib import passwd; passwd()
447 448
448 449 The string should be of the form type:salt:hashed-password.
449 450 """
450 451 )
451 452
452 453 open_browser = Bool(True, config=True,
453 454 help="""Whether to open in a browser after starting.
454 455 The specific browser used is platform dependent and
455 456 determined by the python standard library `webbrowser`
456 457 module, unless it is overridden using the --browser
457 458 (NotebookApp.browser) configuration option.
458 459 """)
459 460
460 461 browser = Unicode(u'', config=True,
461 462 help="""Specify what command to use to invoke a web
462 463 browser when opening the notebook. If not specified, the
463 464 default browser will be determined by the `webbrowser`
464 465 standard library module, which allows setting of the
465 466 BROWSER environment variable to override it.
466 467 """)
467 468
468 469 webapp_settings = Dict(config=True,
469 470 help="DEPRECATED, use tornado_settings"
470 471 )
471 472 def _webapp_settings_changed(self, name, old, new):
472 473 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
473 474 self.tornado_settings = new
474 475
475 476 tornado_settings = Dict(config=True,
476 477 help="Supply overrides for the tornado.web.Application that the "
477 478 "IPython notebook uses.")
478 479
479 480 jinja_environment_options = Dict(config=True,
480 481 help="Supply extra arguments that will be passed to Jinja environment.")
481 482
482 483
483 484 enable_mathjax = Bool(True, config=True,
484 485 help="""Whether to enable MathJax for typesetting math/TeX
485 486
486 487 MathJax is the javascript library IPython uses to render math/LaTeX. It is
487 488 very large, so you may want to disable it if you have a slow internet
488 489 connection, or for offline use of the notebook.
489 490
490 491 When disabled, equations etc. will appear as their untransformed TeX source.
491 492 """
492 493 )
493 494 def _enable_mathjax_changed(self, name, old, new):
494 495 """set mathjax url to empty if mathjax is disabled"""
495 496 if not new:
496 497 self.mathjax_url = u''
497 498
498 499 base_url = Unicode('/', config=True,
499 500 help='''The base URL for the notebook server.
500 501
501 502 Leading and trailing slashes can be omitted,
502 503 and will automatically be added.
503 504 ''')
504 505 def _base_url_changed(self, name, old, new):
505 506 if not new.startswith('/'):
506 507 self.base_url = '/'+new
507 508 elif not new.endswith('/'):
508 509 self.base_url = new+'/'
509 510
510 511 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
511 512 def _base_project_url_changed(self, name, old, new):
512 513 self.log.warn("base_project_url is deprecated, use base_url")
513 514 self.base_url = new
514 515
515 516 extra_static_paths = List(Unicode, config=True,
516 517 help="""Extra paths to search for serving static files.
517 518
518 519 This allows adding javascript/css to be available from the notebook server machine,
519 520 or overriding individual files in the IPython"""
520 521 )
521 522 def _extra_static_paths_default(self):
522 523 return [os.path.join(self.profile_dir.location, 'static')]
523 524
524 525 @property
525 526 def static_file_path(self):
526 527 """return extra paths + the default location"""
527 528 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
528 529
529 530 extra_template_paths = List(Unicode, config=True,
530 531 help="""Extra paths to search for serving jinja templates.
531 532
532 533 Can be used to override templates from IPython.html.templates."""
533 534 )
534 535 def _extra_template_paths_default(self):
535 536 return []
536 537
537 538 @property
538 539 def template_file_path(self):
539 540 """return extra paths + the default locations"""
540 541 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
541 542
542 543 nbextensions_path = List(Unicode, config=True,
543 544 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
544 545 )
545 546 def _nbextensions_path_default(self):
546 547 return [os.path.join(get_ipython_dir(), 'nbextensions')]
547 548
548 549 websocket_url = Unicode("", config=True,
549 550 help="""The base URL for websockets,
550 551 if it differs from the HTTP server (hint: it almost certainly doesn't).
551 552
552 553 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
553 554 """
554 555 )
555 556 mathjax_url = Unicode("", config=True,
556 557 help="""The url for MathJax.js."""
557 558 )
558 559 def _mathjax_url_default(self):
559 560 if not self.enable_mathjax:
560 561 return u''
561 562 static_url_prefix = self.tornado_settings.get("static_url_prefix",
562 563 url_path_join(self.base_url, "static")
563 564 )
564 565
565 566 # try local mathjax, either in nbextensions/mathjax or static/mathjax
566 567 for (url_prefix, search_path) in [
567 568 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
568 569 (static_url_prefix, self.static_file_path),
569 570 ]:
570 571 self.log.debug("searching for local mathjax in %s", search_path)
571 572 try:
572 573 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
573 574 except IOError:
574 575 continue
575 576 else:
576 577 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
577 578 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
578 579 return url
579 580
580 581 # no local mathjax, serve from CDN
581 582 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
582 583 self.log.info("Using MathJax from CDN: %s", url)
583 584 return url
584 585
585 586 def _mathjax_url_changed(self, name, old, new):
586 587 if new and not self.enable_mathjax:
587 588 # enable_mathjax=False overrides mathjax_url
588 589 self.mathjax_url = u''
589 590 else:
590 591 self.log.info("Using MathJax: %s", new)
591 592
592 593 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
593 594 config=True,
594 595 help='The notebook manager class to use.'
595 596 )
596 597 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
597 598 config=True,
598 599 help='The kernel manager class to use.'
599 600 )
600 601 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
601 602 config=True,
602 603 help='The session manager class to use.'
603 604 )
604 605 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
605 606 config=True,
606 607 help='The cluster manager class to use.'
607 608 )
608 609
609 610 kernel_spec_manager = Instance(KernelSpecManager)
610 611
611 612 def _kernel_spec_manager_default(self):
612 613 return KernelSpecManager(ipython_dir=self.ipython_dir)
613 614
614 615
615 616 kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager',
616 617 config=True,
617 618 help="""
618 619 The kernel spec manager class to use. Should be a subclass
619 620 of `IPython.kernel.kernelspec.KernelSpecManager`.
620 621
621 622 The Api of KernelSpecManager is provisional and might change
622 623 without warning between this version of IPython and the next stable one.
623 624 """)
624 625
625 626 trust_xheaders = Bool(False, config=True,
626 627 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
627 628 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
628 629 )
629 630
630 631 info_file = Unicode()
631 632
632 633 def _info_file_default(self):
633 634 info_file = "nbserver-%s.json"%os.getpid()
634 635 return os.path.join(self.profile_dir.security_dir, info_file)
635 636
636 637 pylab = Unicode('disabled', config=True,
637 638 help="""
638 639 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
639 640 """
640 641 )
641 642 def _pylab_changed(self, name, old, new):
642 643 """when --pylab is specified, display a warning and exit"""
643 644 if new != 'warn':
644 645 backend = ' %s' % new
645 646 else:
646 647 backend = ''
647 648 self.log.error("Support for specifying --pylab on the command line has been removed.")
648 649 self.log.error(
649 650 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
650 651 )
651 652 self.exit(1)
652 653
653 654 notebook_dir = Unicode(config=True,
654 655 help="The directory to use for notebooks and kernels."
655 656 )
656 657
657 658 def _notebook_dir_default(self):
658 659 if self.file_to_run:
659 660 return os.path.dirname(os.path.abspath(self.file_to_run))
660 661 else:
661 662 return py3compat.getcwd()
662 663
663 664 def _notebook_dir_changed(self, name, old, new):
664 665 """Do a bit of validation of the notebook dir."""
665 666 if not os.path.isabs(new):
666 667 # If we receive a non-absolute path, make it absolute.
667 668 self.notebook_dir = os.path.abspath(new)
668 669 return
669 670 if not os.path.isdir(new):
670 671 raise TraitError("No such notebook dir: %r" % new)
671 672
672 673 # setting App.notebook_dir implies setting notebook and kernel dirs as well
673 674 self.config.FileContentsManager.root_dir = new
674 675 self.config.MappingKernelManager.root_dir = new
675 676
676 677
677 678 def parse_command_line(self, argv=None):
678 679 super(NotebookApp, self).parse_command_line(argv)
679 680
680 681 if self.extra_args:
681 682 arg0 = self.extra_args[0]
682 683 f = os.path.abspath(arg0)
683 684 self.argv.remove(arg0)
684 685 if not os.path.exists(f):
685 686 self.log.critical("No such file or directory: %s", f)
686 687 self.exit(1)
687 688
688 689 # Use config here, to ensure that it takes higher priority than
689 690 # anything that comes from the profile.
690 691 c = Config()
691 692 if os.path.isdir(f):
692 693 c.NotebookApp.notebook_dir = f
693 694 elif os.path.isfile(f):
694 695 c.NotebookApp.file_to_run = f
695 696 self.update_config(c)
696 697
697 698 def init_kernel_argv(self):
698 699 """add the profile-dir to arguments to be passed to IPython kernels"""
699 700 # FIXME: remove special treatment of IPython kernels
700 701 # Kernel should get *absolute* path to profile directory
701 702 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
702 703
703 704 def init_configurables(self):
704 705 # force Session default to be secure
705 706 default_secure(self.config)
706 707 kls = import_item(self.kernel_spec_manager_class)
707 708 self.kernel_spec_manager = kls(ipython_dir=self.ipython_dir)
708 709
709 710 kls = import_item(self.kernel_manager_class)
710 711 self.kernel_manager = kls(
711 712 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
712 713 connection_dir = self.profile_dir.security_dir,
713 714 )
714 715 kls = import_item(self.contents_manager_class)
715 716 self.contents_manager = kls(parent=self, log=self.log)
716 717 kls = import_item(self.session_manager_class)
717 718 self.session_manager = kls(parent=self, log=self.log,
718 719 kernel_manager=self.kernel_manager,
719 720 contents_manager=self.contents_manager)
720 721 kls = import_item(self.cluster_manager_class)
721 722 self.cluster_manager = kls(parent=self, log=self.log)
722 723 self.cluster_manager.update_profiles()
723 724
724 725 def init_logging(self):
725 726 # This prevents double log messages because tornado use a root logger that
726 727 # self.log is a child of. The logging module dipatches log messages to a log
727 728 # and all of its ancenstors until propagate is set to False.
728 729 self.log.propagate = False
729 730
730 731 for log in app_log, access_log, gen_log:
731 732 # consistent log output name (NotebookApp instead of tornado.access, etc.)
732 733 log.name = self.log.name
733 734 # hook up tornado 3's loggers to our app handlers
734 735 logger = logging.getLogger('tornado')
735 736 logger.propagate = True
736 737 logger.parent = self.log
737 738 logger.setLevel(self.log.level)
738 739
739 740 def init_webapp(self):
740 741 """initialize tornado webapp and httpserver"""
741 742 self.tornado_settings['allow_origin'] = self.allow_origin
742 743 if self.allow_origin_pat:
743 744 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
744 745 self.tornado_settings['allow_credentials'] = self.allow_credentials
745 746
746 747 self.web_app = NotebookWebApplication(
747 748 self, self.kernel_manager, self.contents_manager,
748 749 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
749 750 self.log, self.base_url, self.default_url, self.tornado_settings,
750 751 self.jinja_environment_options
751 752 )
752 753 if self.certfile:
753 754 ssl_options = dict(certfile=self.certfile)
754 755 if self.keyfile:
755 756 ssl_options['keyfile'] = self.keyfile
756 757 else:
757 758 ssl_options = None
758 759 self.web_app.password = self.password
759 760 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
760 761 xheaders=self.trust_xheaders)
761 762 if not self.ip:
762 763 warning = "WARNING: The notebook server is listening on all IP addresses"
763 764 if ssl_options is None:
764 765 self.log.critical(warning + " and not using encryption. This "
765 766 "is not recommended.")
766 767 if not self.password:
767 768 self.log.critical(warning + " and not using authentication. "
768 769 "This is highly insecure and not recommended.")
769 770 success = None
770 771 for port in random_ports(self.port, self.port_retries+1):
771 772 try:
772 773 self.http_server.listen(port, self.ip)
773 774 except socket.error as e:
774 775 if e.errno == errno.EADDRINUSE:
775 776 self.log.info('The port %i is already in use, trying another random port.' % port)
776 777 continue
777 778 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
778 779 self.log.warn("Permission to listen on port %i denied" % port)
779 780 continue
780 781 else:
781 782 raise
782 783 else:
783 784 self.port = port
784 785 success = True
785 786 break
786 787 if not success:
787 788 self.log.critical('ERROR: the notebook server could not be started because '
788 789 'no available port could be found.')
789 790 self.exit(1)
790 791
791 792 @property
792 793 def display_url(self):
793 794 ip = self.ip if self.ip else '[all ip addresses on your system]'
794 795 return self._url(ip)
795 796
796 797 @property
797 798 def connection_url(self):
798 799 ip = self.ip if self.ip else 'localhost'
799 800 return self._url(ip)
800 801
801 802 def _url(self, ip):
802 803 proto = 'https' if self.certfile else 'http'
803 804 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
804 805
805 806 def init_terminals(self):
806 807 try:
807 808 from .terminal import initialize
808 809 initialize(self.web_app)
809 810 self.web_app.settings['terminals_available'] = True
810 811 except ImportError as e:
811 812 self.log.info("Terminals not available (error was %s)", e)
812 813
813 814 def init_signal(self):
814 815 if not sys.platform.startswith('win'):
815 816 signal.signal(signal.SIGINT, self._handle_sigint)
816 817 signal.signal(signal.SIGTERM, self._signal_stop)
817 818 if hasattr(signal, 'SIGUSR1'):
818 819 # Windows doesn't support SIGUSR1
819 820 signal.signal(signal.SIGUSR1, self._signal_info)
820 821 if hasattr(signal, 'SIGINFO'):
821 822 # only on BSD-based systems
822 823 signal.signal(signal.SIGINFO, self._signal_info)
823 824
824 825 def _handle_sigint(self, sig, frame):
825 826 """SIGINT handler spawns confirmation dialog"""
826 827 # register more forceful signal handler for ^C^C case
827 828 signal.signal(signal.SIGINT, self._signal_stop)
828 829 # request confirmation dialog in bg thread, to avoid
829 830 # blocking the App
830 831 thread = threading.Thread(target=self._confirm_exit)
831 832 thread.daemon = True
832 833 thread.start()
833 834
834 835 def _restore_sigint_handler(self):
835 836 """callback for restoring original SIGINT handler"""
836 837 signal.signal(signal.SIGINT, self._handle_sigint)
837 838
838 839 def _confirm_exit(self):
839 840 """confirm shutdown on ^C
840 841
841 842 A second ^C, or answering 'y' within 5s will cause shutdown,
842 843 otherwise original SIGINT handler will be restored.
843 844
844 845 This doesn't work on Windows.
845 846 """
846 847 info = self.log.info
847 848 info('interrupted')
848 849 print(self.notebook_info())
849 850 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
850 851 sys.stdout.flush()
851 852 r,w,x = select.select([sys.stdin], [], [], 5)
852 853 if r:
853 854 line = sys.stdin.readline()
854 855 if line.lower().startswith('y') and 'n' not in line.lower():
855 856 self.log.critical("Shutdown confirmed")
856 857 ioloop.IOLoop.instance().stop()
857 858 return
858 859 else:
859 860 print("No answer for 5s:", end=' ')
860 861 print("resuming operation...")
861 862 # no answer, or answer is no:
862 863 # set it back to original SIGINT handler
863 864 # use IOLoop.add_callback because signal.signal must be called
864 865 # from main thread
865 866 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
866 867
867 868 def _signal_stop(self, sig, frame):
868 869 self.log.critical("received signal %s, stopping", sig)
869 870 ioloop.IOLoop.instance().stop()
870 871
871 872 def _signal_info(self, sig, frame):
872 873 print(self.notebook_info())
873 874
874 875 def init_components(self):
875 876 """Check the components submodule, and warn if it's unclean"""
876 877 status = submodule.check_submodule_status()
877 878 if status == 'missing':
878 879 self.log.warn("components submodule missing, running `git submodule update`")
879 880 submodule.update_submodules(submodule.ipython_parent())
880 881 elif status == 'unclean':
881 882 self.log.warn("components submodule unclean, you may see 404s on static/components")
882 883 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
883 884
884 885 @catch_config_error
885 886 def initialize(self, argv=None):
886 887 super(NotebookApp, self).initialize(argv)
887 888 self.init_logging()
888 889 self.init_kernel_argv()
889 890 self.init_configurables()
890 891 self.init_components()
891 892 self.init_webapp()
892 893 self.init_terminals()
893 894 self.init_signal()
894 895
895 896 def cleanup_kernels(self):
896 897 """Shutdown all kernels.
897 898
898 899 The kernels will shutdown themselves when this process no longer exists,
899 900 but explicit shutdown allows the KernelManagers to cleanup the connection files.
900 901 """
901 902 self.log.info('Shutting down kernels')
902 903 self.kernel_manager.shutdown_all()
903 904
904 905 def notebook_info(self):
905 906 "Return the current working directory and the server url information"
906 907 info = self.contents_manager.info_string() + "\n"
907 908 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
908 909 return info + "The IPython Notebook is running at: %s" % self.display_url
909 910
910 911 def server_info(self):
911 912 """Return a JSONable dict of information about this server."""
912 913 return {'url': self.connection_url,
913 914 'hostname': self.ip if self.ip else 'localhost',
914 915 'port': self.port,
915 916 'secure': bool(self.certfile),
916 917 'base_url': self.base_url,
917 918 'notebook_dir': os.path.abspath(self.notebook_dir),
918 919 'pid': os.getpid()
919 920 }
920 921
921 922 def write_server_info_file(self):
922 923 """Write the result of server_info() to the JSON file info_file."""
923 924 with open(self.info_file, 'w') as f:
924 925 json.dump(self.server_info(), f, indent=2)
925 926
926 927 def remove_server_info_file(self):
927 928 """Remove the nbserver-<pid>.json file created for this server.
928 929
929 930 Ignores the error raised when the file has already been removed.
930 931 """
931 932 try:
932 933 os.unlink(self.info_file)
933 934 except OSError as e:
934 935 if e.errno != errno.ENOENT:
935 936 raise
936 937
937 938 def start(self):
938 939 """ Start the IPython Notebook server app, after initialization
939 940
940 941 This method takes no arguments so all configuration and initialization
941 942 must be done prior to calling this method."""
942 943 if self.subapp is not None:
943 944 return self.subapp.start()
944 945
945 946 info = self.log.info
946 947 for line in self.notebook_info().split("\n"):
947 948 info(line)
948 949 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
949 950
950 951 self.write_server_info_file()
951 952
952 953 if self.open_browser or self.file_to_run:
953 954 try:
954 955 browser = webbrowser.get(self.browser or None)
955 956 except webbrowser.Error as e:
956 957 self.log.warn('No web browser found: %s.' % e)
957 958 browser = None
958 959
959 960 if self.file_to_run:
960 961 if not os.path.exists(self.file_to_run):
961 962 self.log.critical("%s does not exist" % self.file_to_run)
962 963 self.exit(1)
963 964
964 965 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
965 966 uri = url_path_join('notebooks', *relpath.split(os.sep))
966 967 else:
967 968 uri = 'tree'
968 969 if browser:
969 970 b = lambda : browser.open(url_path_join(self.connection_url, uri),
970 971 new=2)
971 972 threading.Thread(target=b).start()
972 973 try:
973 974 ioloop.IOLoop.instance().start()
974 975 except KeyboardInterrupt:
975 976 info("Interrupted...")
976 977 finally:
977 978 self.cleanup_kernels()
978 979 self.remove_server_info_file()
979 980
980 981
981 982 def list_running_servers(profile='default'):
982 983 """Iterate over the server info files of running notebook servers.
983 984
984 985 Given a profile name, find nbserver-* files in the security directory of
985 986 that profile, and yield dicts of their information, each one pertaining to
986 987 a currently running notebook server instance.
987 988 """
988 989 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
989 990 for file in os.listdir(pd.security_dir):
990 991 if file.startswith('nbserver-'):
991 992 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
992 993 info = json.load(f)
993 994
994 995 # Simple check whether that process is really still running
995 996 if check_pid(info['pid']):
996 997 yield info
997 998 else:
998 999 # If the process has died, try to delete its info file
999 1000 try:
1000 1001 os.unlink(file)
1001 1002 except OSError:
1002 1003 pass # TODO: This should warn or log or something
1003 1004 #-----------------------------------------------------------------------------
1004 1005 # Main entry point
1005 1006 #-----------------------------------------------------------------------------
1006 1007
1007 1008 launch_new_instance = NotebookApp.launch_instance
1008 1009
@@ -1,614 +1,619 b''
1 1 """A contents manager that uses the local file system for storage."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import base64
7 7 import errno
8 8 import io
9 9 import os
10 10 import shutil
11 11 from contextlib import contextmanager
12 import mimetypes
12 13
13 14 from tornado import web
14 15
15 16 from .manager import ContentsManager
16 17 from IPython import nbformat
17 18 from IPython.utils.io import atomic_writing
18 19 from IPython.utils.path import ensure_dir_exists
19 20 from IPython.utils.traitlets import Unicode, Bool, TraitError
20 21 from IPython.utils.py3compat import getcwd, str_to_unicode
21 22 from IPython.utils import tz
22 23 from IPython.html.utils import is_hidden, to_os_path, to_api_path
23 24
24 25
25 26 class FileContentsManager(ContentsManager):
26 27
27 28 root_dir = Unicode(config=True)
28 29
29 30 def _root_dir_default(self):
30 31 try:
31 32 return self.parent.notebook_dir
32 33 except AttributeError:
33 34 return getcwd()
34 35
35 36 @contextmanager
36 37 def perm_to_403(self, os_path=''):
37 38 """context manager for turning permission errors into 403"""
38 39 try:
39 40 yield
40 41 except OSError as e:
41 42 if e.errno in {errno.EPERM, errno.EACCES}:
42 43 # make 403 error message without root prefix
43 44 # this may not work perfectly on unicode paths on Python 2,
44 45 # but nobody should be doing that anyway.
45 46 if not os_path:
46 47 os_path = str_to_unicode(e.filename or 'unknown file')
47 48 path = to_api_path(os_path, self.root_dir)
48 49 raise web.HTTPError(403, u'Permission denied: %s' % path)
49 50 else:
50 51 raise
51 52
52 53 @contextmanager
53 54 def open(self, os_path, *args, **kwargs):
54 55 """wrapper around io.open that turns permission errors into 403"""
55 56 with self.perm_to_403(os_path):
56 57 with io.open(os_path, *args, **kwargs) as f:
57 58 yield f
58 59
59 60 @contextmanager
60 61 def atomic_writing(self, os_path, *args, **kwargs):
61 62 """wrapper around atomic_writing that turns permission errors into 403"""
62 63 with self.perm_to_403(os_path):
63 64 with atomic_writing(os_path, *args, **kwargs) as f:
64 65 yield f
65 66
66 67 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
67 68 def _save_script_changed(self):
68 69 self.log.warn("""
69 70 Automatically saving notebooks as scripts has been removed.
70 71 Use `ipython nbconvert --to python [notebook]` instead.
71 72 """)
72 73
73 74 def _root_dir_changed(self, name, old, new):
74 75 """Do a bit of validation of the root_dir."""
75 76 if not os.path.isabs(new):
76 77 # If we receive a non-absolute path, make it absolute.
77 78 self.root_dir = os.path.abspath(new)
78 79 return
79 80 if not os.path.isdir(new):
80 81 raise TraitError("%r is not a directory" % new)
81 82
82 83 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
83 84 help="""The directory name in which to keep file checkpoints
84 85
85 86 This is a path relative to the file's own directory.
86 87
87 88 By default, it is .ipynb_checkpoints
88 89 """
89 90 )
90 91
91 92 def _copy(self, src, dest):
92 93 """copy src to dest
93 94
94 95 like shutil.copy2, but log errors in copystat
95 96 """
96 97 shutil.copyfile(src, dest)
97 98 try:
98 99 shutil.copystat(src, dest)
99 100 except OSError as e:
100 101 self.log.debug("copystat on %s failed", dest, exc_info=True)
101 102
102 103 def _get_os_path(self, path):
103 104 """Given an API path, return its file system path.
104 105
105 106 Parameters
106 107 ----------
107 108 path : string
108 109 The relative API path to the named file.
109 110
110 111 Returns
111 112 -------
112 113 path : string
113 114 Native, absolute OS path to for a file.
114 115 """
115 116 return to_os_path(path, self.root_dir)
116 117
117 118 def dir_exists(self, path):
118 119 """Does the API-style path refer to an extant directory?
119 120
120 121 API-style wrapper for os.path.isdir
121 122
122 123 Parameters
123 124 ----------
124 125 path : string
125 126 The path to check. This is an API path (`/` separated,
126 127 relative to root_dir).
127 128
128 129 Returns
129 130 -------
130 131 exists : bool
131 132 Whether the path is indeed a directory.
132 133 """
133 134 path = path.strip('/')
134 135 os_path = self._get_os_path(path=path)
135 136 return os.path.isdir(os_path)
136 137
137 138 def is_hidden(self, path):
138 139 """Does the API style path correspond to a hidden directory or file?
139 140
140 141 Parameters
141 142 ----------
142 143 path : string
143 144 The path to check. This is an API path (`/` separated,
144 145 relative to root_dir).
145 146
146 147 Returns
147 148 -------
148 149 hidden : bool
149 150 Whether the path exists and is hidden.
150 151 """
151 152 path = path.strip('/')
152 153 os_path = self._get_os_path(path=path)
153 154 return is_hidden(os_path, self.root_dir)
154 155
155 156 def file_exists(self, path):
156 157 """Returns True if the file exists, else returns False.
157 158
158 159 API-style wrapper for os.path.isfile
159 160
160 161 Parameters
161 162 ----------
162 163 path : string
163 164 The relative path to the file (with '/' as separator)
164 165
165 166 Returns
166 167 -------
167 168 exists : bool
168 169 Whether the file exists.
169 170 """
170 171 path = path.strip('/')
171 172 os_path = self._get_os_path(path)
172 173 return os.path.isfile(os_path)
173 174
174 175 def exists(self, path):
175 176 """Returns True if the path exists, else returns False.
176 177
177 178 API-style wrapper for os.path.exists
178 179
179 180 Parameters
180 181 ----------
181 182 path : string
182 183 The API path to the file (with '/' as separator)
183 184
184 185 Returns
185 186 -------
186 187 exists : bool
187 188 Whether the target exists.
188 189 """
189 190 path = path.strip('/')
190 191 os_path = self._get_os_path(path=path)
191 192 return os.path.exists(os_path)
192 193
193 194 def _base_model(self, path):
194 195 """Build the common base of a contents model"""
195 196 os_path = self._get_os_path(path)
196 197 info = os.stat(os_path)
197 198 last_modified = tz.utcfromtimestamp(info.st_mtime)
198 199 created = tz.utcfromtimestamp(info.st_ctime)
199 200 # Create the base model.
200 201 model = {}
201 202 model['name'] = path.rsplit('/', 1)[-1]
202 203 model['path'] = path
203 204 model['last_modified'] = last_modified
204 205 model['created'] = created
205 206 model['content'] = None
206 207 model['format'] = None
208 model['mimetype'] = None
207 209 try:
208 210 model['writable'] = os.access(os_path, os.W_OK)
209 211 except OSError:
210 212 self.log.error("Failed to check write permissions on %s", os_path)
211 213 model['writable'] = False
212 214 return model
213 215
214 216 def _dir_model(self, path, content=True):
215 217 """Build a model for a directory
216 218
217 219 if content is requested, will include a listing of the directory
218 220 """
219 221 os_path = self._get_os_path(path)
220 222
221 223 four_o_four = u'directory does not exist: %r' % path
222 224
223 225 if not os.path.isdir(os_path):
224 226 raise web.HTTPError(404, four_o_four)
225 227 elif is_hidden(os_path, self.root_dir):
226 228 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
227 229 os_path
228 230 )
229 231 raise web.HTTPError(404, four_o_four)
230 232
231 233 model = self._base_model(path)
232 234 model['type'] = 'directory'
233 235 if content:
234 236 model['content'] = contents = []
235 237 os_dir = self._get_os_path(path)
236 238 for name in os.listdir(os_dir):
237 239 os_path = os.path.join(os_dir, name)
238 240 # skip over broken symlinks in listing
239 241 if not os.path.exists(os_path):
240 242 self.log.warn("%s doesn't exist", os_path)
241 243 continue
242 244 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
243 245 self.log.debug("%s not a regular file", os_path)
244 246 continue
245 247 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
246 248 contents.append(self.get(
247 249 path='%s/%s' % (path, name),
248 250 content=False)
249 251 )
250 252
251 253 model['format'] = 'json'
252 254
253 255 return model
254 256
255 257 def _file_model(self, path, content=True, format=None):
256 258 """Build a model for a file
257 259
258 260 if content is requested, include the file contents.
259 261
260 262 format:
261 263 If 'text', the contents will be decoded as UTF-8.
262 264 If 'base64', the raw bytes contents will be encoded as base64.
263 265 If not specified, try to decode as UTF-8, and fall back to base64
264 266 """
265 267 model = self._base_model(path)
266 268 model['type'] = 'file'
267 if content:
269
268 270 os_path = self._get_os_path(path)
271 model['mimetype'] = mimetypes.guess_type(os_path)[0] or 'text/plain'
272
273 if content:
269 274 if not os.path.isfile(os_path):
270 275 # could be FIFO
271 276 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
272 277 with self.open(os_path, 'rb') as f:
273 278 bcontent = f.read()
274 279
275 280 if format != 'base64':
276 281 try:
277 282 model['content'] = bcontent.decode('utf8')
278 283 except UnicodeError as e:
279 284 if format == 'text':
280 285 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
281 286 else:
282 287 model['format'] = 'text'
283 288
284 289 if model['content'] is None:
285 290 model['content'] = base64.encodestring(bcontent).decode('ascii')
286 291 model['format'] = 'base64'
287 292
288 293 return model
289 294
290 295
291 296 def _notebook_model(self, path, content=True):
292 297 """Build a notebook model
293 298
294 299 if content is requested, the notebook content will be populated
295 300 as a JSON structure (not double-serialized)
296 301 """
297 302 model = self._base_model(path)
298 303 model['type'] = 'notebook'
299 304 if content:
300 305 os_path = self._get_os_path(path)
301 306 with self.open(os_path, 'r', encoding='utf-8') as f:
302 307 try:
303 308 nb = nbformat.read(f, as_version=4)
304 309 except Exception as e:
305 310 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
306 311 self.mark_trusted_cells(nb, path)
307 312 model['content'] = nb
308 313 model['format'] = 'json'
309 314 self.validate_notebook_model(model)
310 315 return model
311 316
312 317 def get(self, path, content=True, type_=None, format=None):
313 318 """ Takes a path for an entity and returns its model
314 319
315 320 Parameters
316 321 ----------
317 322 path : str
318 323 the API path that describes the relative path for the target
319 324 content : bool
320 325 Whether to include the contents in the reply
321 326 type_ : str, optional
322 327 The requested type - 'file', 'notebook', or 'directory'.
323 328 Will raise HTTPError 400 if the content doesn't match.
324 329 format : str, optional
325 330 The requested format for file contents. 'text' or 'base64'.
326 331 Ignored if this returns a notebook or directory model.
327 332
328 333 Returns
329 334 -------
330 335 model : dict
331 336 the contents model. If content=True, returns the contents
332 337 of the file or directory as well.
333 338 """
334 339 path = path.strip('/')
335 340
336 341 if not self.exists(path):
337 342 raise web.HTTPError(404, u'No such file or directory: %s' % path)
338 343
339 344 os_path = self._get_os_path(path)
340 345 if os.path.isdir(os_path):
341 346 if type_ not in (None, 'directory'):
342 347 raise web.HTTPError(400,
343 348 u'%s is a directory, not a %s' % (path, type_))
344 349 model = self._dir_model(path, content=content)
345 350 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
346 351 model = self._notebook_model(path, content=content)
347 352 else:
348 353 if type_ == 'directory':
349 354 raise web.HTTPError(400,
350 355 u'%s is not a directory')
351 356 model = self._file_model(path, content=content, format=format)
352 357 return model
353 358
354 359 def _save_notebook(self, os_path, model, path=''):
355 360 """save a notebook file"""
356 361 # Save the notebook file
357 362 nb = nbformat.from_dict(model['content'])
358 363
359 364 self.check_and_sign(nb, path)
360 365
361 366 with self.atomic_writing(os_path, encoding='utf-8') as f:
362 367 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
363 368
364 369 def _save_file(self, os_path, model, path=''):
365 370 """save a non-notebook file"""
366 371 fmt = model.get('format', None)
367 372 if fmt not in {'text', 'base64'}:
368 373 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
369 374 try:
370 375 content = model['content']
371 376 if fmt == 'text':
372 377 bcontent = content.encode('utf8')
373 378 else:
374 379 b64_bytes = content.encode('ascii')
375 380 bcontent = base64.decodestring(b64_bytes)
376 381 except Exception as e:
377 382 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
378 383 with self.atomic_writing(os_path, text=False) as f:
379 384 f.write(bcontent)
380 385
381 386 def _save_directory(self, os_path, model, path=''):
382 387 """create a directory"""
383 388 if is_hidden(os_path, self.root_dir):
384 389 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
385 390 if not os.path.exists(os_path):
386 391 with self.perm_to_403():
387 392 os.mkdir(os_path)
388 393 elif not os.path.isdir(os_path):
389 394 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
390 395 else:
391 396 self.log.debug("Directory %r already exists", os_path)
392 397
393 398 def save(self, model, path=''):
394 399 """Save the file model and return the model with no content."""
395 400 path = path.strip('/')
396 401
397 402 if 'type' not in model:
398 403 raise web.HTTPError(400, u'No file type provided')
399 404 if 'content' not in model and model['type'] != 'directory':
400 405 raise web.HTTPError(400, u'No file content provided')
401 406
402 407 # One checkpoint should always exist
403 408 if self.file_exists(path) and not self.list_checkpoints(path):
404 409 self.create_checkpoint(path)
405 410
406 411 os_path = self._get_os_path(path)
407 412 self.log.debug("Saving %s", os_path)
408 413 try:
409 414 if model['type'] == 'notebook':
410 415 self._save_notebook(os_path, model, path)
411 416 elif model['type'] == 'file':
412 417 self._save_file(os_path, model, path)
413 418 elif model['type'] == 'directory':
414 419 self._save_directory(os_path, model, path)
415 420 else:
416 421 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
417 422 except web.HTTPError:
418 423 raise
419 424 except Exception as e:
420 425 self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
421 426 raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
422 427
423 428 validation_message = None
424 429 if model['type'] == 'notebook':
425 430 self.validate_notebook_model(model)
426 431 validation_message = model.get('message', None)
427 432
428 433 model = self.get(path, content=False)
429 434 if validation_message:
430 435 model['message'] = validation_message
431 436 return model
432 437
433 438 def update(self, model, path):
434 439 """Update the file's path
435 440
436 441 For use in PATCH requests, to enable renaming a file without
437 442 re-uploading its contents. Only used for renaming at the moment.
438 443 """
439 444 path = path.strip('/')
440 445 new_path = model.get('path', path).strip('/')
441 446 if path != new_path:
442 447 self.rename(path, new_path)
443 448 model = self.get(new_path, content=False)
444 449 return model
445 450
446 451 def delete(self, path):
447 452 """Delete file at path."""
448 453 path = path.strip('/')
449 454 os_path = self._get_os_path(path)
450 455 rm = os.unlink
451 456 if os.path.isdir(os_path):
452 457 listing = os.listdir(os_path)
453 458 # don't delete non-empty directories (checkpoints dir doesn't count)
454 459 if listing and listing != [self.checkpoint_dir]:
455 460 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
456 461 elif not os.path.isfile(os_path):
457 462 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
458 463
459 464 # clear checkpoints
460 465 for checkpoint in self.list_checkpoints(path):
461 466 checkpoint_id = checkpoint['id']
462 467 cp_path = self.get_checkpoint_path(checkpoint_id, path)
463 468 if os.path.isfile(cp_path):
464 469 self.log.debug("Unlinking checkpoint %s", cp_path)
465 470 with self.perm_to_403():
466 471 rm(cp_path)
467 472
468 473 if os.path.isdir(os_path):
469 474 self.log.debug("Removing directory %s", os_path)
470 475 with self.perm_to_403():
471 476 shutil.rmtree(os_path)
472 477 else:
473 478 self.log.debug("Unlinking file %s", os_path)
474 479 with self.perm_to_403():
475 480 rm(os_path)
476 481
477 482 def rename(self, old_path, new_path):
478 483 """Rename a file."""
479 484 old_path = old_path.strip('/')
480 485 new_path = new_path.strip('/')
481 486 if new_path == old_path:
482 487 return
483 488
484 489 new_os_path = self._get_os_path(new_path)
485 490 old_os_path = self._get_os_path(old_path)
486 491
487 492 # Should we proceed with the move?
488 493 if os.path.exists(new_os_path):
489 494 raise web.HTTPError(409, u'File already exists: %s' % new_path)
490 495
491 496 # Move the file
492 497 try:
493 498 with self.perm_to_403():
494 499 shutil.move(old_os_path, new_os_path)
495 500 except web.HTTPError:
496 501 raise
497 502 except Exception as e:
498 503 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
499 504
500 505 # Move the checkpoints
501 506 old_checkpoints = self.list_checkpoints(old_path)
502 507 for cp in old_checkpoints:
503 508 checkpoint_id = cp['id']
504 509 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
505 510 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
506 511 if os.path.isfile(old_cp_path):
507 512 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
508 513 with self.perm_to_403():
509 514 shutil.move(old_cp_path, new_cp_path)
510 515
511 516 # Checkpoint-related utilities
512 517
513 518 def get_checkpoint_path(self, checkpoint_id, path):
514 519 """find the path to a checkpoint"""
515 520 path = path.strip('/')
516 521 parent, name = ('/' + path).rsplit('/', 1)
517 522 parent = parent.strip('/')
518 523 basename, ext = os.path.splitext(name)
519 524 filename = u"{name}-{checkpoint_id}{ext}".format(
520 525 name=basename,
521 526 checkpoint_id=checkpoint_id,
522 527 ext=ext,
523 528 )
524 529 os_path = self._get_os_path(path=parent)
525 530 cp_dir = os.path.join(os_path, self.checkpoint_dir)
526 531 with self.perm_to_403():
527 532 ensure_dir_exists(cp_dir)
528 533 cp_path = os.path.join(cp_dir, filename)
529 534 return cp_path
530 535
531 536 def get_checkpoint_model(self, checkpoint_id, path):
532 537 """construct the info dict for a given checkpoint"""
533 538 path = path.strip('/')
534 539 cp_path = self.get_checkpoint_path(checkpoint_id, path)
535 540 stats = os.stat(cp_path)
536 541 last_modified = tz.utcfromtimestamp(stats.st_mtime)
537 542 info = dict(
538 543 id = checkpoint_id,
539 544 last_modified = last_modified,
540 545 )
541 546 return info
542 547
543 548 # public checkpoint API
544 549
545 550 def create_checkpoint(self, path):
546 551 """Create a checkpoint from the current state of a file"""
547 552 path = path.strip('/')
548 553 if not self.file_exists(path):
549 554 raise web.HTTPError(404)
550 555 src_path = self._get_os_path(path)
551 556 # only the one checkpoint ID:
552 557 checkpoint_id = u"checkpoint"
553 558 cp_path = self.get_checkpoint_path(checkpoint_id, path)
554 559 self.log.debug("creating checkpoint for %s", path)
555 560 with self.perm_to_403():
556 561 self._copy(src_path, cp_path)
557 562
558 563 # return the checkpoint info
559 564 return self.get_checkpoint_model(checkpoint_id, path)
560 565
561 566 def list_checkpoints(self, path):
562 567 """list the checkpoints for a given file
563 568
564 569 This contents manager currently only supports one checkpoint per file.
565 570 """
566 571 path = path.strip('/')
567 572 checkpoint_id = "checkpoint"
568 573 os_path = self.get_checkpoint_path(checkpoint_id, path)
569 574 if not os.path.exists(os_path):
570 575 return []
571 576 else:
572 577 return [self.get_checkpoint_model(checkpoint_id, path)]
573 578
574 579
575 580 def restore_checkpoint(self, checkpoint_id, path):
576 581 """restore a file to a checkpointed state"""
577 582 path = path.strip('/')
578 583 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
579 584 nb_path = self._get_os_path(path)
580 585 cp_path = self.get_checkpoint_path(checkpoint_id, path)
581 586 if not os.path.isfile(cp_path):
582 587 self.log.debug("checkpoint file does not exist: %s", cp_path)
583 588 raise web.HTTPError(404,
584 589 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
585 590 )
586 591 # ensure notebook is readable (never restore from an unreadable notebook)
587 592 if cp_path.endswith('.ipynb'):
588 593 with self.open(cp_path, 'r', encoding='utf-8') as f:
589 594 nbformat.read(f, as_version=4)
590 595 self.log.debug("copying %s -> %s", cp_path, nb_path)
591 596 with self.perm_to_403():
592 597 self._copy(cp_path, nb_path)
593 598
594 599 def delete_checkpoint(self, checkpoint_id, path):
595 600 """delete a file's checkpoint"""
596 601 path = path.strip('/')
597 602 cp_path = self.get_checkpoint_path(checkpoint_id, path)
598 603 if not os.path.isfile(cp_path):
599 604 raise web.HTTPError(404,
600 605 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
601 606 )
602 607 self.log.debug("unlinking %s", cp_path)
603 608 os.unlink(cp_path)
604 609
605 610 def info_string(self):
606 611 return "Serving notebooks from local directory: %s" % self.root_dir
607 612
608 613 def get_kernel_path(self, path, model=None):
609 614 """Return the initial working dir a kernel associated with a given notebook"""
610 615 if '/' in path:
611 616 parent_dir = path.rsplit('/', 1)[0]
612 617 else:
613 618 parent_dir = ''
614 619 return self._get_os_path(parent_dir)
1 NO CONTENT: file renamed from IPython/html/static/notebook/js/notificationwidget.js to IPython/html/static/base/js/notificationwidget.js
@@ -1,163 +1,163 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 require([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'notebook/js/notebook',
8 8 'contents',
9 9 'base/js/utils',
10 10 'base/js/page',
11 11 'notebook/js/layoutmanager',
12 12 'base/js/events',
13 13 'auth/js/loginwidget',
14 14 'notebook/js/maintoolbar',
15 15 'notebook/js/pager',
16 16 'notebook/js/quickhelp',
17 17 'notebook/js/menubar',
18 18 'notebook/js/notificationarea',
19 19 'notebook/js/savewidget',
20 20 'notebook/js/actions',
21 21 'notebook/js/keyboardmanager',
22 22 'notebook/js/config',
23 23 'notebook/js/kernelselector',
24 24 'codemirror/lib/codemirror',
25 25 'notebook/js/about',
26 26 // only loaded, not used, please keep sure this is loaded last
27 27 'custom/custom'
28 28 ], function(
29 29 IPython,
30 30 $,
31 31 notebook,
32 32 contents,
33 33 utils,
34 34 page,
35 35 layoutmanager,
36 36 events,
37 37 loginwidget,
38 38 maintoolbar,
39 39 pager,
40 40 quickhelp,
41 41 menubar,
42 42 notificationarea,
43 43 savewidget,
44 44 actions,
45 45 keyboardmanager,
46 46 config,
47 47 kernelselector,
48 48 CodeMirror,
49 49 about,
50 50 // please keep sure that even if not used, this is loaded last
51 51 custom
52 52 ) {
53 53 "use strict";
54 54
55 55 // compat with old IPython, remove for IPython > 3.0
56 56 window.CodeMirror = CodeMirror;
57 57
58 58 var common_options = {
59 59 ws_url : utils.get_body_data("wsUrl"),
60 60 base_url : utils.get_body_data("baseUrl"),
61 61 notebook_path : utils.get_body_data("notebookPath"),
62 62 notebook_name : utils.get_body_data('notebookName')
63 63 };
64 64
65 65 var user_config = $.extend({}, config.default_config);
66 66 var page = new page.Page();
67 67 var layout_manager = new layoutmanager.LayoutManager();
68 68 var pager = new pager.Pager('div#pager', 'div#pager_splitter', {
69 69 layout_manager: layout_manager,
70 70 events: events});
71 71 var acts = new actions.init();
72 72 var keyboard_manager = new keyboardmanager.KeyboardManager({
73 73 pager: pager,
74 74 events: events,
75 75 actions: acts });
76 76 var save_widget = new savewidget.SaveWidget('span#save_widget', {
77 77 events: events,
78 78 keyboard_manager: keyboard_manager});
79 79 var contents = new contents.Contents($.extend({
80 80 events: events},
81 81 common_options));
82 82 var notebook = new notebook.Notebook('div#notebook', $.extend({
83 83 events: events,
84 84 keyboard_manager: keyboard_manager,
85 85 save_widget: save_widget,
86 86 contents: contents,
87 87 config: user_config},
88 88 common_options));
89 89 var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options);
90 90 var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', {
91 91 notebook: notebook,
92 92 events: events,
93 93 actions: acts});
94 94 var quick_help = new quickhelp.QuickHelp({
95 95 keyboard_manager: keyboard_manager,
96 96 events: events,
97 97 notebook: notebook});
98 98 keyboard_manager.set_notebook(notebook);
99 99 keyboard_manager.set_quickhelp(quick_help);
100 100 var menubar = new menubar.MenuBar('#menubar', $.extend({
101 101 notebook: notebook,
102 102 contents: contents,
103 103 layout_manager: layout_manager,
104 104 events: events,
105 105 save_widget: save_widget,
106 106 quick_help: quick_help},
107 107 common_options));
108 var notification_area = new notificationarea.NotificationArea(
108 var notification_area = new notificationarea.NotebookNotificationArea(
109 109 '#notification_area', {
110 110 events: events,
111 111 save_widget: save_widget,
112 112 notebook: notebook,
113 113 keyboard_manager: keyboard_manager});
114 114 notification_area.init_notification_widgets();
115 115 var kernel_selector = new kernelselector.KernelSelector(
116 116 '#kernel_selector_widget', notebook);
117 117
118 118 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
119 119 '<span id="test2" style="font-weight: bold;">x</span>'+
120 120 '<span id="test3" style="font-style: italic;">x</span></pre></div>');
121 121 var nh = $('#test1').innerHeight();
122 122 var bh = $('#test2').innerHeight();
123 123 var ih = $('#test3').innerHeight();
124 124 if(nh != bh || nh != ih) {
125 125 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
126 126 }
127 127 $('#fonttest').remove();
128 128
129 129 page.show();
130 130
131 131 layout_manager.do_resize();
132 132 var first_load = function () {
133 133 layout_manager.do_resize();
134 134 var hash = document.location.hash;
135 135 if (hash) {
136 136 document.location.hash = '';
137 137 document.location.hash = hash;
138 138 }
139 139 notebook.set_autosave_interval(notebook.minimum_autosave_interval);
140 140 // only do this once
141 141 events.off('notebook_loaded.Notebook', first_load);
142 142 };
143 143 events.on('notebook_loaded.Notebook', first_load);
144 144
145 145 IPython.page = page;
146 146 IPython.layout_manager = layout_manager;
147 147 IPython.notebook = notebook;
148 148 IPython.contents = contents;
149 149 IPython.pager = pager;
150 150 IPython.quick_help = quick_help;
151 151 IPython.login_widget = login_widget;
152 152 IPython.menubar = menubar;
153 153 IPython.toolbar = toolbar;
154 154 IPython.notification_area = notification_area;
155 155 IPython.keyboard_manager = keyboard_manager;
156 156 IPython.save_widget = save_widget;
157 157 IPython.config = user_config;
158 158 IPython.tooltip = notebook.tooltip;
159 159
160 160 events.trigger('app_initialized.NotebookApp');
161 161 notebook.load_notebook(common_options.notebook_path);
162 162
163 163 });
@@ -1,387 +1,320 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 1 define([
5 2 'base/js/namespace',
6 3 'jquery',
7 4 'base/js/utils',
8 5 'base/js/dialog',
9 'notebook/js/notificationwidget',
6 'base/js/notificationarea',
10 7 'moment'
11 ], function(IPython, $, utils, dialog, notificationwidget, moment) {
8 ], function(IPython, $, utils, dialog, notificationarea, moment) {
12 9 "use strict";
10 var NotificationArea = notificationarea.NotificationArea;
13 11
14 // store reference to the NotificationWidget class
15 var NotificationWidget = notificationwidget.NotificationWidget;
16
17 /**
18 * Construct the NotificationArea object. Options are:
19 * events: $(Events) instance
20 * save_widget: SaveWidget instance
21 * notebook: Notebook instance
22 * keyboard_manager: KeyboardManager instance
23 *
24 * @constructor
25 * @param {string} selector - a jQuery selector string for the
26 * notification area element
27 * @param {Object} [options] - a dictionary of keyword arguments.
28 */
29 var NotificationArea = function (selector, options) {
30 this.selector = selector;
31 this.events = options.events;
12 var NotebookNotificationArea = function(selector, options) {
13 NotificationArea.apply(this, [selector, options]);
32 14 this.save_widget = options.save_widget;
33 15 this.notebook = options.notebook;
34 16 this.keyboard_manager = options.keyboard_manager;
35 if (this.selector !== undefined) {
36 this.element = $(selector);
37 17 }
38 this.widget_dict = {};
39 };
40 18
41 /**
42 * Get a widget by name, creating it if it doesn't exist.
43 *
44 * @method widget
45 * @param {string} name - the widget name
46 */
47 NotificationArea.prototype.widget = function (name) {
48 if (this.widget_dict[name] === undefined) {
49 return this.new_notification_widget(name);
50 }
51 return this.get_widget(name);
52 };
53
54 /**
55 * Get a widget by name, throwing an error if it doesn't exist.
56 *
57 * @method get_widget
58 * @param {string} name - the widget name
59 */
60 NotificationArea.prototype.get_widget = function (name) {
61 if(this.widget_dict[name] === undefined) {
62 throw('no widgets with this name');
63 }
64 return this.widget_dict[name];
65 };
66
67 /**
68 * Create a new notification widget with the given name. The
69 * widget must not already exist.
70 *
71 * @method new_notification_widget
72 * @param {string} name - the widget name
73 */
74 NotificationArea.prototype.new_notification_widget = function (name) {
75 if (this.widget_dict[name] !== undefined) {
76 throw('widget with that name already exists!');
77 }
78
79 // create the element for the notification widget and add it
80 // to the notification aread element
81 var div = $('<div/>').attr('id', 'notification_' + name);
82 $(this.selector).append(div);
83
84 // create the widget object and return it
85 this.widget_dict[name] = new NotificationWidget('#notification_' + name);
86 return this.widget_dict[name];
87 };
19 NotebookNotificationArea.prototype = Object.create(NotificationArea.prototype);
88 20
89 21 /**
90 22 * Initialize the default set of notification widgets.
91 23 *
92 24 * @method init_notification_widgets
93 25 */
94 NotificationArea.prototype.init_notification_widgets = function () {
26 NotebookNotificationArea.prototype.init_notification_widgets = function () {
95 27 this.init_kernel_notification_widget();
96 28 this.init_notebook_notification_widget();
97 29 };
98 30
99 31 /**
100 32 * Initialize the notification widget for kernel status messages.
101 33 *
102 34 * @method init_kernel_notification_widget
103 35 */
104 NotificationArea.prototype.init_kernel_notification_widget = function () {
36 NotebookNotificationArea.prototype.init_kernel_notification_widget = function () {
105 37 var that = this;
106 38 var knw = this.new_notification_widget('kernel');
107 39 var $kernel_ind_icon = $("#kernel_indicator_icon");
108 40 var $modal_ind_icon = $("#modal_indicator_icon");
109 41
110 42 // Command/Edit mode
111 43 this.events.on('edit_mode.Notebook', function () {
112 44 that.save_widget.update_document_title();
113 45 $modal_ind_icon.attr('class','edit_mode_icon').attr('title','Edit Mode');
114 46 });
115 47
116 48 this.events.on('command_mode.Notebook', function () {
117 49 that.save_widget.update_document_title();
118 50 $modal_ind_icon.attr('class','command_mode_icon').attr('title','Command Mode');
119 51 });
120 52
121 53 // Implicitly start off in Command mode, switching to Edit mode will trigger event
122 54 $modal_ind_icon.attr('class','command_mode_icon').attr('title','Command Mode');
123 55
124 56 // Kernel events
125 57
126 58 // this can be either kernel_created.Kernel or kernel_created.Session
127 59 this.events.on('kernel_created.Kernel kernel_created.Session', function () {
128 60 knw.info("Kernel Created", 500);
129 61 });
130 62
131 63 this.events.on('kernel_reconnecting.Kernel', function () {
132 64 knw.warning("Connecting to kernel");
133 65 });
134 66
135 67 this.events.on('kernel_connection_dead.Kernel', function (evt, info) {
136 68 knw.danger("Not Connected", undefined, function () {
137 69 // schedule reconnect a short time in the future, don't reconnect immediately
138 70 setTimeout($.proxy(info.kernel.reconnect, info.kernel), 500);
139 71 }, {title: 'click to reconnect'});
140 72 });
141 73
142 74 this.events.on('kernel_connected.Kernel', function () {
143 75 knw.info("Connected", 500);
144 76 });
145 77
146 78 this.events.on('kernel_restarting.Kernel', function () {
147 79 that.save_widget.update_document_title();
148 80 knw.set_message("Restarting kernel", 2000);
149 81 });
150 82
151 83 this.events.on('kernel_autorestarting.Kernel', function (evt, info) {
152 84 // Only show the dialog on the first restart attempt. This
153 85 // number gets tracked by the `Kernel` object and passed
154 86 // along here, because we don't want to show the user 5
155 87 // dialogs saying the same thing (which is the number of
156 88 // times it tries restarting).
157 89 if (info.attempt === 1) {
158 90
159 91 dialog.kernel_modal({
160 92 notebook: that.notebook,
161 93 keyboard_manager: that.keyboard_manager,
162 94 title: "Kernel Restarting",
163 95 body: "The kernel appears to have died. It will restart automatically.",
164 96 buttons: {
165 97 OK : {
166 98 class : "btn-primary"
167 99 }
168 100 }
169 101 });
170 102 };
171 103
172 104 that.save_widget.update_document_title();
173 105 knw.danger("Dead kernel");
174 106 $kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
175 107 });
176 108
177 109 this.events.on('kernel_interrupting.Kernel', function () {
178 110 knw.set_message("Interrupting kernel", 2000);
179 111 });
180 112
181 113 this.events.on('kernel_disconnected.Kernel', function () {
182 114 $kernel_ind_icon
183 115 .attr('class', 'kernel_disconnected_icon')
184 116 .attr('title', 'No Connection to Kernel');
185 117 });
186 118
187 119 this.events.on('kernel_connection_failed.Kernel', function (evt, info) {
188 120 // only show the dialog if this is the first failed
189 121 // connect attempt, because the kernel will continue
190 122 // trying to reconnect and we don't want to spam the user
191 123 // with messages
192 124 if (info.attempt === 1) {
193 125
194 126 var msg = "A connection to the notebook server could not be established." +
195 127 " The notebook will continue trying to reconnect, but" +
196 128 " until it does, you will NOT be able to run code. Check your" +
197 129 " network connection or notebook server configuration.";
198 130
199 131 dialog.kernel_modal({
200 132 title: "Connection failed",
201 133 body: msg,
202 134 keyboard_manager: that.keyboard_manager,
203 135 notebook: that.notebook,
204 136 buttons : {
205 137 "OK": {}
206 138 }
207 139 });
208 140 }
209 141 });
210 142
211 143 this.events.on('kernel_killed.Kernel kernel_killed.Session', function () {
212 144 that.save_widget.update_document_title();
213 145 knw.danger("Dead kernel");
214 146 $kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
215 147 });
216 148
217 149 this.events.on('kernel_dead.Kernel', function () {
218 150
219 151 var showMsg = function () {
220 152
221 153 var msg = 'The kernel has died, and the automatic restart has failed.' +
222 154 ' It is possible the kernel cannot be restarted.' +
223 155 ' If you are not able to restart the kernel, you will still be able to save' +
224 156 ' the notebook, but running code will no longer work until the notebook' +
225 157 ' is reopened.';
226 158
227 159 dialog.kernel_modal({
228 160 title: "Dead kernel",
229 161 body : msg,
230 162 keyboard_manager: that.keyboard_manager,
231 163 notebook: that.notebook,
232 164 buttons : {
233 165 "Manual Restart": {
234 166 class: "btn-danger",
235 167 click: function () {
236 168 that.notebook.start_session();
237 169 }
238 170 },
239 171 "Don't restart": {}
240 172 }
241 173 });
242 174
243 175 return false;
244 176 };
245 177
246 178 that.save_widget.update_document_title();
247 179 knw.danger("Dead kernel", undefined, showMsg);
248 180 $kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
249 181
250 182 showMsg();
251 183 });
252 184
253 185 this.events.on('kernel_dead.Session', function (evt, info) {
254 186 var full = info.xhr.responseJSON.message;
255 187 var short = info.xhr.responseJSON.short_message || 'Kernel error';
256 188 var traceback = info.xhr.responseJSON.traceback;
257 189
258 190 var showMsg = function () {
259 191 var msg = $('<div/>').append($('<p/>').text(full));
260 192 var cm, cm_elem, cm_open;
261 193
262 194 if (traceback) {
263 195 cm_elem = $('<div/>')
264 196 .css('margin-top', '1em')
265 197 .css('padding', '1em')
266 198 .addClass('output_scroll');
267 199 msg.append(cm_elem);
268 200 cm = CodeMirror(cm_elem.get(0), {
269 201 mode: "python",
270 202 readOnly : true
271 203 });
272 204 cm.setValue(traceback);
273 205 cm_open = $.proxy(cm.refresh, cm);
274 206 }
275 207
276 208 dialog.kernel_modal({
277 209 title: "Failed to start the kernel",
278 210 body : msg,
279 211 keyboard_manager: that.keyboard_manager,
280 212 notebook: that.notebook,
281 213 open: cm_open,
282 214 buttons : {
283 215 "Ok": { class: 'btn-primary' }
284 216 }
285 217 });
286 218
287 219 return false;
288 220 };
289 221
290 222 that.save_widget.update_document_title();
291 223 $kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
292 224 knw.danger(short, undefined, showMsg);
293 225 });
294 226
295 227 this.events.on('kernel_starting.Kernel', function () {
296 228 window.document.title='(Starting) '+window.document.title;
297 229 $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
298 230 knw.set_message("Kernel starting, please wait...");
299 231 });
300 232
301 233 this.events.on('kernel_ready.Kernel', function () {
302 234 that.save_widget.update_document_title();
303 235 $kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
304 236 knw.info("Kernel ready", 500);
305 237 });
306 238
307 239 this.events.on('kernel_idle.Kernel', function () {
308 240 that.save_widget.update_document_title();
309 241 $kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
310 242 });
311 243
312 244 this.events.on('kernel_busy.Kernel', function () {
313 245 window.document.title='(Busy) '+window.document.title;
314 246 $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
315 247 });
316 248
317 249 // Start the kernel indicator in the busy state, and send a kernel_info request.
318 250 // When the kernel_info reply arrives, the kernel is idle.
319 251 $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
320 252 };
321 253
322 254 /**
323 255 * Initialize the notification widget for notebook status messages.
324 256 *
325 257 * @method init_notebook_notification_widget
326 258 */
327 NotificationArea.prototype.init_notebook_notification_widget = function () {
259 NotebookNotificationArea.prototype.init_notebook_notification_widget = function () {
328 260 var nnw = this.new_notification_widget('notebook');
329 261
330 262 // Notebook events
331 263 this.events.on('notebook_loading.Notebook', function () {
332 264 nnw.set_message("Loading notebook",500);
333 265 });
334 266 this.events.on('notebook_loaded.Notebook', function () {
335 267 nnw.set_message("Notebook loaded",500);
336 268 });
337 269 this.events.on('notebook_saving.Notebook', function () {
338 270 nnw.set_message("Saving notebook",500);
339 271 });
340 272 this.events.on('notebook_saved.Notebook', function () {
341 273 nnw.set_message("Notebook saved",2000);
342 274 });
343 275 this.events.on('notebook_save_failed.Notebook', function (evt, error) {
344 276 nnw.warning(error.message || "Notebook save failed");
345 277 });
346 278 this.events.on('notebook_copy_failed.Notebook', function (evt, error) {
347 279 nnw.warning(error.message || "Notebook copy failed");
348 280 });
349 281
350 282 // Checkpoint events
351 283 this.events.on('checkpoint_created.Notebook', function (evt, data) {
352 284 var msg = "Checkpoint created";
353 285 if (data.last_modified) {
354 286 var d = new Date(data.last_modified);
355 287 msg = msg + ": " + moment(d).format("HH:mm:ss");
356 288 }
357 289 nnw.set_message(msg, 2000);
358 290 });
359 291 this.events.on('checkpoint_failed.Notebook', function () {
360 292 nnw.warning("Checkpoint failed");
361 293 });
362 294 this.events.on('checkpoint_deleted.Notebook', function () {
363 295 nnw.set_message("Checkpoint deleted", 500);
364 296 });
365 297 this.events.on('checkpoint_delete_failed.Notebook', function () {
366 298 nnw.warning("Checkpoint delete failed");
367 299 });
368 300 this.events.on('checkpoint_restoring.Notebook', function () {
369 301 nnw.set_message("Restoring to checkpoint...", 500);
370 302 });
371 303 this.events.on('checkpoint_restore_failed.Notebook', function () {
372 304 nnw.warning("Checkpoint restore failed");
373 305 });
374 306
375 307 // Autosave events
376 308 this.events.on('autosave_disabled.Notebook', function () {
377 309 nnw.set_message("Autosave disabled", 2000);
378 310 });
379 311 this.events.on('autosave_enabled.Notebook', function (evt, interval) {
380 312 nnw.set_message("Saving every " + interval / 1000 + "s", 1000);
381 313 });
382 314 };
383 315
384 IPython.NotificationArea = NotificationArea;
316 // Backwards compatibility.
317 IPython.NotificationArea = NotebookNotificationArea;
385 318
386 return {'NotificationArea': NotificationArea};
319 return {'NotebookNotificationArea': NotebookNotificationArea};
387 320 });
General Comments 0
You need to be logged in to leave comments. Login now