##// END OF EJS Templates
Specify UTF-8 encoding for saving notebook as script.
Thomas Kluyver -
Show More
@@ -1,275 +1,275 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import datetime
20 20 import io
21 21 import os
22 22 import uuid
23 23 import glob
24 24
25 25 from tornado import web
26 26
27 27 from IPython.config.configurable import LoggingConfigurable
28 28 from IPython.nbformat import current
29 29 from IPython.utils.traitlets import Unicode, List, Dict, Bool
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Classes
33 33 #-----------------------------------------------------------------------------
34 34
35 35 class NotebookManager(LoggingConfigurable):
36 36
37 37 notebook_dir = Unicode(os.getcwd(), config=True, help="""
38 38 The directory to use for notebooks.
39 39 """)
40 40
41 41 save_script = Bool(False, config=True,
42 42 help="""Automatically create a Python script when saving the notebook.
43 43
44 44 For easier use of import, %run and %loadpy across notebooks, a
45 45 <notebook-name>.py script will be created next to any
46 46 <notebook-name>.ipynb on each save. This can also be set with the
47 47 short `--script` flag.
48 48 """
49 49 )
50 50
51 51 filename_ext = Unicode(u'.ipynb')
52 52 allowed_formats = List([u'json',u'py'])
53 53
54 54 # Map notebook_ids to notebook names
55 55 mapping = Dict()
56 56 # Map notebook names to notebook_ids
57 57 rev_mapping = Dict()
58 58
59 59 def list_notebooks(self):
60 60 """List all notebooks in the notebook dir.
61 61
62 62 This returns a list of dicts of the form::
63 63
64 64 dict(notebook_id=notebook,name=name)
65 65 """
66 66 names = glob.glob(os.path.join(self.notebook_dir,
67 67 '*' + self.filename_ext))
68 68 names = [os.path.splitext(os.path.basename(name))[0]
69 69 for name in names]
70 70
71 71 data = []
72 72 for name in names:
73 73 if name not in self.rev_mapping:
74 74 notebook_id = self.new_notebook_id(name)
75 75 else:
76 76 notebook_id = self.rev_mapping[name]
77 77 data.append(dict(notebook_id=notebook_id,name=name))
78 78 data = sorted(data, key=lambda item: item['name'])
79 79 return data
80 80
81 81 def new_notebook_id(self, name):
82 82 """Generate a new notebook_id for a name and store its mappings."""
83 83 # TODO: the following will give stable urls for notebooks, but unless
84 84 # the notebooks are immediately redirected to their new urls when their
85 85 # filemname changes, nasty inconsistencies result. So for now it's
86 86 # disabled and instead we use a random uuid4() call. But we leave the
87 87 # logic here so that we can later reactivate it, whhen the necessary
88 88 # url redirection code is written.
89 89 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
90 90 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
91 91
92 92 notebook_id = unicode(uuid.uuid4())
93 93
94 94 self.mapping[notebook_id] = name
95 95 self.rev_mapping[name] = notebook_id
96 96 return notebook_id
97 97
98 98 def delete_notebook_id(self, notebook_id):
99 99 """Delete a notebook's id only. This doesn't delete the actual notebook."""
100 100 name = self.mapping[notebook_id]
101 101 del self.mapping[notebook_id]
102 102 del self.rev_mapping[name]
103 103
104 104 def notebook_exists(self, notebook_id):
105 105 """Does a notebook exist?"""
106 106 if notebook_id not in self.mapping:
107 107 return False
108 108 path = self.get_path_by_name(self.mapping[notebook_id])
109 109 return os.path.isfile(path)
110 110
111 111 def find_path(self, notebook_id):
112 112 """Return a full path to a notebook given its notebook_id."""
113 113 try:
114 114 name = self.mapping[notebook_id]
115 115 except KeyError:
116 116 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
117 117 return self.get_path_by_name(name)
118 118
119 119 def get_path_by_name(self, name):
120 120 """Return a full path to a notebook given its name."""
121 121 filename = name + self.filename_ext
122 122 path = os.path.join(self.notebook_dir, filename)
123 123 return path
124 124
125 125 def get_notebook(self, notebook_id, format=u'json'):
126 126 """Get the representation of a notebook in format by notebook_id."""
127 127 format = unicode(format)
128 128 if format not in self.allowed_formats:
129 129 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
130 130 last_modified, nb = self.get_notebook_object(notebook_id)
131 131 kwargs = {}
132 132 if format == 'json':
133 133 # don't split lines for sending over the wire, because it
134 134 # should match the Python in-memory format.
135 135 kwargs['split_lines'] = False
136 136 data = current.writes(nb, format, **kwargs)
137 137 name = nb.get('name','notebook')
138 138 return last_modified, name, data
139 139
140 140 def get_notebook_object(self, notebook_id):
141 141 """Get the NotebookNode representation of a notebook by notebook_id."""
142 142 path = self.find_path(notebook_id)
143 143 if not os.path.isfile(path):
144 144 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
145 145 info = os.stat(path)
146 146 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
147 147 with open(path,'r') as f:
148 148 s = f.read()
149 149 try:
150 150 # v1 and v2 and json in the .ipynb files.
151 151 nb = current.reads(s, u'json')
152 152 except:
153 153 raise web.HTTPError(500, u'Unreadable JSON notebook.')
154 154 if 'name' not in nb:
155 155 nb.name = os.path.split(path)[-1].split(u'.')[0]
156 156 return last_modified, nb
157 157
158 158 def save_new_notebook(self, data, name=None, format=u'json'):
159 159 """Save a new notebook and return its notebook_id.
160 160
161 161 If a name is passed in, it overrides any values in the notebook data
162 162 and the value in the data is updated to use that value.
163 163 """
164 164 if format not in self.allowed_formats:
165 165 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
166 166
167 167 try:
168 168 nb = current.reads(data.decode('utf-8'), format)
169 169 except:
170 170 raise web.HTTPError(400, u'Invalid JSON data')
171 171
172 172 if name is None:
173 173 try:
174 174 name = nb.metadata.name
175 175 except AttributeError:
176 176 raise web.HTTPError(400, u'Missing notebook name')
177 177 nb.metadata.name = name
178 178
179 179 notebook_id = self.new_notebook_id(name)
180 180 self.save_notebook_object(notebook_id, nb)
181 181 return notebook_id
182 182
183 183 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
184 184 """Save an existing notebook by notebook_id."""
185 185 if format not in self.allowed_formats:
186 186 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
187 187
188 188 try:
189 189 nb = current.reads(data.decode('utf-8'), format)
190 190 except:
191 191 raise web.HTTPError(400, u'Invalid JSON data')
192 192
193 193 if name is not None:
194 194 nb.metadata.name = name
195 195 self.save_notebook_object(notebook_id, nb)
196 196
197 197 def save_notebook_object(self, notebook_id, nb):
198 198 """Save an existing notebook object by notebook_id."""
199 199 if notebook_id not in self.mapping:
200 200 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
201 201 old_name = self.mapping[notebook_id]
202 202 try:
203 203 new_name = nb.metadata.name
204 204 except AttributeError:
205 205 raise web.HTTPError(400, u'Missing notebook name')
206 206 path = self.get_path_by_name(new_name)
207 207 try:
208 208 with open(path,'w') as f:
209 209 current.write(nb, f, u'json')
210 210 except Exception as e:
211 211 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
212 212 # save .py script as well
213 213 if self.save_script:
214 214 pypath = os.path.splitext(path)[0] + '.py'
215 215 try:
216 with io.open(pypath,'w') as f:
216 with io.open(pypath,'w', encoding='utf-8') as f:
217 217 current.write(nb, f, u'py')
218 218 except Exception as e:
219 219 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
220 220
221 221 if old_name != new_name:
222 222 old_path = self.get_path_by_name(old_name)
223 223 if os.path.isfile(old_path):
224 224 os.unlink(old_path)
225 225 if self.save_script:
226 226 old_pypath = os.path.splitext(old_path)[0] + '.py'
227 227 if os.path.isfile(old_pypath):
228 228 os.unlink(old_pypath)
229 229 self.mapping[notebook_id] = new_name
230 230 self.rev_mapping[new_name] = notebook_id
231 231
232 232 def delete_notebook(self, notebook_id):
233 233 """Delete notebook by notebook_id."""
234 234 path = self.find_path(notebook_id)
235 235 if not os.path.isfile(path):
236 236 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
237 237 os.unlink(path)
238 238 self.delete_notebook_id(notebook_id)
239 239
240 240 def increment_filename(self, basename):
241 241 """Return a non-used filename of the form basename<int>.
242 242
243 243 This searches through the filenames (basename0, basename1, ...)
244 244 until is find one that is not already being used. It is used to
245 245 create Untitled and Copy names that are unique.
246 246 """
247 247 i = 0
248 248 while True:
249 249 name = u'%s%i' % (basename,i)
250 250 path = self.get_path_by_name(name)
251 251 if not os.path.isfile(path):
252 252 break
253 253 else:
254 254 i = i+1
255 255 return path, name
256 256
257 257 def new_notebook(self):
258 258 """Create a new notebook and return its notebook_id."""
259 259 path, name = self.increment_filename('Untitled')
260 260 notebook_id = self.new_notebook_id(name)
261 261 metadata = current.new_metadata(name=name)
262 262 nb = current.new_notebook(metadata=metadata)
263 263 with open(path,'w') as f:
264 264 current.write(nb, f, u'json')
265 265 return notebook_id
266 266
267 267 def copy_notebook(self, notebook_id):
268 268 """Copy an existing notebook and return its notebook_id."""
269 269 last_mod, nb = self.get_notebook_object(notebook_id)
270 270 name = nb.metadata.name + '-Copy'
271 271 path, name = self.increment_filename(name)
272 272 nb.metadata.name = name
273 273 notebook_id = self.new_notebook_id(name)
274 274 self.save_notebook_object(notebook_id, nb)
275 275 return notebook_id
General Comments 0
You need to be logged in to leave comments. Login now