##// END OF EJS Templates
split likely multiline strings when writing to/from JSON
MinRK -
Show More
@@ -1,228 +1,233 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) 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 import datetime
19 import datetime
20 import os
20 import os
21 import uuid
21 import uuid
22 import glob
22 import glob
23
23
24 from tornado import web
24 from tornado import web
25
25
26 from IPython.config.configurable import LoggingConfigurable
26 from IPython.config.configurable import LoggingConfigurable
27 from IPython.nbformat import current
27 from IPython.nbformat import current
28 from IPython.utils.traitlets import Unicode, List, Dict
28 from IPython.utils.traitlets import Unicode, List, Dict
29
29
30
30
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32 # Code
32 # Code
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34
34
35
35
36 class NotebookManager(LoggingConfigurable):
36 class NotebookManager(LoggingConfigurable):
37
37
38 notebook_dir = Unicode(os.getcwd(), config=True, help="""
38 notebook_dir = Unicode(os.getcwd(), config=True, help="""
39 The directory to use for notebooks.
39 The directory to use for notebooks.
40 """)
40 """)
41 filename_ext = Unicode(u'.ipynb')
41 filename_ext = Unicode(u'.ipynb')
42 allowed_formats = List([u'json',u'py'])
42 allowed_formats = List([u'json',u'py'])
43
43
44 # Map notebook_ids to notebook names
44 # Map notebook_ids to notebook names
45 mapping = Dict()
45 mapping = Dict()
46 # Map notebook names to notebook_ids
46 # Map notebook names to notebook_ids
47 rev_mapping = Dict()
47 rev_mapping = Dict()
48
48
49 def list_notebooks(self):
49 def list_notebooks(self):
50 """List all notebooks in the notebook dir.
50 """List all notebooks in the notebook dir.
51
51
52 This returns a list of dicts of the form::
52 This returns a list of dicts of the form::
53
53
54 dict(notebook_id=notebook,name=name)
54 dict(notebook_id=notebook,name=name)
55 """
55 """
56 names = glob.glob(os.path.join(self.notebook_dir,
56 names = glob.glob(os.path.join(self.notebook_dir,
57 '*' + self.filename_ext))
57 '*' + self.filename_ext))
58 names = [os.path.splitext(os.path.basename(name))[0]
58 names = [os.path.splitext(os.path.basename(name))[0]
59 for name in names]
59 for name in names]
60
60
61 data = []
61 data = []
62 for name in names:
62 for name in names:
63 if name not in self.rev_mapping:
63 if name not in self.rev_mapping:
64 notebook_id = self.new_notebook_id(name)
64 notebook_id = self.new_notebook_id(name)
65 else:
65 else:
66 notebook_id = self.rev_mapping[name]
66 notebook_id = self.rev_mapping[name]
67 data.append(dict(notebook_id=notebook_id,name=name))
67 data.append(dict(notebook_id=notebook_id,name=name))
68 data = sorted(data, key=lambda item: item['name'])
68 data = sorted(data, key=lambda item: item['name'])
69 return data
69 return data
70
70
71 def new_notebook_id(self, name):
71 def new_notebook_id(self, name):
72 """Generate a new notebook_id for a name and store its mappings."""
72 """Generate a new notebook_id for a name and store its mappings."""
73 # TODO: the following will give stable urls for notebooks, but unless
73 # TODO: the following will give stable urls for notebooks, but unless
74 # the notebooks are immediately redirected to their new urls when their
74 # the notebooks are immediately redirected to their new urls when their
75 # filemname changes, nasty inconsistencies result. So for now it's
75 # filemname changes, nasty inconsistencies result. So for now it's
76 # disabled and instead we use a random uuid4() call. But we leave the
76 # disabled and instead we use a random uuid4() call. But we leave the
77 # logic here so that we can later reactivate it, whhen the necessary
77 # logic here so that we can later reactivate it, whhen the necessary
78 # url redirection code is written.
78 # url redirection code is written.
79 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
79 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
80 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
80 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
81
81
82 notebook_id = unicode(uuid.uuid4())
82 notebook_id = unicode(uuid.uuid4())
83
83
84 self.mapping[notebook_id] = name
84 self.mapping[notebook_id] = name
85 self.rev_mapping[name] = notebook_id
85 self.rev_mapping[name] = notebook_id
86 return notebook_id
86 return notebook_id
87
87
88 def delete_notebook_id(self, notebook_id):
88 def delete_notebook_id(self, notebook_id):
89 """Delete a notebook's id only. This doesn't delete the actual notebook."""
89 """Delete a notebook's id only. This doesn't delete the actual notebook."""
90 name = self.mapping[notebook_id]
90 name = self.mapping[notebook_id]
91 del self.mapping[notebook_id]
91 del self.mapping[notebook_id]
92 del self.rev_mapping[name]
92 del self.rev_mapping[name]
93
93
94 def notebook_exists(self, notebook_id):
94 def notebook_exists(self, notebook_id):
95 """Does a notebook exist?"""
95 """Does a notebook exist?"""
96 if notebook_id not in self.mapping:
96 if notebook_id not in self.mapping:
97 return False
97 return False
98 path = self.get_path_by_name(self.mapping[notebook_id])
98 path = self.get_path_by_name(self.mapping[notebook_id])
99 return os.path.isfile(path)
99 return os.path.isfile(path)
100
100
101 def find_path(self, notebook_id):
101 def find_path(self, notebook_id):
102 """Return a full path to a notebook given its notebook_id."""
102 """Return a full path to a notebook given its notebook_id."""
103 try:
103 try:
104 name = self.mapping[notebook_id]
104 name = self.mapping[notebook_id]
105 except KeyError:
105 except KeyError:
106 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
106 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
107 return self.get_path_by_name(name)
107 return self.get_path_by_name(name)
108
108
109 def get_path_by_name(self, name):
109 def get_path_by_name(self, name):
110 """Return a full path to a notebook given its name."""
110 """Return a full path to a notebook given its name."""
111 filename = name + self.filename_ext
111 filename = name + self.filename_ext
112 path = os.path.join(self.notebook_dir, filename)
112 path = os.path.join(self.notebook_dir, filename)
113 return path
113 return path
114
114
115 def get_notebook(self, notebook_id, format=u'json'):
115 def get_notebook(self, notebook_id, format=u'json'):
116 """Get the representation of a notebook in format by notebook_id."""
116 """Get the representation of a notebook in format by notebook_id."""
117 format = unicode(format)
117 format = unicode(format)
118 if format not in self.allowed_formats:
118 if format not in self.allowed_formats:
119 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
119 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
120 last_modified, nb = self.get_notebook_object(notebook_id)
120 last_modified, nb = self.get_notebook_object(notebook_id)
121 data = current.writes(nb, format)
121 kwargs = {}
122 if format == 'json':
123 # don't split lines for sending over the wire, because it
124 # should match the Python in-memory format.
125 kwargs['split_lines'] = False
126 data = current.writes(nb, format, **kwargs)
122 name = nb.get('name','notebook')
127 name = nb.get('name','notebook')
123 return last_modified, name, data
128 return last_modified, name, data
124
129
125 def get_notebook_object(self, notebook_id):
130 def get_notebook_object(self, notebook_id):
126 """Get the NotebookNode representation of a notebook by notebook_id."""
131 """Get the NotebookNode representation of a notebook by notebook_id."""
127 path = self.find_path(notebook_id)
132 path = self.find_path(notebook_id)
128 if not os.path.isfile(path):
133 if not os.path.isfile(path):
129 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
134 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
130 info = os.stat(path)
135 info = os.stat(path)
131 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
136 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
132 with open(path,'r') as f:
137 with open(path,'r') as f:
133 s = f.read()
138 s = f.read()
134 try:
139 try:
135 # v1 and v2 and json in the .ipynb files.
140 # v1 and v2 and json in the .ipynb files.
136 nb = current.reads(s, u'json')
141 nb = current.reads(s, u'json')
137 except:
142 except:
138 raise web.HTTPError(500, u'Unreadable JSON notebook.')
143 raise web.HTTPError(500, u'Unreadable JSON notebook.')
139 if 'name' not in nb:
144 if 'name' not in nb:
140 nb.name = os.path.split(path)[-1].split(u'.')[0]
145 nb.name = os.path.split(path)[-1].split(u'.')[0]
141 return last_modified, nb
146 return last_modified, nb
142
147
143 def save_new_notebook(self, data, name=None, format=u'json'):
148 def save_new_notebook(self, data, name=None, format=u'json'):
144 """Save a new notebook and return its notebook_id.
149 """Save a new notebook and return its notebook_id.
145
150
146 If a name is passed in, it overrides any values in the notebook data
151 If a name is passed in, it overrides any values in the notebook data
147 and the value in the data is updated to use that value.
152 and the value in the data is updated to use that value.
148 """
153 """
149 if format not in self.allowed_formats:
154 if format not in self.allowed_formats:
150 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
155 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
151
156
152 try:
157 try:
153 nb = current.reads(data.decode('utf-8'), format)
158 nb = current.reads(data.decode('utf-8'), format)
154 except:
159 except:
155 raise web.HTTPError(400, u'Invalid JSON data')
160 raise web.HTTPError(400, u'Invalid JSON data')
156
161
157 if name is None:
162 if name is None:
158 try:
163 try:
159 name = nb.metadata.name
164 name = nb.metadata.name
160 except AttributeError:
165 except AttributeError:
161 raise web.HTTPError(400, u'Missing notebook name')
166 raise web.HTTPError(400, u'Missing notebook name')
162 nb.metadata.name = name
167 nb.metadata.name = name
163
168
164 notebook_id = self.new_notebook_id(name)
169 notebook_id = self.new_notebook_id(name)
165 self.save_notebook_object(notebook_id, nb)
170 self.save_notebook_object(notebook_id, nb)
166 return notebook_id
171 return notebook_id
167
172
168 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
173 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
169 """Save an existing notebook by notebook_id."""
174 """Save an existing notebook by notebook_id."""
170 if format not in self.allowed_formats:
175 if format not in self.allowed_formats:
171 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
176 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
172
177
173 try:
178 try:
174 nb = current.reads(data.decode('utf-8'), format)
179 nb = current.reads(data.decode('utf-8'), format)
175 except:
180 except:
176 raise web.HTTPError(400, u'Invalid JSON data')
181 raise web.HTTPError(400, u'Invalid JSON data')
177
182
178 if name is not None:
183 if name is not None:
179 nb.metadata.name = name
184 nb.metadata.name = name
180 self.save_notebook_object(notebook_id, nb)
185 self.save_notebook_object(notebook_id, nb)
181
186
182 def save_notebook_object(self, notebook_id, nb):
187 def save_notebook_object(self, notebook_id, nb):
183 """Save an existing notebook object by notebook_id."""
188 """Save an existing notebook object by notebook_id."""
184 if notebook_id not in self.mapping:
189 if notebook_id not in self.mapping:
185 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
190 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
186 old_name = self.mapping[notebook_id]
191 old_name = self.mapping[notebook_id]
187 try:
192 try:
188 new_name = nb.metadata.name
193 new_name = nb.metadata.name
189 except AttributeError:
194 except AttributeError:
190 raise web.HTTPError(400, u'Missing notebook name')
195 raise web.HTTPError(400, u'Missing notebook name')
191 path = self.get_path_by_name(new_name)
196 path = self.get_path_by_name(new_name)
192 try:
197 try:
193 with open(path,'w') as f:
198 with open(path,'w') as f:
194 current.write(nb, f, u'json')
199 current.write(nb, f, u'json')
195 except:
200 except:
196 raise web.HTTPError(400, u'Unexpected error while saving notebook')
201 raise web.HTTPError(400, u'Unexpected error while saving notebook')
197 if old_name != new_name:
202 if old_name != new_name:
198 old_path = self.get_path_by_name(old_name)
203 old_path = self.get_path_by_name(old_name)
199 if os.path.isfile(old_path):
204 if os.path.isfile(old_path):
200 os.unlink(old_path)
205 os.unlink(old_path)
201 self.mapping[notebook_id] = new_name
206 self.mapping[notebook_id] = new_name
202 self.rev_mapping[new_name] = notebook_id
207 self.rev_mapping[new_name] = notebook_id
203
208
204 def delete_notebook(self, notebook_id):
209 def delete_notebook(self, notebook_id):
205 """Delete notebook by notebook_id."""
210 """Delete notebook by notebook_id."""
206 path = self.find_path(notebook_id)
211 path = self.find_path(notebook_id)
207 if not os.path.isfile(path):
212 if not os.path.isfile(path):
208 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
213 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
209 os.unlink(path)
214 os.unlink(path)
210 self.delete_notebook_id(notebook_id)
215 self.delete_notebook_id(notebook_id)
211
216
212 def new_notebook(self):
217 def new_notebook(self):
213 """Create a new notebook and returns its notebook_id."""
218 """Create a new notebook and returns its notebook_id."""
214 i = 0
219 i = 0
215 while True:
220 while True:
216 name = u'Untitled%i' % i
221 name = u'Untitled%i' % i
217 path = self.get_path_by_name(name)
222 path = self.get_path_by_name(name)
218 if not os.path.isfile(path):
223 if not os.path.isfile(path):
219 break
224 break
220 else:
225 else:
221 i = i+1
226 i = i+1
222 notebook_id = self.new_notebook_id(name)
227 notebook_id = self.new_notebook_id(name)
223 metadata = current.new_metadata(name=name)
228 metadata = current.new_metadata(name=name)
224 nb = current.new_notebook(metadata=metadata)
229 nb = current.new_notebook(metadata=metadata)
225 with open(path,'w') as f:
230 with open(path,'w') as f:
226 current.write(nb, f, u'json')
231 current.write(nb, f, u'json')
227 return notebook_id
232 return notebook_id
228
233
@@ -1,63 +1,69 b''
1 """Read and write notebooks in JSON format.
1 """Read and write notebooks in JSON format.
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) 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 from .nbbase import from_dict
19 import copy
20 from .rwbase import NotebookReader, NotebookWriter, restore_bytes
21 import json
20 import json
22
21
22 from .nbbase import from_dict
23 from .rwbase import (
24 NotebookReader, NotebookWriter, restore_bytes, rejoin_lines, split_lines
25 )
26
23 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
24 # Code
28 # Code
25 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
26
30
27 class BytesEncoder(json.JSONEncoder):
31 class BytesEncoder(json.JSONEncoder):
28 """A JSON encoder that accepts b64 (and other *ascii*) bytestrings."""
32 """A JSON encoder that accepts b64 (and other *ascii*) bytestrings."""
29 def default(self, obj):
33 def default(self, obj):
30 if isinstance(obj, bytes):
34 if isinstance(obj, bytes):
31 return obj.decode('ascii')
35 return obj.decode('ascii')
32 return json.JSONEncoder.default(self, obj)
36 return json.JSONEncoder.default(self, obj)
33
37
34
38
35 class JSONReader(NotebookReader):
39 class JSONReader(NotebookReader):
36
40
37 def reads(self, s, **kwargs):
41 def reads(self, s, **kwargs):
38 nb = json.loads(s, **kwargs)
42 nb = json.loads(s, **kwargs)
39 nb = self.to_notebook(nb, **kwargs)
43 nb = self.to_notebook(nb, **kwargs)
40 return nb
44 return nb
41
45
42 def to_notebook(self, d, **kwargs):
46 def to_notebook(self, d, **kwargs):
43 return restore_bytes(from_dict(d))
47 return restore_bytes(rejoin_lines(from_dict(d)))
44
48
45
49
46 class JSONWriter(NotebookWriter):
50 class JSONWriter(NotebookWriter):
47
51
48 def writes(self, nb, **kwargs):
52 def writes(self, nb, **kwargs):
49 kwargs['cls'] = BytesEncoder
53 kwargs['cls'] = BytesEncoder
50 kwargs['indent'] = 4
54 kwargs['indent'] = 4
51 kwargs['sort_keys'] = True
55 kwargs['sort_keys'] = True
56 if kwargs.pop('split_lines', True):
57 nb = split_lines(copy.deepcopy(nb))
52 return json.dumps(nb, **kwargs)
58 return json.dumps(nb, **kwargs)
53
59
54
60
55 _reader = JSONReader()
61 _reader = JSONReader()
56 _writer = JSONWriter()
62 _writer = JSONWriter()
57
63
58 reads = _reader.reads
64 reads = _reader.reads
59 read = _reader.read
65 read = _reader.read
60 to_notebook = _reader.to_notebook
66 to_notebook = _reader.to_notebook
61 write = _writer.write
67 write = _writer.write
62 writes = _writer.writes
68 writes = _writer.writes
63
69
@@ -1,110 +1,165 b''
1 """Base classes and utilities for readers and writers.
1 """Base classes and utilities for readers and writers.
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) 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 from base64 import encodestring, decodestring
19 from base64 import encodestring, decodestring
20 import pprint
20 import pprint
21
21
22 from IPython.utils.py3compat import str_to_bytes
22 from IPython.utils.py3compat import str_to_bytes
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Code
25 # Code
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27
27
28 def restore_bytes(nb):
28 def restore_bytes(nb):
29 """Restore bytes of image data from unicode-only formats.
29 """Restore bytes of image data from unicode-only formats.
30
30
31 Base64 encoding is handled elsewhere. Bytes objects in the notebook are
31 Base64 encoding is handled elsewhere. Bytes objects in the notebook are
32 always b64-encoded. We DO NOT encode/decode around file formats.
32 always b64-encoded. We DO NOT encode/decode around file formats.
33 """
33 """
34 for ws in nb.worksheets:
34 for ws in nb.worksheets:
35 for cell in ws.cells:
35 for cell in ws.cells:
36 if cell.cell_type == 'code':
36 if cell.cell_type == 'code':
37 for output in cell.outputs:
37 for output in cell.outputs:
38 if 'png' in output:
38 if 'png' in output:
39 output.png = str_to_bytes(output.png, 'ascii')
39 output.png = str_to_bytes(output.png, 'ascii')
40 if 'jpeg' in output:
40 if 'jpeg' in output:
41 output.jpeg = str_to_bytes(output.jpeg, 'ascii')
41 output.jpeg = str_to_bytes(output.jpeg, 'ascii')
42 return nb
42 return nb
43
43
44 # output keys that are likely to have multiline values
45 _multiline_outputs = ['text', 'html', 'svg', 'latex', 'javascript', 'json']
46
47 def rejoin_lines(nb):
48 """rejoin multiline text into strings
49
50 For reversing effects of ``split_lines(nb)``.
51
52 This only rejoins lines that have been split, so if text objects were not split
53 they will pass through unchanged.
54
55 Used when reading JSON files that may have been passed through split_lines.
56 """
57 for ws in nb.worksheets:
58 for cell in ws.cells:
59 if cell.cell_type == 'code':
60 if 'input' in cell and isinstance(cell.input, list):
61 cell.input = u'\n'.join(cell.input)
62 for output in cell.outputs:
63 for key in _multiline_outputs:
64 item = output.get(key, None)
65 if isinstance(item, list):
66 output[key] = u'\n'.join(item)
67 else: # text cell
68 for key in ['source', 'rendered']:
69 item = cell.get(key, None)
70 if isinstance(item, list):
71 cell[key] = u'\n'.join(item)
72 return nb
73
74
75 def split_lines(nb):
76 """split likely multiline text into lists of strings
77
78 For file output more friendly to line-based VCS. ``rejoin_lines(nb)`` will
79 reverse the effects of ``split_lines(nb)``.
80
81 Used when writing JSON files.
82 """
83 for ws in nb.worksheets:
84 for cell in ws.cells:
85 if cell.cell_type == 'code':
86 if 'input' in cell and isinstance(cell.input, basestring):
87 cell.input = cell.input.splitlines()
88 for output in cell.outputs:
89 for key in _multiline_outputs:
90 item = output.get(key, None)
91 if isinstance(item, basestring):
92 output[key] = item.splitlines()
93 else: # text cell
94 for key in ['source', 'rendered']:
95 item = cell.get(key, None)
96 if isinstance(item, basestring):
97 cell[key] = item.splitlines()
98 return nb
44
99
45 # b64 encode/decode are never actually used, because all bytes objects in
100 # b64 encode/decode are never actually used, because all bytes objects in
46 # the notebook are already b64-encoded, and we don't need/want to double-encode
101 # the notebook are already b64-encoded, and we don't need/want to double-encode
47
102
48 def base64_decode(nb):
103 def base64_decode(nb):
49 """Restore all bytes objects in the notebook from base64-encoded strings.
104 """Restore all bytes objects in the notebook from base64-encoded strings.
50
105
51 Note: This is never used
106 Note: This is never used
52 """
107 """
53 for ws in nb.worksheets:
108 for ws in nb.worksheets:
54 for cell in ws.cells:
109 for cell in ws.cells:
55 if cell.cell_type == 'code':
110 if cell.cell_type == 'code':
56 for output in cell.outputs:
111 for output in cell.outputs:
57 if 'png' in output:
112 if 'png' in output:
58 if isinstance(output.png, unicode):
113 if isinstance(output.png, unicode):
59 output.png = output.png.encode('ascii')
114 output.png = output.png.encode('ascii')
60 output.png = decodestring(output.png)
115 output.png = decodestring(output.png)
61 if 'jpeg' in output:
116 if 'jpeg' in output:
62 if isinstance(output.jpeg, unicode):
117 if isinstance(output.jpeg, unicode):
63 output.jpeg = output.jpeg.encode('ascii')
118 output.jpeg = output.jpeg.encode('ascii')
64 output.jpeg = decodestring(output.jpeg)
119 output.jpeg = decodestring(output.jpeg)
65 return nb
120 return nb
66
121
67
122
68 def base64_encode(nb):
123 def base64_encode(nb):
69 """Base64 encode all bytes objects in the notebook.
124 """Base64 encode all bytes objects in the notebook.
70
125
71 These will be b64-encoded unicode strings
126 These will be b64-encoded unicode strings
72
127
73 Note: This is never used
128 Note: This is never used
74 """
129 """
75 for ws in nb.worksheets:
130 for ws in nb.worksheets:
76 for cell in ws.cells:
131 for cell in ws.cells:
77 if cell.cell_type == 'code':
132 if cell.cell_type == 'code':
78 for output in cell.outputs:
133 for output in cell.outputs:
79 if 'png' in output:
134 if 'png' in output:
80 output.png = encodestring(output.png).decode('ascii')
135 output.png = encodestring(output.png).decode('ascii')
81 if 'jpeg' in output:
136 if 'jpeg' in output:
82 output.jpeg = encodestring(output.jpeg).decode('ascii')
137 output.jpeg = encodestring(output.jpeg).decode('ascii')
83 return nb
138 return nb
84
139
85
140
86 class NotebookReader(object):
141 class NotebookReader(object):
87 """A class for reading notebooks."""
142 """A class for reading notebooks."""
88
143
89 def reads(self, s, **kwargs):
144 def reads(self, s, **kwargs):
90 """Read a notebook from a string."""
145 """Read a notebook from a string."""
91 raise NotImplementedError("loads must be implemented in a subclass")
146 raise NotImplementedError("loads must be implemented in a subclass")
92
147
93 def read(self, fp, **kwargs):
148 def read(self, fp, **kwargs):
94 """Read a notebook from a file like object"""
149 """Read a notebook from a file like object"""
95 return self.read(fp.read(), **kwargs)
150 return self.read(fp.read(), **kwargs)
96
151
97
152
98 class NotebookWriter(object):
153 class NotebookWriter(object):
99 """A class for writing notebooks."""
154 """A class for writing notebooks."""
100
155
101 def writes(self, nb, **kwargs):
156 def writes(self, nb, **kwargs):
102 """Write a notebook to a string."""
157 """Write a notebook to a string."""
103 raise NotImplementedError("loads must be implemented in a subclass")
158 raise NotImplementedError("loads must be implemented in a subclass")
104
159
105 def write(self, nb, fp, **kwargs):
160 def write(self, nb, fp, **kwargs):
106 """Write a notebook to a file like object"""
161 """Write a notebook to a file like object"""
107 return fp.write(self.writes(nb,**kwargs))
162 return fp.write(self.writes(nb,**kwargs))
108
163
109
164
110
165
@@ -1,21 +1,34 b''
1 import pprint
1 import pprint
2 from unittest import TestCase
2 from unittest import TestCase
3
3
4 from ..nbjson import reads, writes
4 from ..nbjson import reads, writes
5 from .nbexamples import nb0
5 from .nbexamples import nb0
6
6
7
7
8 class TestJSON(TestCase):
8 class TestJSON(TestCase):
9
9
10 def test_roundtrip(self):
10 def test_roundtrip(self):
11 s = writes(nb0)
11 s = writes(nb0)
12 # print
12 # print
13 # print pprint.pformat(nb0,indent=2)
13 # print pprint.pformat(nb0,indent=2)
14 # print
14 # print
15 # print pprint.pformat(reads(s),indent=2)
15 # print pprint.pformat(reads(s),indent=2)
16 # print
16 # print
17 # print s
17 # print s
18 self.assertEquals(reads(s),nb0)
18 self.assertEquals(reads(s),nb0)
19
19
20 def test_roundtrip_nosplit(self):
21 """Ensure that multiline blobs are still readable"""
22 # ensures that notebooks written prior to splitlines change
23 # are still readable.
24 s = writes(nb0, split_lines=False)
25 self.assertEquals(reads(s),nb0)
26
27 def test_roundtrip_split(self):
28 """Ensure that splitting multiline blocks is safe"""
29 # This won't differ from test_roundtrip unless the default changes
30 s = writes(nb0, split_lines=True)
31 self.assertEquals(reads(s),nb0)
32
20
33
21
34
General Comments 0
You need to be logged in to leave comments. Login now