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