##// END OF EJS Templates
Merge pull request #2045 from ellisonbg/azurenb...
Brian E. Granger -
r8186:c8c254cc merge
parent child Browse files
Show More
@@ -0,0 +1,143 b''
1 """A notebook manager that uses Azure blob storage.
2
3 Authors:
4
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2012 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
19 import datetime
20
21 import azure
22 from azure.storage import BlobService
23
24 from tornado import web
25
26 from .basenbmanager import BaseNotebookManager
27 from IPython.nbformat import current
28 from IPython.utils.traitlets import Unicode, Instance
29
30
31 #-----------------------------------------------------------------------------
32 # Classes
33 #-----------------------------------------------------------------------------
34
35 class AzureNotebookManager(BaseNotebookManager):
36
37 account_name = Unicode('', config=True, help='Azure storage account name.')
38 account_key = Unicode('', config=True, help='Azure storage account key.')
39 container = Unicode('', config=True, help='Container name for notebooks.')
40
41 blob_service_host_base = Unicode('.blob.core.windows.net', config=True,
42 help='The basename for the blob service URL. If running on the preview site this '
43 'will be .blob.core.azure-preview.com.')
44 def _blob_service_host_base_changed(self, new):
45 self._update_service_host_base(new)
46
47 blob_service = Instance('azure.storage.BlobService')
48 def _blob_service_default(self):
49 return BlobService(account_name=self.account_name, account_key=self.account_key)
50
51 def __init__(self, **kwargs):
52 super(AzureNotebookManager, self).__init__(**kwargs)
53 self._update_service_host_base(self.blob_service_host_base)
54 self._create_container()
55
56 def _update_service_host_base(self, shb):
57 azure.BLOB_SERVICE_HOST_BASE = shb
58
59 def _create_container(self):
60 self.blob_service.create_container(self.container)
61
62 def load_notebook_names(self):
63 """On startup load the notebook ids and names from Azure.
64
65 The blob names are the notebook ids and the notebook names are stored
66 as blob metadata.
67 """
68 self.mapping = {}
69 blobs = self.blob_service.list_blobs(self.container)
70 ids = [blob.name for blob in blobs]
71
72 for id in ids:
73 md = self.blob_service.get_blob_metadata(self.container, id)
74 name = md['x-ms-meta-nbname']
75 self.mapping[id] = name
76
77 def list_notebooks(self):
78 """List all notebooks in the container.
79
80 This version uses `self.mapping` as the authoritative notebook list.
81 """
82 data = [dict(notebook_id=id,name=name) for id, name in self.mapping.items()]
83 data = sorted(data, key=lambda item: item['name'])
84 return data
85
86 def read_notebook_object(self, notebook_id):
87 """Get the object representation of a notebook by notebook_id."""
88 if not self.notebook_exists(notebook_id):
89 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
90 try:
91 s = self.blob_service.get_blob(self.container, notebook_id)
92 except:
93 raise web.HTTPError(500, u'Notebook cannot be read.')
94 try:
95 # v1 and v2 and json in the .ipynb files.
96 nb = current.reads(s, u'json')
97 except:
98 raise web.HTTPError(500, u'Unreadable JSON notebook.')
99 # Todo: The last modified should actually be saved in the notebook document.
100 # We are just using the current datetime until that is implemented.
101 last_modified = datetime.datetime.utcnow()
102 return last_modified, nb
103
104 def write_notebook_object(self, nb, notebook_id=None):
105 """Save an existing notebook object by notebook_id."""
106 try:
107 new_name = nb.metadata.name
108 except AttributeError:
109 raise web.HTTPError(400, u'Missing notebook name')
110
111 if notebook_id is None:
112 notebook_id = self.new_notebook_id(new_name)
113
114 if notebook_id not in self.mapping:
115 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
116
117 try:
118 data = current.writes(nb, u'json')
119 except Exception as e:
120 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
121
122 metadata = {'nbname': new_name}
123 try:
124 self.blob_service.put_blob(self.container, notebook_id, data, 'BlockBlob', x_ms_meta_name_values=metadata)
125 except Exception as e:
126 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
127
128 self.mapping[notebook_id] = new_name
129 return notebook_id
130
131 def delete_notebook(self, notebook_id):
132 """Delete notebook by notebook_id."""
133 if not self.notebook_exists(notebook_id):
134 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
135 try:
136 self.blob_service.delete_blob(self.container, notebook_id)
137 except Exception as e:
138 raise web.HTTPError(400, u'Unexpected error while deleting notebook: %s' % e)
139 else:
140 self.delete_notebook_id(notebook_id)
141
142 def log_info(self):
143 self.log.info("Serving notebooks from Azure storage: %s, %s", self.account_name, self.container)
@@ -0,0 +1,205 b''
1 """A base class notebook manager.
2
3 Authors:
4
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
19 import os
20 import uuid
21
22 from tornado import web
23
24 from IPython.config.configurable import LoggingConfigurable
25 from IPython.nbformat import current
26 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
27
28 #-----------------------------------------------------------------------------
29 # Classes
30 #-----------------------------------------------------------------------------
31
32 class BaseNotebookManager(LoggingConfigurable):
33
34 # Todo:
35 # The notebook_dir attribute is used to mean a couple of different things:
36 # 1. Where the notebooks are stored if FileNotebookManager is used.
37 # 2. The cwd of the kernel for a project.
38 # Right now we use this attribute in a number of different places and
39 # we are going to have to disentagle all of this.
40 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
41 The directory to use for notebooks.
42 """)
43 def _notebook_dir_changed(self, name, old, new):
44 """do a bit of validation of the notebook dir"""
45 if os.path.exists(new) and not os.path.isdir(new):
46 raise TraitError("notebook dir %r is not a directory" % new)
47 if not os.path.exists(new):
48 self.log.info("Creating notebook dir %s", new)
49 try:
50 os.mkdir(new)
51 except:
52 raise TraitError("Couldn't create notebook dir %r" % new)
53
54 allowed_formats = List([u'json',u'py'])
55
56 # Map notebook_ids to notebook names
57 mapping = Dict()
58
59 def load_notebook_names(self):
60 """Load the notebook names into memory.
61
62 This should be called once immediately after the notebook manager
63 is created to load the existing notebooks into the mapping in
64 memory.
65 """
66 self.list_notebooks()
67
68 def list_notebooks(self):
69 """List all notebooks.
70
71 This returns a list of dicts, each of the form::
72
73 dict(notebook_id=notebook,name=name)
74
75 This list of dicts should be sorted by name::
76
77 data = sorted(data, key=lambda item: item['name'])
78 """
79 raise NotImplementedError('must be implemented in a subclass')
80
81
82 def new_notebook_id(self, name):
83 """Generate a new notebook_id for a name and store its mapping."""
84 # TODO: the following will give stable urls for notebooks, but unless
85 # the notebooks are immediately redirected to their new urls when their
86 # filemname changes, nasty inconsistencies result. So for now it's
87 # disabled and instead we use a random uuid4() call. But we leave the
88 # logic here so that we can later reactivate it, whhen the necessary
89 # url redirection code is written.
90 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
91 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
92
93 notebook_id = unicode(uuid.uuid4())
94 self.mapping[notebook_id] = name
95 return notebook_id
96
97 def delete_notebook_id(self, notebook_id):
98 """Delete a notebook's id in the mapping.
99
100 This doesn't delete the actual notebook, only its entry in the mapping.
101 """
102 del self.mapping[notebook_id]
103
104 def notebook_exists(self, notebook_id):
105 """Does a notebook exist?"""
106 return notebook_id in self.mapping
107
108 def get_notebook(self, notebook_id, format=u'json'):
109 """Get the representation of a notebook in format by notebook_id."""
110 format = unicode(format)
111 if format not in self.allowed_formats:
112 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
113 last_modified, nb = self.read_notebook_object(notebook_id)
114 kwargs = {}
115 if format == 'json':
116 # don't split lines for sending over the wire, because it
117 # should match the Python in-memory format.
118 kwargs['split_lines'] = False
119 data = current.writes(nb, format, **kwargs)
120 name = nb.get('name','notebook')
121 return last_modified, name, data
122
123 def read_notebook_object(self, notebook_id):
124 """Get the object representation of a notebook by notebook_id."""
125 raise NotImplementedError('must be implemented in a subclass')
126
127 def save_new_notebook(self, data, name=None, format=u'json'):
128 """Save a new notebook and return its notebook_id.
129
130 If a name is passed in, it overrides any values in the notebook data
131 and the value in the data is updated to use that value.
132 """
133 if format not in self.allowed_formats:
134 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
135
136 try:
137 nb = current.reads(data.decode('utf-8'), format)
138 except:
139 raise web.HTTPError(400, u'Invalid JSON data')
140
141 if name is None:
142 try:
143 name = nb.metadata.name
144 except AttributeError:
145 raise web.HTTPError(400, u'Missing notebook name')
146 nb.metadata.name = name
147
148 notebook_id = self.write_notebook_object(nb)
149 return notebook_id
150
151 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
152 """Save an existing notebook by notebook_id."""
153 if format not in self.allowed_formats:
154 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
155
156 try:
157 nb = current.reads(data.decode('utf-8'), format)
158 except:
159 raise web.HTTPError(400, u'Invalid JSON data')
160
161 if name is not None:
162 nb.metadata.name = name
163 self.write_notebook_object(nb, notebook_id)
164
165 def write_notebook_object(self, nb, notebook_id=None):
166 """Write a notebook object and return its notebook_id.
167
168 If notebook_id is None, this method should create a new notebook_id.
169 If notebook_id is not None, this method should check to make sure it
170 exists and is valid.
171 """
172 raise NotImplementedError('must be implemented in a subclass')
173
174 def delete_notebook(self, notebook_id):
175 """Delete notebook by notebook_id."""
176 raise NotImplementedError('must be implemented in a subclass')
177
178 def increment_filename(self, name):
179 """Increment a filename to make it unique.
180
181 This exists for notebook stores that must have unique names. When a notebook
182 is created or copied this method constructs a unique filename, typically
183 by appending an integer to the name.
184 """
185 return name
186
187 def new_notebook(self):
188 """Create a new notebook and return its notebook_id."""
189 name = self.increment_filename('Untitled')
190 metadata = current.new_metadata(name=name)
191 nb = current.new_notebook(metadata=metadata)
192 notebook_id = self.write_notebook_object(nb)
193 return notebook_id
194
195 def copy_notebook(self, notebook_id):
196 """Copy an existing notebook and return its notebook_id."""
197 last_mod, nb = self.read_notebook_object(notebook_id)
198 name = nb.metadata.name + '-Copy'
199 name = self.increment_filename(name)
200 nb.metadata.name = name
201 notebook_id = self.write_notebook_object(nb)
202 return notebook_id
203
204 def log_info(self):
205 self.log.info("Serving notebooks") No newline at end of file
@@ -1,286 +1,196 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import datetime
20 20 import io
21 21 import os
22 import uuid
23 22 import glob
24 23
25 24 from tornado import web
26 25
27 from IPython.config.configurable import LoggingConfigurable
26 from .basenbmanager import BaseNotebookManager
28 27 from IPython.nbformat import current
29 from IPython.utils.traitlets import Unicode, List, Dict, Bool, TraitError
28 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 29
31 30 #-----------------------------------------------------------------------------
32 31 # Classes
33 32 #-----------------------------------------------------------------------------
34 33
35 class NotebookManager(LoggingConfigurable):
36
37 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
38 The directory to use for notebooks.
39 """)
40 def _notebook_dir_changed(self, name, old, new):
41 """do a bit of validation of the notebook dir"""
42 if os.path.exists(new) and not os.path.isdir(new):
43 raise TraitError("notebook dir %r is not a directory" % new)
44 if not os.path.exists(new):
45 self.log.info("Creating notebook dir %s", new)
46 try:
47 os.mkdir(new)
48 except:
49 raise TraitError("Couldn't create notebook dir %r" % new)
34 class FileNotebookManager(BaseNotebookManager):
50 35
51 36 save_script = Bool(False, config=True,
52 37 help="""Automatically create a Python script when saving the notebook.
53 38
54 39 For easier use of import, %run and %load across notebooks, a
55 40 <notebook-name>.py script will be created next to any
56 41 <notebook-name>.ipynb on each save. This can also be set with the
57 42 short `--script` flag.
58 43 """
59 44 )
60 45
61 46 filename_ext = Unicode(u'.ipynb')
62 allowed_formats = List([u'json',u'py'])
63 47
64 # Map notebook_ids to notebook names
65 mapping = Dict()
66 48 # Map notebook names to notebook_ids
67 49 rev_mapping = Dict()
68 50
69 def list_notebooks(self):
70 """List all notebooks in the notebook dir.
71
72 This returns a list of dicts of the form::
73
74 dict(notebook_id=notebook,name=name)
75 """
51 def get_notebook_names(self):
52 """List all notebook names in the notebook dir."""
76 53 names = glob.glob(os.path.join(self.notebook_dir,
77 54 '*' + self.filename_ext))
78 55 names = [os.path.splitext(os.path.basename(name))[0]
79 56 for name in names]
57 return names
58
59 def list_notebooks(self):
60 """List all notebooks in the notebook dir."""
61 names = self.get_notebook_names()
80 62
81 63 data = []
82 64 for name in names:
83 65 if name not in self.rev_mapping:
84 66 notebook_id = self.new_notebook_id(name)
85 67 else:
86 68 notebook_id = self.rev_mapping[name]
87 69 data.append(dict(notebook_id=notebook_id,name=name))
88 70 data = sorted(data, key=lambda item: item['name'])
89 71 return data
90 72
91 73 def new_notebook_id(self, name):
92 74 """Generate a new notebook_id for a name and store its mappings."""
93 # TODO: the following will give stable urls for notebooks, but unless
94 # the notebooks are immediately redirected to their new urls when their
95 # filemname changes, nasty inconsistencies result. So for now it's
96 # disabled and instead we use a random uuid4() call. But we leave the
97 # logic here so that we can later reactivate it, whhen the necessary
98 # url redirection code is written.
99 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
100 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
101
102 notebook_id = unicode(uuid.uuid4())
103
104 self.mapping[notebook_id] = name
75 notebook_id = super(FileNotebookManager, self).new_notebook_id(name)
105 76 self.rev_mapping[name] = notebook_id
106 77 return notebook_id
107 78
108 79 def delete_notebook_id(self, notebook_id):
109 """Delete a notebook's id only. This doesn't delete the actual notebook."""
80 """Delete a notebook's id in the mapping."""
110 81 name = self.mapping[notebook_id]
111 del self.mapping[notebook_id]
82 super(FileNotebookManager, self).delete_notebook_id(notebook_id)
112 83 del self.rev_mapping[name]
113 84
114 85 def notebook_exists(self, notebook_id):
115 86 """Does a notebook exist?"""
116 if notebook_id not in self.mapping:
87 exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
88 if not exists:
117 89 return False
118 90 path = self.get_path_by_name(self.mapping[notebook_id])
119 91 return os.path.isfile(path)
120 92
121 93 def find_path(self, notebook_id):
122 94 """Return a full path to a notebook given its notebook_id."""
123 95 try:
124 96 name = self.mapping[notebook_id]
125 97 except KeyError:
126 98 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
127 99 return self.get_path_by_name(name)
128 100
129 101 def get_path_by_name(self, name):
130 102 """Return a full path to a notebook given its name."""
131 103 filename = name + self.filename_ext
132 104 path = os.path.join(self.notebook_dir, filename)
133 105 return path
134 106
135 def get_notebook(self, notebook_id, format=u'json'):
136 """Get the representation of a notebook in format by notebook_id."""
137 format = unicode(format)
138 if format not in self.allowed_formats:
139 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
140 last_modified, nb = self.get_notebook_object(notebook_id)
141 kwargs = {}
142 if format == 'json':
143 # don't split lines for sending over the wire, because it
144 # should match the Python in-memory format.
145 kwargs['split_lines'] = False
146 data = current.writes(nb, format, **kwargs)
147 name = nb.get('name','notebook')
148 return last_modified, name, data
149
150 def get_notebook_object(self, notebook_id):
107 def read_notebook_object(self, notebook_id):
151 108 """Get the NotebookNode representation of a notebook by notebook_id."""
152 109 path = self.find_path(notebook_id)
153 110 if not os.path.isfile(path):
154 111 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
155 112 info = os.stat(path)
156 113 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
157 114 with open(path,'r') as f:
158 115 s = f.read()
159 116 try:
160 117 # v1 and v2 and json in the .ipynb files.
161 118 nb = current.reads(s, u'json')
162 119 except:
163 120 raise web.HTTPError(500, u'Unreadable JSON notebook.')
164 121 # Always use the filename as the notebook name.
165 122 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
166 123 return last_modified, nb
167 124
168 def save_new_notebook(self, data, name=None, format=u'json'):
169 """Save a new notebook and return its notebook_id.
170
171 If a name is passed in, it overrides any values in the notebook data
172 and the value in the data is updated to use that value.
173 """
174 if format not in self.allowed_formats:
175 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
176
177 try:
178 nb = current.reads(data.decode('utf-8'), format)
179 except:
180 raise web.HTTPError(400, u'Invalid JSON data')
181
182 if name is None:
183 try:
184 name = nb.metadata.name
185 except AttributeError:
186 raise web.HTTPError(400, u'Missing notebook name')
187 nb.metadata.name = name
188
189 notebook_id = self.new_notebook_id(name)
190 self.save_notebook_object(notebook_id, nb)
191 return notebook_id
192
193 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
194 """Save an existing notebook by notebook_id."""
195 if format not in self.allowed_formats:
196 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
197
125 def write_notebook_object(self, nb, notebook_id=None):
126 """Save an existing notebook object by notebook_id."""
198 127 try:
199 nb = current.reads(data.decode('utf-8'), format)
200 except:
201 raise web.HTTPError(400, u'Invalid JSON data')
128 new_name = nb.metadata.name
129 except AttributeError:
130 raise web.HTTPError(400, u'Missing notebook name')
202 131
203 if name is not None:
204 nb.metadata.name = name
205 self.save_notebook_object(notebook_id, nb)
132 if notebook_id is None:
133 notebook_id = self.new_notebook_id(new_name)
206 134
207 def save_notebook_object(self, notebook_id, nb):
208 """Save an existing notebook object by notebook_id."""
209 135 if notebook_id not in self.mapping:
210 136 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
137
211 138 old_name = self.mapping[notebook_id]
212 try:
213 new_name = nb.metadata.name
214 except AttributeError:
215 raise web.HTTPError(400, u'Missing notebook name')
216 139 path = self.get_path_by_name(new_name)
217 140 try:
218 141 with open(path,'w') as f:
219 142 current.write(nb, f, u'json')
220 143 except Exception as e:
221 144 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
145
222 146 # save .py script as well
223 147 if self.save_script:
224 148 pypath = os.path.splitext(path)[0] + '.py'
225 149 try:
226 150 with io.open(pypath,'w', encoding='utf-8') as f:
227 151 current.write(nb, f, u'py')
228 152 except Exception as e:
229 153 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
230 154
155 # remove old files if the name changed
231 156 if old_name != new_name:
232 157 old_path = self.get_path_by_name(old_name)
233 158 if os.path.isfile(old_path):
234 159 os.unlink(old_path)
235 160 if self.save_script:
236 161 old_pypath = os.path.splitext(old_path)[0] + '.py'
237 162 if os.path.isfile(old_pypath):
238 163 os.unlink(old_pypath)
239 164 self.mapping[notebook_id] = new_name
240 165 self.rev_mapping[new_name] = notebook_id
241 166 del self.rev_mapping[old_name]
167
168 return notebook_id
242 169
243 170 def delete_notebook(self, notebook_id):
244 171 """Delete notebook by notebook_id."""
245 172 path = self.find_path(notebook_id)
246 173 if not os.path.isfile(path):
247 174 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
248 175 os.unlink(path)
249 176 self.delete_notebook_id(notebook_id)
250 177
251 178 def increment_filename(self, basename):
252 179 """Return a non-used filename of the form basename<int>.
253 180
254 181 This searches through the filenames (basename0, basename1, ...)
255 182 until is find one that is not already being used. It is used to
256 183 create Untitled and Copy names that are unique.
257 184 """
258 185 i = 0
259 186 while True:
260 187 name = u'%s%i' % (basename,i)
261 188 path = self.get_path_by_name(name)
262 189 if not os.path.isfile(path):
263 190 break
264 191 else:
265 192 i = i+1
266 return path, name
267
268 def new_notebook(self):
269 """Create a new notebook and return its notebook_id."""
270 path, name = self.increment_filename('Untitled')
271 notebook_id = self.new_notebook_id(name)
272 metadata = current.new_metadata(name=name)
273 nb = current.new_notebook(metadata=metadata)
274 with open(path,'w') as f:
275 current.write(nb, f, u'json')
276 return notebook_id
193 return name
277 194
278 def copy_notebook(self, notebook_id):
279 """Copy an existing notebook and return its notebook_id."""
280 last_mod, nb = self.get_notebook_object(notebook_id)
281 name = nb.metadata.name + '-Copy'
282 path, name = self.increment_filename(name)
283 nb.metadata.name = name
284 notebook_id = self.new_notebook_id(name)
285 self.save_notebook_object(notebook_id, nb)
286 return notebook_id
195 def log_info(self):
196 self.log.info("Serving notebooks from local directory: %s", self.notebook_dir)
@@ -1,606 +1,617 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import random
24 24 import re
25 25 import select
26 26 import signal
27 27 import socket
28 28 import sys
29 29 import threading
30 30 import time
31 31 import webbrowser
32 32
33 33 # Third party
34 34 import zmq
35 35
36 36 # Install the pyzmq ioloop. This has to be done before anything else from
37 37 # tornado is imported.
38 38 from zmq.eventloop import ioloop
39 39 ioloop.install()
40 40
41 41 from tornado import httpserver
42 42 from tornado import web
43 43
44 44 # Our own libraries
45 45 from .kernelmanager import MappingKernelManager
46 46 from .handlers import (LoginHandler, LogoutHandler,
47 47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
48 48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
49 49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
50 50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
51 51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
52 52 FileFindHandler,
53 53 )
54 from .notebookmanager import NotebookManager
54 from .basenbmanager import BaseNotebookManager
55 from .filenbmanager import FileNotebookManager
55 56 from .clustermanager import ClusterManager
56 57
57 58 from IPython.config.application import catch_config_error, boolean_flag
58 59 from IPython.core.application import BaseIPythonApplication
59 60 from IPython.core.profiledir import ProfileDir
60 61 from IPython.frontend.consoleapp import IPythonConsoleApp
61 62 from IPython.lib.kernel import swallow_argv
62 63 from IPython.zmq.session import Session, default_secure
63 64 from IPython.zmq.zmqshell import ZMQInteractiveShell
64 65 from IPython.zmq.ipkernel import (
65 66 flags as ipkernel_flags,
66 67 aliases as ipkernel_aliases,
67 68 IPKernelApp
68 69 )
69 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
70 from IPython.utils.importstring import import_item
71 from IPython.utils.traitlets import (
72 Dict, Unicode, Integer, List, Enum, Bool,
73 DottedObjectName
74 )
70 75 from IPython.utils import py3compat
71 76 from IPython.utils.path import filefind
72 77
73 78 #-----------------------------------------------------------------------------
74 79 # Module globals
75 80 #-----------------------------------------------------------------------------
76 81
77 82 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
78 83 _kernel_action_regex = r"(?P<action>restart|interrupt)"
79 84 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
80 85 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
81 86 _cluster_action_regex = r"(?P<action>start|stop)"
82 87
83 88
84 89 LOCALHOST = '127.0.0.1'
85 90
86 91 _examples = """
87 92 ipython notebook # start the notebook
88 93 ipython notebook --profile=sympy # use the sympy profile
89 94 ipython notebook --pylab=inline # pylab in inline plotting mode
90 95 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
91 96 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
92 97 """
93 98
94 99 #-----------------------------------------------------------------------------
95 100 # Helper functions
96 101 #-----------------------------------------------------------------------------
97 102
98 103 def url_path_join(a,b):
99 104 if a.endswith('/') and b.startswith('/'):
100 105 return a[:-1]+b
101 106 else:
102 107 return a+b
103 108
104 109 def random_ports(port, n):
105 110 """Generate a list of n random ports near the given port.
106 111
107 112 The first 5 ports will be sequential, and the remaining n-5 will be
108 113 randomly selected in the range [port-2*n, port+2*n].
109 114 """
110 115 for i in range(min(5, n)):
111 116 yield port + i
112 117 for i in range(n-5):
113 118 yield port + random.randint(-2*n, 2*n)
114 119
115 120 #-----------------------------------------------------------------------------
116 121 # The Tornado web application
117 122 #-----------------------------------------------------------------------------
118 123
119 124 class NotebookWebApplication(web.Application):
120 125
121 126 def __init__(self, ipython_app, kernel_manager, notebook_manager,
122 127 cluster_manager, log,
123 128 base_project_url, settings_overrides):
124 129 handlers = [
125 130 (r"/", ProjectDashboardHandler),
126 131 (r"/login", LoginHandler),
127 132 (r"/logout", LogoutHandler),
128 133 (r"/new", NewHandler),
129 134 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
130 135 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
131 136 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
132 137 (r"/kernels", MainKernelHandler),
133 138 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
134 139 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
135 140 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
136 141 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
137 142 (r"/notebooks", NotebookRootHandler),
138 143 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
139 144 (r"/rstservice/render", RSTHandler),
140 145 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
141 146 (r"/clusters", MainClusterHandler),
142 147 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
143 148 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
144 149 ]
145 150
146 151 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
147 152 # base_project_url will always be unicode, which will in turn
148 153 # make the patterns unicode, and ultimately result in unicode
149 154 # keys in kwargs to handler._execute(**kwargs) in tornado.
150 155 # This enforces that base_project_url be ascii in that situation.
151 156 #
152 157 # Note that the URLs these patterns check against are escaped,
153 158 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
154 159 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
155 160
156 161 settings = dict(
157 162 template_path=os.path.join(os.path.dirname(__file__), "templates"),
158 163 static_path=ipython_app.static_file_path,
159 164 static_handler_class = FileFindHandler,
160 165 cookie_secret=os.urandom(1024),
161 166 login_url="%s/login"%(base_project_url.rstrip('/')),
162 167 )
163 168
164 169 # allow custom overrides for the tornado web app.
165 170 settings.update(settings_overrides)
166 171
167 172 # prepend base_project_url onto the patterns that we match
168 173 new_handlers = []
169 174 for handler in handlers:
170 175 pattern = url_path_join(base_project_url, handler[0])
171 176 new_handler = tuple([pattern]+list(handler[1:]))
172 177 new_handlers.append( new_handler )
173 178
174 179 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
175 180
176 181 self.kernel_manager = kernel_manager
177 182 self.notebook_manager = notebook_manager
178 183 self.cluster_manager = cluster_manager
179 184 self.ipython_app = ipython_app
180 185 self.read_only = self.ipython_app.read_only
181 186 self.log = log
182 187
183 188
184 189 #-----------------------------------------------------------------------------
185 190 # Aliases and Flags
186 191 #-----------------------------------------------------------------------------
187 192
188 193 flags = dict(ipkernel_flags)
189 194 flags['no-browser']=(
190 195 {'NotebookApp' : {'open_browser' : False}},
191 196 "Don't open the notebook in a browser after startup."
192 197 )
193 198 flags['no-mathjax']=(
194 199 {'NotebookApp' : {'enable_mathjax' : False}},
195 200 """Disable MathJax
196 201
197 202 MathJax is the javascript library IPython uses to render math/LaTeX. It is
198 203 very large, so you may want to disable it if you have a slow internet
199 204 connection, or for offline use of the notebook.
200 205
201 206 When disabled, equations etc. will appear as their untransformed TeX source.
202 207 """
203 208 )
204 209 flags['read-only'] = (
205 210 {'NotebookApp' : {'read_only' : True}},
206 211 """Allow read-only access to notebooks.
207 212
208 213 When using a password to protect the notebook server, this flag
209 214 allows unauthenticated clients to view the notebook list, and
210 215 individual notebooks, but not edit them, start kernels, or run
211 216 code.
212 217
213 218 If no password is set, the server will be entirely read-only.
214 219 """
215 220 )
216 221
217 222 # Add notebook manager flags
218 flags.update(boolean_flag('script', 'NotebookManager.save_script',
223 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
219 224 'Auto-save a .py script everytime the .ipynb notebook is saved',
220 225 'Do not auto-save .py scripts for every notebook'))
221 226
222 227 # the flags that are specific to the frontend
223 228 # these must be scrubbed before being passed to the kernel,
224 229 # or it will raise an error on unrecognized flags
225 230 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
226 231
227 232 aliases = dict(ipkernel_aliases)
228 233
229 234 aliases.update({
230 235 'ip': 'NotebookApp.ip',
231 236 'port': 'NotebookApp.port',
232 237 'port-retries': 'NotebookApp.port_retries',
233 238 'keyfile': 'NotebookApp.keyfile',
234 239 'certfile': 'NotebookApp.certfile',
235 'notebook-dir': 'NotebookManager.notebook_dir',
240 'notebook-dir': 'BaseNotebookManager.notebook_dir',
236 241 'browser': 'NotebookApp.browser',
237 242 })
238 243
239 244 # remove ipkernel flags that are singletons, and don't make sense in
240 245 # multi-kernel evironment:
241 246 aliases.pop('f', None)
242 247
243 248 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
244 249 u'notebook-dir']
245 250
246 251 #-----------------------------------------------------------------------------
247 252 # NotebookApp
248 253 #-----------------------------------------------------------------------------
249 254
250 255 class NotebookApp(BaseIPythonApplication):
251 256
252 257 name = 'ipython-notebook'
253 258 default_config_file_name='ipython_notebook_config.py'
254 259
255 260 description = """
256 261 The IPython HTML Notebook.
257 262
258 263 This launches a Tornado based HTML Notebook Server that serves up an
259 264 HTML5/Javascript Notebook client.
260 265 """
261 266 examples = _examples
262 267
263 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
268 classes = IPythonConsoleApp.classes + [MappingKernelManager, BaseNotebookManager,
269 FileNotebookManager]
264 270 flags = Dict(flags)
265 271 aliases = Dict(aliases)
266 272
267 273 kernel_argv = List(Unicode)
268 274
269 275 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
270 276 default_value=logging.INFO,
271 277 config=True,
272 278 help="Set the log level by value or name.")
273 279
274 280 # create requested profiles by default, if they don't exist:
275 281 auto_create = Bool(True)
276 282
277 283 # file to be opened in the notebook server
278 284 file_to_run = Unicode('')
279 285
280 286 # Network related information.
281 287
282 288 ip = Unicode(LOCALHOST, config=True,
283 289 help="The IP address the notebook server will listen on."
284 290 )
285 291
286 292 def _ip_changed(self, name, old, new):
287 293 if new == u'*': self.ip = u''
288 294
289 295 port = Integer(8888, config=True,
290 296 help="The port the notebook server will listen on."
291 297 )
292 298 port_retries = Integer(50, config=True,
293 299 help="The number of additional ports to try if the specified port is not available."
294 300 )
295 301
296 302 certfile = Unicode(u'', config=True,
297 303 help="""The full path to an SSL/TLS certificate file."""
298 304 )
299 305
300 306 keyfile = Unicode(u'', config=True,
301 307 help="""The full path to a private key file for usage with SSL/TLS."""
302 308 )
303 309
304 310 password = Unicode(u'', config=True,
305 311 help="""Hashed password to use for web authentication.
306 312
307 313 To generate, type in a python/IPython shell:
308 314
309 315 from IPython.lib import passwd; passwd()
310 316
311 317 The string should be of the form type:salt:hashed-password.
312 318 """
313 319 )
314 320
315 321 open_browser = Bool(True, config=True,
316 322 help="""Whether to open in a browser after starting.
317 323 The specific browser used is platform dependent and
318 324 determined by the python standard library `webbrowser`
319 325 module, unless it is overridden using the --browser
320 326 (NotebookApp.browser) configuration option.
321 327 """)
322 328
323 329 browser = Unicode(u'', config=True,
324 330 help="""Specify what command to use to invoke a web
325 331 browser when opening the notebook. If not specified, the
326 332 default browser will be determined by the `webbrowser`
327 333 standard library module, which allows setting of the
328 334 BROWSER environment variable to override it.
329 335 """)
330 336
331 337 read_only = Bool(False, config=True,
332 338 help="Whether to prevent editing/execution of notebooks."
333 339 )
334 340
335 341 webapp_settings = Dict(config=True,
336 342 help="Supply overrides for the tornado.web.Application that the "
337 343 "IPython notebook uses.")
338 344
339 345 enable_mathjax = Bool(True, config=True,
340 346 help="""Whether to enable MathJax for typesetting math/TeX
341 347
342 348 MathJax is the javascript library IPython uses to render math/LaTeX. It is
343 349 very large, so you may want to disable it if you have a slow internet
344 350 connection, or for offline use of the notebook.
345 351
346 352 When disabled, equations etc. will appear as their untransformed TeX source.
347 353 """
348 354 )
349 355 def _enable_mathjax_changed(self, name, old, new):
350 356 """set mathjax url to empty if mathjax is disabled"""
351 357 if not new:
352 358 self.mathjax_url = u''
353 359
354 360 base_project_url = Unicode('/', config=True,
355 361 help='''The base URL for the notebook server''')
356 362 base_kernel_url = Unicode('/', config=True,
357 363 help='''The base URL for the kernel server''')
358 364 websocket_host = Unicode("", config=True,
359 365 help="""The hostname for the websocket server."""
360 366 )
361 367
362 368 extra_static_paths = List(Unicode, config=True,
363 369 help="""Extra paths to search for serving static files.
364 370
365 371 This allows adding javascript/css to be available from the notebook server machine,
366 372 or overriding individual files in the IPython"""
367 373 )
368 374 def _extra_static_paths_default(self):
369 375 return [os.path.join(self.profile_dir.location, 'static')]
370 376
371 377 @property
372 378 def static_file_path(self):
373 379 """return extra paths + the default location"""
374 380 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
375 381
376 382 mathjax_url = Unicode("", config=True,
377 383 help="""The url for MathJax.js."""
378 384 )
379 385 def _mathjax_url_default(self):
380 386 if not self.enable_mathjax:
381 387 return u''
382 388 static_url_prefix = self.webapp_settings.get("static_url_prefix",
383 389 "/static/")
384 390 try:
385 391 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
386 392 except IOError:
387 393 if self.certfile:
388 394 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
389 395 base = u"https://c328740.ssl.cf1.rackcdn.com"
390 396 else:
391 397 base = u"http://cdn.mathjax.org"
392 398
393 399 url = base + u"/mathjax/latest/MathJax.js"
394 400 self.log.info("Using MathJax from CDN: %s", url)
395 401 return url
396 402 else:
397 403 self.log.info("Using local MathJax from %s" % mathjax)
398 404 return static_url_prefix+u"mathjax/MathJax.js"
399 405
400 406 def _mathjax_url_changed(self, name, old, new):
401 407 if new and not self.enable_mathjax:
402 408 # enable_mathjax=False overrides mathjax_url
403 409 self.mathjax_url = u''
404 410 else:
405 411 self.log.info("Using MathJax: %s", new)
406 412
413 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
414 config=True,
415 help='The notebook manager class to use.')
416
407 417 def parse_command_line(self, argv=None):
408 418 super(NotebookApp, self).parse_command_line(argv)
409 419 if argv is None:
410 420 argv = sys.argv[1:]
411 421
412 422 # Scrub frontend-specific flags
413 423 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
414 424 # Kernel should inherit default config file from frontend
415 425 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
416 426
417 427 if self.extra_args:
418 428 f = os.path.abspath(self.extra_args[0])
419 429 if os.path.isdir(f):
420 430 nbdir = f
421 431 else:
422 432 self.file_to_run = f
423 433 nbdir = os.path.dirname(f)
424 self.config.NotebookManager.notebook_dir = nbdir
434 self.config.BaseNotebookManager.notebook_dir = nbdir
425 435
426 436 def init_configurables(self):
427 437 # force Session default to be secure
428 438 default_secure(self.config)
429 439 self.kernel_manager = MappingKernelManager(
430 440 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
431 441 connection_dir = self.profile_dir.security_dir,
432 442 )
433 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
434 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
435 self.notebook_manager.list_notebooks()
443 kls = import_item(self.notebook_manager_class)
444 self.notebook_manager = kls(config=self.config, log=self.log)
445 self.notebook_manager.log_info()
446 self.notebook_manager.load_notebook_names()
436 447 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
437 448 self.cluster_manager.update_profiles()
438 449
439 450 def init_logging(self):
440 451 # This prevents double log messages because tornado use a root logger that
441 452 # self.log is a child of. The logging module dipatches log messages to a log
442 453 # and all of its ancenstors until propagate is set to False.
443 454 self.log.propagate = False
444 455
445 456 def init_webapp(self):
446 457 """initialize tornado webapp and httpserver"""
447 458 self.web_app = NotebookWebApplication(
448 459 self, self.kernel_manager, self.notebook_manager,
449 460 self.cluster_manager, self.log,
450 461 self.base_project_url, self.webapp_settings
451 462 )
452 463 if self.certfile:
453 464 ssl_options = dict(certfile=self.certfile)
454 465 if self.keyfile:
455 466 ssl_options['keyfile'] = self.keyfile
456 467 else:
457 468 ssl_options = None
458 469 self.web_app.password = self.password
459 470 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
460 471 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
461 472 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
462 473 'but not using any encryption or authentication. This is highly '
463 474 'insecure and not recommended.')
464 475
465 476 success = None
466 477 for port in random_ports(self.port, self.port_retries+1):
467 478 try:
468 479 self.http_server.listen(port, self.ip)
469 480 except socket.error as e:
470 481 if e.errno != errno.EADDRINUSE:
471 482 raise
472 483 self.log.info('The port %i is already in use, trying another random port.' % port)
473 484 else:
474 485 self.port = port
475 486 success = True
476 487 break
477 488 if not success:
478 489 self.log.critical('ERROR: the notebook server could not be started because '
479 490 'no available port could be found.')
480 491 self.exit(1)
481 492
482 493 def init_signal(self):
483 494 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
484 495 # safely extract zmq version info:
485 496 try:
486 497 zmq_v = zmq.pyzmq_version_info()
487 498 except AttributeError:
488 499 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
489 500 if 'dev' in zmq.__version__:
490 501 zmq_v.append(999)
491 502 zmq_v = tuple(zmq_v)
492 503 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
493 504 # This won't work with 2.1.7 and
494 505 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
495 506 # but it will work
496 507 signal.signal(signal.SIGINT, self._handle_sigint)
497 508 signal.signal(signal.SIGTERM, self._signal_stop)
498 509
499 510 def _handle_sigint(self, sig, frame):
500 511 """SIGINT handler spawns confirmation dialog"""
501 512 # register more forceful signal handler for ^C^C case
502 513 signal.signal(signal.SIGINT, self._signal_stop)
503 514 # request confirmation dialog in bg thread, to avoid
504 515 # blocking the App
505 516 thread = threading.Thread(target=self._confirm_exit)
506 517 thread.daemon = True
507 518 thread.start()
508 519
509 520 def _restore_sigint_handler(self):
510 521 """callback for restoring original SIGINT handler"""
511 522 signal.signal(signal.SIGINT, self._handle_sigint)
512 523
513 524 def _confirm_exit(self):
514 525 """confirm shutdown on ^C
515 526
516 527 A second ^C, or answering 'y' within 5s will cause shutdown,
517 528 otherwise original SIGINT handler will be restored.
518 529
519 530 This doesn't work on Windows.
520 531 """
521 532 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
522 533 time.sleep(0.1)
523 534 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
524 535 sys.stdout.flush()
525 536 r,w,x = select.select([sys.stdin], [], [], 5)
526 537 if r:
527 538 line = sys.stdin.readline()
528 539 if line.lower().startswith('y'):
529 540 self.log.critical("Shutdown confirmed")
530 541 ioloop.IOLoop.instance().stop()
531 542 return
532 543 else:
533 544 print "No answer for 5s:",
534 545 print "resuming operation..."
535 546 # no answer, or answer is no:
536 547 # set it back to original SIGINT handler
537 548 # use IOLoop.add_callback because signal.signal must be called
538 549 # from main thread
539 550 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
540 551
541 552 def _signal_stop(self, sig, frame):
542 553 self.log.critical("received signal %s, stopping", sig)
543 554 ioloop.IOLoop.instance().stop()
544 555
545 556 @catch_config_error
546 557 def initialize(self, argv=None):
547 558 self.init_logging()
548 559 super(NotebookApp, self).initialize(argv)
549 560 self.init_configurables()
550 561 self.init_webapp()
551 562 self.init_signal()
552 563
553 564 def cleanup_kernels(self):
554 565 """shutdown all kernels
555 566
556 567 The kernels will shutdown themselves when this process no longer exists,
557 568 but explicit shutdown allows the KernelManagers to cleanup the connection files.
558 569 """
559 570 self.log.info('Shutting down kernels')
560 571 km = self.kernel_manager
561 572 # copy list, since shutdown_kernel deletes keys
562 573 for kid in list(km.kernel_ids):
563 574 km.shutdown_kernel(kid)
564 575
565 576 def start(self):
566 577 ip = self.ip if self.ip else '[all ip addresses on your system]'
567 578 proto = 'https' if self.certfile else 'http'
568 579 info = self.log.info
569 580 info("The IPython Notebook is running at: %s://%s:%i%s" %
570 581 (proto, ip, self.port,self.base_project_url) )
571 582 info("Use Control-C to stop this server and shut down all kernels.")
572 583
573 584 if self.open_browser or self.file_to_run:
574 585 ip = self.ip or '127.0.0.1'
575 586 try:
576 587 browser = webbrowser.get(self.browser or None)
577 588 except webbrowser.Error as e:
578 589 self.log.warn('No web browser found: %s.' % e)
579 590 browser = None
580 591
581 592 if self.file_to_run:
582 593 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
583 594 url = self.notebook_manager.rev_mapping.get(name, '')
584 595 else:
585 596 url = ''
586 597 if browser:
587 598 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
588 599 self.port, self.base_project_url, url), new=2)
589 600 threading.Thread(target=b).start()
590 601 try:
591 602 ioloop.IOLoop.instance().start()
592 603 except KeyboardInterrupt:
593 604 info("Interrupted...")
594 605 finally:
595 606 self.cleanup_kernels()
596 607
597 608
598 609 #-----------------------------------------------------------------------------
599 610 # Main entry point
600 611 #-----------------------------------------------------------------------------
601 612
602 613 def launch_new_instance():
603 614 app = NotebookApp.instance()
604 615 app.initialize()
605 616 app.start()
606 617
@@ -1,34 +1,34 b''
1 1 """Tests for the notebook manager."""
2 2
3 3 import os
4 4 from unittest import TestCase
5 5 from tempfile import NamedTemporaryFile
6 6
7 7 from IPython.utils.tempdir import TemporaryDirectory
8 8 from IPython.utils.traitlets import TraitError
9 9
10 from IPython.frontend.html.notebook.notebookmanager import NotebookManager
10 from IPython.frontend.html.notebook.filenbmanager import FileNotebookManager
11 11
12 12 class TestNotebookManager(TestCase):
13 13
14 14 def test_nb_dir(self):
15 15 with TemporaryDirectory() as td:
16 km = NotebookManager(notebook_dir=td)
17 self.assertEqual(km.notebook_dir, td)
16 km = FileNotebookManager(notebook_dir=td)
17 self.assertEquals(km.notebook_dir, td)
18 18
19 19 def test_create_nb_dir(self):
20 20 with TemporaryDirectory() as td:
21 21 nbdir = os.path.join(td, 'notebooks')
22 km = NotebookManager(notebook_dir=nbdir)
23 self.assertEqual(km.notebook_dir, nbdir)
22 km = FileNotebookManager(notebook_dir=nbdir)
23 self.assertEquals(km.notebook_dir, nbdir)
24 24
25 25 def test_missing_nb_dir(self):
26 26 with TemporaryDirectory() as td:
27 27 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
28 self.assertRaises(TraitError, NotebookManager, notebook_dir=nbdir)
28 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
29 29
30 30 def test_invalid_nb_dir(self):
31 31 with NamedTemporaryFile() as tf:
32 self.assertRaises(TraitError, NotebookManager, notebook_dir=tf.name)
32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
33 33
34 34
@@ -1,579 +1,583 b''
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Suite Runner.
3 3
4 4 This module provides a main entry point to a user script to test IPython
5 5 itself from the command line. There are two ways of running this script:
6 6
7 7 1. With the syntax `iptest all`. This runs our entire test suite by
8 8 calling this script (with different arguments) recursively. This
9 9 causes modules and package to be tested in different processes, using nose
10 10 or trial where appropriate.
11 11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 12 the script simply calls nose, but with special command line flags and
13 13 plugins loaded.
14 14
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Copyright (C) 2009-2011 The IPython Development Team
19 19 #
20 20 # Distributed under the terms of the BSD License. The full license is in
21 21 # the file COPYING, distributed as part of this software.
22 22 #-----------------------------------------------------------------------------
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Imports
26 26 #-----------------------------------------------------------------------------
27 27 from __future__ import print_function
28 28
29 29 # Stdlib
30 30 import glob
31 31 import os
32 32 import os.path as path
33 33 import signal
34 34 import sys
35 35 import subprocess
36 36 import tempfile
37 37 import time
38 38 import warnings
39 39
40 40 # Note: monkeypatch!
41 41 # We need to monkeypatch a small problem in nose itself first, before importing
42 42 # it for actual use. This should get into nose upstream, but its release cycle
43 43 # is slow and we need it for our parametric tests to work correctly.
44 44 from IPython.testing import nosepatch
45 45
46 46 # Monkeypatch extra assert methods into nose.tools if they're not already there.
47 47 # This can be dropped once we no longer test on Python 2.6
48 48 from IPython.testing import nose_assert_methods
49 49
50 50 # Now, proceed to import nose itself
51 51 import nose.plugins.builtin
52 52 from nose.plugins.xunit import Xunit
53 53 from nose import SkipTest
54 54 from nose.core import TestProgram
55 55
56 56 # Our own imports
57 57 from IPython.utils import py3compat
58 58 from IPython.utils.importstring import import_item
59 59 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
60 60 from IPython.utils.process import find_cmd, pycmd2argv
61 61 from IPython.utils.sysinfo import sys_info
62 62 from IPython.utils.tempdir import TemporaryDirectory
63 63 from IPython.utils.warn import warn
64 64
65 65 from IPython.testing import globalipapp
66 66 from IPython.testing.plugin.ipdoctest import IPythonDoctest
67 67 from IPython.external.decorators import KnownFailure, knownfailureif
68 68
69 69 pjoin = path.join
70 70
71 71
72 72 #-----------------------------------------------------------------------------
73 73 # Globals
74 74 #-----------------------------------------------------------------------------
75 75
76 76
77 77 #-----------------------------------------------------------------------------
78 78 # Warnings control
79 79 #-----------------------------------------------------------------------------
80 80
81 81 # Twisted generates annoying warnings with Python 2.6, as will do other code
82 82 # that imports 'sets' as of today
83 83 warnings.filterwarnings('ignore', 'the sets module is deprecated',
84 84 DeprecationWarning )
85 85
86 86 # This one also comes from Twisted
87 87 warnings.filterwarnings('ignore', 'the sha module is deprecated',
88 88 DeprecationWarning)
89 89
90 90 # Wx on Fedora11 spits these out
91 91 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
92 92 UserWarning)
93 93
94 94 # ------------------------------------------------------------------------------
95 95 # Monkeypatch Xunit to count known failures as skipped.
96 96 # ------------------------------------------------------------------------------
97 97 def monkeypatch_xunit():
98 98 try:
99 99 knownfailureif(True)(lambda: None)()
100 100 except Exception as e:
101 101 KnownFailureTest = type(e)
102 102
103 103 def addError(self, test, err, capt=None):
104 104 if issubclass(err[0], KnownFailureTest):
105 105 err = (SkipTest,) + err[1:]
106 106 return self.orig_addError(test, err, capt)
107 107
108 108 Xunit.orig_addError = Xunit.addError
109 109 Xunit.addError = addError
110 110
111 111 #-----------------------------------------------------------------------------
112 112 # Logic for skipping doctests
113 113 #-----------------------------------------------------------------------------
114 114 def extract_version(mod):
115 115 return mod.__version__
116 116
117 117 def test_for(item, min_version=None, callback=extract_version):
118 118 """Test to see if item is importable, and optionally check against a minimum
119 119 version.
120 120
121 121 If min_version is given, the default behavior is to check against the
122 122 `__version__` attribute of the item, but specifying `callback` allows you to
123 123 extract the value you are interested in. e.g::
124 124
125 125 In [1]: import sys
126 126
127 127 In [2]: from IPython.testing.iptest import test_for
128 128
129 129 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
130 130 Out[3]: True
131 131
132 132 """
133 133 try:
134 134 check = import_item(item)
135 135 except (ImportError, RuntimeError):
136 136 # GTK reports Runtime error if it can't be initialized even if it's
137 137 # importable.
138 138 return False
139 139 else:
140 140 if min_version:
141 141 if callback:
142 142 # extra processing step to get version to compare
143 143 check = callback(check)
144 144
145 145 return check >= min_version
146 146 else:
147 147 return True
148 148
149 149 # Global dict where we can store information on what we have and what we don't
150 150 # have available at test run time
151 151 have = {}
152 152
153 153 have['curses'] = test_for('_curses')
154 154 have['matplotlib'] = test_for('matplotlib')
155 155 have['numpy'] = test_for('numpy')
156 156 have['pexpect'] = test_for('IPython.external.pexpect')
157 157 have['pymongo'] = test_for('pymongo')
158 158 have['pygments'] = test_for('pygments')
159 159 have['qt'] = test_for('IPython.external.qt')
160 160 have['rpy2'] = test_for('rpy2')
161 161 have['sqlite3'] = test_for('sqlite3')
162 162 have['cython'] = test_for('Cython')
163 163 have['oct2py'] = test_for('oct2py')
164 164 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
165 165 have['wx'] = test_for('wx')
166 166 have['wx.aui'] = test_for('wx.aui')
167 have['azure'] = test_for('azure')
167 168
168 169 if os.name == 'nt':
169 170 min_zmq = (2,1,7)
170 171 else:
171 172 min_zmq = (2,1,4)
172 173
173 174 def version_tuple(mod):
174 175 "turn '2.1.9' into (2,1,9), and '2.1dev' into (2,1,999)"
175 176 # turn 'dev' into 999, because Python3 rejects str-int comparisons
176 177 vs = mod.__version__.replace('dev', '.999')
177 178 tup = tuple([int(v) for v in vs.split('.') ])
178 179 return tup
179 180
180 181 have['zmq'] = test_for('zmq', min_zmq, version_tuple)
181 182
182 183 #-----------------------------------------------------------------------------
183 184 # Functions and classes
184 185 #-----------------------------------------------------------------------------
185 186
186 187 def report():
187 188 """Return a string with a summary report of test-related variables."""
188 189
189 190 out = [ sys_info(), '\n']
190 191
191 192 avail = []
192 193 not_avail = []
193 194
194 195 for k, is_avail in have.items():
195 196 if is_avail:
196 197 avail.append(k)
197 198 else:
198 199 not_avail.append(k)
199 200
200 201 if avail:
201 202 out.append('\nTools and libraries available at test time:\n')
202 203 avail.sort()
203 204 out.append(' ' + ' '.join(avail)+'\n')
204 205
205 206 if not_avail:
206 207 out.append('\nTools and libraries NOT available at test time:\n')
207 208 not_avail.sort()
208 209 out.append(' ' + ' '.join(not_avail)+'\n')
209 210
210 211 return ''.join(out)
211 212
212 213
213 214 def make_exclude():
214 215 """Make patterns of modules and packages to exclude from testing.
215 216
216 217 For the IPythonDoctest plugin, we need to exclude certain patterns that
217 218 cause testing problems. We should strive to minimize the number of
218 219 skipped modules, since this means untested code.
219 220
220 221 These modules and packages will NOT get scanned by nose at all for tests.
221 222 """
222 223 # Simple utility to make IPython paths more readably, we need a lot of
223 224 # these below
224 225 ipjoin = lambda *paths: pjoin('IPython', *paths)
225 226
226 227 exclusions = [ipjoin('external'),
227 228 ipjoin('quarantine'),
228 229 ipjoin('deathrow'),
229 230 # This guy is probably attic material
230 231 ipjoin('testing', 'mkdoctests'),
231 232 # Testing inputhook will need a lot of thought, to figure out
232 233 # how to have tests that don't lock up with the gui event
233 234 # loops in the picture
234 235 ipjoin('lib', 'inputhook'),
235 236 # Config files aren't really importable stand-alone
236 237 ipjoin('config', 'profile'),
237 238 # The notebook 'static' directory contains JS, css and other
238 239 # files for web serving. Occasionally projects may put a .py
239 240 # file in there (MathJax ships a conf.py), so we might as
240 241 # well play it safe and skip the whole thing.
241 242 ipjoin('frontend', 'html', 'notebook', 'static')
242 243 ]
243 244 if not have['sqlite3']:
244 245 exclusions.append(ipjoin('core', 'tests', 'test_history'))
245 246 exclusions.append(ipjoin('core', 'history'))
246 247 if not have['wx']:
247 248 exclusions.append(ipjoin('lib', 'inputhookwx'))
248 249
249 250 # FIXME: temporarily disable autoreload tests, as they can produce
250 251 # spurious failures in subsequent tests (cythonmagic).
251 252 exclusions.append(ipjoin('extensions', 'autoreload'))
252 253 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
253 254
254 255 # We do this unconditionally, so that the test suite doesn't import
255 256 # gtk, changing the default encoding and masking some unicode bugs.
256 257 exclusions.append(ipjoin('lib', 'inputhookgtk'))
257 258 exclusions.append(ipjoin('zmq', 'gui', 'gtkembed'))
258 259
259 260 # These have to be skipped on win32 because the use echo, rm, cd, etc.
260 261 # See ticket https://github.com/ipython/ipython/issues/87
261 262 if sys.platform == 'win32':
262 263 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
263 264 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
264 265
265 266 if not have['pexpect']:
266 267 exclusions.extend([ipjoin('lib', 'irunner'),
267 268 ipjoin('lib', 'tests', 'test_irunner'),
268 269 ipjoin('frontend', 'terminal', 'console'),
269 270 ])
270 271
271 272 if not have['zmq']:
272 273 exclusions.append(ipjoin('zmq'))
273 274 exclusions.append(ipjoin('frontend', 'qt'))
274 275 exclusions.append(ipjoin('frontend', 'html'))
275 276 exclusions.append(ipjoin('frontend', 'consoleapp.py'))
276 277 exclusions.append(ipjoin('frontend', 'terminal', 'console'))
277 278 exclusions.append(ipjoin('parallel'))
278 279 elif not have['qt'] or not have['pygments']:
279 280 exclusions.append(ipjoin('frontend', 'qt'))
280 281
281 282 if not have['pymongo']:
282 283 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
283 284 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
284 285
285 286 if not have['matplotlib']:
286 287 exclusions.extend([ipjoin('core', 'pylabtools'),
287 288 ipjoin('core', 'tests', 'test_pylabtools'),
288 289 ipjoin('zmq', 'pylab'),
289 290 ])
290 291
291 292 if not have['cython']:
292 293 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
293 294 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
294 295
295 296 if not have['oct2py']:
296 297 exclusions.extend([ipjoin('extensions', 'octavemagic')])
297 298 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
298 299
299 300 if not have['tornado']:
300 301 exclusions.append(ipjoin('frontend', 'html'))
301 302
302 303 if not have['rpy2'] or not have['numpy']:
303 304 exclusions.append(ipjoin('extensions', 'rmagic'))
304 305 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
305 306
307 if not have['azure']:
308 exclusions.append(ipjoin('frontend', 'html', 'notebook', 'azurenbmanager'))
309
306 310 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
307 311 if sys.platform == 'win32':
308 312 exclusions = [s.replace('\\','\\\\') for s in exclusions]
309 313
310 314 # check for any exclusions that don't seem to exist:
311 315 parent, _ = os.path.split(get_ipython_package_dir())
312 316 for exclusion in exclusions:
313 317 if exclusion.endswith(('deathrow', 'quarantine')):
314 318 # ignore deathrow/quarantine, which exist in dev, but not install
315 319 continue
316 320 fullpath = pjoin(parent, exclusion)
317 321 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
318 322 warn("Excluding nonexistent file: %r\n" % exclusion)
319 323
320 324 return exclusions
321 325
322 326
323 327 class IPTester(object):
324 328 """Call that calls iptest or trial in a subprocess.
325 329 """
326 330 #: string, name of test runner that will be called
327 331 runner = None
328 332 #: list, parameters for test runner
329 333 params = None
330 334 #: list, arguments of system call to be made to call test runner
331 335 call_args = None
332 336 #: list, subprocesses we start (for cleanup)
333 337 processes = None
334 338 #: str, coverage xml output file
335 339 coverage_xml = None
336 340
337 341 def __init__(self, runner='iptest', params=None):
338 342 """Create new test runner."""
339 343 p = os.path
340 344 if runner == 'iptest':
341 345 iptest_app = get_ipython_module_path('IPython.testing.iptest')
342 346 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
343 347 else:
344 348 raise Exception('Not a valid test runner: %s' % repr(runner))
345 349 if params is None:
346 350 params = []
347 351 if isinstance(params, str):
348 352 params = [params]
349 353 self.params = params
350 354
351 355 # Assemble call
352 356 self.call_args = self.runner+self.params
353 357
354 358 # Find the section we're testing (IPython.foo)
355 359 for sect in self.params:
356 360 if sect.startswith('IPython'): break
357 361 else:
358 362 raise ValueError("Section not found", self.params)
359 363
360 364 if '--with-xunit' in self.call_args:
361 365
362 366 self.call_args.append('--xunit-file')
363 367 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
364 368 xunit_file = path.abspath(sect+'.xunit.xml')
365 369 if sys.platform == 'win32':
366 370 xunit_file = '"%s"' % xunit_file
367 371 self.call_args.append(xunit_file)
368 372
369 373 if '--with-xml-coverage' in self.call_args:
370 374 self.coverage_xml = path.abspath(sect+".coverage.xml")
371 375 self.call_args.remove('--with-xml-coverage')
372 376 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
373 377
374 378 # Store anything we start to clean up on deletion
375 379 self.processes = []
376 380
377 381 def _run_cmd(self):
378 382 with TemporaryDirectory() as IPYTHONDIR:
379 383 env = os.environ.copy()
380 384 env['IPYTHONDIR'] = IPYTHONDIR
381 385 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
382 386 subp = subprocess.Popen(self.call_args, env=env)
383 387 self.processes.append(subp)
384 388 # If this fails, the process will be left in self.processes and
385 389 # cleaned up later, but if the wait call succeeds, then we can
386 390 # clear the stored process.
387 391 retcode = subp.wait()
388 392 self.processes.pop()
389 393 return retcode
390 394
391 395 def run(self):
392 396 """Run the stored commands"""
393 397 try:
394 398 retcode = self._run_cmd()
395 399 except:
396 400 import traceback
397 401 traceback.print_exc()
398 402 return 1 # signal failure
399 403
400 404 if self.coverage_xml:
401 405 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
402 406 return retcode
403 407
404 408 def __del__(self):
405 409 """Cleanup on exit by killing any leftover processes."""
406 410 for subp in self.processes:
407 411 if subp.poll() is not None:
408 412 continue # process is already dead
409 413
410 414 try:
411 415 print('Cleaning stale PID: %d' % subp.pid)
412 416 subp.kill()
413 417 except: # (OSError, WindowsError) ?
414 418 # This is just a best effort, if we fail or the process was
415 419 # really gone, ignore it.
416 420 pass
417 421
418 422 if subp.poll() is None:
419 423 # The process did not die...
420 424 print('... failed. Manual cleanup may be required.'
421 425 % subp.pid)
422 426
423 427 def make_runners(inc_slow=False):
424 428 """Define the top-level packages that need to be tested.
425 429 """
426 430
427 431 # Packages to be tested via nose, that only depend on the stdlib
428 432 nose_pkg_names = ['config', 'core', 'extensions', 'frontend', 'lib',
429 433 'testing', 'utils', 'nbformat' ]
430 434
431 435 if have['zmq']:
432 436 nose_pkg_names.append('zmq')
433 437 if inc_slow:
434 438 nose_pkg_names.append('parallel')
435 439
436 440 # For debugging this code, only load quick stuff
437 441 #nose_pkg_names = ['core', 'extensions'] # dbg
438 442
439 443 # Make fully qualified package names prepending 'IPython.' to our name lists
440 444 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
441 445
442 446 # Make runners
443 447 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
444 448
445 449 return runners
446 450
447 451
448 452 def run_iptest():
449 453 """Run the IPython test suite using nose.
450 454
451 455 This function is called when this script is **not** called with the form
452 456 `iptest all`. It simply calls nose with appropriate command line flags
453 457 and accepts all of the standard nose arguments.
454 458 """
455 459 # Apply our monkeypatch to Xunit
456 460 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
457 461 monkeypatch_xunit()
458 462
459 463 warnings.filterwarnings('ignore',
460 464 'This will be removed soon. Use IPython.testing.util instead')
461 465
462 466 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
463 467
464 468 '--with-ipdoctest',
465 469 '--ipdoctest-tests','--ipdoctest-extension=txt',
466 470
467 471 # We add --exe because of setuptools' imbecility (it
468 472 # blindly does chmod +x on ALL files). Nose does the
469 473 # right thing and it tries to avoid executables,
470 474 # setuptools unfortunately forces our hand here. This
471 475 # has been discussed on the distutils list and the
472 476 # setuptools devs refuse to fix this problem!
473 477 '--exe',
474 478 ]
475 479
476 480 if nose.__version__ >= '0.11':
477 481 # I don't fully understand why we need this one, but depending on what
478 482 # directory the test suite is run from, if we don't give it, 0 tests
479 483 # get run. Specifically, if the test suite is run from the source dir
480 484 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
481 485 # even if the same call done in this directory works fine). It appears
482 486 # that if the requested package is in the current dir, nose bails early
483 487 # by default. Since it's otherwise harmless, leave it in by default
484 488 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
485 489 argv.append('--traverse-namespace')
486 490
487 491 # use our plugin for doctesting. It will remove the standard doctest plugin
488 492 # if it finds it enabled
489 493 plugins = [IPythonDoctest(make_exclude()), KnownFailure()]
490 494 # We need a global ipython running in this process
491 495 globalipapp.start_ipython()
492 496 # Now nose can run
493 497 TestProgram(argv=argv, addplugins=plugins)
494 498
495 499
496 500 def run_iptestall(inc_slow=False):
497 501 """Run the entire IPython test suite by calling nose and trial.
498 502
499 503 This function constructs :class:`IPTester` instances for all IPython
500 504 modules and package and then runs each of them. This causes the modules
501 505 and packages of IPython to be tested each in their own subprocess using
502 506 nose.
503 507
504 508 Parameters
505 509 ----------
506 510
507 511 inc_slow : bool, optional
508 512 Include slow tests, like IPython.parallel. By default, these tests aren't
509 513 run.
510 514 """
511 515
512 516 runners = make_runners(inc_slow=inc_slow)
513 517
514 518 # Run the test runners in a temporary dir so we can nuke it when finished
515 519 # to clean up any junk files left over by accident. This also makes it
516 520 # robust against being run in non-writeable directories by mistake, as the
517 521 # temp dir will always be user-writeable.
518 522 curdir = os.getcwdu()
519 523 testdir = tempfile.gettempdir()
520 524 os.chdir(testdir)
521 525
522 526 # Run all test runners, tracking execution time
523 527 failed = []
524 528 t_start = time.time()
525 529 try:
526 530 for (name, runner) in runners:
527 531 print('*'*70)
528 532 print('IPython test group:',name)
529 533 res = runner.run()
530 534 if res:
531 535 failed.append( (name, runner) )
532 536 finally:
533 537 os.chdir(curdir)
534 538 t_end = time.time()
535 539 t_tests = t_end - t_start
536 540 nrunners = len(runners)
537 541 nfail = len(failed)
538 542 # summarize results
539 543 print()
540 544 print('*'*70)
541 545 print('Test suite completed for system with the following information:')
542 546 print(report())
543 547 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
544 548 print()
545 549 print('Status:')
546 550 if not failed:
547 551 print('OK')
548 552 else:
549 553 # If anything went wrong, point out what command to rerun manually to
550 554 # see the actual errors and individual summary
551 555 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
552 556 for name, failed_runner in failed:
553 557 print('-'*40)
554 558 print('Runner failed:',name)
555 559 print('You may wish to rerun this one individually, with:')
556 560 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
557 561 print(u' '.join(failed_call_args))
558 562 print()
559 563 # Ensure that our exit code indicates failure
560 564 sys.exit(1)
561 565
562 566
563 567 def main():
564 568 for arg in sys.argv[1:]:
565 569 if arg.startswith('IPython'):
566 570 # This is in-process
567 571 run_iptest()
568 572 else:
569 573 if "--all" in sys.argv:
570 574 sys.argv.remove("--all")
571 575 inc_slow = True
572 576 else:
573 577 inc_slow = False
574 578 # This starts subprocesses
575 579 run_iptestall(inc_slow=inc_slow)
576 580
577 581
578 582 if __name__ == '__main__':
579 583 main()
@@ -1,439 +1,461 b''
1 1 .. _htmlnotebook:
2 2
3 3 =========================
4 4 An HTML Notebook IPython
5 5 =========================
6 6
7 7 .. seealso::
8 8
9 9 :ref:`Installation requirements <installnotebook>` for the Notebook.
10 10
11 11 The IPython Notebook consists of two related components:
12 12
13 13 * An JSON based Notebook document format for recording and distributing
14 14 Python code and rich text.
15 15 * A web-based user interface for authoring and running notebook documents.
16 16
17 17 The Notebook can be used by starting the Notebook server with the
18 18 command::
19 19
20 20 $ ipython notebook
21 21
22 22 Note that by default, the notebook doesn't load pylab, it's just a normal
23 23 IPython session like any other. If you want pylab support, you must use::
24 24
25 25 $ ipython notebook --pylab
26 26
27 27 which will behave similar to the terminal and Qt console versions, using your
28 28 default matplotlib backend and providing floating interactive plot windows. If
29 29 you want inline figures, you must manually select the ``inline`` backend::
30 30
31 31 $ ipython notebook --pylab inline
32 32
33 33 This server uses the same ZeroMQ-based two process kernel architecture as
34 34 the QT Console as well Tornado for serving HTTP/S requests. Some of the main
35 35 features of the Notebook include:
36 36
37 37 * Display rich data (png/html/latex/svg) in the browser as a result of
38 38 computations.
39 39 * Compose text cells using HTML and Markdown.
40 40 * Import and export notebook documents in range of formats (.ipynb, .py).
41 41 * In browser syntax highlighting, tab completion and autoindentation.
42 42 * Inline matplotlib plots that can be stored in Notebook documents and opened
43 43 later.
44 44
45 45 See :ref:`our installation documentation <install_index>` for directions on
46 46 how to install the notebook and its dependencies.
47 47
48 48 .. note::
49 49
50 50 You can start more than one notebook server at the same time, if you want to
51 51 work on notebooks in different directories. By default the first notebook
52 52 server starts in port 8888, later notebooks search for random ports near
53 53 that one. You can also manually specify the port with the ``--port``
54 54 option.
55 55
56 56
57 57 Basic Usage
58 58 ===========
59 59
60 60 The landing page of the notebook server application, which we call the IPython
61 61 Notebook *dashboard*, shows the notebooks currently available in the directory
62 62 in which the application was started, and allows you to create new notebooks.
63 63
64 64 A notebook is a combination of two things:
65 65
66 66 1. An interactive session connected to an IPython kernel, controlled by a web
67 67 application that can send input to the console and display many types of
68 68 output (text, graphics, mathematics and more). This is the same kernel used
69 69 by the :ref:`Qt console <qtconsole>`, but in this case the web console sends
70 70 input in persistent cells that you can edit in-place instead of the
71 71 vertically scrolling terminal style used by the Qt console.
72 72
73 73 2. A document that can save the inputs and outputs of the session as well as
74 74 additional text that accompanies the code but is not meant for execution.
75 75 In this way, notebook files serve as a complete computational record of a
76 76 session including explanatory text and mathematics, code and resulting
77 77 figures. These documents are internally JSON files and are saved with the
78 78 ``.ipynb`` extension.
79 79
80 80 If you have ever used the Mathematica or Sage notebooks (the latter is also
81 81 web-based__) you should feel right at home. If you have not, you should be
82 82 able to learn how to use it in just a few minutes.
83 83
84 84 .. __: http://sagenb.org
85 85
86 86
87 87 Creating and editing notebooks
88 88 ------------------------------
89 89
90 90 You can create new notebooks from the dashboard with the ``New Notebook``
91 91 button or open existing ones by clicking on their name. Once in a notebook,
92 92 your browser tab will reflect the name of that notebook (prefixed with "IPy:").
93 93 The URL for that notebook is not meant to be human-readable and is *not*
94 94 persistent across invocations of the notebook server.
95 95
96 96 You can also drag and drop into the area listing files any python file: it
97 97 will be imported into a notebook with the same name (but ``.ipynb`` extension)
98 98 located in the directory where the notebook server was started. This notebook
99 99 will consist of a single cell with all the code in the file, which you can
100 100 later manually partition into individual cells for gradual execution, add text
101 101 and graphics, etc.
102 102
103 103
104 104 Workflow and limitations
105 105 ------------------------
106 106
107 107 The normal workflow in a notebook is quite similar to a normal IPython session,
108 108 with the difference that you can edit a cell in-place multiple times until you
109 109 obtain the desired results rather than having to rerun separate scripts with
110 110 the ``%run`` magic (though magics also work in the notebook). Typically
111 111 you'll work on a problem in pieces, organizing related pieces into cells and
112 112 moving forward as previous parts work correctly. This is much more convenient
113 113 for interactive exploration than breaking up a computation into scripts that
114 114 must be executed together, especially if parts of them take a long time to run
115 115 (In the traditional terminal-based IPython, you can use tricks with namespaces
116 116 and ``%run -i`` to achieve this capability, but we think the notebook is a more
117 117 natural solution for that kind of problem).
118 118
119 119 The only significant limitation the notebook currently has, compared to the qt
120 120 console, is that it can not run any code that expects input from the kernel
121 121 (such as scripts that call :func:`raw_input`). Very importantly, this means
122 122 that the ``%debug`` magic does *not* work in the notebook! We intend to
123 123 correct this limitation, but in the meantime, there is a way to debug problems
124 124 in the notebook: you can attach a Qt console to your existing notebook kernel,
125 125 and run ``%debug`` from the Qt console. If your notebook is running on a local
126 126 computer (i.e. if you are accessing it via your localhost address at
127 127 127.0.0.1), you can just type ``%qtconsole`` in the notebook and a Qt console
128 128 will open up connected to that same kernel.
129 129
130 130 In general, the notebook server prints the full details of how to connect to
131 131 each kernel at the terminal, with lines like::
132 132
133 133 [IPKernelApp] To connect another client to this kernel, use:
134 134 [IPKernelApp] --existing kernel-3bb93edd-6b5a-455c-99c8-3b658f45dde5.json
135 135
136 136 This is the name of a JSON file that contains all the port and validation
137 137 information necessary to connect to the kernel. You can manually start a
138 138 qt console with::
139 139
140 140 ipython qtconsole --existing kernel-3bb93edd-6b5a-455c-99c8-3b658f45dde5.json
141 141
142 142 and if you only have a single kernel running, simply typing::
143 143
144 144 ipython qtconsole --existing
145 145
146 146 will automatically find it (it will always find the most recently started
147 147 kernel if there is more than one). You can also request this connection data
148 148 by typing ``%connect_info``; this will print the same file information as well
149 149 as the content of the JSON data structure it contains.
150 150
151 151
152 152 Text input
153 153 ----------
154 154
155 155 In addition to code cells and the output they produce (such as figures), you
156 156 can also type text not meant for execution. To type text, change the type of a
157 157 cell from ``Code`` to ``Markdown`` by using the button or the :kbd:`Ctrl-m m`
158 158 keybinding (see below). You can then type any text in Markdown_ syntax, as
159 159 well as mathematical expressions if you use ``$...$`` for inline math or
160 160 ``$$...$$`` for displayed math.
161 161
162 162
163 163 Exporting a notebook and importing existing scripts
164 164 ---------------------------------------------------
165 165
166 166 If you want to provide others with a static HTML or PDF view of your notebook,
167 167 use the ``Print`` button. This opens a static view of the document, which you
168 168 can print to PDF using your operating system's facilities, or save to a file
169 169 with your web browser's 'Save' option (note that typically, this will create
170 170 both an html file *and* a directory called `notebook_name_files` next to it
171 171 that contains all the necessary style information, so if you intend to share
172 172 this, you must send the directory along with the main html file).
173 173
174 174 The `Download` button lets you save a notebook file to the Download area
175 175 configured by your web browser (particularly useful if you are running the
176 176 notebook server on a remote host and need a file locally). The notebook is
177 177 saved by default with the ``.ipynb`` extension and the files contain JSON data
178 178 that is not meant for human editing or consumption. But you can always export
179 179 the input part of a notebook to a plain python script by choosing Python format
180 180 in the `Download` drop list. This removes all output and saves the text cells
181 181 in comment areas. See ref:`below <notebook_format>` for more details on the
182 182 notebook format.
183 183
184 184 The notebook can also *import* ``.py`` files as notebooks, by dragging and
185 185 dropping the file into the notebook dashboard file list area. By default, the
186 186 entire contents of the file will be loaded into a single code cell. But if
187 187 prior to import, you manually add the ``# <nbformat>2</nbformat>`` marker at
188 188 the start and then add separators for text/code cells, you can get a cleaner
189 189 import with the file broken into individual cells.
190 190
191 191 .. warning::
192 192
193 193 While in simple cases you can roundtrip a notebook to Python, edit the
194 194 python file and import it back without loss of main content, this is in
195 195 general *not guaranteed to work at all*. First, there is extra metadata
196 196 saved in the notebook that may not be saved to the ``.py`` format. And as
197 197 the notebook format evolves in complexity, there will be attributes of the
198 198 notebook that will not survive a roundtrip through the Python form. You
199 199 should think of the Python format as a way to output a script version of a
200 200 notebook and the import capabilities as a way to load existing code to get a
201 201 notebook started. But the Python version is *not* an alternate notebook
202 202 format.
203 203
204 204
205 205 Importing or executing a notebook as a normal Python file
206 206 ---------------------------------------------------------
207 207
208 208 The native format of the notebook, a file with a ``.ipynb`` extension, is a
209 209 JSON container of all the input and output of the notebook, and therefore not
210 210 valid Python by itself. This means that by default, you can not import a
211 211 notebook or execute it as a normal python script. But if you want use
212 212 notebooks as regular Python files, you can start the notebook server with::
213 213
214 214 ipython notebook --script
215 215
216 216 or you can set this option permanently in your configuration file with::
217 217
218 218 c.NotebookManager.save_script=True
219 219
220 220 This will instruct the notebook server to save the ``.py`` export of each
221 221 notebook adjacent to the ``.ipynb`` at every save. These files can be
222 222 ``%run``, imported from regular IPython sessions or other notebooks, or
223 223 executed at the command-line as normal Python files. Since we export the raw
224 224 code you have typed, for these files to be importable from other code you will
225 225 have to avoid using syntax such as ``%magics`` and other IPython-specific
226 226 extensions to the language.
227 227
228 228 In regular practice, the standard way to differentiate importable code from the
229 229 'executable' part of a script is to put at the bottom::
230 230
231 231 if __name__ == '__main__':
232 232 # rest of the code...
233 233
234 234 Since all cells in the notebook are run as top-level code, you'll need to
235 235 similarly protect *all* cells that you do not want executed when other scripts
236 236 try to import your notebook. A convenient shortand for this is to define early
237 237 on::
238 238
239 239 script = __name__ == '__main__'
240 240
241 241 and then on any cell that you need to protect, use::
242 242
243 243 if script:
244 244 # rest of the cell...
245 245
246
247 246 Keyboard use
248 247 ------------
249 248
250 249 All actions in the notebook can be achieved with the mouse, but we have also
251 250 added keyboard shortcuts for the most common ones, so that productive use of
252 251 the notebook can be achieved with minimal mouse intervention. The main
253 252 key bindings you need to remember are:
254 253
255 254 * :kbd:`Shift-Enter`: execute the current cell (similar to the Qt console),
256 255 show output (if any) and jump to the next cell below. If :kbd:`Shift-Enter`
257 256 was invoked on the last input line, a new code cell will also be created. Note
258 257 that in the notebook, simply using :kbd:`Enter` *never* forces execution,
259 258 it simply inserts a new line in the current cell. Therefore, in the notebook
260 259 you must always use :kbd:`Shift-Enter` to get execution (or use the mouse and
261 260 click on the ``Run Selected`` button).
262 261
263 262 * :kbd:`Alt-Enter`: this combination is similar to the previous one, with the
264 263 exception that, if the next cell below is not empty, a new code cell will be
265 264 added to the notebook, even if the cell execution happens not in the last cell.
266 265 In this regard, :kbd:`Alt-Enter`: is simply a shortcut for the :kbd:`Shift-Enter`,
267 266 :kbd:`Ctrl-m a` sequence.
268 267
269 268 * :kbd:`Ctrl-Enter`: execute the current cell in "terminal mode", where any
270 269 output is shown but the cursor stays in the current cell, whose input
271 270 area is flushed empty. This is convenient to do quick in-place experiments
272 271 or query things like filesystem content without creating additional cells you
273 272 may not want saved in your notebook.
274 273
275 274 * :kbd:`Ctrl-m`: this is the prefix for all other keybindings, which consist
276 275 of an additional single letter. Type :kbd:`Ctrl-m h` (that is, the sole
277 276 letter :kbd:`h` after :kbd:`Ctrl-m`) and IPython will show you the remaining
278 277 available keybindings.
279 278
280 279
281 280 .. _notebook_security:
282 281
283 282 Security
284 283 ========
285 284
286 285 You can protect your notebook server with a simple single-password by
287 286 setting the :attr:`NotebookApp.password` configurable. You can prepare a
288 287 hashed password using the function :func:`IPython.lib.security.passwd`:
289 288
290 289 .. sourcecode:: ipython
291 290
292 291 In [1]: from IPython.lib import passwd
293 292 In [2]: passwd()
294 293 Enter password:
295 294 Verify password:
296 295 Out[2]: 'sha1:67c9e60bb8b6:9ffede0825894254b2e042ea597d771089e11aed'
297 296
298 297 .. note::
299 298
300 299 :func:`~IPython.lib.security.passwd` can also take the password as a string
301 300 argument. **Do not** pass it as an argument inside an IPython session, as it
302 301 will be saved in your input history.
303 302
304 303 You can then add this to your :file:`ipython_notebook_config.py`, e.g.::
305 304
306 305 # Password to use for web authentication
307 306 c.NotebookApp.password = u'sha1:67c9e60bb8b6:9ffede0825894254b2e042ea597d771089e11aed'
308 307
309 308 When using a password, it is a good idea to also use SSL, so that your password
310 309 is not sent unencrypted by your browser. You can start the notebook to
311 310 communicate via a secure protocol mode using a self-signed certificate by
312 311 typing::
313 312
314 313 $ ipython notebook --certfile=mycert.pem
315 314
316 315 .. note::
317 316
318 317 A self-signed certificate can be generated with openssl. For example, the
319 318 following command will create a certificate valid for 365 days with both
320 319 the key and certificate data written to the same file::
321 320
322 321 $ openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem
323 322
324 323 Your browser will warn you of a dangerous certificate because it is
325 324 self-signed. If you want to have a fully compliant certificate that will not
326 325 raise warnings, it is possible (but rather involved) to obtain one for free,
327 326 `as explained in detailed in this tutorial`__.
328 327
329 328 .. __: http://arstechnica.com/security/news/2009/12/how-to-get-set-with-a-secure-sertificate-for-free.ars
330 329
331 330 Keep in mind that when you enable SSL support, you'll need to access the
332 331 notebook server over ``https://``, not over plain ``http://``. The startup
333 332 message from the server prints this, but it's easy to overlook and think the
334 333 server is for some reason non-responsive.
335 334
335 Quick how to's
336 ==============
336 337
337 Quick Howto: running a public notebook server
338 =============================================
338 Running a public notebook server
339 --------------------------------
339 340
340 341 If you want to access your notebook server remotely with just a web browser,
341 342 here is a quick set of instructions. Start by creating a certificate file and
342 343 a hashed password as explained above. Then, create a custom profile for the
343 344 notebook. At the command line, type::
344 345
345 346 ipython profile create nbserver
346 347
347 348 In the profile directory, edit the file ``ipython_notebook_config.py``. By
348 349 default the file has all fields commented, the minimum set you need to
349 350 uncomment and edit is here::
350 351
351 352 c = get_config()
352 353
353 354 # Kernel config
354 355 c.IPKernelApp.pylab = 'inline' # if you want plotting support always
355 356
356 357 # Notebook config
357 358 c.NotebookApp.certfile = u'/absolute/path/to/your/certificate/mycert.pem'
358 359 c.NotebookApp.ip = '*'
359 360 c.NotebookApp.open_browser = False
360 361 c.NotebookApp.password = u'sha1:bcd259ccf...your hashed password here'
361 362 # It's a good idea to put it on a known, fixed port
362 363 c.NotebookApp.port = 9999
363 364
364 365 You can then start the notebook and access it later by pointing your browser to
365 366 ``https://your.host.com:9999`` with ``ipython notebook --profile=nbserver``.
366 367
367 368 Running with a different URL prefix
368 ===================================
369 -----------------------------------
369 370
370 371 The notebook dashboard (i.e. the default landing page with an overview
371 372 of all your notebooks) typically lives at a URL path of
372 373 "http://localhost:8888/". If you want to have it, and the rest of the
373 374 notebook, live under a sub-directory,
374 375 e.g. "http://localhost:8888/ipython/", you can do so with
375 376 configuration options like these (see above for instructions about
376 377 modifying ``ipython_notebook_config.py``)::
377 378
378 379 c.NotebookApp.base_project_url = '/ipython/'
379 380 c.NotebookApp.base_kernel_url = '/ipython/'
380 381 c.NotebookApp.webapp_settings = {'static_url_prefix':'/ipython/static/'}
381 382
383 Using a different notebook store
384 --------------------------------
385
386 By default the notebook server stores notebooks as files in the working
387 directory of the notebook server, also known as the ``notebook_dir``. This
388 logic is implemented in the :class:`FileNotebookManager` class. However, the
389 server can be configured to use a different notebook manager class, which can
390 store the notebooks in a different format. Currently, we ship a
391 :class:`AzureNotebookManager` class that stores notebooks in Azure blob
392 storage. This can be used by adding the following lines to your
393 ``ipython_notebook_config.py`` file::
394
395 c.NotebookApp.notebook_manager_class = 'IPython.frontend.html.notebook.azurenbmanager.AzureNotebookManager'
396 c.AzureNotebookManager.account_name = u'paste_your_account_name_here'
397 c.AzureNotebookManager.account_key = u'paste_your_account_key_here'
398 c.AzureNotebookManager.container = u'notebooks'
399
400 In addition to providing your Azure Blob Storage account name and key, you will
401 have to provide a container name; you can use multiple containers to organize
402 your Notebooks.
403
382 404 .. _notebook_format:
383 405
384 406 The notebook format
385 407 ===================
386 408
387 409 The notebooks themselves are JSON files with an ``ipynb`` extension, formatted
388 410 as legibly as possible with minimal extra indentation and cell content broken
389 411 across lines to make them reasonably friendly to use in version-control
390 412 workflows. You should be very careful if you ever edit manually this JSON
391 413 data, as it is extremely easy to corrupt its internal structure and make the
392 414 file impossible to load. In general, you should consider the notebook as a
393 415 file meant only to be edited by IPython itself, not for hand-editing.
394 416
395 417 .. note::
396 418
397 419 Binary data such as figures are directly saved in the JSON file. This
398 420 provides convenient single-file portability but means the files can be
399 421 large and diffs of binary data aren't very meaningful. Since the binary
400 422 blobs are encoded in a single line they only affect one line of the diff
401 423 output, but they are typically very long lines. You can use the
402 424 'ClearAll' button to remove all output from a notebook prior to
403 425 committing it to version control, if this is a concern.
404 426
405 427 The notebook server can also generate a pure-python version of your notebook,
406 428 by clicking on the 'Download' button and selecting ``py`` as the format. This
407 429 file will contain all the code cells from your notebook verbatim, and all text
408 430 cells prepended with a comment marker. The separation between code and text
409 431 cells is indicated with special comments and there is a header indicating the
410 432 format version. All output is stripped out when exporting to python.
411 433
412 434 Here is an example of a simple notebook with one text cell and one code input
413 435 cell, when exported to python format::
414 436
415 437 # <nbformat>2</nbformat>
416 438
417 439 # <markdowncell>
418 440
419 441 # A text cell
420 442
421 443 # <codecell>
422 444
423 445 print "hello IPython"
424 446
425 447
426 Known Issues
448 Known issues
427 449 ============
428 450
429 451 When behind a proxy, especially if your system or browser is set to autodetect
430 452 the proxy, the html notebook might fail to connect to the server's websockets,
431 453 and present you with a warning at startup. In this case, you need to configure
432 454 your system not to use the proxy for the server's address.
433 455
434 456 In Firefox, for example, go to the Preferences panel, Advanced section,
435 457 Network tab, click 'Settings...', and add the address of the notebook server
436 458 to the 'No proxy for' field.
437 459
438 460
439 461 .. _Markdown: http://daringfireball.net/projects/markdown/basics
General Comments 0
You need to be logged in to leave comments. Login now