##// END OF EJS Templates
Merge pull request #6624...
MinRK -
r18366:7ad97d20 merge
parent child Browse files
Show More
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,48 b''
1 """Serve files directly from the ContentsManager."""
2
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5
6 import os
7 import mimetypes
8 import json
9 import base64
10
11 from tornado import web
12
13 from IPython.html.base.handlers import IPythonHandler
14
15 class FilesHandler(IPythonHandler):
16 """serve files via ContentsManager"""
17
18 @web.authenticated
19 def get(self, path):
20 cm = self.settings['contents_manager']
21 if cm.is_hidden(path):
22 self.log.info("Refusing to serve hidden file, via 404 Error")
23 raise web.HTTPError(404)
24
25 path, name = os.path.split(path)
26 model = cm.get_model(name, path)
27
28 if model['type'] == 'notebook':
29 self.set_header('Content-Type', 'application/json')
30 else:
31 cur_mime = mimetypes.guess_type(name)[0]
32 if cur_mime is not None:
33 self.set_header('Content-Type', cur_mime)
34
35 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
36
37 if model['format'] == 'base64':
38 b64_bytes = model['content'].encode('ascii')
39 self.write(base64.decodestring(b64_bytes))
40 elif model['format'] == 'json':
41 self.write(json.dumps(model['content']))
42 else:
43 self.write(model['content'])
44 self.flush()
45
46 default_handlers = [
47 (r"/files/(.*)", FilesHandler),
48 ] No newline at end of file
@@ -1,963 +1,958 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server."""
2 """A tornado based IPython notebook server."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import base64
9 import base64
10 import errno
10 import errno
11 import io
11 import io
12 import json
12 import json
13 import logging
13 import logging
14 import os
14 import os
15 import random
15 import random
16 import re
16 import re
17 import select
17 import select
18 import signal
18 import signal
19 import socket
19 import socket
20 import sys
20 import sys
21 import threading
21 import threading
22 import time
22 import time
23 import webbrowser
23 import webbrowser
24
24
25
25
26 # check for pyzmq 2.1.11
26 # check for pyzmq 2.1.11
27 from IPython.utils.zmqrelated import check_for_zmq
27 from IPython.utils.zmqrelated import check_for_zmq
28 check_for_zmq('2.1.11', 'IPython.html')
28 check_for_zmq('2.1.11', 'IPython.html')
29
29
30 from jinja2 import Environment, FileSystemLoader
30 from jinja2 import Environment, FileSystemLoader
31
31
32 # Install the pyzmq ioloop. This has to be done before anything else from
32 # Install the pyzmq ioloop. This has to be done before anything else from
33 # tornado is imported.
33 # tornado is imported.
34 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
35 ioloop.install()
35 ioloop.install()
36
36
37 # check for tornado 3.1.0
37 # check for tornado 3.1.0
38 msg = "The IPython Notebook requires tornado >= 3.1.0"
38 msg = "The IPython Notebook requires tornado >= 3.1.0"
39 try:
39 try:
40 import tornado
40 import tornado
41 except ImportError:
41 except ImportError:
42 raise ImportError(msg)
42 raise ImportError(msg)
43 try:
43 try:
44 version_info = tornado.version_info
44 version_info = tornado.version_info
45 except AttributeError:
45 except AttributeError:
46 raise ImportError(msg + ", but you have < 1.1.0")
46 raise ImportError(msg + ", but you have < 1.1.0")
47 if version_info < (3,1,0):
47 if version_info < (3,1,0):
48 raise ImportError(msg + ", but you have %s" % tornado.version)
48 raise ImportError(msg + ", but you have %s" % tornado.version)
49
49
50 from tornado import httpserver
50 from tornado import httpserver
51 from tornado import web
51 from tornado import web
52 from tornado.log import LogFormatter, app_log, access_log, gen_log
52 from tornado.log import LogFormatter, app_log, access_log, gen_log
53
53
54 from IPython.html import DEFAULT_STATIC_FILES_PATH
54 from IPython.html import DEFAULT_STATIC_FILES_PATH
55 from .base.handlers import Template404
55 from .base.handlers import Template404
56 from .log import log_request
56 from .log import log_request
57 from .services.kernels.kernelmanager import MappingKernelManager
57 from .services.kernels.kernelmanager import MappingKernelManager
58 from .services.contents.manager import ContentsManager
58 from .services.contents.manager import ContentsManager
59 from .services.contents.filemanager import FileContentsManager
59 from .services.contents.filemanager import FileContentsManager
60 from .services.clusters.clustermanager import ClusterManager
60 from .services.clusters.clustermanager import ClusterManager
61 from .services.sessions.sessionmanager import SessionManager
61 from .services.sessions.sessionmanager import SessionManager
62
62
63 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
63 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
64
64
65 from IPython.config import Config
65 from IPython.config import Config
66 from IPython.config.application import catch_config_error, boolean_flag
66 from IPython.config.application import catch_config_error, boolean_flag
67 from IPython.core.application import (
67 from IPython.core.application import (
68 BaseIPythonApplication, base_flags, base_aliases,
68 BaseIPythonApplication, base_flags, base_aliases,
69 )
69 )
70 from IPython.core.profiledir import ProfileDir
70 from IPython.core.profiledir import ProfileDir
71 from IPython.kernel import KernelManager
71 from IPython.kernel import KernelManager
72 from IPython.kernel.kernelspec import KernelSpecManager
72 from IPython.kernel.kernelspec import KernelSpecManager
73 from IPython.kernel.zmq.session import default_secure, Session
73 from IPython.kernel.zmq.session import default_secure, Session
74 from IPython.nbformat.sign import NotebookNotary
74 from IPython.nbformat.sign import NotebookNotary
75 from IPython.utils.importstring import import_item
75 from IPython.utils.importstring import import_item
76 from IPython.utils import submodule
76 from IPython.utils import submodule
77 from IPython.utils.process import check_pid
77 from IPython.utils.process import check_pid
78 from IPython.utils.traitlets import (
78 from IPython.utils.traitlets import (
79 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
79 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
80 DottedObjectName, TraitError,
80 DottedObjectName, TraitError,
81 )
81 )
82 from IPython.utils import py3compat
82 from IPython.utils import py3compat
83 from IPython.utils.path import filefind, get_ipython_dir
83 from IPython.utils.path import filefind, get_ipython_dir
84
84
85 from .utils import url_path_join
85 from .utils import url_path_join
86
86
87 #-----------------------------------------------------------------------------
87 #-----------------------------------------------------------------------------
88 # Module globals
88 # Module globals
89 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
90
90
91 _examples = """
91 _examples = """
92 ipython notebook # start the notebook
92 ipython notebook # start the notebook
93 ipython notebook --profile=sympy # use the sympy profile
93 ipython notebook --profile=sympy # use the sympy profile
94 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
94 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
95 """
95 """
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Helper functions
98 # Helper functions
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 def random_ports(port, n):
101 def random_ports(port, n):
102 """Generate a list of n random ports near the given port.
102 """Generate a list of n random ports near the given port.
103
103
104 The first 5 ports will be sequential, and the remaining n-5 will be
104 The first 5 ports will be sequential, and the remaining n-5 will be
105 randomly selected in the range [port-2*n, port+2*n].
105 randomly selected in the range [port-2*n, port+2*n].
106 """
106 """
107 for i in range(min(5, n)):
107 for i in range(min(5, n)):
108 yield port + i
108 yield port + i
109 for i in range(n-5):
109 for i in range(n-5):
110 yield max(1, port + random.randint(-2*n, 2*n))
110 yield max(1, port + random.randint(-2*n, 2*n))
111
111
112 def load_handlers(name):
112 def load_handlers(name):
113 """Load the (URL pattern, handler) tuples for each component."""
113 """Load the (URL pattern, handler) tuples for each component."""
114 name = 'IPython.html.' + name
114 name = 'IPython.html.' + name
115 mod = __import__(name, fromlist=['default_handlers'])
115 mod = __import__(name, fromlist=['default_handlers'])
116 return mod.default_handlers
116 return mod.default_handlers
117
117
118 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
119 # The Tornado web application
119 # The Tornado web application
120 #-----------------------------------------------------------------------------
120 #-----------------------------------------------------------------------------
121
121
122 class NotebookWebApplication(web.Application):
122 class NotebookWebApplication(web.Application):
123
123
124 def __init__(self, ipython_app, kernel_manager, contents_manager,
124 def __init__(self, ipython_app, kernel_manager, contents_manager,
125 cluster_manager, session_manager, kernel_spec_manager, log,
125 cluster_manager, session_manager, kernel_spec_manager, log,
126 base_url, default_url, settings_overrides, jinja_env_options):
126 base_url, default_url, settings_overrides, jinja_env_options):
127
127
128 settings = self.init_settings(
128 settings = self.init_settings(
129 ipython_app, kernel_manager, contents_manager, cluster_manager,
129 ipython_app, kernel_manager, contents_manager, cluster_manager,
130 session_manager, kernel_spec_manager, log, base_url, default_url,
130 session_manager, kernel_spec_manager, log, base_url, default_url,
131 settings_overrides, jinja_env_options)
131 settings_overrides, jinja_env_options)
132 handlers = self.init_handlers(settings)
132 handlers = self.init_handlers(settings)
133
133
134 super(NotebookWebApplication, self).__init__(handlers, **settings)
134 super(NotebookWebApplication, self).__init__(handlers, **settings)
135
135
136 def init_settings(self, ipython_app, kernel_manager, contents_manager,
136 def init_settings(self, ipython_app, kernel_manager, contents_manager,
137 cluster_manager, session_manager, kernel_spec_manager,
137 cluster_manager, session_manager, kernel_spec_manager,
138 log, base_url, default_url, settings_overrides,
138 log, base_url, default_url, settings_overrides,
139 jinja_env_options=None):
139 jinja_env_options=None):
140
140
141 _template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
141 _template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
142 if isinstance(_template_path, str):
142 if isinstance(_template_path, str):
143 _template_path = (_template_path,)
143 _template_path = (_template_path,)
144 template_path = [os.path.expanduser(path) for path in _template_path]
144 template_path = [os.path.expanduser(path) for path in _template_path]
145
145
146 jenv_opt = jinja_env_options if jinja_env_options else {}
146 jenv_opt = jinja_env_options if jinja_env_options else {}
147 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
147 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
148 settings = dict(
148 settings = dict(
149 # basics
149 # basics
150 log_function=log_request,
150 log_function=log_request,
151 base_url=base_url,
151 base_url=base_url,
152 default_url=default_url,
152 default_url=default_url,
153 template_path=template_path,
153 template_path=template_path,
154 static_path=ipython_app.static_file_path,
154 static_path=ipython_app.static_file_path,
155 static_handler_class = FileFindHandler,
155 static_handler_class = FileFindHandler,
156 static_url_prefix = url_path_join(base_url,'/static/'),
156 static_url_prefix = url_path_join(base_url,'/static/'),
157
157
158 # authentication
158 # authentication
159 cookie_secret=ipython_app.cookie_secret,
159 cookie_secret=ipython_app.cookie_secret,
160 login_url=url_path_join(base_url,'/login'),
160 login_url=url_path_join(base_url,'/login'),
161 password=ipython_app.password,
161 password=ipython_app.password,
162
162
163 # managers
163 # managers
164 kernel_manager=kernel_manager,
164 kernel_manager=kernel_manager,
165 contents_manager=contents_manager,
165 contents_manager=contents_manager,
166 cluster_manager=cluster_manager,
166 cluster_manager=cluster_manager,
167 session_manager=session_manager,
167 session_manager=session_manager,
168 kernel_spec_manager=kernel_spec_manager,
168 kernel_spec_manager=kernel_spec_manager,
169
169
170 # IPython stuff
170 # IPython stuff
171 nbextensions_path = ipython_app.nbextensions_path,
171 nbextensions_path = ipython_app.nbextensions_path,
172 websocket_url=ipython_app.websocket_url,
172 websocket_url=ipython_app.websocket_url,
173 mathjax_url=ipython_app.mathjax_url,
173 mathjax_url=ipython_app.mathjax_url,
174 config=ipython_app.config,
174 config=ipython_app.config,
175 jinja2_env=env,
175 jinja2_env=env,
176 )
176 )
177
177
178 # allow custom overrides for the tornado web app.
178 # allow custom overrides for the tornado web app.
179 settings.update(settings_overrides)
179 settings.update(settings_overrides)
180 return settings
180 return settings
181
181
182 def init_handlers(self, settings):
182 def init_handlers(self, settings):
183 # Load the (URL pattern, handler) tuples for each component.
183 # Load the (URL pattern, handler) tuples for each component.
184 handlers = []
184 handlers = []
185 handlers.extend(load_handlers('base.handlers'))
185 handlers.extend(load_handlers('base.handlers'))
186 handlers.extend(load_handlers('tree.handlers'))
186 handlers.extend(load_handlers('tree.handlers'))
187 handlers.extend(load_handlers('auth.login'))
187 handlers.extend(load_handlers('auth.login'))
188 handlers.extend(load_handlers('auth.logout'))
188 handlers.extend(load_handlers('auth.logout'))
189 handlers.extend(load_handlers('files.handlers'))
189 handlers.extend(load_handlers('notebook.handlers'))
190 handlers.extend(load_handlers('notebook.handlers'))
190 handlers.extend(load_handlers('nbconvert.handlers'))
191 handlers.extend(load_handlers('nbconvert.handlers'))
191 handlers.extend(load_handlers('kernelspecs.handlers'))
192 handlers.extend(load_handlers('kernelspecs.handlers'))
192 handlers.extend(load_handlers('services.kernels.handlers'))
193 handlers.extend(load_handlers('services.kernels.handlers'))
193 handlers.extend(load_handlers('services.contents.handlers'))
194 handlers.extend(load_handlers('services.contents.handlers'))
194 handlers.extend(load_handlers('services.clusters.handlers'))
195 handlers.extend(load_handlers('services.clusters.handlers'))
195 handlers.extend(load_handlers('services.sessions.handlers'))
196 handlers.extend(load_handlers('services.sessions.handlers'))
196 handlers.extend(load_handlers('services.nbconvert.handlers'))
197 handlers.extend(load_handlers('services.nbconvert.handlers'))
197 handlers.extend(load_handlers('services.kernelspecs.handlers'))
198 handlers.extend(load_handlers('services.kernelspecs.handlers'))
198 # FIXME: /files/ should be handled by the Contents service when it exists
199 cm = settings['contents_manager']
200 if hasattr(cm, 'root_dir'):
201 handlers.append(
202 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : cm.root_dir}),
203 )
204 handlers.append(
199 handlers.append(
205 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
200 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
206 )
201 )
207 # set the URL that will be redirected from `/`
202 # set the URL that will be redirected from `/`
208 handlers.append(
203 handlers.append(
209 (r'/?', web.RedirectHandler, {
204 (r'/?', web.RedirectHandler, {
210 'url' : url_path_join(settings['base_url'], settings['default_url']),
205 'url' : url_path_join(settings['base_url'], settings['default_url']),
211 'permanent': False, # want 302, not 301
206 'permanent': False, # want 302, not 301
212 })
207 })
213 )
208 )
214 # prepend base_url onto the patterns that we match
209 # prepend base_url onto the patterns that we match
215 new_handlers = []
210 new_handlers = []
216 for handler in handlers:
211 for handler in handlers:
217 pattern = url_path_join(settings['base_url'], handler[0])
212 pattern = url_path_join(settings['base_url'], handler[0])
218 new_handler = tuple([pattern] + list(handler[1:]))
213 new_handler = tuple([pattern] + list(handler[1:]))
219 new_handlers.append(new_handler)
214 new_handlers.append(new_handler)
220 # add 404 on the end, which will catch everything that falls through
215 # add 404 on the end, which will catch everything that falls through
221 new_handlers.append((r'(.*)', Template404))
216 new_handlers.append((r'(.*)', Template404))
222 return new_handlers
217 return new_handlers
223
218
224
219
225 class NbserverListApp(BaseIPythonApplication):
220 class NbserverListApp(BaseIPythonApplication):
226
221
227 description="List currently running notebook servers in this profile."
222 description="List currently running notebook servers in this profile."
228
223
229 flags = dict(
224 flags = dict(
230 json=({'NbserverListApp': {'json': True}},
225 json=({'NbserverListApp': {'json': True}},
231 "Produce machine-readable JSON output."),
226 "Produce machine-readable JSON output."),
232 )
227 )
233
228
234 json = Bool(False, config=True,
229 json = Bool(False, config=True,
235 help="If True, each line of output will be a JSON object with the "
230 help="If True, each line of output will be a JSON object with the "
236 "details from the server info file.")
231 "details from the server info file.")
237
232
238 def start(self):
233 def start(self):
239 if not self.json:
234 if not self.json:
240 print("Currently running servers:")
235 print("Currently running servers:")
241 for serverinfo in list_running_servers(self.profile):
236 for serverinfo in list_running_servers(self.profile):
242 if self.json:
237 if self.json:
243 print(json.dumps(serverinfo))
238 print(json.dumps(serverinfo))
244 else:
239 else:
245 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
240 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
246
241
247 #-----------------------------------------------------------------------------
242 #-----------------------------------------------------------------------------
248 # Aliases and Flags
243 # Aliases and Flags
249 #-----------------------------------------------------------------------------
244 #-----------------------------------------------------------------------------
250
245
251 flags = dict(base_flags)
246 flags = dict(base_flags)
252 flags['no-browser']=(
247 flags['no-browser']=(
253 {'NotebookApp' : {'open_browser' : False}},
248 {'NotebookApp' : {'open_browser' : False}},
254 "Don't open the notebook in a browser after startup."
249 "Don't open the notebook in a browser after startup."
255 )
250 )
256 flags['pylab']=(
251 flags['pylab']=(
257 {'NotebookApp' : {'pylab' : 'warn'}},
252 {'NotebookApp' : {'pylab' : 'warn'}},
258 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
253 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
259 )
254 )
260 flags['no-mathjax']=(
255 flags['no-mathjax']=(
261 {'NotebookApp' : {'enable_mathjax' : False}},
256 {'NotebookApp' : {'enable_mathjax' : False}},
262 """Disable MathJax
257 """Disable MathJax
263
258
264 MathJax is the javascript library IPython uses to render math/LaTeX. It is
259 MathJax is the javascript library IPython uses to render math/LaTeX. It is
265 very large, so you may want to disable it if you have a slow internet
260 very large, so you may want to disable it if you have a slow internet
266 connection, or for offline use of the notebook.
261 connection, or for offline use of the notebook.
267
262
268 When disabled, equations etc. will appear as their untransformed TeX source.
263 When disabled, equations etc. will appear as their untransformed TeX source.
269 """
264 """
270 )
265 )
271
266
272 # Add notebook manager flags
267 # Add notebook manager flags
273 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
268 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
274 'DEPRECATED, IGNORED',
269 'DEPRECATED, IGNORED',
275 'DEPRECATED, IGNORED'))
270 'DEPRECATED, IGNORED'))
276
271
277 aliases = dict(base_aliases)
272 aliases = dict(base_aliases)
278
273
279 aliases.update({
274 aliases.update({
280 'ip': 'NotebookApp.ip',
275 'ip': 'NotebookApp.ip',
281 'port': 'NotebookApp.port',
276 'port': 'NotebookApp.port',
282 'port-retries': 'NotebookApp.port_retries',
277 'port-retries': 'NotebookApp.port_retries',
283 'transport': 'KernelManager.transport',
278 'transport': 'KernelManager.transport',
284 'keyfile': 'NotebookApp.keyfile',
279 'keyfile': 'NotebookApp.keyfile',
285 'certfile': 'NotebookApp.certfile',
280 'certfile': 'NotebookApp.certfile',
286 'notebook-dir': 'NotebookApp.notebook_dir',
281 'notebook-dir': 'NotebookApp.notebook_dir',
287 'browser': 'NotebookApp.browser',
282 'browser': 'NotebookApp.browser',
288 'pylab': 'NotebookApp.pylab',
283 'pylab': 'NotebookApp.pylab',
289 })
284 })
290
285
291 #-----------------------------------------------------------------------------
286 #-----------------------------------------------------------------------------
292 # NotebookApp
287 # NotebookApp
293 #-----------------------------------------------------------------------------
288 #-----------------------------------------------------------------------------
294
289
295 class NotebookApp(BaseIPythonApplication):
290 class NotebookApp(BaseIPythonApplication):
296
291
297 name = 'ipython-notebook'
292 name = 'ipython-notebook'
298
293
299 description = """
294 description = """
300 The IPython HTML Notebook.
295 The IPython HTML Notebook.
301
296
302 This launches a Tornado based HTML Notebook Server that serves up an
297 This launches a Tornado based HTML Notebook Server that serves up an
303 HTML5/Javascript Notebook client.
298 HTML5/Javascript Notebook client.
304 """
299 """
305 examples = _examples
300 examples = _examples
306 aliases = aliases
301 aliases = aliases
307 flags = flags
302 flags = flags
308
303
309 classes = [
304 classes = [
310 KernelManager, ProfileDir, Session, MappingKernelManager,
305 KernelManager, ProfileDir, Session, MappingKernelManager,
311 ContentsManager, FileContentsManager, NotebookNotary,
306 ContentsManager, FileContentsManager, NotebookNotary,
312 ]
307 ]
313 flags = Dict(flags)
308 flags = Dict(flags)
314 aliases = Dict(aliases)
309 aliases = Dict(aliases)
315
310
316 subcommands = dict(
311 subcommands = dict(
317 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
312 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
318 )
313 )
319
314
320 kernel_argv = List(Unicode)
315 kernel_argv = List(Unicode)
321
316
322 _log_formatter_cls = LogFormatter
317 _log_formatter_cls = LogFormatter
323
318
324 def _log_level_default(self):
319 def _log_level_default(self):
325 return logging.INFO
320 return logging.INFO
326
321
327 def _log_datefmt_default(self):
322 def _log_datefmt_default(self):
328 """Exclude date from default date format"""
323 """Exclude date from default date format"""
329 return "%H:%M:%S"
324 return "%H:%M:%S"
330
325
331 def _log_format_default(self):
326 def _log_format_default(self):
332 """override default log format to include time"""
327 """override default log format to include time"""
333 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
328 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
334
329
335 # create requested profiles by default, if they don't exist:
330 # create requested profiles by default, if they don't exist:
336 auto_create = Bool(True)
331 auto_create = Bool(True)
337
332
338 # file to be opened in the notebook server
333 # file to be opened in the notebook server
339 file_to_run = Unicode('', config=True)
334 file_to_run = Unicode('', config=True)
340 def _file_to_run_changed(self, name, old, new):
335 def _file_to_run_changed(self, name, old, new):
341 path, base = os.path.split(new)
336 path, base = os.path.split(new)
342 if path:
337 if path:
343 self.file_to_run = base
338 self.file_to_run = base
344 self.notebook_dir = path
339 self.notebook_dir = path
345
340
346 # Network related information
341 # Network related information
347
342
348 allow_origin = Unicode('', config=True,
343 allow_origin = Unicode('', config=True,
349 help="""Set the Access-Control-Allow-Origin header
344 help="""Set the Access-Control-Allow-Origin header
350
345
351 Use '*' to allow any origin to access your server.
346 Use '*' to allow any origin to access your server.
352
347
353 Takes precedence over allow_origin_pat.
348 Takes precedence over allow_origin_pat.
354 """
349 """
355 )
350 )
356
351
357 allow_origin_pat = Unicode('', config=True,
352 allow_origin_pat = Unicode('', config=True,
358 help="""Use a regular expression for the Access-Control-Allow-Origin header
353 help="""Use a regular expression for the Access-Control-Allow-Origin header
359
354
360 Requests from an origin matching the expression will get replies with:
355 Requests from an origin matching the expression will get replies with:
361
356
362 Access-Control-Allow-Origin: origin
357 Access-Control-Allow-Origin: origin
363
358
364 where `origin` is the origin of the request.
359 where `origin` is the origin of the request.
365
360
366 Ignored if allow_origin is set.
361 Ignored if allow_origin is set.
367 """
362 """
368 )
363 )
369
364
370 allow_credentials = Bool(False, config=True,
365 allow_credentials = Bool(False, config=True,
371 help="Set the Access-Control-Allow-Credentials: true header"
366 help="Set the Access-Control-Allow-Credentials: true header"
372 )
367 )
373
368
374 default_url = Unicode('/tree', config=True,
369 default_url = Unicode('/tree', config=True,
375 help="The default URL to redirect to from `/`"
370 help="The default URL to redirect to from `/`"
376 )
371 )
377
372
378 ip = Unicode('localhost', config=True,
373 ip = Unicode('localhost', config=True,
379 help="The IP address the notebook server will listen on."
374 help="The IP address the notebook server will listen on."
380 )
375 )
381
376
382 def _ip_changed(self, name, old, new):
377 def _ip_changed(self, name, old, new):
383 if new == u'*': self.ip = u''
378 if new == u'*': self.ip = u''
384
379
385 port = Integer(8888, config=True,
380 port = Integer(8888, config=True,
386 help="The port the notebook server will listen on."
381 help="The port the notebook server will listen on."
387 )
382 )
388 port_retries = Integer(50, config=True,
383 port_retries = Integer(50, config=True,
389 help="The number of additional ports to try if the specified port is not available."
384 help="The number of additional ports to try if the specified port is not available."
390 )
385 )
391
386
392 certfile = Unicode(u'', config=True,
387 certfile = Unicode(u'', config=True,
393 help="""The full path to an SSL/TLS certificate file."""
388 help="""The full path to an SSL/TLS certificate file."""
394 )
389 )
395
390
396 keyfile = Unicode(u'', config=True,
391 keyfile = Unicode(u'', config=True,
397 help="""The full path to a private key file for usage with SSL/TLS."""
392 help="""The full path to a private key file for usage with SSL/TLS."""
398 )
393 )
399
394
400 cookie_secret_file = Unicode(config=True,
395 cookie_secret_file = Unicode(config=True,
401 help="""The file where the cookie secret is stored."""
396 help="""The file where the cookie secret is stored."""
402 )
397 )
403 def _cookie_secret_file_default(self):
398 def _cookie_secret_file_default(self):
404 if self.profile_dir is None:
399 if self.profile_dir is None:
405 return ''
400 return ''
406 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
401 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
407
402
408 cookie_secret = Bytes(b'', config=True,
403 cookie_secret = Bytes(b'', config=True,
409 help="""The random bytes used to secure cookies.
404 help="""The random bytes used to secure cookies.
410 By default this is a new random number every time you start the Notebook.
405 By default this is a new random number every time you start the Notebook.
411 Set it to a value in a config file to enable logins to persist across server sessions.
406 Set it to a value in a config file to enable logins to persist across server sessions.
412
407
413 Note: Cookie secrets should be kept private, do not share config files with
408 Note: Cookie secrets should be kept private, do not share config files with
414 cookie_secret stored in plaintext (you can read the value from a file).
409 cookie_secret stored in plaintext (you can read the value from a file).
415 """
410 """
416 )
411 )
417 def _cookie_secret_default(self):
412 def _cookie_secret_default(self):
418 if os.path.exists(self.cookie_secret_file):
413 if os.path.exists(self.cookie_secret_file):
419 with io.open(self.cookie_secret_file, 'rb') as f:
414 with io.open(self.cookie_secret_file, 'rb') as f:
420 return f.read()
415 return f.read()
421 else:
416 else:
422 secret = base64.encodestring(os.urandom(1024))
417 secret = base64.encodestring(os.urandom(1024))
423 self._write_cookie_secret_file(secret)
418 self._write_cookie_secret_file(secret)
424 return secret
419 return secret
425
420
426 def _write_cookie_secret_file(self, secret):
421 def _write_cookie_secret_file(self, secret):
427 """write my secret to my secret_file"""
422 """write my secret to my secret_file"""
428 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
423 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
429 with io.open(self.cookie_secret_file, 'wb') as f:
424 with io.open(self.cookie_secret_file, 'wb') as f:
430 f.write(secret)
425 f.write(secret)
431 try:
426 try:
432 os.chmod(self.cookie_secret_file, 0o600)
427 os.chmod(self.cookie_secret_file, 0o600)
433 except OSError:
428 except OSError:
434 self.log.warn(
429 self.log.warn(
435 "Could not set permissions on %s",
430 "Could not set permissions on %s",
436 self.cookie_secret_file
431 self.cookie_secret_file
437 )
432 )
438
433
439 password = Unicode(u'', config=True,
434 password = Unicode(u'', config=True,
440 help="""Hashed password to use for web authentication.
435 help="""Hashed password to use for web authentication.
441
436
442 To generate, type in a python/IPython shell:
437 To generate, type in a python/IPython shell:
443
438
444 from IPython.lib import passwd; passwd()
439 from IPython.lib import passwd; passwd()
445
440
446 The string should be of the form type:salt:hashed-password.
441 The string should be of the form type:salt:hashed-password.
447 """
442 """
448 )
443 )
449
444
450 open_browser = Bool(True, config=True,
445 open_browser = Bool(True, config=True,
451 help="""Whether to open in a browser after starting.
446 help="""Whether to open in a browser after starting.
452 The specific browser used is platform dependent and
447 The specific browser used is platform dependent and
453 determined by the python standard library `webbrowser`
448 determined by the python standard library `webbrowser`
454 module, unless it is overridden using the --browser
449 module, unless it is overridden using the --browser
455 (NotebookApp.browser) configuration option.
450 (NotebookApp.browser) configuration option.
456 """)
451 """)
457
452
458 browser = Unicode(u'', config=True,
453 browser = Unicode(u'', config=True,
459 help="""Specify what command to use to invoke a web
454 help="""Specify what command to use to invoke a web
460 browser when opening the notebook. If not specified, the
455 browser when opening the notebook. If not specified, the
461 default browser will be determined by the `webbrowser`
456 default browser will be determined by the `webbrowser`
462 standard library module, which allows setting of the
457 standard library module, which allows setting of the
463 BROWSER environment variable to override it.
458 BROWSER environment variable to override it.
464 """)
459 """)
465
460
466 webapp_settings = Dict(config=True,
461 webapp_settings = Dict(config=True,
467 help="DEPRECATED, use tornado_settings"
462 help="DEPRECATED, use tornado_settings"
468 )
463 )
469 def _webapp_settings_changed(self, name, old, new):
464 def _webapp_settings_changed(self, name, old, new):
470 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
465 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
471 self.tornado_settings = new
466 self.tornado_settings = new
472
467
473 tornado_settings = Dict(config=True,
468 tornado_settings = Dict(config=True,
474 help="Supply overrides for the tornado.web.Application that the "
469 help="Supply overrides for the tornado.web.Application that the "
475 "IPython notebook uses.")
470 "IPython notebook uses.")
476
471
477 jinja_environment_options = Dict(config=True,
472 jinja_environment_options = Dict(config=True,
478 help="Supply extra arguments that will be passed to Jinja environment.")
473 help="Supply extra arguments that will be passed to Jinja environment.")
479
474
480
475
481 enable_mathjax = Bool(True, config=True,
476 enable_mathjax = Bool(True, config=True,
482 help="""Whether to enable MathJax for typesetting math/TeX
477 help="""Whether to enable MathJax for typesetting math/TeX
483
478
484 MathJax is the javascript library IPython uses to render math/LaTeX. It is
479 MathJax is the javascript library IPython uses to render math/LaTeX. It is
485 very large, so you may want to disable it if you have a slow internet
480 very large, so you may want to disable it if you have a slow internet
486 connection, or for offline use of the notebook.
481 connection, or for offline use of the notebook.
487
482
488 When disabled, equations etc. will appear as their untransformed TeX source.
483 When disabled, equations etc. will appear as their untransformed TeX source.
489 """
484 """
490 )
485 )
491 def _enable_mathjax_changed(self, name, old, new):
486 def _enable_mathjax_changed(self, name, old, new):
492 """set mathjax url to empty if mathjax is disabled"""
487 """set mathjax url to empty if mathjax is disabled"""
493 if not new:
488 if not new:
494 self.mathjax_url = u''
489 self.mathjax_url = u''
495
490
496 base_url = Unicode('/', config=True,
491 base_url = Unicode('/', config=True,
497 help='''The base URL for the notebook server.
492 help='''The base URL for the notebook server.
498
493
499 Leading and trailing slashes can be omitted,
494 Leading and trailing slashes can be omitted,
500 and will automatically be added.
495 and will automatically be added.
501 ''')
496 ''')
502 def _base_url_changed(self, name, old, new):
497 def _base_url_changed(self, name, old, new):
503 if not new.startswith('/'):
498 if not new.startswith('/'):
504 self.base_url = '/'+new
499 self.base_url = '/'+new
505 elif not new.endswith('/'):
500 elif not new.endswith('/'):
506 self.base_url = new+'/'
501 self.base_url = new+'/'
507
502
508 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
503 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
509 def _base_project_url_changed(self, name, old, new):
504 def _base_project_url_changed(self, name, old, new):
510 self.log.warn("base_project_url is deprecated, use base_url")
505 self.log.warn("base_project_url is deprecated, use base_url")
511 self.base_url = new
506 self.base_url = new
512
507
513 extra_static_paths = List(Unicode, config=True,
508 extra_static_paths = List(Unicode, config=True,
514 help="""Extra paths to search for serving static files.
509 help="""Extra paths to search for serving static files.
515
510
516 This allows adding javascript/css to be available from the notebook server machine,
511 This allows adding javascript/css to be available from the notebook server machine,
517 or overriding individual files in the IPython"""
512 or overriding individual files in the IPython"""
518 )
513 )
519 def _extra_static_paths_default(self):
514 def _extra_static_paths_default(self):
520 return [os.path.join(self.profile_dir.location, 'static')]
515 return [os.path.join(self.profile_dir.location, 'static')]
521
516
522 @property
517 @property
523 def static_file_path(self):
518 def static_file_path(self):
524 """return extra paths + the default location"""
519 """return extra paths + the default location"""
525 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
520 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
526
521
527 nbextensions_path = List(Unicode, config=True,
522 nbextensions_path = List(Unicode, config=True,
528 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
523 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
529 )
524 )
530 def _nbextensions_path_default(self):
525 def _nbextensions_path_default(self):
531 return [os.path.join(get_ipython_dir(), 'nbextensions')]
526 return [os.path.join(get_ipython_dir(), 'nbextensions')]
532
527
533 websocket_url = Unicode("", config=True,
528 websocket_url = Unicode("", config=True,
534 help="""The base URL for websockets,
529 help="""The base URL for websockets,
535 if it differs from the HTTP server (hint: it almost certainly doesn't).
530 if it differs from the HTTP server (hint: it almost certainly doesn't).
536
531
537 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
532 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
538 """
533 """
539 )
534 )
540 mathjax_url = Unicode("", config=True,
535 mathjax_url = Unicode("", config=True,
541 help="""The url for MathJax.js."""
536 help="""The url for MathJax.js."""
542 )
537 )
543 def _mathjax_url_default(self):
538 def _mathjax_url_default(self):
544 if not self.enable_mathjax:
539 if not self.enable_mathjax:
545 return u''
540 return u''
546 static_url_prefix = self.tornado_settings.get("static_url_prefix",
541 static_url_prefix = self.tornado_settings.get("static_url_prefix",
547 url_path_join(self.base_url, "static")
542 url_path_join(self.base_url, "static")
548 )
543 )
549
544
550 # try local mathjax, either in nbextensions/mathjax or static/mathjax
545 # try local mathjax, either in nbextensions/mathjax or static/mathjax
551 for (url_prefix, search_path) in [
546 for (url_prefix, search_path) in [
552 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
547 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
553 (static_url_prefix, self.static_file_path),
548 (static_url_prefix, self.static_file_path),
554 ]:
549 ]:
555 self.log.debug("searching for local mathjax in %s", search_path)
550 self.log.debug("searching for local mathjax in %s", search_path)
556 try:
551 try:
557 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
552 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
558 except IOError:
553 except IOError:
559 continue
554 continue
560 else:
555 else:
561 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
556 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
562 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
557 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
563 return url
558 return url
564
559
565 # no local mathjax, serve from CDN
560 # no local mathjax, serve from CDN
566 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
561 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
567 self.log.info("Using MathJax from CDN: %s", url)
562 self.log.info("Using MathJax from CDN: %s", url)
568 return url
563 return url
569
564
570 def _mathjax_url_changed(self, name, old, new):
565 def _mathjax_url_changed(self, name, old, new):
571 if new and not self.enable_mathjax:
566 if new and not self.enable_mathjax:
572 # enable_mathjax=False overrides mathjax_url
567 # enable_mathjax=False overrides mathjax_url
573 self.mathjax_url = u''
568 self.mathjax_url = u''
574 else:
569 else:
575 self.log.info("Using MathJax: %s", new)
570 self.log.info("Using MathJax: %s", new)
576
571
577 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
572 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
578 config=True,
573 config=True,
579 help='The notebook manager class to use.'
574 help='The notebook manager class to use.'
580 )
575 )
581 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
576 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
582 config=True,
577 config=True,
583 help='The kernel manager class to use.'
578 help='The kernel manager class to use.'
584 )
579 )
585 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
580 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
586 config=True,
581 config=True,
587 help='The session manager class to use.'
582 help='The session manager class to use.'
588 )
583 )
589 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
584 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
590 config=True,
585 config=True,
591 help='The cluster manager class to use.'
586 help='The cluster manager class to use.'
592 )
587 )
593
588
594 kernel_spec_manager = Instance(KernelSpecManager)
589 kernel_spec_manager = Instance(KernelSpecManager)
595
590
596 def _kernel_spec_manager_default(self):
591 def _kernel_spec_manager_default(self):
597 return KernelSpecManager(ipython_dir=self.ipython_dir)
592 return KernelSpecManager(ipython_dir=self.ipython_dir)
598
593
599 trust_xheaders = Bool(False, config=True,
594 trust_xheaders = Bool(False, config=True,
600 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
595 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
601 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
596 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
602 )
597 )
603
598
604 info_file = Unicode()
599 info_file = Unicode()
605
600
606 def _info_file_default(self):
601 def _info_file_default(self):
607 info_file = "nbserver-%s.json"%os.getpid()
602 info_file = "nbserver-%s.json"%os.getpid()
608 return os.path.join(self.profile_dir.security_dir, info_file)
603 return os.path.join(self.profile_dir.security_dir, info_file)
609
604
610 notebook_dir = Unicode(py3compat.getcwd(), config=True,
605 notebook_dir = Unicode(py3compat.getcwd(), config=True,
611 help="The directory to use for notebooks and kernels."
606 help="The directory to use for notebooks and kernels."
612 )
607 )
613
608
614 pylab = Unicode('disabled', config=True,
609 pylab = Unicode('disabled', config=True,
615 help="""
610 help="""
616 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
611 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
617 """
612 """
618 )
613 )
619 def _pylab_changed(self, name, old, new):
614 def _pylab_changed(self, name, old, new):
620 """when --pylab is specified, display a warning and exit"""
615 """when --pylab is specified, display a warning and exit"""
621 if new != 'warn':
616 if new != 'warn':
622 backend = ' %s' % new
617 backend = ' %s' % new
623 else:
618 else:
624 backend = ''
619 backend = ''
625 self.log.error("Support for specifying --pylab on the command line has been removed.")
620 self.log.error("Support for specifying --pylab on the command line has been removed.")
626 self.log.error(
621 self.log.error(
627 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
622 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
628 )
623 )
629 self.exit(1)
624 self.exit(1)
630
625
631 def _notebook_dir_changed(self, name, old, new):
626 def _notebook_dir_changed(self, name, old, new):
632 """Do a bit of validation of the notebook dir."""
627 """Do a bit of validation of the notebook dir."""
633 if not os.path.isabs(new):
628 if not os.path.isabs(new):
634 # If we receive a non-absolute path, make it absolute.
629 # If we receive a non-absolute path, make it absolute.
635 self.notebook_dir = os.path.abspath(new)
630 self.notebook_dir = os.path.abspath(new)
636 return
631 return
637 if not os.path.isdir(new):
632 if not os.path.isdir(new):
638 raise TraitError("No such notebook dir: %r" % new)
633 raise TraitError("No such notebook dir: %r" % new)
639
634
640 # setting App.notebook_dir implies setting notebook and kernel dirs as well
635 # setting App.notebook_dir implies setting notebook and kernel dirs as well
641 self.config.FileContentsManager.root_dir = new
636 self.config.FileContentsManager.root_dir = new
642 self.config.MappingKernelManager.root_dir = new
637 self.config.MappingKernelManager.root_dir = new
643
638
644
639
645 def parse_command_line(self, argv=None):
640 def parse_command_line(self, argv=None):
646 super(NotebookApp, self).parse_command_line(argv)
641 super(NotebookApp, self).parse_command_line(argv)
647
642
648 if self.extra_args:
643 if self.extra_args:
649 arg0 = self.extra_args[0]
644 arg0 = self.extra_args[0]
650 f = os.path.abspath(arg0)
645 f = os.path.abspath(arg0)
651 self.argv.remove(arg0)
646 self.argv.remove(arg0)
652 if not os.path.exists(f):
647 if not os.path.exists(f):
653 self.log.critical("No such file or directory: %s", f)
648 self.log.critical("No such file or directory: %s", f)
654 self.exit(1)
649 self.exit(1)
655
650
656 # Use config here, to ensure that it takes higher priority than
651 # Use config here, to ensure that it takes higher priority than
657 # anything that comes from the profile.
652 # anything that comes from the profile.
658 c = Config()
653 c = Config()
659 if os.path.isdir(f):
654 if os.path.isdir(f):
660 c.NotebookApp.notebook_dir = f
655 c.NotebookApp.notebook_dir = f
661 elif os.path.isfile(f):
656 elif os.path.isfile(f):
662 c.NotebookApp.file_to_run = f
657 c.NotebookApp.file_to_run = f
663 self.update_config(c)
658 self.update_config(c)
664
659
665 def init_kernel_argv(self):
660 def init_kernel_argv(self):
666 """construct the kernel arguments"""
661 """construct the kernel arguments"""
667 # Kernel should get *absolute* path to profile directory
662 # Kernel should get *absolute* path to profile directory
668 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
663 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
669
664
670 def init_configurables(self):
665 def init_configurables(self):
671 # force Session default to be secure
666 # force Session default to be secure
672 default_secure(self.config)
667 default_secure(self.config)
673 kls = import_item(self.kernel_manager_class)
668 kls = import_item(self.kernel_manager_class)
674 self.kernel_manager = kls(
669 self.kernel_manager = kls(
675 parent=self, log=self.log, kernel_argv=self.kernel_argv,
670 parent=self, log=self.log, kernel_argv=self.kernel_argv,
676 connection_dir = self.profile_dir.security_dir,
671 connection_dir = self.profile_dir.security_dir,
677 )
672 )
678 kls = import_item(self.contents_manager_class)
673 kls = import_item(self.contents_manager_class)
679 self.contents_manager = kls(parent=self, log=self.log)
674 self.contents_manager = kls(parent=self, log=self.log)
680 kls = import_item(self.session_manager_class)
675 kls = import_item(self.session_manager_class)
681 self.session_manager = kls(parent=self, log=self.log,
676 self.session_manager = kls(parent=self, log=self.log,
682 kernel_manager=self.kernel_manager,
677 kernel_manager=self.kernel_manager,
683 contents_manager=self.contents_manager)
678 contents_manager=self.contents_manager)
684 kls = import_item(self.cluster_manager_class)
679 kls = import_item(self.cluster_manager_class)
685 self.cluster_manager = kls(parent=self, log=self.log)
680 self.cluster_manager = kls(parent=self, log=self.log)
686 self.cluster_manager.update_profiles()
681 self.cluster_manager.update_profiles()
687
682
688 def init_logging(self):
683 def init_logging(self):
689 # This prevents double log messages because tornado use a root logger that
684 # This prevents double log messages because tornado use a root logger that
690 # self.log is a child of. The logging module dipatches log messages to a log
685 # self.log is a child of. The logging module dipatches log messages to a log
691 # and all of its ancenstors until propagate is set to False.
686 # and all of its ancenstors until propagate is set to False.
692 self.log.propagate = False
687 self.log.propagate = False
693
688
694 for log in app_log, access_log, gen_log:
689 for log in app_log, access_log, gen_log:
695 # consistent log output name (NotebookApp instead of tornado.access, etc.)
690 # consistent log output name (NotebookApp instead of tornado.access, etc.)
696 log.name = self.log.name
691 log.name = self.log.name
697 # hook up tornado 3's loggers to our app handlers
692 # hook up tornado 3's loggers to our app handlers
698 logger = logging.getLogger('tornado')
693 logger = logging.getLogger('tornado')
699 logger.propagate = True
694 logger.propagate = True
700 logger.parent = self.log
695 logger.parent = self.log
701 logger.setLevel(self.log.level)
696 logger.setLevel(self.log.level)
702
697
703 def init_webapp(self):
698 def init_webapp(self):
704 """initialize tornado webapp and httpserver"""
699 """initialize tornado webapp and httpserver"""
705 self.tornado_settings['allow_origin'] = self.allow_origin
700 self.tornado_settings['allow_origin'] = self.allow_origin
706 if self.allow_origin_pat:
701 if self.allow_origin_pat:
707 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
702 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
708 self.tornado_settings['allow_credentials'] = self.allow_credentials
703 self.tornado_settings['allow_credentials'] = self.allow_credentials
709
704
710 self.web_app = NotebookWebApplication(
705 self.web_app = NotebookWebApplication(
711 self, self.kernel_manager, self.contents_manager,
706 self, self.kernel_manager, self.contents_manager,
712 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
707 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
713 self.log, self.base_url, self.default_url, self.tornado_settings,
708 self.log, self.base_url, self.default_url, self.tornado_settings,
714 self.jinja_environment_options
709 self.jinja_environment_options
715 )
710 )
716 if self.certfile:
711 if self.certfile:
717 ssl_options = dict(certfile=self.certfile)
712 ssl_options = dict(certfile=self.certfile)
718 if self.keyfile:
713 if self.keyfile:
719 ssl_options['keyfile'] = self.keyfile
714 ssl_options['keyfile'] = self.keyfile
720 else:
715 else:
721 ssl_options = None
716 ssl_options = None
722 self.web_app.password = self.password
717 self.web_app.password = self.password
723 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
718 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
724 xheaders=self.trust_xheaders)
719 xheaders=self.trust_xheaders)
725 if not self.ip:
720 if not self.ip:
726 warning = "WARNING: The notebook server is listening on all IP addresses"
721 warning = "WARNING: The notebook server is listening on all IP addresses"
727 if ssl_options is None:
722 if ssl_options is None:
728 self.log.critical(warning + " and not using encryption. This "
723 self.log.critical(warning + " and not using encryption. This "
729 "is not recommended.")
724 "is not recommended.")
730 if not self.password:
725 if not self.password:
731 self.log.critical(warning + " and not using authentication. "
726 self.log.critical(warning + " and not using authentication. "
732 "This is highly insecure and not recommended.")
727 "This is highly insecure and not recommended.")
733 success = None
728 success = None
734 for port in random_ports(self.port, self.port_retries+1):
729 for port in random_ports(self.port, self.port_retries+1):
735 try:
730 try:
736 self.http_server.listen(port, self.ip)
731 self.http_server.listen(port, self.ip)
737 except socket.error as e:
732 except socket.error as e:
738 if e.errno == errno.EADDRINUSE:
733 if e.errno == errno.EADDRINUSE:
739 self.log.info('The port %i is already in use, trying another random port.' % port)
734 self.log.info('The port %i is already in use, trying another random port.' % port)
740 continue
735 continue
741 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
736 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
742 self.log.warn("Permission to listen on port %i denied" % port)
737 self.log.warn("Permission to listen on port %i denied" % port)
743 continue
738 continue
744 else:
739 else:
745 raise
740 raise
746 else:
741 else:
747 self.port = port
742 self.port = port
748 success = True
743 success = True
749 break
744 break
750 if not success:
745 if not success:
751 self.log.critical('ERROR: the notebook server could not be started because '
746 self.log.critical('ERROR: the notebook server could not be started because '
752 'no available port could be found.')
747 'no available port could be found.')
753 self.exit(1)
748 self.exit(1)
754
749
755 @property
750 @property
756 def display_url(self):
751 def display_url(self):
757 ip = self.ip if self.ip else '[all ip addresses on your system]'
752 ip = self.ip if self.ip else '[all ip addresses on your system]'
758 return self._url(ip)
753 return self._url(ip)
759
754
760 @property
755 @property
761 def connection_url(self):
756 def connection_url(self):
762 ip = self.ip if self.ip else 'localhost'
757 ip = self.ip if self.ip else 'localhost'
763 return self._url(ip)
758 return self._url(ip)
764
759
765 def _url(self, ip):
760 def _url(self, ip):
766 proto = 'https' if self.certfile else 'http'
761 proto = 'https' if self.certfile else 'http'
767 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
762 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
768
763
769 def init_signal(self):
764 def init_signal(self):
770 if not sys.platform.startswith('win'):
765 if not sys.platform.startswith('win'):
771 signal.signal(signal.SIGINT, self._handle_sigint)
766 signal.signal(signal.SIGINT, self._handle_sigint)
772 signal.signal(signal.SIGTERM, self._signal_stop)
767 signal.signal(signal.SIGTERM, self._signal_stop)
773 if hasattr(signal, 'SIGUSR1'):
768 if hasattr(signal, 'SIGUSR1'):
774 # Windows doesn't support SIGUSR1
769 # Windows doesn't support SIGUSR1
775 signal.signal(signal.SIGUSR1, self._signal_info)
770 signal.signal(signal.SIGUSR1, self._signal_info)
776 if hasattr(signal, 'SIGINFO'):
771 if hasattr(signal, 'SIGINFO'):
777 # only on BSD-based systems
772 # only on BSD-based systems
778 signal.signal(signal.SIGINFO, self._signal_info)
773 signal.signal(signal.SIGINFO, self._signal_info)
779
774
780 def _handle_sigint(self, sig, frame):
775 def _handle_sigint(self, sig, frame):
781 """SIGINT handler spawns confirmation dialog"""
776 """SIGINT handler spawns confirmation dialog"""
782 # register more forceful signal handler for ^C^C case
777 # register more forceful signal handler for ^C^C case
783 signal.signal(signal.SIGINT, self._signal_stop)
778 signal.signal(signal.SIGINT, self._signal_stop)
784 # request confirmation dialog in bg thread, to avoid
779 # request confirmation dialog in bg thread, to avoid
785 # blocking the App
780 # blocking the App
786 thread = threading.Thread(target=self._confirm_exit)
781 thread = threading.Thread(target=self._confirm_exit)
787 thread.daemon = True
782 thread.daemon = True
788 thread.start()
783 thread.start()
789
784
790 def _restore_sigint_handler(self):
785 def _restore_sigint_handler(self):
791 """callback for restoring original SIGINT handler"""
786 """callback for restoring original SIGINT handler"""
792 signal.signal(signal.SIGINT, self._handle_sigint)
787 signal.signal(signal.SIGINT, self._handle_sigint)
793
788
794 def _confirm_exit(self):
789 def _confirm_exit(self):
795 """confirm shutdown on ^C
790 """confirm shutdown on ^C
796
791
797 A second ^C, or answering 'y' within 5s will cause shutdown,
792 A second ^C, or answering 'y' within 5s will cause shutdown,
798 otherwise original SIGINT handler will be restored.
793 otherwise original SIGINT handler will be restored.
799
794
800 This doesn't work on Windows.
795 This doesn't work on Windows.
801 """
796 """
802 info = self.log.info
797 info = self.log.info
803 info('interrupted')
798 info('interrupted')
804 print(self.notebook_info())
799 print(self.notebook_info())
805 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
800 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
806 sys.stdout.flush()
801 sys.stdout.flush()
807 r,w,x = select.select([sys.stdin], [], [], 5)
802 r,w,x = select.select([sys.stdin], [], [], 5)
808 if r:
803 if r:
809 line = sys.stdin.readline()
804 line = sys.stdin.readline()
810 if line.lower().startswith('y') and 'n' not in line.lower():
805 if line.lower().startswith('y') and 'n' not in line.lower():
811 self.log.critical("Shutdown confirmed")
806 self.log.critical("Shutdown confirmed")
812 ioloop.IOLoop.instance().stop()
807 ioloop.IOLoop.instance().stop()
813 return
808 return
814 else:
809 else:
815 print("No answer for 5s:", end=' ')
810 print("No answer for 5s:", end=' ')
816 print("resuming operation...")
811 print("resuming operation...")
817 # no answer, or answer is no:
812 # no answer, or answer is no:
818 # set it back to original SIGINT handler
813 # set it back to original SIGINT handler
819 # use IOLoop.add_callback because signal.signal must be called
814 # use IOLoop.add_callback because signal.signal must be called
820 # from main thread
815 # from main thread
821 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
816 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
822
817
823 def _signal_stop(self, sig, frame):
818 def _signal_stop(self, sig, frame):
824 self.log.critical("received signal %s, stopping", sig)
819 self.log.critical("received signal %s, stopping", sig)
825 ioloop.IOLoop.instance().stop()
820 ioloop.IOLoop.instance().stop()
826
821
827 def _signal_info(self, sig, frame):
822 def _signal_info(self, sig, frame):
828 print(self.notebook_info())
823 print(self.notebook_info())
829
824
830 def init_components(self):
825 def init_components(self):
831 """Check the components submodule, and warn if it's unclean"""
826 """Check the components submodule, and warn if it's unclean"""
832 status = submodule.check_submodule_status()
827 status = submodule.check_submodule_status()
833 if status == 'missing':
828 if status == 'missing':
834 self.log.warn("components submodule missing, running `git submodule update`")
829 self.log.warn("components submodule missing, running `git submodule update`")
835 submodule.update_submodules(submodule.ipython_parent())
830 submodule.update_submodules(submodule.ipython_parent())
836 elif status == 'unclean':
831 elif status == 'unclean':
837 self.log.warn("components submodule unclean, you may see 404s on static/components")
832 self.log.warn("components submodule unclean, you may see 404s on static/components")
838 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
833 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
839
834
840 @catch_config_error
835 @catch_config_error
841 def initialize(self, argv=None):
836 def initialize(self, argv=None):
842 super(NotebookApp, self).initialize(argv)
837 super(NotebookApp, self).initialize(argv)
843 self.init_logging()
838 self.init_logging()
844 self.init_kernel_argv()
839 self.init_kernel_argv()
845 self.init_configurables()
840 self.init_configurables()
846 self.init_components()
841 self.init_components()
847 self.init_webapp()
842 self.init_webapp()
848 self.init_signal()
843 self.init_signal()
849
844
850 def cleanup_kernels(self):
845 def cleanup_kernels(self):
851 """Shutdown all kernels.
846 """Shutdown all kernels.
852
847
853 The kernels will shutdown themselves when this process no longer exists,
848 The kernels will shutdown themselves when this process no longer exists,
854 but explicit shutdown allows the KernelManagers to cleanup the connection files.
849 but explicit shutdown allows the KernelManagers to cleanup the connection files.
855 """
850 """
856 self.log.info('Shutting down kernels')
851 self.log.info('Shutting down kernels')
857 self.kernel_manager.shutdown_all()
852 self.kernel_manager.shutdown_all()
858
853
859 def notebook_info(self):
854 def notebook_info(self):
860 "Return the current working directory and the server url information"
855 "Return the current working directory and the server url information"
861 info = self.contents_manager.info_string() + "\n"
856 info = self.contents_manager.info_string() + "\n"
862 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
857 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
863 return info + "The IPython Notebook is running at: %s" % self.display_url
858 return info + "The IPython Notebook is running at: %s" % self.display_url
864
859
865 def server_info(self):
860 def server_info(self):
866 """Return a JSONable dict of information about this server."""
861 """Return a JSONable dict of information about this server."""
867 return {'url': self.connection_url,
862 return {'url': self.connection_url,
868 'hostname': self.ip if self.ip else 'localhost',
863 'hostname': self.ip if self.ip else 'localhost',
869 'port': self.port,
864 'port': self.port,
870 'secure': bool(self.certfile),
865 'secure': bool(self.certfile),
871 'base_url': self.base_url,
866 'base_url': self.base_url,
872 'notebook_dir': os.path.abspath(self.notebook_dir),
867 'notebook_dir': os.path.abspath(self.notebook_dir),
873 'pid': os.getpid()
868 'pid': os.getpid()
874 }
869 }
875
870
876 def write_server_info_file(self):
871 def write_server_info_file(self):
877 """Write the result of server_info() to the JSON file info_file."""
872 """Write the result of server_info() to the JSON file info_file."""
878 with open(self.info_file, 'w') as f:
873 with open(self.info_file, 'w') as f:
879 json.dump(self.server_info(), f, indent=2)
874 json.dump(self.server_info(), f, indent=2)
880
875
881 def remove_server_info_file(self):
876 def remove_server_info_file(self):
882 """Remove the nbserver-<pid>.json file created for this server.
877 """Remove the nbserver-<pid>.json file created for this server.
883
878
884 Ignores the error raised when the file has already been removed.
879 Ignores the error raised when the file has already been removed.
885 """
880 """
886 try:
881 try:
887 os.unlink(self.info_file)
882 os.unlink(self.info_file)
888 except OSError as e:
883 except OSError as e:
889 if e.errno != errno.ENOENT:
884 if e.errno != errno.ENOENT:
890 raise
885 raise
891
886
892 def start(self):
887 def start(self):
893 """ Start the IPython Notebook server app, after initialization
888 """ Start the IPython Notebook server app, after initialization
894
889
895 This method takes no arguments so all configuration and initialization
890 This method takes no arguments so all configuration and initialization
896 must be done prior to calling this method."""
891 must be done prior to calling this method."""
897 if self.subapp is not None:
892 if self.subapp is not None:
898 return self.subapp.start()
893 return self.subapp.start()
899
894
900 info = self.log.info
895 info = self.log.info
901 for line in self.notebook_info().split("\n"):
896 for line in self.notebook_info().split("\n"):
902 info(line)
897 info(line)
903 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
898 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
904
899
905 self.write_server_info_file()
900 self.write_server_info_file()
906
901
907 if self.open_browser or self.file_to_run:
902 if self.open_browser or self.file_to_run:
908 try:
903 try:
909 browser = webbrowser.get(self.browser or None)
904 browser = webbrowser.get(self.browser or None)
910 except webbrowser.Error as e:
905 except webbrowser.Error as e:
911 self.log.warn('No web browser found: %s.' % e)
906 self.log.warn('No web browser found: %s.' % e)
912 browser = None
907 browser = None
913
908
914 if self.file_to_run:
909 if self.file_to_run:
915 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
910 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
916 if not os.path.exists(fullpath):
911 if not os.path.exists(fullpath):
917 self.log.critical("%s does not exist" % fullpath)
912 self.log.critical("%s does not exist" % fullpath)
918 self.exit(1)
913 self.exit(1)
919
914
920 uri = url_path_join('notebooks', self.file_to_run)
915 uri = url_path_join('notebooks', self.file_to_run)
921 else:
916 else:
922 uri = 'tree'
917 uri = 'tree'
923 if browser:
918 if browser:
924 b = lambda : browser.open(url_path_join(self.connection_url, uri),
919 b = lambda : browser.open(url_path_join(self.connection_url, uri),
925 new=2)
920 new=2)
926 threading.Thread(target=b).start()
921 threading.Thread(target=b).start()
927 try:
922 try:
928 ioloop.IOLoop.instance().start()
923 ioloop.IOLoop.instance().start()
929 except KeyboardInterrupt:
924 except KeyboardInterrupt:
930 info("Interrupted...")
925 info("Interrupted...")
931 finally:
926 finally:
932 self.cleanup_kernels()
927 self.cleanup_kernels()
933 self.remove_server_info_file()
928 self.remove_server_info_file()
934
929
935
930
936 def list_running_servers(profile='default'):
931 def list_running_servers(profile='default'):
937 """Iterate over the server info files of running notebook servers.
932 """Iterate over the server info files of running notebook servers.
938
933
939 Given a profile name, find nbserver-* files in the security directory of
934 Given a profile name, find nbserver-* files in the security directory of
940 that profile, and yield dicts of their information, each one pertaining to
935 that profile, and yield dicts of their information, each one pertaining to
941 a currently running notebook server instance.
936 a currently running notebook server instance.
942 """
937 """
943 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
938 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
944 for file in os.listdir(pd.security_dir):
939 for file in os.listdir(pd.security_dir):
945 if file.startswith('nbserver-'):
940 if file.startswith('nbserver-'):
946 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
941 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
947 info = json.load(f)
942 info = json.load(f)
948
943
949 # Simple check whether that process is really still running
944 # Simple check whether that process is really still running
950 if check_pid(info['pid']):
945 if check_pid(info['pid']):
951 yield info
946 yield info
952 else:
947 else:
953 # If the process has died, try to delete its info file
948 # If the process has died, try to delete its info file
954 try:
949 try:
955 os.unlink(file)
950 os.unlink(file)
956 except OSError:
951 except OSError:
957 pass # TODO: This should warn or log or something
952 pass # TODO: This should warn or log or something
958 #-----------------------------------------------------------------------------
953 #-----------------------------------------------------------------------------
959 # Main entry point
954 # Main entry point
960 #-----------------------------------------------------------------------------
955 #-----------------------------------------------------------------------------
961
956
962 launch_new_instance = NotebookApp.launch_instance
957 launch_new_instance = NotebookApp.launch_instance
963
958
@@ -1,85 +1,135 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Test the /files/ handler."""
2 """Test the /files/ handler."""
3
3
4 import io
4 import io
5 import os
5 import os
6 from unicodedata import normalize
6 from unicodedata import normalize
7
7
8 pjoin = os.path.join
8 pjoin = os.path.join
9
9
10 import requests
10 import requests
11 import json
12
13 from IPython.nbformat.current import (new_notebook, write, new_worksheet,
14 new_heading_cell, new_code_cell,
15 new_output)
11
16
12 from IPython.html.utils import url_path_join
17 from IPython.html.utils import url_path_join
13 from .launchnotebook import NotebookTestBase
18 from .launchnotebook import NotebookTestBase
14 from IPython.utils import py3compat
19 from IPython.utils import py3compat
15
20
21
16 class FilesTest(NotebookTestBase):
22 class FilesTest(NotebookTestBase):
17 def test_hidden_files(self):
23 def test_hidden_files(self):
18 not_hidden = [
24 not_hidden = [
19 u'å b',
25 u'å b',
20 u'å b/ç. d',
26 u'å b/ç. d',
21 ]
27 ]
22 hidden = [
28 hidden = [
23 u'.å b',
29 u'.å b',
24 u'å b/.ç d',
30 u'å b/.ç d',
25 ]
31 ]
26 dirs = not_hidden + hidden
32 dirs = not_hidden + hidden
27
33
28 nbdir = self.notebook_dir.name
34 nbdir = self.notebook_dir.name
29 for d in dirs:
35 for d in dirs:
30 path = pjoin(nbdir, d.replace('/', os.sep))
36 path = pjoin(nbdir, d.replace('/', os.sep))
31 if not os.path.exists(path):
37 if not os.path.exists(path):
32 os.mkdir(path)
38 os.mkdir(path)
33 with open(pjoin(path, 'foo'), 'w') as f:
39 with open(pjoin(path, 'foo'), 'w') as f:
34 f.write('foo')
40 f.write('foo')
35 with open(pjoin(path, '.foo'), 'w') as f:
41 with open(pjoin(path, '.foo'), 'w') as f:
36 f.write('.foo')
42 f.write('.foo')
37 url = self.base_url()
43 url = self.base_url()
38
44
39 for d in not_hidden:
45 for d in not_hidden:
40 path = pjoin(nbdir, d.replace('/', os.sep))
46 path = pjoin(nbdir, d.replace('/', os.sep))
41 r = requests.get(url_path_join(url, 'files', d, 'foo'))
47 r = requests.get(url_path_join(url, 'files', d, 'foo'))
42 r.raise_for_status()
48 r.raise_for_status()
43 self.assertEqual(r.text, 'foo')
49 self.assertEqual(r.text, 'foo')
44 r = requests.get(url_path_join(url, 'files', d, '.foo'))
50 r = requests.get(url_path_join(url, 'files', d, '.foo'))
45 self.assertEqual(r.status_code, 404)
51 self.assertEqual(r.status_code, 404)
46
52
47 for d in hidden:
53 for d in hidden:
48 path = pjoin(nbdir, d.replace('/', os.sep))
54 path = pjoin(nbdir, d.replace('/', os.sep))
49 for foo in ('foo', '.foo'):
55 for foo in ('foo', '.foo'):
50 r = requests.get(url_path_join(url, 'files', d, foo))
56 r = requests.get(url_path_join(url, 'files', d, foo))
51 self.assertEqual(r.status_code, 404)
57 self.assertEqual(r.status_code, 404)
52
58
59 def test_contents_manager(self):
60 "make sure ContentsManager returns right files (ipynb, bin, txt)."
61
62 nbdir = self.notebook_dir.name
63 base = self.base_url()
64
65 nb = new_notebook(name='testnb')
66
67 ws = new_worksheet()
68 nb.worksheets = [ws]
69 ws.cells.append(new_heading_cell(u'Created by test ³'))
70 cc1 = new_code_cell(input=u'print(2*6)')
71 cc1.outputs.append(new_output(output_text=u'12', output_type='stream'))
72 ws.cells.append(cc1)
73
74 with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w',
75 encoding='utf-8') as f:
76 write(nb, f, format='ipynb')
77
78 with io.open(pjoin(nbdir, 'test.bin'), 'wb') as f:
79 f.write(b'\xff' + os.urandom(5))
80 f.close()
81
82 with io.open(pjoin(nbdir, 'test.txt'), 'w') as f:
83 f.write(u'foobar')
84 f.close()
85
86 r = requests.get(url_path_join(base, 'files', 'testnb.ipynb'))
87 self.assertEqual(r.status_code, 200)
88 self.assertIn('print(2*6)', r.text)
89 json.loads(r.text)
90
91 r = requests.get(url_path_join(base, 'files', 'test.bin'))
92 self.assertEqual(r.status_code, 200)
93 self.assertEqual(r.headers['content-type'], 'application/octet-stream')
94 self.assertEqual(r.content[:1], b'\xff')
95 self.assertEqual(len(r.content), 6)
96
97 r = requests.get(url_path_join(base, 'files', 'test.txt'))
98 self.assertEqual(r.status_code, 200)
99 self.assertEqual(r.headers['content-type'], 'text/plain')
100 self.assertEqual(r.text, 'foobar')
101
102
53 def test_old_files_redirect(self):
103 def test_old_files_redirect(self):
54 """pre-2.0 'files/' prefixed links are properly redirected"""
104 """pre-2.0 'files/' prefixed links are properly redirected"""
55 nbdir = self.notebook_dir.name
105 nbdir = self.notebook_dir.name
56 base = self.base_url()
106 base = self.base_url()
57
107
58 os.mkdir(pjoin(nbdir, 'files'))
108 os.mkdir(pjoin(nbdir, 'files'))
59 os.makedirs(pjoin(nbdir, 'sub', 'files'))
109 os.makedirs(pjoin(nbdir, 'sub', 'files'))
60
110
61 for prefix in ('', 'sub'):
111 for prefix in ('', 'sub'):
62 with open(pjoin(nbdir, prefix, 'files', 'f1.txt'), 'w') as f:
112 with open(pjoin(nbdir, prefix, 'files', 'f1.txt'), 'w') as f:
63 f.write(prefix + '/files/f1')
113 f.write(prefix + '/files/f1')
64 with open(pjoin(nbdir, prefix, 'files', 'f2.txt'), 'w') as f:
114 with open(pjoin(nbdir, prefix, 'files', 'f2.txt'), 'w') as f:
65 f.write(prefix + '/files/f2')
115 f.write(prefix + '/files/f2')
66 with open(pjoin(nbdir, prefix, 'f2.txt'), 'w') as f:
116 with open(pjoin(nbdir, prefix, 'f2.txt'), 'w') as f:
67 f.write(prefix + '/f2')
117 f.write(prefix + '/f2')
68 with open(pjoin(nbdir, prefix, 'f3.txt'), 'w') as f:
118 with open(pjoin(nbdir, prefix, 'f3.txt'), 'w') as f:
69 f.write(prefix + '/f3')
119 f.write(prefix + '/f3')
70
120
71 url = url_path_join(base, 'notebooks', prefix, 'files', 'f1.txt')
121 url = url_path_join(base, 'notebooks', prefix, 'files', 'f1.txt')
72 r = requests.get(url)
122 r = requests.get(url)
73 self.assertEqual(r.status_code, 200)
123 self.assertEqual(r.status_code, 200)
74 self.assertEqual(r.text, prefix + '/files/f1')
124 self.assertEqual(r.text, prefix + '/files/f1')
75
125
76 url = url_path_join(base, 'notebooks', prefix, 'files', 'f2.txt')
126 url = url_path_join(base, 'notebooks', prefix, 'files', 'f2.txt')
77 r = requests.get(url)
127 r = requests.get(url)
78 self.assertEqual(r.status_code, 200)
128 self.assertEqual(r.status_code, 200)
79 self.assertEqual(r.text, prefix + '/files/f2')
129 self.assertEqual(r.text, prefix + '/files/f2')
80
130
81 url = url_path_join(base, 'notebooks', prefix, 'files', 'f3.txt')
131 url = url_path_join(base, 'notebooks', prefix, 'files', 'f3.txt')
82 r = requests.get(url)
132 r = requests.get(url)
83 self.assertEqual(r.status_code, 200)
133 self.assertEqual(r.status_code, 200)
84 self.assertEqual(r.text, prefix + '/f3')
134 self.assertEqual(r.text, prefix + '/f3')
85
135
General Comments 0
You need to be logged in to leave comments. Login now