##// END OF EJS Templates
allow system-wide paths for nbextensions...
Min RK -
Show More
@@ -1,265 +1,327 b''
1 1 # coding: utf-8
2 2 """Utilities for installing Javascript extensions for the notebook"""
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 os
10 10 import shutil
11 import sys
11 12 import tarfile
12 13 import zipfile
13 14 from os.path import basename, join as pjoin
14 15
15 16 # Deferred imports
16 17 try:
17 18 from urllib.parse import urlparse # Py3
18 19 from urllib.request import urlretrieve
19 20 except ImportError:
20 21 from urlparse import urlparse
21 22 from urllib import urlretrieve
22 23
23 24 from IPython.utils.path import get_ipython_dir, ensure_dir_exists
24 25 from IPython.utils.py3compat import string_types, cast_unicode_py2
25 26 from IPython.utils.tempdir import TemporaryDirectory
26 27
27 28
29 # Packagers: modify the next block if you store system-installed nbextensions elsewhere (unlikely)
30 SYSTEM_NBEXTENSIONS_DIRS = []
31
32 if os.name == 'nt':
33 programdata = os.environ.get('PROGRAMDATA', None)
34 if programdata: # PROGRAMDATA is not defined by default on XP.
35 SYSTEM_NBEXTENSIONS_DIRS = [pjoin(programdata, 'jupyter', 'nbextensions')]
36 prefixes = []
37 else:
38 prefixes = ['/usr/local', '/usr']
39
40 # add sys.prefix at the front
41 if sys.prefix not in prefixes:
42 prefixes.insert(0, sys.prefix)
43
44 for prefix in prefixes:
45 nbext = os.path.join(prefix, 'share', 'jupyter', 'nbextensions')
46 if nbext not in SYSTEM_NBEXTENSIONS_DIRS:
47 SYSTEM_NBEXTENSIONS_DIRS.append(nbext)
48
49 if os.name == 'nt':
50 # PROGRAMDATA
51 SYSTEM_NBEXTENSIONS_INSTALL_DIR = SYSTEM_NBEXTENSIONS_DIRS[-1]
52 else:
53 # /usr/local
54 SYSTEM_NBEXTENSIONS_INSTALL_DIR = SYSTEM_NBEXTENSIONS_DIRS[-2]
55
56
28 57 def _should_copy(src, dest, verbose=1):
29 58 """should a file be copied?"""
30 59 if not os.path.exists(dest):
31 60 return True
32 61 if os.stat(dest).st_mtime < os.stat(src).st_mtime:
33 62 if verbose >= 2:
34 63 print("%s is out of date" % dest)
35 64 return True
36 65 if verbose >= 2:
37 66 print("%s is up to date" % dest)
38 67 return False
39 68
40 69
41 70 def _maybe_copy(src, dest, verbose=1):
42 71 """copy a file if it needs updating"""
43 72 if _should_copy(src, dest, verbose):
44 73 if verbose >= 1:
45 74 print("copying %s -> %s" % (src, dest))
46 75 shutil.copy2(src, dest)
47 76
48 77
49 78 def _safe_is_tarfile(path):
50 79 """safe version of is_tarfile, return False on IOError"""
51 80 try:
52 81 return tarfile.is_tarfile(path)
53 82 except IOError:
54 83 return False
55 84
56 85
57 def check_nbextension(files, ipython_dir=None):
86 def check_nbextension(files, nbextensions=None):
58 87 """Check whether nbextension files have been installed
59 88
60 89 files should be a list of relative paths within nbextensions.
61 90
62 91 Returns True if all files are found, False if any are missing.
63 92 """
64 ipython_dir = ipython_dir or get_ipython_dir()
65 nbext = pjoin(ipython_dir, u'nbextensions')
93 if nbextensions:
94 nbext = nbextensions
95 else:
96 nbext = pjoin(get_ipython_dir(), u'nbextensions')
66 97 # make sure nbextensions dir exists
67 98 if not os.path.exists(nbext):
68 99 return False
69 100
70 101 if isinstance(files, string_types):
71 102 # one file given, turn it into a list
72 103 files = [files]
73 104
74 105 return all(os.path.exists(pjoin(nbext, f)) for f in files)
75 106
76 107
77 def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None, verbose=1):
108 def install_nbextension(files, overwrite=False, symlink=False, user=False, prefix=None, nbextensions=None, verbose=1):
78 109 """Install a Javascript extension for the notebook
79 110
80 111 Stages files and/or directories into IPYTHONDIR/nbextensions.
81 112 By default, this compares modification time, and only stages files that need updating.
82 113 If `overwrite` is specified, matching files are purged before proceeding.
83 114
84 115 Parameters
85 116 ----------
86 117
87 118 files : list(paths or URLs)
88 119 One or more paths or URLs to existing files directories to install.
89 120 These will be installed with their base name, so '/path/to/foo'
90 121 will install to 'nbextensions/foo'.
91 122 Archives (zip or tarballs) will be extracted into the nbextensions directory.
92 123 overwrite : bool [default: False]
93 124 If True, always install the files, regardless of what may already be installed.
94 125 symlink : bool [default: False]
95 126 If True, create a symlink in nbextensions, rather than copying files.
96 127 Not allowed with URLs or archives. Windows support for symlinks requires
97 128 Vista or above, Python 3, and a permission bit which only admin users
98 129 have by default, so don't rely on it.
99 ipython_dir : str [optional]
100 The path to an IPython directory, if the default value is not desired.
101 get_ipython_dir() is used by default.
130 user : bool [default: False]
131 Whether to install to the user's .ipython/nbextensions directory.
132 Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/nbextensions).
133 prefix : str [optional]
134 Specify install prefix, if it should differ from default (e.g. /usr/local).
135 Will install to prefix/share/jupyter/nbextensions
136 nbextensions : str [optional]
137 Specify absolute path of nbextensions directory explicitly.
102 138 verbose : int [default: 1]
103 139 Set verbosity level. The default is 1, where file actions are printed.
104 140 set verbose=2 for more output, or verbose=0 for silence.
105 141 """
106
107 ipython_dir = ipython_dir or get_ipython_dir()
108 nbext = pjoin(ipython_dir, u'nbextensions')
142 if sum(map(bool, [user, prefix, nbextensions])) > 1:
143 raise ValueError("Cannot specify more than one of user, prefix, or nbextensions.")
144 if user:
145 nbext = pjoin(get_ipython_dir(), u'nbextensions')
146 else:
147 if prefix:
148 nbext = pjoin(prefix, 'share', 'jupyter', 'nbextensions')
149 elif nbextensions:
150 nbext = nbextensions
151 else:
152 nbext = SYSTEM_NBEXTENSIONS_INSTALL_DIR
109 153 # make sure nbextensions dir exists
110 154 ensure_dir_exists(nbext)
111 155
112 156 if isinstance(files, string_types):
113 157 # one file given, turn it into a list
114 158 files = [files]
115 159
116 160 for path in map(cast_unicode_py2, files):
117 161
118 162 if path.startswith(('https://', 'http://')):
119 163 if symlink:
120 164 raise ValueError("Cannot symlink from URLs")
121 165 # Given a URL, download it
122 166 with TemporaryDirectory() as td:
123 167 filename = urlparse(path).path.split('/')[-1]
124 168 local_path = os.path.join(td, filename)
125 169 if verbose >= 1:
126 170 print("downloading %s to %s" % (path, local_path))
127 171 urlretrieve(path, local_path)
128 172 # now install from the local copy
129 install_nbextension(local_path, overwrite, symlink, ipython_dir, verbose)
173 install_nbextension(local_path, overwrite=overwrite, symlink=symlink, nbextensions=nbext, verbose=verbose)
130 174 continue
131 175
132 176 # handle archives
133 177 archive = None
134 178 if path.endswith('.zip'):
135 179 archive = zipfile.ZipFile(path)
136 180 elif _safe_is_tarfile(path):
137 181 archive = tarfile.open(path)
138 182
139 183 if archive:
140 184 if symlink:
141 185 raise ValueError("Cannot symlink from archives")
142 186 if verbose >= 1:
143 187 print("extracting %s to %s" % (path, nbext))
144 188 archive.extractall(nbext)
145 189 archive.close()
146 190 continue
147 191
148 192 dest = pjoin(nbext, basename(path))
149 193 if overwrite and os.path.exists(dest):
150 194 if verbose >= 1:
151 195 print("removing %s" % dest)
152 196 if os.path.isdir(dest) and not os.path.islink(dest):
153 197 shutil.rmtree(dest)
154 198 else:
155 199 os.remove(dest)
156 200
157 201 if symlink:
158 202 path = os.path.abspath(path)
159 203 if not os.path.exists(dest):
160 204 if verbose >= 1:
161 205 print("symlink %s -> %s" % (dest, path))
162 206 os.symlink(path, dest)
163 207 continue
164 208
165 209 if os.path.isdir(path):
166 210 strip_prefix_len = len(path) - len(basename(path))
167 211 for parent, dirs, files in os.walk(path):
168 212 dest_dir = pjoin(nbext, parent[strip_prefix_len:])
169 213 if not os.path.exists(dest_dir):
170 214 if verbose >= 2:
171 215 print("making directory %s" % dest_dir)
172 216 os.makedirs(dest_dir)
173 217 for file in files:
174 218 src = pjoin(parent, file)
175 219 # print("%r, %r" % (dest_dir, file))
176 220 dest = pjoin(dest_dir, file)
177 221 _maybe_copy(src, dest, verbose)
178 222 else:
179 223 src = path
180 224 _maybe_copy(src, dest, verbose)
181 225
182 226 #----------------------------------------------------------------------
183 227 # install nbextension app
184 228 #----------------------------------------------------------------------
185 229
186 from IPython.utils.traitlets import Bool, Enum
230 from IPython.utils.traitlets import Bool, Enum, Unicode, TraitError
187 231 from IPython.core.application import BaseIPythonApplication
188 232
189 233 flags = {
190 234 "overwrite" : ({
191 235 "NBExtensionApp" : {
192 236 "overwrite" : True,
193 237 }}, "Force overwrite of existing files"
194 238 ),
195 239 "debug" : ({
196 240 "NBExtensionApp" : {
197 241 "verbose" : 2,
198 242 }}, "Extra output"
199 243 ),
200 244 "quiet" : ({
201 245 "NBExtensionApp" : {
202 246 "verbose" : 0,
203 247 }}, "Minimal output"
204 248 ),
205 249 "symlink" : ({
206 250 "NBExtensionApp" : {
207 251 "symlink" : True,
208 252 }}, "Create symlinks instead of copying files"
209 253 ),
254 "user" : ({
255 "NBExtensionApp" : {
256 "user" : True,
257 }}, "Install to the user's IPython directory"
258 ),
210 259 }
211 260 flags['s'] = flags['symlink']
212 261
213 262 aliases = {
214 "ipython-dir" : "NBExtensionApp.ipython_dir"
263 "ipython-dir" : "NBExtensionApp.ipython_dir",
264 "prefix" : "NBExtensionApp.prefix",
265 "nbextensions" : "NBExtensionApp.nbextensions",
215 266 }
216 267
217 268 class NBExtensionApp(BaseIPythonApplication):
218 269 """Entry point for installing notebook extensions"""
219 270
220 271 description = """Install IPython notebook extensions
221 272
222 273 Usage
223 274
224 275 ipython install-nbextension file [more files, folders, archives or urls]
225 276
226 277 This copies files and/or folders into the IPython nbextensions directory.
227 278 If a URL is given, it will be downloaded.
228 279 If an archive is given, it will be extracted into nbextensions.
229 280 If the requested files are already up to date, no action is taken
230 281 unless --overwrite is specified.
231 282 """
232 283
233 284 examples = """
234 285 ipython install-nbextension /path/to/d3.js /path/to/myextension
235 286 """
236 287 aliases = aliases
237 288 flags = flags
238 289
239 290 overwrite = Bool(False, config=True, help="Force overwrite of existing files")
240 291 symlink = Bool(False, config=True, help="Create symlinks instead of copying files")
292 user = Bool(False, config=True, help="Whether to do a user install")
293 prefix = Unicode('', config=True, help="Installation prefix")
294 nbextensions = Unicode('', config=True, help="Full path to nbextensions (probably use prefix or user)")
241 295 verbose = Enum((0,1,2), default_value=1, config=True,
242 296 help="Verbosity level"
243 297 )
244 298
299 def check_install():
300 if sum(map(bool, [user, prefix, nbextensions])) > 1:
301 raise TraitError("Cannot specify more than one of user, prefix, or nbextensions.")
302
245 303 def install_extensions(self):
246 304 install_nbextension(self.extra_args,
247 305 overwrite=self.overwrite,
248 306 symlink=self.symlink,
249 307 verbose=self.verbose,
250 ipython_dir=self.ipython_dir,
308 user=self.user,
309 prefix=self.prefix,
310 nbextensions=self.nbextensions,
251 311 )
252 312
253 313 def start(self):
254 314 if not self.extra_args:
255 nbext = pjoin(self.ipython_dir, u'nbextensions')
256 print("Notebook extensions in %s:" % nbext)
257 for ext in os.listdir(nbext):
258 print(u" %s" % ext)
315 for nbext in [pjoin(self.ipython_dir, u'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS:
316 if os.path.exists(nbext):
317 print("Notebook extensions in %s:" % nbext)
318 for ext in os.listdir(nbext):
319 print(u" %s" % ext)
259 320 else:
321 self.check_install()
260 322 self.install_extensions()
261 323
262 324
263 325 if __name__ == '__main__':
264 326 NBExtensionApp.launch_instance()
265 327 No newline at end of file
@@ -1,1100 +1,1104 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 datetime
11 11 import errno
12 12 import importlib
13 13 import io
14 14 import json
15 15 import logging
16 16 import os
17 17 import random
18 18 import re
19 19 import select
20 20 import signal
21 21 import socket
22 22 import sys
23 23 import threading
24 24 import webbrowser
25 25
26 26
27 27 # check for pyzmq 2.1.11
28 28 from IPython.utils.zmqrelated import check_for_zmq
29 29 check_for_zmq('2.1.11', 'IPython.html')
30 30
31 31 from jinja2 import Environment, FileSystemLoader
32 32
33 33 # Install the pyzmq ioloop. This has to be done before anything else from
34 34 # tornado is imported.
35 35 from zmq.eventloop import ioloop
36 36 ioloop.install()
37 37
38 38 # check for tornado 3.1.0
39 39 msg = "The IPython Notebook requires tornado >= 4.0"
40 40 try:
41 41 import tornado
42 42 except ImportError:
43 43 raise ImportError(msg)
44 44 try:
45 45 version_info = tornado.version_info
46 46 except AttributeError:
47 47 raise ImportError(msg + ", but you have < 1.1.0")
48 48 if version_info < (4,0):
49 49 raise ImportError(msg + ", but you have %s" % tornado.version)
50 50
51 51 from tornado import httpserver
52 52 from tornado import web
53 53 from tornado.log import LogFormatter, app_log, access_log, gen_log
54 54
55 55 from IPython.html import (
56 56 DEFAULT_STATIC_FILES_PATH,
57 57 DEFAULT_TEMPLATE_PATH_LIST,
58 58 )
59 59 from .base.handlers import Template404
60 60 from .log import log_request
61 61 from .services.kernels.kernelmanager import MappingKernelManager
62 62 from .services.config import ConfigManager
63 63 from .services.contents.manager import ContentsManager
64 64 from .services.contents.filemanager import FileContentsManager
65 65 from .services.clusters.clustermanager import ClusterManager
66 66 from .services.sessions.sessionmanager import SessionManager
67 67
68 68 from .auth.login import LoginHandler
69 69 from .auth.logout import LogoutHandler
70 70 from .base.handlers import IPythonHandler, FileFindHandler
71 71
72 72 from IPython.config import Config
73 73 from IPython.config.application import catch_config_error, boolean_flag
74 74 from IPython.core.application import (
75 75 BaseIPythonApplication, base_flags, base_aliases,
76 76 )
77 77 from IPython.core.profiledir import ProfileDir
78 78 from IPython.kernel import KernelManager
79 79 from IPython.kernel.kernelspec import KernelSpecManager
80 80 from IPython.kernel.zmq.session import default_secure, Session
81 81 from IPython.nbformat.sign import NotebookNotary
82 82 from IPython.utils.importstring import import_item
83 83 from IPython.utils import submodule
84 84 from IPython.utils.process import check_pid
85 85 from IPython.utils.traitlets import (
86 86 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
87 87 TraitError, Type,
88 88 )
89 89 from IPython.utils import py3compat
90 90 from IPython.utils.path import filefind, get_ipython_dir
91 91 from IPython.utils.sysinfo import get_sys_info
92 92
93 from .nbextensions import SYSTEM_NBEXTENSIONS_DIRS
93 94 from .utils import url_path_join
94 95
95 96 #-----------------------------------------------------------------------------
96 97 # Module globals
97 98 #-----------------------------------------------------------------------------
98 99
99 100 _examples = """
100 101 ipython notebook # start the notebook
101 102 ipython notebook --profile=sympy # use the sympy profile
102 103 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
103 104 """
104 105
105 106 #-----------------------------------------------------------------------------
106 107 # Helper functions
107 108 #-----------------------------------------------------------------------------
108 109
109 110 def random_ports(port, n):
110 111 """Generate a list of n random ports near the given port.
111 112
112 113 The first 5 ports will be sequential, and the remaining n-5 will be
113 114 randomly selected in the range [port-2*n, port+2*n].
114 115 """
115 116 for i in range(min(5, n)):
116 117 yield port + i
117 118 for i in range(n-5):
118 119 yield max(1, port + random.randint(-2*n, 2*n))
119 120
120 121 def load_handlers(name):
121 122 """Load the (URL pattern, handler) tuples for each component."""
122 123 name = 'IPython.html.' + name
123 124 mod = __import__(name, fromlist=['default_handlers'])
124 125 return mod.default_handlers
125 126
126 127 #-----------------------------------------------------------------------------
127 128 # The Tornado web application
128 129 #-----------------------------------------------------------------------------
129 130
130 131 class NotebookWebApplication(web.Application):
131 132
132 133 def __init__(self, ipython_app, kernel_manager, contents_manager,
133 134 cluster_manager, session_manager, kernel_spec_manager,
134 135 config_manager, log,
135 136 base_url, default_url, settings_overrides, jinja_env_options):
136 137
137 138 settings = self.init_settings(
138 139 ipython_app, kernel_manager, contents_manager, cluster_manager,
139 140 session_manager, kernel_spec_manager, config_manager, log, base_url,
140 141 default_url, settings_overrides, jinja_env_options)
141 142 handlers = self.init_handlers(settings)
142 143
143 144 super(NotebookWebApplication, self).__init__(handlers, **settings)
144 145
145 146 def init_settings(self, ipython_app, kernel_manager, contents_manager,
146 147 cluster_manager, session_manager, kernel_spec_manager,
147 148 config_manager,
148 149 log, base_url, default_url, settings_overrides,
149 150 jinja_env_options=None):
150 151
151 152 _template_path = settings_overrides.get(
152 153 "template_path",
153 154 ipython_app.template_file_path,
154 155 )
155 156 if isinstance(_template_path, str):
156 157 _template_path = (_template_path,)
157 158 template_path = [os.path.expanduser(path) for path in _template_path]
158 159
159 160 jenv_opt = jinja_env_options if jinja_env_options else {}
160 161 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
161 162
162 163 sys_info = get_sys_info()
163 164 if sys_info['commit_source'] == 'repository':
164 165 # don't cache (rely on 304) when working from master
165 166 version_hash = ''
166 167 else:
167 168 # reset the cache on server restart
168 169 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
169 170
170 171 settings = dict(
171 172 # basics
172 173 log_function=log_request,
173 174 base_url=base_url,
174 175 default_url=default_url,
175 176 template_path=template_path,
176 177 static_path=ipython_app.static_file_path,
177 178 static_handler_class = FileFindHandler,
178 179 static_url_prefix = url_path_join(base_url,'/static/'),
179 180 static_handler_args = {
180 181 # don't cache custom.js
181 182 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
182 183 },
183 184 version_hash=version_hash,
184 185
185 186 # authentication
186 187 cookie_secret=ipython_app.cookie_secret,
187 188 login_url=url_path_join(base_url,'/login'),
188 189 login_handler_class=ipython_app.login_handler_class,
189 190 logout_handler_class=ipython_app.logout_handler_class,
190 191 password=ipython_app.password,
191 192
192 193 # managers
193 194 kernel_manager=kernel_manager,
194 195 contents_manager=contents_manager,
195 196 cluster_manager=cluster_manager,
196 197 session_manager=session_manager,
197 198 kernel_spec_manager=kernel_spec_manager,
198 199 config_manager=config_manager,
199 200
200 201 # IPython stuff
201 202 nbextensions_path=ipython_app.nbextensions_path,
202 203 websocket_url=ipython_app.websocket_url,
203 204 mathjax_url=ipython_app.mathjax_url,
204 205 config=ipython_app.config,
205 206 jinja2_env=env,
206 207 terminals_available=False, # Set later if terminals are available
207 208 )
208 209
209 210 # allow custom overrides for the tornado web app.
210 211 settings.update(settings_overrides)
211 212 return settings
212 213
213 214 def init_handlers(self, settings):
214 215 """Load the (URL pattern, handler) tuples for each component."""
215 216
216 217 # Order matters. The first handler to match the URL will handle the request.
217 218 handlers = []
218 219 handlers.extend(load_handlers('tree.handlers'))
219 220 handlers.extend([(r"/login", settings['login_handler_class'])])
220 221 handlers.extend([(r"/logout", settings['logout_handler_class'])])
221 222 handlers.extend(load_handlers('files.handlers'))
222 223 handlers.extend(load_handlers('notebook.handlers'))
223 224 handlers.extend(load_handlers('nbconvert.handlers'))
224 225 handlers.extend(load_handlers('kernelspecs.handlers'))
225 226 handlers.extend(load_handlers('edit.handlers'))
226 227 handlers.extend(load_handlers('services.config.handlers'))
227 228 handlers.extend(load_handlers('services.kernels.handlers'))
228 229 handlers.extend(load_handlers('services.contents.handlers'))
229 230 handlers.extend(load_handlers('services.clusters.handlers'))
230 231 handlers.extend(load_handlers('services.sessions.handlers'))
231 232 handlers.extend(load_handlers('services.nbconvert.handlers'))
232 233 handlers.extend(load_handlers('services.kernelspecs.handlers'))
233 234 handlers.extend(load_handlers('services.security.handlers'))
234 235 handlers.append(
235 236 (r"/nbextensions/(.*)", FileFindHandler, {
236 237 'path': settings['nbextensions_path'],
237 238 'no_cache_paths': ['/'], # don't cache anything in nbextensions
238 239 }),
239 240 )
240 241 # register base handlers last
241 242 handlers.extend(load_handlers('base.handlers'))
242 243 # set the URL that will be redirected from `/`
243 244 handlers.append(
244 245 (r'/?', web.RedirectHandler, {
245 246 'url' : url_path_join(settings['base_url'], settings['default_url']),
246 247 'permanent': False, # want 302, not 301
247 248 })
248 249 )
249 250 # prepend base_url onto the patterns that we match
250 251 new_handlers = []
251 252 for handler in handlers:
252 253 pattern = url_path_join(settings['base_url'], handler[0])
253 254 new_handler = tuple([pattern] + list(handler[1:]))
254 255 new_handlers.append(new_handler)
255 256 # add 404 on the end, which will catch everything that falls through
256 257 new_handlers.append((r'(.*)', Template404))
257 258 return new_handlers
258 259
259 260
260 261 class NbserverListApp(BaseIPythonApplication):
261 262
262 263 description="List currently running notebook servers in this profile."
263 264
264 265 flags = dict(
265 266 json=({'NbserverListApp': {'json': True}},
266 267 "Produce machine-readable JSON output."),
267 268 )
268 269
269 270 json = Bool(False, config=True,
270 271 help="If True, each line of output will be a JSON object with the "
271 272 "details from the server info file.")
272 273
273 274 def start(self):
274 275 if not self.json:
275 276 print("Currently running servers:")
276 277 for serverinfo in list_running_servers(self.profile):
277 278 if self.json:
278 279 print(json.dumps(serverinfo))
279 280 else:
280 281 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
281 282
282 283 #-----------------------------------------------------------------------------
283 284 # Aliases and Flags
284 285 #-----------------------------------------------------------------------------
285 286
286 287 flags = dict(base_flags)
287 288 flags['no-browser']=(
288 289 {'NotebookApp' : {'open_browser' : False}},
289 290 "Don't open the notebook in a browser after startup."
290 291 )
291 292 flags['pylab']=(
292 293 {'NotebookApp' : {'pylab' : 'warn'}},
293 294 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
294 295 )
295 296 flags['no-mathjax']=(
296 297 {'NotebookApp' : {'enable_mathjax' : False}},
297 298 """Disable MathJax
298 299
299 300 MathJax is the javascript library IPython uses to render math/LaTeX. It is
300 301 very large, so you may want to disable it if you have a slow internet
301 302 connection, or for offline use of the notebook.
302 303
303 304 When disabled, equations etc. will appear as their untransformed TeX source.
304 305 """
305 306 )
306 307
307 308 # Add notebook manager flags
308 309 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
309 310 'DEPRECATED, IGNORED',
310 311 'DEPRECATED, IGNORED'))
311 312
312 313 aliases = dict(base_aliases)
313 314
314 315 aliases.update({
315 316 'ip': 'NotebookApp.ip',
316 317 'port': 'NotebookApp.port',
317 318 'port-retries': 'NotebookApp.port_retries',
318 319 'transport': 'KernelManager.transport',
319 320 'keyfile': 'NotebookApp.keyfile',
320 321 'certfile': 'NotebookApp.certfile',
321 322 'notebook-dir': 'NotebookApp.notebook_dir',
322 323 'browser': 'NotebookApp.browser',
323 324 'pylab': 'NotebookApp.pylab',
324 325 })
325 326
326 327 #-----------------------------------------------------------------------------
327 328 # NotebookApp
328 329 #-----------------------------------------------------------------------------
329 330
330 331 class NotebookApp(BaseIPythonApplication):
331 332
332 333 name = 'ipython-notebook'
333 334
334 335 description = """
335 336 The IPython HTML Notebook.
336 337
337 338 This launches a Tornado based HTML Notebook Server that serves up an
338 339 HTML5/Javascript Notebook client.
339 340 """
340 341 examples = _examples
341 342 aliases = aliases
342 343 flags = flags
343 344
344 345 classes = [
345 346 KernelManager, ProfileDir, Session, MappingKernelManager,
346 347 ContentsManager, FileContentsManager, NotebookNotary,
347 348 ]
348 349 flags = Dict(flags)
349 350 aliases = Dict(aliases)
350 351
351 352 subcommands = dict(
352 353 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
353 354 )
354 355
355 356 ipython_kernel_argv = List(Unicode)
356 357
357 358 _log_formatter_cls = LogFormatter
358 359
359 360 def _log_level_default(self):
360 361 return logging.INFO
361 362
362 363 def _log_datefmt_default(self):
363 364 """Exclude date from default date format"""
364 365 return "%H:%M:%S"
365 366
366 367 def _log_format_default(self):
367 368 """override default log format to include time"""
368 369 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
369 370
370 371 # create requested profiles by default, if they don't exist:
371 372 auto_create = Bool(True)
372 373
373 374 # file to be opened in the notebook server
374 375 file_to_run = Unicode('', config=True)
375 376
376 377 # Network related information
377 378
378 379 allow_origin = Unicode('', config=True,
379 380 help="""Set the Access-Control-Allow-Origin header
380 381
381 382 Use '*' to allow any origin to access your server.
382 383
383 384 Takes precedence over allow_origin_pat.
384 385 """
385 386 )
386 387
387 388 allow_origin_pat = Unicode('', config=True,
388 389 help="""Use a regular expression for the Access-Control-Allow-Origin header
389 390
390 391 Requests from an origin matching the expression will get replies with:
391 392
392 393 Access-Control-Allow-Origin: origin
393 394
394 395 where `origin` is the origin of the request.
395 396
396 397 Ignored if allow_origin is set.
397 398 """
398 399 )
399 400
400 401 allow_credentials = Bool(False, config=True,
401 402 help="Set the Access-Control-Allow-Credentials: true header"
402 403 )
403 404
404 405 default_url = Unicode('/tree', config=True,
405 406 help="The default URL to redirect to from `/`"
406 407 )
407 408
408 409 ip = Unicode('localhost', config=True,
409 410 help="The IP address the notebook server will listen on."
410 411 )
411 412
412 413 def _ip_changed(self, name, old, new):
413 414 if new == u'*': self.ip = u''
414 415
415 416 port = Integer(8888, config=True,
416 417 help="The port the notebook server will listen on."
417 418 )
418 419 port_retries = Integer(50, config=True,
419 420 help="The number of additional ports to try if the specified port is not available."
420 421 )
421 422
422 423 certfile = Unicode(u'', config=True,
423 424 help="""The full path to an SSL/TLS certificate file."""
424 425 )
425 426
426 427 keyfile = Unicode(u'', config=True,
427 428 help="""The full path to a private key file for usage with SSL/TLS."""
428 429 )
429 430
430 431 cookie_secret_file = Unicode(config=True,
431 432 help="""The file where the cookie secret is stored."""
432 433 )
433 434 def _cookie_secret_file_default(self):
434 435 if self.profile_dir is None:
435 436 return ''
436 437 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
437 438
438 439 cookie_secret = Bytes(b'', config=True,
439 440 help="""The random bytes used to secure cookies.
440 441 By default this is a new random number every time you start the Notebook.
441 442 Set it to a value in a config file to enable logins to persist across server sessions.
442 443
443 444 Note: Cookie secrets should be kept private, do not share config files with
444 445 cookie_secret stored in plaintext (you can read the value from a file).
445 446 """
446 447 )
447 448 def _cookie_secret_default(self):
448 449 if os.path.exists(self.cookie_secret_file):
449 450 with io.open(self.cookie_secret_file, 'rb') as f:
450 451 return f.read()
451 452 else:
452 453 secret = base64.encodestring(os.urandom(1024))
453 454 self._write_cookie_secret_file(secret)
454 455 return secret
455 456
456 457 def _write_cookie_secret_file(self, secret):
457 458 """write my secret to my secret_file"""
458 459 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
459 460 with io.open(self.cookie_secret_file, 'wb') as f:
460 461 f.write(secret)
461 462 try:
462 463 os.chmod(self.cookie_secret_file, 0o600)
463 464 except OSError:
464 465 self.log.warn(
465 466 "Could not set permissions on %s",
466 467 self.cookie_secret_file
467 468 )
468 469
469 470 password = Unicode(u'', config=True,
470 471 help="""Hashed password to use for web authentication.
471 472
472 473 To generate, type in a python/IPython shell:
473 474
474 475 from IPython.lib import passwd; passwd()
475 476
476 477 The string should be of the form type:salt:hashed-password.
477 478 """
478 479 )
479 480
480 481 open_browser = Bool(True, config=True,
481 482 help="""Whether to open in a browser after starting.
482 483 The specific browser used is platform dependent and
483 484 determined by the python standard library `webbrowser`
484 485 module, unless it is overridden using the --browser
485 486 (NotebookApp.browser) configuration option.
486 487 """)
487 488
488 489 browser = Unicode(u'', config=True,
489 490 help="""Specify what command to use to invoke a web
490 491 browser when opening the notebook. If not specified, the
491 492 default browser will be determined by the `webbrowser`
492 493 standard library module, which allows setting of the
493 494 BROWSER environment variable to override it.
494 495 """)
495 496
496 497 webapp_settings = Dict(config=True,
497 498 help="DEPRECATED, use tornado_settings"
498 499 )
499 500 def _webapp_settings_changed(self, name, old, new):
500 501 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
501 502 self.tornado_settings = new
502 503
503 504 tornado_settings = Dict(config=True,
504 505 help="Supply overrides for the tornado.web.Application that the "
505 506 "IPython notebook uses.")
506 507
507 508 jinja_environment_options = Dict(config=True,
508 509 help="Supply extra arguments that will be passed to Jinja environment.")
509 510
510 511 enable_mathjax = Bool(True, config=True,
511 512 help="""Whether to enable MathJax for typesetting math/TeX
512 513
513 514 MathJax is the javascript library IPython uses to render math/LaTeX. It is
514 515 very large, so you may want to disable it if you have a slow internet
515 516 connection, or for offline use of the notebook.
516 517
517 518 When disabled, equations etc. will appear as their untransformed TeX source.
518 519 """
519 520 )
520 521 def _enable_mathjax_changed(self, name, old, new):
521 522 """set mathjax url to empty if mathjax is disabled"""
522 523 if not new:
523 524 self.mathjax_url = u''
524 525
525 526 base_url = Unicode('/', config=True,
526 527 help='''The base URL for the notebook server.
527 528
528 529 Leading and trailing slashes can be omitted,
529 530 and will automatically be added.
530 531 ''')
531 532 def _base_url_changed(self, name, old, new):
532 533 if not new.startswith('/'):
533 534 self.base_url = '/'+new
534 535 elif not new.endswith('/'):
535 536 self.base_url = new+'/'
536 537
537 538 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
538 539 def _base_project_url_changed(self, name, old, new):
539 540 self.log.warn("base_project_url is deprecated, use base_url")
540 541 self.base_url = new
541 542
542 543 extra_static_paths = List(Unicode, config=True,
543 544 help="""Extra paths to search for serving static files.
544 545
545 546 This allows adding javascript/css to be available from the notebook server machine,
546 547 or overriding individual files in the IPython"""
547 548 )
548 549 def _extra_static_paths_default(self):
549 550 return [os.path.join(self.profile_dir.location, 'static')]
550 551
551 552 @property
552 553 def static_file_path(self):
553 554 """return extra paths + the default location"""
554 555 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
555 556
556 557 extra_template_paths = List(Unicode, config=True,
557 558 help="""Extra paths to search for serving jinja templates.
558 559
559 560 Can be used to override templates from IPython.html.templates."""
560 561 )
561 562 def _extra_template_paths_default(self):
562 563 return []
563 564
564 565 @property
565 566 def template_file_path(self):
566 567 """return extra paths + the default locations"""
567 568 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
568 569
569 nbextensions_path = List(Unicode, config=True,
570 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
570 extra_nbextensions_path = List(Unicode, config=True,
571 help="""extra paths to look for Javascript notebook extensions"""
571 572 )
572 def _nbextensions_path_default(self):
573 return [os.path.join(get_ipython_dir(), 'nbextensions')]
573
574 @property
575 def nbextensions_path(self):
576 """The path to look for Javascript notebook extensions"""
577 return self.extra_nbextensions_path + [os.path.join(get_ipython_dir(), 'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS
574 578
575 579 websocket_url = Unicode("", config=True,
576 580 help="""The base URL for websockets,
577 581 if it differs from the HTTP server (hint: it almost certainly doesn't).
578 582
579 583 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
580 584 """
581 585 )
582 586 mathjax_url = Unicode("", config=True,
583 587 help="""The url for MathJax.js."""
584 588 )
585 589 def _mathjax_url_default(self):
586 590 if not self.enable_mathjax:
587 591 return u''
588 592 static_url_prefix = self.tornado_settings.get("static_url_prefix",
589 593 url_path_join(self.base_url, "static")
590 594 )
591 595
592 596 # try local mathjax, either in nbextensions/mathjax or static/mathjax
593 597 for (url_prefix, search_path) in [
594 598 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
595 599 (static_url_prefix, self.static_file_path),
596 600 ]:
597 601 self.log.debug("searching for local mathjax in %s", search_path)
598 602 try:
599 603 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
600 604 except IOError:
601 605 continue
602 606 else:
603 607 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
604 608 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
605 609 return url
606 610
607 611 # no local mathjax, serve from CDN
608 612 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
609 613 self.log.info("Using MathJax from CDN: %s", url)
610 614 return url
611 615
612 616 def _mathjax_url_changed(self, name, old, new):
613 617 if new and not self.enable_mathjax:
614 618 # enable_mathjax=False overrides mathjax_url
615 619 self.mathjax_url = u''
616 620 else:
617 621 self.log.info("Using MathJax: %s", new)
618 622
619 623 contents_manager_class = Type(
620 624 default_value=FileContentsManager,
621 625 klass=ContentsManager,
622 626 config=True,
623 627 help='The notebook manager class to use.'
624 628 )
625 629 kernel_manager_class = Type(
626 630 default_value=MappingKernelManager,
627 631 config=True,
628 632 help='The kernel manager class to use.'
629 633 )
630 634 session_manager_class = Type(
631 635 default_value=SessionManager,
632 636 config=True,
633 637 help='The session manager class to use.'
634 638 )
635 639 cluster_manager_class = Type(
636 640 default_value=ClusterManager,
637 641 config=True,
638 642 help='The cluster manager class to use.'
639 643 )
640 644
641 645 config_manager_class = Type(
642 646 default_value=ConfigManager,
643 647 config = True,
644 648 help='The config manager class to use'
645 649 )
646 650
647 651 kernel_spec_manager = Instance(KernelSpecManager)
648 652
649 653 def _kernel_spec_manager_default(self):
650 654 return KernelSpecManager(ipython_dir=self.ipython_dir)
651 655
652 656 kernel_spec_manager_class = Type(
653 657 default_value=KernelSpecManager,
654 658 config=True,
655 659 help="""
656 660 The kernel spec manager class to use. Should be a subclass
657 661 of `IPython.kernel.kernelspec.KernelSpecManager`.
658 662
659 663 The Api of KernelSpecManager is provisional and might change
660 664 without warning between this version of IPython and the next stable one.
661 665 """
662 666 )
663 667
664 668 login_handler_class = Type(
665 669 default_value=LoginHandler,
666 670 klass=web.RequestHandler,
667 671 config=True,
668 672 help='The login handler class to use.',
669 673 )
670 674
671 675 logout_handler_class = Type(
672 676 default_value=LogoutHandler,
673 677 klass=web.RequestHandler,
674 678 config=True,
675 679 help='The logout handler class to use.',
676 680 )
677 681
678 682 trust_xheaders = Bool(False, config=True,
679 683 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
680 684 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
681 685 )
682 686
683 687 info_file = Unicode()
684 688
685 689 def _info_file_default(self):
686 690 info_file = "nbserver-%s.json"%os.getpid()
687 691 return os.path.join(self.profile_dir.security_dir, info_file)
688 692
689 693 pylab = Unicode('disabled', config=True,
690 694 help="""
691 695 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
692 696 """
693 697 )
694 698 def _pylab_changed(self, name, old, new):
695 699 """when --pylab is specified, display a warning and exit"""
696 700 if new != 'warn':
697 701 backend = ' %s' % new
698 702 else:
699 703 backend = ''
700 704 self.log.error("Support for specifying --pylab on the command line has been removed.")
701 705 self.log.error(
702 706 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
703 707 )
704 708 self.exit(1)
705 709
706 710 notebook_dir = Unicode(config=True,
707 711 help="The directory to use for notebooks and kernels."
708 712 )
709 713
710 714 def _notebook_dir_default(self):
711 715 if self.file_to_run:
712 716 return os.path.dirname(os.path.abspath(self.file_to_run))
713 717 else:
714 718 return py3compat.getcwd()
715 719
716 720 def _notebook_dir_changed(self, name, old, new):
717 721 """Do a bit of validation of the notebook dir."""
718 722 if not os.path.isabs(new):
719 723 # If we receive a non-absolute path, make it absolute.
720 724 self.notebook_dir = os.path.abspath(new)
721 725 return
722 726 if not os.path.isdir(new):
723 727 raise TraitError("No such notebook dir: %r" % new)
724 728
725 729 # setting App.notebook_dir implies setting notebook and kernel dirs as well
726 730 self.config.FileContentsManager.root_dir = new
727 731 self.config.MappingKernelManager.root_dir = new
728 732
729 733 server_extensions = List(Unicode(), config=True,
730 734 help=("Python modules to load as notebook server extensions. "
731 735 "This is an experimental API, and may change in future releases.")
732 736 )
733 737
734 738 def parse_command_line(self, argv=None):
735 739 super(NotebookApp, self).parse_command_line(argv)
736 740
737 741 if self.extra_args:
738 742 arg0 = self.extra_args[0]
739 743 f = os.path.abspath(arg0)
740 744 self.argv.remove(arg0)
741 745 if not os.path.exists(f):
742 746 self.log.critical("No such file or directory: %s", f)
743 747 self.exit(1)
744 748
745 749 # Use config here, to ensure that it takes higher priority than
746 750 # anything that comes from the profile.
747 751 c = Config()
748 752 if os.path.isdir(f):
749 753 c.NotebookApp.notebook_dir = f
750 754 elif os.path.isfile(f):
751 755 c.NotebookApp.file_to_run = f
752 756 self.update_config(c)
753 757
754 758 def init_kernel_argv(self):
755 759 """add the profile-dir to arguments to be passed to IPython kernels"""
756 760 # FIXME: remove special treatment of IPython kernels
757 761 # Kernel should get *absolute* path to profile directory
758 762 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
759 763
760 764 def init_configurables(self):
761 765 # force Session default to be secure
762 766 default_secure(self.config)
763 767
764 768 self.kernel_spec_manager = self.kernel_spec_manager_class(
765 769 ipython_dir=self.ipython_dir,
766 770 )
767 771 self.kernel_manager = self.kernel_manager_class(
768 772 parent=self,
769 773 log=self.log,
770 774 ipython_kernel_argv=self.ipython_kernel_argv,
771 775 connection_dir=self.profile_dir.security_dir,
772 776 )
773 777 self.contents_manager = self.contents_manager_class(
774 778 parent=self,
775 779 log=self.log,
776 780 )
777 781 self.session_manager = self.session_manager_class(
778 782 parent=self,
779 783 log=self.log,
780 784 kernel_manager=self.kernel_manager,
781 785 contents_manager=self.contents_manager,
782 786 )
783 787 self.cluster_manager = self.cluster_manager_class(
784 788 parent=self,
785 789 log=self.log,
786 790 )
787 791
788 792 self.config_manager = self.config_manager_class(
789 793 parent=self,
790 794 log=self.log,
791 795 profile_dir=self.profile_dir.location,
792 796 )
793 797
794 798 def init_logging(self):
795 799 # This prevents double log messages because tornado use a root logger that
796 800 # self.log is a child of. The logging module dipatches log messages to a log
797 801 # and all of its ancenstors until propagate is set to False.
798 802 self.log.propagate = False
799 803
800 804 for log in app_log, access_log, gen_log:
801 805 # consistent log output name (NotebookApp instead of tornado.access, etc.)
802 806 log.name = self.log.name
803 807 # hook up tornado 3's loggers to our app handlers
804 808 logger = logging.getLogger('tornado')
805 809 logger.propagate = True
806 810 logger.parent = self.log
807 811 logger.setLevel(self.log.level)
808 812
809 813 def init_webapp(self):
810 814 """initialize tornado webapp and httpserver"""
811 815 self.tornado_settings['allow_origin'] = self.allow_origin
812 816 if self.allow_origin_pat:
813 817 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
814 818 self.tornado_settings['allow_credentials'] = self.allow_credentials
815 819
816 820 self.web_app = NotebookWebApplication(
817 821 self, self.kernel_manager, self.contents_manager,
818 822 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
819 823 self.config_manager,
820 824 self.log, self.base_url, self.default_url, self.tornado_settings,
821 825 self.jinja_environment_options
822 826 )
823 827 if self.certfile:
824 828 ssl_options = dict(certfile=self.certfile)
825 829 if self.keyfile:
826 830 ssl_options['keyfile'] = self.keyfile
827 831 else:
828 832 ssl_options = None
829 833 self.login_handler_class.validate_security(self, ssl_options=ssl_options)
830 834 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
831 835 xheaders=self.trust_xheaders)
832 836
833 837 success = None
834 838 for port in random_ports(self.port, self.port_retries+1):
835 839 try:
836 840 self.http_server.listen(port, self.ip)
837 841 except socket.error as e:
838 842 if e.errno == errno.EADDRINUSE:
839 843 self.log.info('The port %i is already in use, trying another random port.' % port)
840 844 continue
841 845 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
842 846 self.log.warn("Permission to listen on port %i denied" % port)
843 847 continue
844 848 else:
845 849 raise
846 850 else:
847 851 self.port = port
848 852 success = True
849 853 break
850 854 if not success:
851 855 self.log.critical('ERROR: the notebook server could not be started because '
852 856 'no available port could be found.')
853 857 self.exit(1)
854 858
855 859 @property
856 860 def display_url(self):
857 861 ip = self.ip if self.ip else '[all ip addresses on your system]'
858 862 return self._url(ip)
859 863
860 864 @property
861 865 def connection_url(self):
862 866 ip = self.ip if self.ip else 'localhost'
863 867 return self._url(ip)
864 868
865 869 def _url(self, ip):
866 870 proto = 'https' if self.certfile else 'http'
867 871 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
868 872
869 873 def init_terminals(self):
870 874 try:
871 875 from .terminal import initialize
872 876 initialize(self.web_app)
873 877 self.web_app.settings['terminals_available'] = True
874 878 except ImportError as e:
875 879 self.log.info("Terminals not available (error was %s)", e)
876 880
877 881 def init_signal(self):
878 882 if not sys.platform.startswith('win'):
879 883 signal.signal(signal.SIGINT, self._handle_sigint)
880 884 signal.signal(signal.SIGTERM, self._signal_stop)
881 885 if hasattr(signal, 'SIGUSR1'):
882 886 # Windows doesn't support SIGUSR1
883 887 signal.signal(signal.SIGUSR1, self._signal_info)
884 888 if hasattr(signal, 'SIGINFO'):
885 889 # only on BSD-based systems
886 890 signal.signal(signal.SIGINFO, self._signal_info)
887 891
888 892 def _handle_sigint(self, sig, frame):
889 893 """SIGINT handler spawns confirmation dialog"""
890 894 # register more forceful signal handler for ^C^C case
891 895 signal.signal(signal.SIGINT, self._signal_stop)
892 896 # request confirmation dialog in bg thread, to avoid
893 897 # blocking the App
894 898 thread = threading.Thread(target=self._confirm_exit)
895 899 thread.daemon = True
896 900 thread.start()
897 901
898 902 def _restore_sigint_handler(self):
899 903 """callback for restoring original SIGINT handler"""
900 904 signal.signal(signal.SIGINT, self._handle_sigint)
901 905
902 906 def _confirm_exit(self):
903 907 """confirm shutdown on ^C
904 908
905 909 A second ^C, or answering 'y' within 5s will cause shutdown,
906 910 otherwise original SIGINT handler will be restored.
907 911
908 912 This doesn't work on Windows.
909 913 """
910 914 info = self.log.info
911 915 info('interrupted')
912 916 print(self.notebook_info())
913 917 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
914 918 sys.stdout.flush()
915 919 r,w,x = select.select([sys.stdin], [], [], 5)
916 920 if r:
917 921 line = sys.stdin.readline()
918 922 if line.lower().startswith('y') and 'n' not in line.lower():
919 923 self.log.critical("Shutdown confirmed")
920 924 ioloop.IOLoop.current().stop()
921 925 return
922 926 else:
923 927 print("No answer for 5s:", end=' ')
924 928 print("resuming operation...")
925 929 # no answer, or answer is no:
926 930 # set it back to original SIGINT handler
927 931 # use IOLoop.add_callback because signal.signal must be called
928 932 # from main thread
929 933 ioloop.IOLoop.current().add_callback(self._restore_sigint_handler)
930 934
931 935 def _signal_stop(self, sig, frame):
932 936 self.log.critical("received signal %s, stopping", sig)
933 937 ioloop.IOLoop.current().stop()
934 938
935 939 def _signal_info(self, sig, frame):
936 940 print(self.notebook_info())
937 941
938 942 def init_components(self):
939 943 """Check the components submodule, and warn if it's unclean"""
940 944 status = submodule.check_submodule_status()
941 945 if status == 'missing':
942 946 self.log.warn("components submodule missing, running `git submodule update`")
943 947 submodule.update_submodules(submodule.ipython_parent())
944 948 elif status == 'unclean':
945 949 self.log.warn("components submodule unclean, you may see 404s on static/components")
946 950 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
947 951
948 952 def init_server_extensions(self):
949 953 """Load any extensions specified by config.
950 954
951 955 Import the module, then call the load_jupyter_server_extension function,
952 956 if one exists.
953 957
954 958 The extension API is experimental, and may change in future releases.
955 959 """
956 960 for modulename in self.server_extensions:
957 961 try:
958 962 mod = importlib.import_module(modulename)
959 963 func = getattr(mod, 'load_jupyter_server_extension', None)
960 964 if func is not None:
961 965 func(self)
962 966 except Exception:
963 967 self.log.warn("Error loading server extension %s", modulename,
964 968 exc_info=True)
965 969
966 970 @catch_config_error
967 971 def initialize(self, argv=None):
968 972 super(NotebookApp, self).initialize(argv)
969 973 self.init_logging()
970 974 self.init_kernel_argv()
971 975 self.init_configurables()
972 976 self.init_components()
973 977 self.init_webapp()
974 978 self.init_terminals()
975 979 self.init_signal()
976 980 self.init_server_extensions()
977 981
978 982 def cleanup_kernels(self):
979 983 """Shutdown all kernels.
980 984
981 985 The kernels will shutdown themselves when this process no longer exists,
982 986 but explicit shutdown allows the KernelManagers to cleanup the connection files.
983 987 """
984 988 self.log.info('Shutting down kernels')
985 989 self.kernel_manager.shutdown_all()
986 990
987 991 def notebook_info(self):
988 992 "Return the current working directory and the server url information"
989 993 info = self.contents_manager.info_string() + "\n"
990 994 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
991 995 return info + "The IPython Notebook is running at: %s" % self.display_url
992 996
993 997 def server_info(self):
994 998 """Return a JSONable dict of information about this server."""
995 999 return {'url': self.connection_url,
996 1000 'hostname': self.ip if self.ip else 'localhost',
997 1001 'port': self.port,
998 1002 'secure': bool(self.certfile),
999 1003 'base_url': self.base_url,
1000 1004 'notebook_dir': os.path.abspath(self.notebook_dir),
1001 1005 'pid': os.getpid()
1002 1006 }
1003 1007
1004 1008 def write_server_info_file(self):
1005 1009 """Write the result of server_info() to the JSON file info_file."""
1006 1010 with open(self.info_file, 'w') as f:
1007 1011 json.dump(self.server_info(), f, indent=2)
1008 1012
1009 1013 def remove_server_info_file(self):
1010 1014 """Remove the nbserver-<pid>.json file created for this server.
1011 1015
1012 1016 Ignores the error raised when the file has already been removed.
1013 1017 """
1014 1018 try:
1015 1019 os.unlink(self.info_file)
1016 1020 except OSError as e:
1017 1021 if e.errno != errno.ENOENT:
1018 1022 raise
1019 1023
1020 1024 def start(self):
1021 1025 """ Start the IPython Notebook server app, after initialization
1022 1026
1023 1027 This method takes no arguments so all configuration and initialization
1024 1028 must be done prior to calling this method."""
1025 1029 if self.subapp is not None:
1026 1030 return self.subapp.start()
1027 1031
1028 1032 info = self.log.info
1029 1033 for line in self.notebook_info().split("\n"):
1030 1034 info(line)
1031 1035 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
1032 1036
1033 1037 self.write_server_info_file()
1034 1038
1035 1039 if self.open_browser or self.file_to_run:
1036 1040 try:
1037 1041 browser = webbrowser.get(self.browser or None)
1038 1042 except webbrowser.Error as e:
1039 1043 self.log.warn('No web browser found: %s.' % e)
1040 1044 browser = None
1041 1045
1042 1046 if self.file_to_run:
1043 1047 if not os.path.exists(self.file_to_run):
1044 1048 self.log.critical("%s does not exist" % self.file_to_run)
1045 1049 self.exit(1)
1046 1050
1047 1051 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1048 1052 uri = url_path_join('notebooks', *relpath.split(os.sep))
1049 1053 else:
1050 1054 uri = 'tree'
1051 1055 if browser:
1052 1056 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1053 1057 new=2)
1054 1058 threading.Thread(target=b).start()
1055 1059
1056 1060 self.io_loop = ioloop.IOLoop.current()
1057 1061 try:
1058 1062 self.io_loop.start()
1059 1063 except KeyboardInterrupt:
1060 1064 info("Interrupted...")
1061 1065 finally:
1062 1066 self.cleanup_kernels()
1063 1067 self.remove_server_info_file()
1064 1068
1065 1069 def stop(self):
1066 1070 def _stop():
1067 1071 self.http_server.stop()
1068 1072 self.io_loop.stop()
1069 1073 self.io_loop.add_callback(_stop)
1070 1074
1071 1075
1072 1076 def list_running_servers(profile='default'):
1073 1077 """Iterate over the server info files of running notebook servers.
1074 1078
1075 1079 Given a profile name, find nbserver-* files in the security directory of
1076 1080 that profile, and yield dicts of their information, each one pertaining to
1077 1081 a currently running notebook server instance.
1078 1082 """
1079 1083 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1080 1084 for file in os.listdir(pd.security_dir):
1081 1085 if file.startswith('nbserver-'):
1082 1086 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1083 1087 info = json.load(f)
1084 1088
1085 1089 # Simple check whether that process is really still running
1086 1090 # Also remove leftover files from IPython 2.x without a pid field
1087 1091 if ('pid' in info) and check_pid(info['pid']):
1088 1092 yield info
1089 1093 else:
1090 1094 # If the process has died, try to delete its info file
1091 1095 try:
1092 1096 os.unlink(file)
1093 1097 except OSError:
1094 1098 pass # TODO: This should warn or log or something
1095 1099 #-----------------------------------------------------------------------------
1096 1100 # Main entry point
1097 1101 #-----------------------------------------------------------------------------
1098 1102
1099 1103 launch_new_instance = NotebookApp.launch_instance
1100 1104
@@ -1,272 +1,282 b''
1 1 # coding: utf-8
2 2 """Test installation of notebook extensions"""
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2014 The IPython Development Team
5 #
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING, distributed as part of this software.
8 #-----------------------------------------------------------------------------
9 3
10 #-----------------------------------------------------------------------------
11 # Imports
12 #-----------------------------------------------------------------------------
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
13 6
14 7 import glob
15 8 import os
16 9 import re
17 10 import tarfile
18 11 import zipfile
19 12 from io import BytesIO
20 13 from os.path import basename, join as pjoin
21 14 from unittest import TestCase
22 15
23 16 import IPython.testing.tools as tt
24 17 import IPython.testing.decorators as dec
25 18 from IPython.utils import py3compat
26 19 from IPython.utils.tempdir import TemporaryDirectory
27 20 from IPython.html import nbextensions
28 21 from IPython.html.nbextensions import install_nbextension, check_nbextension
29 22
30 #-----------------------------------------------------------------------------
31 # Test functions
32 #-----------------------------------------------------------------------------
33 23
34 24 def touch(file, mtime=None):
35 25 """ensure a file exists, and set its modification time
36 26
37 27 returns the modification time of the file
38 28 """
39 29 open(file, 'a').close()
40 30 # set explicit mtime
41 31 if mtime:
42 32 atime = os.stat(file).st_atime
43 33 os.utime(file, (atime, mtime))
44 34 return os.stat(file).st_mtime
45 35
46
47 36 class TestInstallNBExtension(TestCase):
48 37
49 38 def tempdir(self):
50 39 td = TemporaryDirectory()
51 40 self.tempdirs.append(td)
52 41 return py3compat.cast_unicode(td.name)
53 42
54 43 def setUp(self):
55 44 self.tempdirs = []
56 45 src = self.src = self.tempdir()
57 46 self.files = files = [
58 47 pjoin(u'ƒile'),
59 48 pjoin(u'∂ir', u'ƒile1'),
60 49 pjoin(u'∂ir', u'∂ir2', u'ƒile2'),
61 50 ]
62 51 for file in files:
63 52 fullpath = os.path.join(self.src, file)
64 53 parent = os.path.dirname(fullpath)
65 54 if not os.path.exists(parent):
66 55 os.makedirs(parent)
67 56 touch(fullpath)
68 57
69 58 self.ipdir = self.tempdir()
70 59 self.save_get_ipython_dir = nbextensions.get_ipython_dir
71 60 nbextensions.get_ipython_dir = lambda : self.ipdir
61 self.save_system_dir = nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR
62 nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR = self.system_nbext = self.tempdir()
72 63
73 64 def tearDown(self):
65 nbextensions.get_ipython_dir = self.save_get_ipython_dir
66 nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR = self.save_system_dir
74 67 for td in self.tempdirs:
75 68 td.cleanup()
76 nbextensions.get_ipython_dir = self.save_get_ipython_dir
77
69
78 70 def assert_dir_exists(self, path):
79 71 if not os.path.exists(path):
80 72 do_exist = os.listdir(os.path.dirname(path))
81 73 self.fail(u"%s should exist (found %s)" % (path, do_exist))
82 74
83 75 def assert_not_dir_exists(self, path):
84 76 if os.path.exists(path):
85 77 self.fail(u"%s should not exist" % path)
86 78
87 def assert_installed(self, relative_path, ipdir=None):
79 def assert_installed(self, relative_path, user=False):
80 if user:
81 nbext = pjoin(self.ipdir, u'nbextensions')
82 else:
83 nbext = self.system_nbext
88 84 self.assert_dir_exists(
89 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
85 pjoin(nbext, relative_path)
90 86 )
91 87
92 def assert_not_installed(self, relative_path, ipdir=None):
88 def assert_not_installed(self, relative_path, user=False):
89 if user:
90 nbext = pjoin(self.ipdir, u'nbextensions')
91 else:
92 nbext = self.system_nbext
93 93 self.assert_not_dir_exists(
94 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
94 pjoin(nbext, relative_path)
95 95 )
96 96
97 97 def test_create_ipython_dir(self):
98 98 """install_nbextension when ipython_dir doesn't exist"""
99 99 with TemporaryDirectory() as td:
100 ipdir = pjoin(td, u'ipython')
101 install_nbextension(self.src, ipython_dir=ipdir)
100 self.ipdir = ipdir = pjoin(td, u'ipython')
101 install_nbextension(self.src, user=True)
102 102 self.assert_dir_exists(ipdir)
103 103 for file in self.files:
104 104 self.assert_installed(
105 105 pjoin(basename(self.src), file),
106 106 ipdir
107 107 )
108 108
109 def test_create_nbextensions(self):
110 with TemporaryDirectory() as ipdir:
111 install_nbextension(self.src, ipython_dir=ipdir)
109 def test_create_nbextensions_user(self):
110 with TemporaryDirectory() as td:
111 self.ipdir = ipdir = pjoin(td, u'ipython')
112 install_nbextension(self.src, user=True)
113 self.assert_installed(
114 pjoin(basename(self.src), u'ƒile'),
115 user=True
116 )
117
118 def test_create_nbextensions_system(self):
119 with TemporaryDirectory() as td:
120 nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR = self.system_nbext = pjoin(td, u'nbextensions')
121 install_nbextension(self.src, user=False)
112 122 self.assert_installed(
113 123 pjoin(basename(self.src), u'ƒile'),
114 ipdir
124 user=False
115 125 )
116 126
117 127 def test_single_file(self):
118 128 file = self.files[0]
119 129 install_nbextension(pjoin(self.src, file))
120 130 self.assert_installed(file)
121 131
122 132 def test_single_dir(self):
123 133 d = u'∂ir'
124 134 install_nbextension(pjoin(self.src, d))
125 135 self.assert_installed(self.files[-1])
126 136
127 137 def test_install_nbextension(self):
128 138 install_nbextension(glob.glob(pjoin(self.src, '*')))
129 139 for file in self.files:
130 140 self.assert_installed(file)
131 141
132 142 def test_overwrite_file(self):
133 143 with TemporaryDirectory() as d:
134 144 fname = u'ƒ.js'
135 145 src = pjoin(d, fname)
136 146 with open(src, 'w') as f:
137 147 f.write('first')
138 148 mtime = touch(src)
139 dest = pjoin(self.ipdir, u'nbextensions', fname)
149 dest = pjoin(self.system_nbext, fname)
140 150 install_nbextension(src)
141 151 with open(src, 'w') as f:
142 152 f.write('overwrite')
143 153 mtime = touch(src, mtime - 100)
144 154 install_nbextension(src, overwrite=True)
145 155 with open(dest) as f:
146 156 self.assertEqual(f.read(), 'overwrite')
147 157
148 158 def test_overwrite_dir(self):
149 159 with TemporaryDirectory() as src:
150 # src = py3compat.cast_unicode_py2(src)
151 160 base = basename(src)
152 161 fname = u'ƒ.js'
153 162 touch(pjoin(src, fname))
154 163 install_nbextension(src)
155 164 self.assert_installed(pjoin(base, fname))
156 165 os.remove(pjoin(src, fname))
157 166 fname2 = u'∂.js'
158 167 touch(pjoin(src, fname2))
159 168 install_nbextension(src, overwrite=True)
160 169 self.assert_installed(pjoin(base, fname2))
161 170 self.assert_not_installed(pjoin(base, fname))
162 171
163 172 def test_update_file(self):
164 173 with TemporaryDirectory() as d:
165 174 fname = u'ƒ.js'
166 175 src = pjoin(d, fname)
167 176 with open(src, 'w') as f:
168 177 f.write('first')
169 178 mtime = touch(src)
170 179 install_nbextension(src)
171 180 self.assert_installed(fname)
172 dest = pjoin(self.ipdir, u'nbextensions', fname)
181 dest = pjoin(self.system_nbext, fname)
173 182 old_mtime = os.stat(dest).st_mtime
174 183 with open(src, 'w') as f:
175 184 f.write('overwrite')
176 185 touch(src, mtime + 10)
177 186 install_nbextension(src)
178 187 with open(dest) as f:
179 188 self.assertEqual(f.read(), 'overwrite')
180 189
181 190 def test_skip_old_file(self):
182 191 with TemporaryDirectory() as d:
183 192 fname = u'ƒ.js'
184 193 src = pjoin(d, fname)
185 194 mtime = touch(src)
186 195 install_nbextension(src)
187 196 self.assert_installed(fname)
188 dest = pjoin(self.ipdir, u'nbextensions', fname)
197 dest = pjoin(self.system_nbext, fname)
189 198 old_mtime = os.stat(dest).st_mtime
190 199
191 200 mtime = touch(src, mtime - 100)
192 201 install_nbextension(src)
193 202 new_mtime = os.stat(dest).st_mtime
194 203 self.assertEqual(new_mtime, old_mtime)
195 204
196 205 def test_quiet(self):
197 206 with tt.AssertNotPrints(re.compile(r'.+')):
198 207 install_nbextension(self.src, verbose=0)
199 208
200 209 def test_install_zip(self):
201 210 path = pjoin(self.src, "myjsext.zip")
202 211 with zipfile.ZipFile(path, 'w') as f:
203 212 f.writestr("a.js", b"b();")
204 213 f.writestr("foo/a.js", b"foo();")
205 214 install_nbextension(path)
206 215 self.assert_installed("a.js")
207 216 self.assert_installed(pjoin("foo", "a.js"))
208 217
209 218 def test_install_tar(self):
210 219 def _add_file(f, fname, buf):
211 220 info = tarfile.TarInfo(fname)
212 221 info.size = len(buf)
213 222 f.addfile(info, BytesIO(buf))
214 223
215 224 for i,ext in enumerate((".tar.gz", ".tgz", ".tar.bz2")):
216 225 path = pjoin(self.src, "myjsext" + ext)
217 226 with tarfile.open(path, 'w') as f:
218 227 _add_file(f, "b%i.js" % i, b"b();")
219 228 _add_file(f, "foo/b%i.js" % i, b"foo();")
220 229 install_nbextension(path)
221 230 self.assert_installed("b%i.js" % i)
222 231 self.assert_installed(pjoin("foo", "b%i.js" % i))
223 232
224 233 def test_install_url(self):
225 234 def fake_urlretrieve(url, dest):
226 235 touch(dest)
227 236 save_urlretrieve = nbextensions.urlretrieve
228 237 nbextensions.urlretrieve = fake_urlretrieve
229 238 try:
230 239 install_nbextension("http://example.com/path/to/foo.js")
231 240 self.assert_installed("foo.js")
232 241 install_nbextension("https://example.com/path/to/another/bar.js")
233 242 self.assert_installed("bar.js")
234 243 finally:
235 244 nbextensions.urlretrieve = save_urlretrieve
236 245
237 246 def test_check_nbextension(self):
238 247 with TemporaryDirectory() as d:
239 248 f = u'ƒ.js'
240 249 src = pjoin(d, f)
241 250 touch(src)
242 install_nbextension(src)
251 install_nbextension(src, user=True)
243 252
244 assert check_nbextension(f, self.ipdir)
245 assert check_nbextension([f], self.ipdir)
246 assert not check_nbextension([f, pjoin('dne', f)], self.ipdir)
253 nbext = pjoin(self.ipdir, u'nbextensions')
254 assert check_nbextension(f, nbext)
255 assert check_nbextension([f], nbext)
256 assert not check_nbextension([f, pjoin('dne', f)], nbext)
247 257
248 258 @dec.skip_win32
249 259 def test_install_symlink(self):
250 260 with TemporaryDirectory() as d:
251 261 f = u'ƒ.js'
252 262 src = pjoin(d, f)
253 263 touch(src)
254 264 install_nbextension(src, symlink=True)
255 dest = pjoin(self.ipdir, u'nbextensions', f)
265 dest = pjoin(self.system_nbext, f)
256 266 assert os.path.islink(dest)
257 267 link = os.readlink(dest)
258 268 self.assertEqual(link, src)
259 269
260 270 def test_install_symlink_bad(self):
261 271 with self.assertRaises(ValueError):
262 272 install_nbextension("http://example.com/foo.js", symlink=True)
263 273
264 274 with TemporaryDirectory() as d:
265 275 zf = u'ƒ.zip'
266 276 zsrc = pjoin(d, zf)
267 277 with zipfile.ZipFile(zsrc, 'w') as z:
268 278 z.writestr("a.js", b"b();")
269 279
270 280 with self.assertRaises(ValueError):
271 281 install_nbextension(zsrc, symlink=True)
272 282
General Comments 0
You need to be logged in to leave comments. Login now