##// END OF EJS Templates
Implement atomic save...
Thomas Kluyver -
Show More
@@ -1,531 +1,532 b''
1 """A contents manager that uses the local file system for storage."""
1 """A contents manager that uses the local file system for storage."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import base64
6 import base64
7 import io
7 import io
8 import os
8 import os
9 import glob
9 import glob
10 import shutil
10 import shutil
11
11
12 from tornado import web
12 from tornado import web
13
13
14 from .manager import ContentsManager
14 from .manager import ContentsManager
15 from IPython.nbformat import current
15 from IPython.nbformat import current
16 from IPython.utils.io import atomic_writing
16 from IPython.utils.path import ensure_dir_exists
17 from IPython.utils.path import ensure_dir_exists
17 from IPython.utils.traitlets import Unicode, Bool, TraitError
18 from IPython.utils.traitlets import Unicode, Bool, TraitError
18 from IPython.utils.py3compat import getcwd
19 from IPython.utils.py3compat import getcwd
19 from IPython.utils import tz
20 from IPython.utils import tz
20 from IPython.html.utils import is_hidden, to_os_path, url_path_join
21 from IPython.html.utils import is_hidden, to_os_path, url_path_join
21
22
22
23
23 class FileContentsManager(ContentsManager):
24 class FileContentsManager(ContentsManager):
24
25
25 root_dir = Unicode(getcwd(), config=True)
26 root_dir = Unicode(getcwd(), config=True)
26
27
27 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
28 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
28 def _save_script_changed(self):
29 def _save_script_changed(self):
29 self.log.warn("""
30 self.log.warn("""
30 Automatically saving notebooks as scripts has been removed.
31 Automatically saving notebooks as scripts has been removed.
31 Use `ipython nbconvert --to python [notebook]` instead.
32 Use `ipython nbconvert --to python [notebook]` instead.
32 """)
33 """)
33
34
34 def _root_dir_changed(self, name, old, new):
35 def _root_dir_changed(self, name, old, new):
35 """Do a bit of validation of the root_dir."""
36 """Do a bit of validation of the root_dir."""
36 if not os.path.isabs(new):
37 if not os.path.isabs(new):
37 # If we receive a non-absolute path, make it absolute.
38 # If we receive a non-absolute path, make it absolute.
38 self.root_dir = os.path.abspath(new)
39 self.root_dir = os.path.abspath(new)
39 return
40 return
40 if not os.path.isdir(new):
41 if not os.path.isdir(new):
41 raise TraitError("%r is not a directory" % new)
42 raise TraitError("%r is not a directory" % new)
42
43
43 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
44 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
44 help="""The directory name in which to keep file checkpoints
45 help="""The directory name in which to keep file checkpoints
45
46
46 This is a path relative to the file's own directory.
47 This is a path relative to the file's own directory.
47
48
48 By default, it is .ipynb_checkpoints
49 By default, it is .ipynb_checkpoints
49 """
50 """
50 )
51 )
51
52
52 def _copy(self, src, dest):
53 def _copy(self, src, dest):
53 """copy src to dest
54 """copy src to dest
54
55
55 like shutil.copy2, but log errors in copystat
56 like shutil.copy2, but log errors in copystat
56 """
57 """
57 shutil.copyfile(src, dest)
58 shutil.copyfile(src, dest)
58 try:
59 try:
59 shutil.copystat(src, dest)
60 shutil.copystat(src, dest)
60 except OSError as e:
61 except OSError as e:
61 self.log.debug("copystat on %s failed", dest, exc_info=True)
62 self.log.debug("copystat on %s failed", dest, exc_info=True)
62
63
63 def _get_os_path(self, name=None, path=''):
64 def _get_os_path(self, name=None, path=''):
64 """Given a filename and API path, return its file system
65 """Given a filename and API path, return its file system
65 path.
66 path.
66
67
67 Parameters
68 Parameters
68 ----------
69 ----------
69 name : string
70 name : string
70 A filename
71 A filename
71 path : string
72 path : string
72 The relative API path to the named file.
73 The relative API path to the named file.
73
74
74 Returns
75 Returns
75 -------
76 -------
76 path : string
77 path : string
77 API path to be evaluated relative to root_dir.
78 API path to be evaluated relative to root_dir.
78 """
79 """
79 if name is not None:
80 if name is not None:
80 path = url_path_join(path, name)
81 path = url_path_join(path, name)
81 return to_os_path(path, self.root_dir)
82 return to_os_path(path, self.root_dir)
82
83
83 def path_exists(self, path):
84 def path_exists(self, path):
84 """Does the API-style path refer to an extant directory?
85 """Does the API-style path refer to an extant directory?
85
86
86 API-style wrapper for os.path.isdir
87 API-style wrapper for os.path.isdir
87
88
88 Parameters
89 Parameters
89 ----------
90 ----------
90 path : string
91 path : string
91 The path to check. This is an API path (`/` separated,
92 The path to check. This is an API path (`/` separated,
92 relative to root_dir).
93 relative to root_dir).
93
94
94 Returns
95 Returns
95 -------
96 -------
96 exists : bool
97 exists : bool
97 Whether the path is indeed a directory.
98 Whether the path is indeed a directory.
98 """
99 """
99 path = path.strip('/')
100 path = path.strip('/')
100 os_path = self._get_os_path(path=path)
101 os_path = self._get_os_path(path=path)
101 return os.path.isdir(os_path)
102 return os.path.isdir(os_path)
102
103
103 def is_hidden(self, path):
104 def is_hidden(self, path):
104 """Does the API style path correspond to a hidden directory or file?
105 """Does the API style path correspond to a hidden directory or file?
105
106
106 Parameters
107 Parameters
107 ----------
108 ----------
108 path : string
109 path : string
109 The path to check. This is an API path (`/` separated,
110 The path to check. This is an API path (`/` separated,
110 relative to root_dir).
111 relative to root_dir).
111
112
112 Returns
113 Returns
113 -------
114 -------
114 exists : bool
115 exists : bool
115 Whether the path is hidden.
116 Whether the path is hidden.
116
117
117 """
118 """
118 path = path.strip('/')
119 path = path.strip('/')
119 os_path = self._get_os_path(path=path)
120 os_path = self._get_os_path(path=path)
120 return is_hidden(os_path, self.root_dir)
121 return is_hidden(os_path, self.root_dir)
121
122
122 def file_exists(self, name, path=''):
123 def file_exists(self, name, path=''):
123 """Returns True if the file exists, else returns False.
124 """Returns True if the file exists, else returns False.
124
125
125 API-style wrapper for os.path.isfile
126 API-style wrapper for os.path.isfile
126
127
127 Parameters
128 Parameters
128 ----------
129 ----------
129 name : string
130 name : string
130 The name of the file you are checking.
131 The name of the file you are checking.
131 path : string
132 path : string
132 The relative path to the file's directory (with '/' as separator)
133 The relative path to the file's directory (with '/' as separator)
133
134
134 Returns
135 Returns
135 -------
136 -------
136 exists : bool
137 exists : bool
137 Whether the file exists.
138 Whether the file exists.
138 """
139 """
139 path = path.strip('/')
140 path = path.strip('/')
140 nbpath = self._get_os_path(name, path=path)
141 nbpath = self._get_os_path(name, path=path)
141 return os.path.isfile(nbpath)
142 return os.path.isfile(nbpath)
142
143
143 def exists(self, name=None, path=''):
144 def exists(self, name=None, path=''):
144 """Returns True if the path [and name] exists, else returns False.
145 """Returns True if the path [and name] exists, else returns False.
145
146
146 API-style wrapper for os.path.exists
147 API-style wrapper for os.path.exists
147
148
148 Parameters
149 Parameters
149 ----------
150 ----------
150 name : string
151 name : string
151 The name of the file you are checking.
152 The name of the file you are checking.
152 path : string
153 path : string
153 The relative path to the file's directory (with '/' as separator)
154 The relative path to the file's directory (with '/' as separator)
154
155
155 Returns
156 Returns
156 -------
157 -------
157 exists : bool
158 exists : bool
158 Whether the target exists.
159 Whether the target exists.
159 """
160 """
160 path = path.strip('/')
161 path = path.strip('/')
161 os_path = self._get_os_path(name, path=path)
162 os_path = self._get_os_path(name, path=path)
162 return os.path.exists(os_path)
163 return os.path.exists(os_path)
163
164
164 def _base_model(self, name, path=''):
165 def _base_model(self, name, path=''):
165 """Build the common base of a contents model"""
166 """Build the common base of a contents model"""
166 os_path = self._get_os_path(name, path)
167 os_path = self._get_os_path(name, path)
167 info = os.stat(os_path)
168 info = os.stat(os_path)
168 last_modified = tz.utcfromtimestamp(info.st_mtime)
169 last_modified = tz.utcfromtimestamp(info.st_mtime)
169 created = tz.utcfromtimestamp(info.st_ctime)
170 created = tz.utcfromtimestamp(info.st_ctime)
170 # Create the base model.
171 # Create the base model.
171 model = {}
172 model = {}
172 model['name'] = name
173 model['name'] = name
173 model['path'] = path
174 model['path'] = path
174 model['last_modified'] = last_modified
175 model['last_modified'] = last_modified
175 model['created'] = created
176 model['created'] = created
176 model['content'] = None
177 model['content'] = None
177 model['format'] = None
178 model['format'] = None
178 return model
179 return model
179
180
180 def _dir_model(self, name, path='', content=True):
181 def _dir_model(self, name, path='', content=True):
181 """Build a model for a directory
182 """Build a model for a directory
182
183
183 if content is requested, will include a listing of the directory
184 if content is requested, will include a listing of the directory
184 """
185 """
185 os_path = self._get_os_path(name, path)
186 os_path = self._get_os_path(name, path)
186
187
187 four_o_four = u'directory does not exist: %r' % os_path
188 four_o_four = u'directory does not exist: %r' % os_path
188
189
189 if not os.path.isdir(os_path):
190 if not os.path.isdir(os_path):
190 raise web.HTTPError(404, four_o_four)
191 raise web.HTTPError(404, four_o_four)
191 elif is_hidden(os_path, self.root_dir):
192 elif is_hidden(os_path, self.root_dir):
192 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
193 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
193 os_path
194 os_path
194 )
195 )
195 raise web.HTTPError(404, four_o_four)
196 raise web.HTTPError(404, four_o_four)
196
197
197 if name is None:
198 if name is None:
198 if '/' in path:
199 if '/' in path:
199 path, name = path.rsplit('/', 1)
200 path, name = path.rsplit('/', 1)
200 else:
201 else:
201 name = ''
202 name = ''
202 model = self._base_model(name, path)
203 model = self._base_model(name, path)
203 model['type'] = 'directory'
204 model['type'] = 'directory'
204 dir_path = u'{}/{}'.format(path, name)
205 dir_path = u'{}/{}'.format(path, name)
205 if content:
206 if content:
206 model['content'] = contents = []
207 model['content'] = contents = []
207 for os_path in glob.glob(self._get_os_path('*', dir_path)):
208 for os_path in glob.glob(self._get_os_path('*', dir_path)):
208 name = os.path.basename(os_path)
209 name = os.path.basename(os_path)
209 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
210 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
210 contents.append(self.get_model(name=name, path=dir_path, content=False))
211 contents.append(self.get_model(name=name, path=dir_path, content=False))
211
212
212 model['format'] = 'json'
213 model['format'] = 'json'
213
214
214 return model
215 return model
215
216
216 def _file_model(self, name, path='', content=True):
217 def _file_model(self, name, path='', content=True):
217 """Build a model for a file
218 """Build a model for a file
218
219
219 if content is requested, include the file contents.
220 if content is requested, include the file contents.
220 UTF-8 text files will be unicode, binary files will be base64-encoded.
221 UTF-8 text files will be unicode, binary files will be base64-encoded.
221 """
222 """
222 model = self._base_model(name, path)
223 model = self._base_model(name, path)
223 model['type'] = 'file'
224 model['type'] = 'file'
224 if content:
225 if content:
225 os_path = self._get_os_path(name, path)
226 os_path = self._get_os_path(name, path)
226 with io.open(os_path, 'rb') as f:
227 with io.open(os_path, 'rb') as f:
227 bcontent = f.read()
228 bcontent = f.read()
228 try:
229 try:
229 model['content'] = bcontent.decode('utf8')
230 model['content'] = bcontent.decode('utf8')
230 except UnicodeError as e:
231 except UnicodeError as e:
231 model['content'] = base64.encodestring(bcontent).decode('ascii')
232 model['content'] = base64.encodestring(bcontent).decode('ascii')
232 model['format'] = 'base64'
233 model['format'] = 'base64'
233 else:
234 else:
234 model['format'] = 'text'
235 model['format'] = 'text'
235 return model
236 return model
236
237
237
238
238 def _notebook_model(self, name, path='', content=True):
239 def _notebook_model(self, name, path='', content=True):
239 """Build a notebook model
240 """Build a notebook model
240
241
241 if content is requested, the notebook content will be populated
242 if content is requested, the notebook content will be populated
242 as a JSON structure (not double-serialized)
243 as a JSON structure (not double-serialized)
243 """
244 """
244 model = self._base_model(name, path)
245 model = self._base_model(name, path)
245 model['type'] = 'notebook'
246 model['type'] = 'notebook'
246 if content:
247 if content:
247 os_path = self._get_os_path(name, path)
248 os_path = self._get_os_path(name, path)
248 with io.open(os_path, 'r', encoding='utf-8') as f:
249 with io.open(os_path, 'r', encoding='utf-8') as f:
249 try:
250 try:
250 nb = current.read(f, u'json')
251 nb = current.read(f, u'json')
251 except Exception as e:
252 except Exception as e:
252 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
253 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
253 self.mark_trusted_cells(nb, name, path)
254 self.mark_trusted_cells(nb, name, path)
254 model['content'] = nb
255 model['content'] = nb
255 model['format'] = 'json'
256 model['format'] = 'json'
256 return model
257 return model
257
258
258 def get_model(self, name, path='', content=True):
259 def get_model(self, name, path='', content=True):
259 """ Takes a path and name for an entity and returns its model
260 """ Takes a path and name for an entity and returns its model
260
261
261 Parameters
262 Parameters
262 ----------
263 ----------
263 name : str
264 name : str
264 the name of the target
265 the name of the target
265 path : str
266 path : str
266 the API path that describes the relative path for the target
267 the API path that describes the relative path for the target
267
268
268 Returns
269 Returns
269 -------
270 -------
270 model : dict
271 model : dict
271 the contents model. If content=True, returns the contents
272 the contents model. If content=True, returns the contents
272 of the file or directory as well.
273 of the file or directory as well.
273 """
274 """
274 path = path.strip('/')
275 path = path.strip('/')
275
276
276 if not self.exists(name=name, path=path):
277 if not self.exists(name=name, path=path):
277 raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
278 raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
278
279
279 os_path = self._get_os_path(name, path)
280 os_path = self._get_os_path(name, path)
280 if os.path.isdir(os_path):
281 if os.path.isdir(os_path):
281 model = self._dir_model(name, path, content)
282 model = self._dir_model(name, path, content)
282 elif name.endswith('.ipynb'):
283 elif name.endswith('.ipynb'):
283 model = self._notebook_model(name, path, content)
284 model = self._notebook_model(name, path, content)
284 else:
285 else:
285 model = self._file_model(name, path, content)
286 model = self._file_model(name, path, content)
286 return model
287 return model
287
288
288 def _save_notebook(self, os_path, model, name='', path=''):
289 def _save_notebook(self, os_path, model, name='', path=''):
289 """save a notebook file"""
290 """save a notebook file"""
290 # Save the notebook file
291 # Save the notebook file
291 nb = current.to_notebook_json(model['content'])
292 nb = current.to_notebook_json(model['content'])
292
293
293 self.check_and_sign(nb, name, path)
294 self.check_and_sign(nb, name, path)
294
295
295 if 'name' in nb['metadata']:
296 if 'name' in nb['metadata']:
296 nb['metadata']['name'] = u''
297 nb['metadata']['name'] = u''
297
298
298 with io.open(os_path, 'w', encoding='utf-8') as f:
299 with atomic_writing(os_path, encoding='utf-8') as f:
299 current.write(nb, f, u'json')
300 current.write(nb, f, u'json')
300
301
301 def _save_file(self, os_path, model, name='', path=''):
302 def _save_file(self, os_path, model, name='', path=''):
302 """save a non-notebook file"""
303 """save a non-notebook file"""
303 fmt = model.get('format', None)
304 fmt = model.get('format', None)
304 if fmt not in {'text', 'base64'}:
305 if fmt not in {'text', 'base64'}:
305 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
306 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
306 try:
307 try:
307 content = model['content']
308 content = model['content']
308 if fmt == 'text':
309 if fmt == 'text':
309 bcontent = content.encode('utf8')
310 bcontent = content.encode('utf8')
310 else:
311 else:
311 b64_bytes = content.encode('ascii')
312 b64_bytes = content.encode('ascii')
312 bcontent = base64.decodestring(b64_bytes)
313 bcontent = base64.decodestring(b64_bytes)
313 except Exception as e:
314 except Exception as e:
314 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
315 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
315 with io.open(os_path, 'wb') as f:
316 with atomic_writing(os_path, 'wb') as f:
316 f.write(bcontent)
317 f.write(bcontent)
317
318
318 def _save_directory(self, os_path, model, name='', path=''):
319 def _save_directory(self, os_path, model, name='', path=''):
319 """create a directory"""
320 """create a directory"""
320 if is_hidden(os_path, self.root_dir):
321 if is_hidden(os_path, self.root_dir):
321 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
322 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
322 if not os.path.exists(os_path):
323 if not os.path.exists(os_path):
323 os.mkdir(os_path)
324 os.mkdir(os_path)
324 elif not os.path.isdir(os_path):
325 elif not os.path.isdir(os_path):
325 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
326 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
326 else:
327 else:
327 self.log.debug("Directory %r already exists", os_path)
328 self.log.debug("Directory %r already exists", os_path)
328
329
329 def save(self, model, name='', path=''):
330 def save(self, model, name='', path=''):
330 """Save the file model and return the model with no content."""
331 """Save the file model and return the model with no content."""
331 path = path.strip('/')
332 path = path.strip('/')
332
333
333 if 'type' not in model:
334 if 'type' not in model:
334 raise web.HTTPError(400, u'No file type provided')
335 raise web.HTTPError(400, u'No file type provided')
335 if 'content' not in model and model['type'] != 'directory':
336 if 'content' not in model and model['type'] != 'directory':
336 raise web.HTTPError(400, u'No file content provided')
337 raise web.HTTPError(400, u'No file content provided')
337
338
338 # One checkpoint should always exist
339 # One checkpoint should always exist
339 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
340 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
340 self.create_checkpoint(name, path)
341 self.create_checkpoint(name, path)
341
342
342 new_path = model.get('path', path).strip('/')
343 new_path = model.get('path', path).strip('/')
343 new_name = model.get('name', name)
344 new_name = model.get('name', name)
344
345
345 if path != new_path or name != new_name:
346 if path != new_path or name != new_name:
346 self.rename(name, path, new_name, new_path)
347 self.rename(name, path, new_name, new_path)
347
348
348 os_path = self._get_os_path(new_name, new_path)
349 os_path = self._get_os_path(new_name, new_path)
349 self.log.debug("Saving %s", os_path)
350 self.log.debug("Saving %s", os_path)
350 try:
351 try:
351 if model['type'] == 'notebook':
352 if model['type'] == 'notebook':
352 self._save_notebook(os_path, model, new_name, new_path)
353 self._save_notebook(os_path, model, new_name, new_path)
353 elif model['type'] == 'file':
354 elif model['type'] == 'file':
354 self._save_file(os_path, model, new_name, new_path)
355 self._save_file(os_path, model, new_name, new_path)
355 elif model['type'] == 'directory':
356 elif model['type'] == 'directory':
356 self._save_directory(os_path, model, new_name, new_path)
357 self._save_directory(os_path, model, new_name, new_path)
357 else:
358 else:
358 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
359 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
359 except web.HTTPError:
360 except web.HTTPError:
360 raise
361 raise
361 except Exception as e:
362 except Exception as e:
362 raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
363 raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
363
364
364 model = self.get_model(new_name, new_path, content=False)
365 model = self.get_model(new_name, new_path, content=False)
365 return model
366 return model
366
367
367 def update(self, model, name, path=''):
368 def update(self, model, name, path=''):
368 """Update the file's path and/or name
369 """Update the file's path and/or name
369
370
370 For use in PATCH requests, to enable renaming a file without
371 For use in PATCH requests, to enable renaming a file without
371 re-uploading its contents. Only used for renaming at the moment.
372 re-uploading its contents. Only used for renaming at the moment.
372 """
373 """
373 path = path.strip('/')
374 path = path.strip('/')
374 new_name = model.get('name', name)
375 new_name = model.get('name', name)
375 new_path = model.get('path', path).strip('/')
376 new_path = model.get('path', path).strip('/')
376 if path != new_path or name != new_name:
377 if path != new_path or name != new_name:
377 self.rename(name, path, new_name, new_path)
378 self.rename(name, path, new_name, new_path)
378 model = self.get_model(new_name, new_path, content=False)
379 model = self.get_model(new_name, new_path, content=False)
379 return model
380 return model
380
381
381 def delete(self, name, path=''):
382 def delete(self, name, path=''):
382 """Delete file by name and path."""
383 """Delete file by name and path."""
383 path = path.strip('/')
384 path = path.strip('/')
384 os_path = self._get_os_path(name, path)
385 os_path = self._get_os_path(name, path)
385 rm = os.unlink
386 rm = os.unlink
386 if os.path.isdir(os_path):
387 if os.path.isdir(os_path):
387 listing = os.listdir(os_path)
388 listing = os.listdir(os_path)
388 # don't delete non-empty directories (checkpoints dir doesn't count)
389 # don't delete non-empty directories (checkpoints dir doesn't count)
389 if listing and listing != [self.checkpoint_dir]:
390 if listing and listing != [self.checkpoint_dir]:
390 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
391 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
391 elif not os.path.isfile(os_path):
392 elif not os.path.isfile(os_path):
392 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
393 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
393
394
394 # clear checkpoints
395 # clear checkpoints
395 for checkpoint in self.list_checkpoints(name, path):
396 for checkpoint in self.list_checkpoints(name, path):
396 checkpoint_id = checkpoint['id']
397 checkpoint_id = checkpoint['id']
397 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
398 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
398 if os.path.isfile(cp_path):
399 if os.path.isfile(cp_path):
399 self.log.debug("Unlinking checkpoint %s", cp_path)
400 self.log.debug("Unlinking checkpoint %s", cp_path)
400 os.unlink(cp_path)
401 os.unlink(cp_path)
401
402
402 if os.path.isdir(os_path):
403 if os.path.isdir(os_path):
403 self.log.debug("Removing directory %s", os_path)
404 self.log.debug("Removing directory %s", os_path)
404 shutil.rmtree(os_path)
405 shutil.rmtree(os_path)
405 else:
406 else:
406 self.log.debug("Unlinking file %s", os_path)
407 self.log.debug("Unlinking file %s", os_path)
407 rm(os_path)
408 rm(os_path)
408
409
409 def rename(self, old_name, old_path, new_name, new_path):
410 def rename(self, old_name, old_path, new_name, new_path):
410 """Rename a file."""
411 """Rename a file."""
411 old_path = old_path.strip('/')
412 old_path = old_path.strip('/')
412 new_path = new_path.strip('/')
413 new_path = new_path.strip('/')
413 if new_name == old_name and new_path == old_path:
414 if new_name == old_name and new_path == old_path:
414 return
415 return
415
416
416 new_os_path = self._get_os_path(new_name, new_path)
417 new_os_path = self._get_os_path(new_name, new_path)
417 old_os_path = self._get_os_path(old_name, old_path)
418 old_os_path = self._get_os_path(old_name, old_path)
418
419
419 # Should we proceed with the move?
420 # Should we proceed with the move?
420 if os.path.isfile(new_os_path):
421 if os.path.isfile(new_os_path):
421 raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path)
422 raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path)
422
423
423 # Move the file
424 # Move the file
424 try:
425 try:
425 shutil.move(old_os_path, new_os_path)
426 shutil.move(old_os_path, new_os_path)
426 except Exception as e:
427 except Exception as e:
427 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
428 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
428
429
429 # Move the checkpoints
430 # Move the checkpoints
430 old_checkpoints = self.list_checkpoints(old_name, old_path)
431 old_checkpoints = self.list_checkpoints(old_name, old_path)
431 for cp in old_checkpoints:
432 for cp in old_checkpoints:
432 checkpoint_id = cp['id']
433 checkpoint_id = cp['id']
433 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
434 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
434 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
435 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
435 if os.path.isfile(old_cp_path):
436 if os.path.isfile(old_cp_path):
436 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
437 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
437 shutil.move(old_cp_path, new_cp_path)
438 shutil.move(old_cp_path, new_cp_path)
438
439
439 # Checkpoint-related utilities
440 # Checkpoint-related utilities
440
441
441 def get_checkpoint_path(self, checkpoint_id, name, path=''):
442 def get_checkpoint_path(self, checkpoint_id, name, path=''):
442 """find the path to a checkpoint"""
443 """find the path to a checkpoint"""
443 path = path.strip('/')
444 path = path.strip('/')
444 basename, ext = os.path.splitext(name)
445 basename, ext = os.path.splitext(name)
445 filename = u"{name}-{checkpoint_id}{ext}".format(
446 filename = u"{name}-{checkpoint_id}{ext}".format(
446 name=basename,
447 name=basename,
447 checkpoint_id=checkpoint_id,
448 checkpoint_id=checkpoint_id,
448 ext=ext,
449 ext=ext,
449 )
450 )
450 os_path = self._get_os_path(path=path)
451 os_path = self._get_os_path(path=path)
451 cp_dir = os.path.join(os_path, self.checkpoint_dir)
452 cp_dir = os.path.join(os_path, self.checkpoint_dir)
452 ensure_dir_exists(cp_dir)
453 ensure_dir_exists(cp_dir)
453 cp_path = os.path.join(cp_dir, filename)
454 cp_path = os.path.join(cp_dir, filename)
454 return cp_path
455 return cp_path
455
456
456 def get_checkpoint_model(self, checkpoint_id, name, path=''):
457 def get_checkpoint_model(self, checkpoint_id, name, path=''):
457 """construct the info dict for a given checkpoint"""
458 """construct the info dict for a given checkpoint"""
458 path = path.strip('/')
459 path = path.strip('/')
459 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
460 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
460 stats = os.stat(cp_path)
461 stats = os.stat(cp_path)
461 last_modified = tz.utcfromtimestamp(stats.st_mtime)
462 last_modified = tz.utcfromtimestamp(stats.st_mtime)
462 info = dict(
463 info = dict(
463 id = checkpoint_id,
464 id = checkpoint_id,
464 last_modified = last_modified,
465 last_modified = last_modified,
465 )
466 )
466 return info
467 return info
467
468
468 # public checkpoint API
469 # public checkpoint API
469
470
470 def create_checkpoint(self, name, path=''):
471 def create_checkpoint(self, name, path=''):
471 """Create a checkpoint from the current state of a file"""
472 """Create a checkpoint from the current state of a file"""
472 path = path.strip('/')
473 path = path.strip('/')
473 src_path = self._get_os_path(name, path)
474 src_path = self._get_os_path(name, path)
474 # only the one checkpoint ID:
475 # only the one checkpoint ID:
475 checkpoint_id = u"checkpoint"
476 checkpoint_id = u"checkpoint"
476 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
477 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
477 self.log.debug("creating checkpoint for %s", name)
478 self.log.debug("creating checkpoint for %s", name)
478 self._copy(src_path, cp_path)
479 self._copy(src_path, cp_path)
479
480
480 # return the checkpoint info
481 # return the checkpoint info
481 return self.get_checkpoint_model(checkpoint_id, name, path)
482 return self.get_checkpoint_model(checkpoint_id, name, path)
482
483
483 def list_checkpoints(self, name, path=''):
484 def list_checkpoints(self, name, path=''):
484 """list the checkpoints for a given file
485 """list the checkpoints for a given file
485
486
486 This contents manager currently only supports one checkpoint per file.
487 This contents manager currently only supports one checkpoint per file.
487 """
488 """
488 path = path.strip('/')
489 path = path.strip('/')
489 checkpoint_id = "checkpoint"
490 checkpoint_id = "checkpoint"
490 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
491 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
491 if not os.path.exists(os_path):
492 if not os.path.exists(os_path):
492 return []
493 return []
493 else:
494 else:
494 return [self.get_checkpoint_model(checkpoint_id, name, path)]
495 return [self.get_checkpoint_model(checkpoint_id, name, path)]
495
496
496
497
497 def restore_checkpoint(self, checkpoint_id, name, path=''):
498 def restore_checkpoint(self, checkpoint_id, name, path=''):
498 """restore a file to a checkpointed state"""
499 """restore a file to a checkpointed state"""
499 path = path.strip('/')
500 path = path.strip('/')
500 self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
501 self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
501 nb_path = self._get_os_path(name, path)
502 nb_path = self._get_os_path(name, path)
502 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
503 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
503 if not os.path.isfile(cp_path):
504 if not os.path.isfile(cp_path):
504 self.log.debug("checkpoint file does not exist: %s", cp_path)
505 self.log.debug("checkpoint file does not exist: %s", cp_path)
505 raise web.HTTPError(404,
506 raise web.HTTPError(404,
506 u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
507 u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
507 )
508 )
508 # ensure notebook is readable (never restore from an unreadable notebook)
509 # ensure notebook is readable (never restore from an unreadable notebook)
509 if cp_path.endswith('.ipynb'):
510 if cp_path.endswith('.ipynb'):
510 with io.open(cp_path, 'r', encoding='utf-8') as f:
511 with io.open(cp_path, 'r', encoding='utf-8') as f:
511 current.read(f, u'json')
512 current.read(f, u'json')
512 self._copy(cp_path, nb_path)
513 self._copy(cp_path, nb_path)
513 self.log.debug("copying %s -> %s", cp_path, nb_path)
514 self.log.debug("copying %s -> %s", cp_path, nb_path)
514
515
515 def delete_checkpoint(self, checkpoint_id, name, path=''):
516 def delete_checkpoint(self, checkpoint_id, name, path=''):
516 """delete a file's checkpoint"""
517 """delete a file's checkpoint"""
517 path = path.strip('/')
518 path = path.strip('/')
518 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
519 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
519 if not os.path.isfile(cp_path):
520 if not os.path.isfile(cp_path):
520 raise web.HTTPError(404,
521 raise web.HTTPError(404,
521 u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
522 u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
522 )
523 )
523 self.log.debug("unlinking %s", cp_path)
524 self.log.debug("unlinking %s", cp_path)
524 os.unlink(cp_path)
525 os.unlink(cp_path)
525
526
526 def info_string(self):
527 def info_string(self):
527 return "Serving notebooks from local directory: %s" % self.root_dir
528 return "Serving notebooks from local directory: %s" % self.root_dir
528
529
529 def get_kernel_path(self, name, path='', model=None):
530 def get_kernel_path(self, name, path='', model=None):
530 """Return the initial working dir a kernel associated with a given notebook"""
531 """Return the initial working dir a kernel associated with a given notebook"""
531 return os.path.join(self.root_dir, path)
532 return os.path.join(self.root_dir, path)
@@ -1,262 +1,280 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 IO related utilities.
3 IO related utilities.
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2008-2011 The IPython Development Team
7 # Copyright (C) 2008-2011 The IPython Development Team
8 #
8 #
9 # Distributed under the terms of the BSD License. The full license is in
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 from __future__ import print_function
12 from __future__ import print_function
13 from __future__ import absolute_import
13 from __future__ import absolute_import
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 import codecs
18 import codecs
19 from contextlib import contextmanager
19 import os
20 import os
20 import sys
21 import sys
21 import tempfile
22 import tempfile
22 from .capture import CapturedIO, capture_output
23 from .capture import CapturedIO, capture_output
23 from .py3compat import string_types, input, PY3
24 from .py3compat import string_types, input, PY3
24
25
25 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
26 # Code
27 # Code
27 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
28
29
29
30
30 class IOStream:
31 class IOStream:
31
32
32 def __init__(self,stream, fallback=None):
33 def __init__(self,stream, fallback=None):
33 if not hasattr(stream,'write') or not hasattr(stream,'flush'):
34 if not hasattr(stream,'write') or not hasattr(stream,'flush'):
34 if fallback is not None:
35 if fallback is not None:
35 stream = fallback
36 stream = fallback
36 else:
37 else:
37 raise ValueError("fallback required, but not specified")
38 raise ValueError("fallback required, but not specified")
38 self.stream = stream
39 self.stream = stream
39 self._swrite = stream.write
40 self._swrite = stream.write
40
41
41 # clone all methods not overridden:
42 # clone all methods not overridden:
42 def clone(meth):
43 def clone(meth):
43 return not hasattr(self, meth) and not meth.startswith('_')
44 return not hasattr(self, meth) and not meth.startswith('_')
44 for meth in filter(clone, dir(stream)):
45 for meth in filter(clone, dir(stream)):
45 setattr(self, meth, getattr(stream, meth))
46 setattr(self, meth, getattr(stream, meth))
46
47
47 def __repr__(self):
48 def __repr__(self):
48 cls = self.__class__
49 cls = self.__class__
49 tpl = '{mod}.{cls}({args})'
50 tpl = '{mod}.{cls}({args})'
50 return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream)
51 return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream)
51
52
52 def write(self,data):
53 def write(self,data):
53 try:
54 try:
54 self._swrite(data)
55 self._swrite(data)
55 except:
56 except:
56 try:
57 try:
57 # print handles some unicode issues which may trip a plain
58 # print handles some unicode issues which may trip a plain
58 # write() call. Emulate write() by using an empty end
59 # write() call. Emulate write() by using an empty end
59 # argument.
60 # argument.
60 print(data, end='', file=self.stream)
61 print(data, end='', file=self.stream)
61 except:
62 except:
62 # if we get here, something is seriously broken.
63 # if we get here, something is seriously broken.
63 print('ERROR - failed to write data to stream:', self.stream,
64 print('ERROR - failed to write data to stream:', self.stream,
64 file=sys.stderr)
65 file=sys.stderr)
65
66
66 def writelines(self, lines):
67 def writelines(self, lines):
67 if isinstance(lines, string_types):
68 if isinstance(lines, string_types):
68 lines = [lines]
69 lines = [lines]
69 for line in lines:
70 for line in lines:
70 self.write(line)
71 self.write(line)
71
72
72 # This class used to have a writeln method, but regular files and streams
73 # This class used to have a writeln method, but regular files and streams
73 # in Python don't have this method. We need to keep this completely
74 # in Python don't have this method. We need to keep this completely
74 # compatible so we removed it.
75 # compatible so we removed it.
75
76
76 @property
77 @property
77 def closed(self):
78 def closed(self):
78 return self.stream.closed
79 return self.stream.closed
79
80
80 def close(self):
81 def close(self):
81 pass
82 pass
82
83
83 # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr
84 # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr
84 devnull = open(os.devnull, 'w')
85 devnull = open(os.devnull, 'w')
85 stdin = IOStream(sys.stdin, fallback=devnull)
86 stdin = IOStream(sys.stdin, fallback=devnull)
86 stdout = IOStream(sys.stdout, fallback=devnull)
87 stdout = IOStream(sys.stdout, fallback=devnull)
87 stderr = IOStream(sys.stderr, fallback=devnull)
88 stderr = IOStream(sys.stderr, fallback=devnull)
88
89
89 class IOTerm:
90 class IOTerm:
90 """ Term holds the file or file-like objects for handling I/O operations.
91 """ Term holds the file or file-like objects for handling I/O operations.
91
92
92 These are normally just sys.stdin, sys.stdout and sys.stderr but for
93 These are normally just sys.stdin, sys.stdout and sys.stderr but for
93 Windows they can can replaced to allow editing the strings before they are
94 Windows they can can replaced to allow editing the strings before they are
94 displayed."""
95 displayed."""
95
96
96 # In the future, having IPython channel all its I/O operations through
97 # In the future, having IPython channel all its I/O operations through
97 # this class will make it easier to embed it into other environments which
98 # this class will make it easier to embed it into other environments which
98 # are not a normal terminal (such as a GUI-based shell)
99 # are not a normal terminal (such as a GUI-based shell)
99 def __init__(self, stdin=None, stdout=None, stderr=None):
100 def __init__(self, stdin=None, stdout=None, stderr=None):
100 mymodule = sys.modules[__name__]
101 mymodule = sys.modules[__name__]
101 self.stdin = IOStream(stdin, mymodule.stdin)
102 self.stdin = IOStream(stdin, mymodule.stdin)
102 self.stdout = IOStream(stdout, mymodule.stdout)
103 self.stdout = IOStream(stdout, mymodule.stdout)
103 self.stderr = IOStream(stderr, mymodule.stderr)
104 self.stderr = IOStream(stderr, mymodule.stderr)
104
105
105
106
106 class Tee(object):
107 class Tee(object):
107 """A class to duplicate an output stream to stdout/err.
108 """A class to duplicate an output stream to stdout/err.
108
109
109 This works in a manner very similar to the Unix 'tee' command.
110 This works in a manner very similar to the Unix 'tee' command.
110
111
111 When the object is closed or deleted, it closes the original file given to
112 When the object is closed or deleted, it closes the original file given to
112 it for duplication.
113 it for duplication.
113 """
114 """
114 # Inspired by:
115 # Inspired by:
115 # http://mail.python.org/pipermail/python-list/2007-May/442737.html
116 # http://mail.python.org/pipermail/python-list/2007-May/442737.html
116
117
117 def __init__(self, file_or_name, mode="w", channel='stdout'):
118 def __init__(self, file_or_name, mode="w", channel='stdout'):
118 """Construct a new Tee object.
119 """Construct a new Tee object.
119
120
120 Parameters
121 Parameters
121 ----------
122 ----------
122 file_or_name : filename or open filehandle (writable)
123 file_or_name : filename or open filehandle (writable)
123 File that will be duplicated
124 File that will be duplicated
124
125
125 mode : optional, valid mode for open().
126 mode : optional, valid mode for open().
126 If a filename was give, open with this mode.
127 If a filename was give, open with this mode.
127
128
128 channel : str, one of ['stdout', 'stderr']
129 channel : str, one of ['stdout', 'stderr']
129 """
130 """
130 if channel not in ['stdout', 'stderr']:
131 if channel not in ['stdout', 'stderr']:
131 raise ValueError('Invalid channel spec %s' % channel)
132 raise ValueError('Invalid channel spec %s' % channel)
132
133
133 if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'):
134 if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'):
134 self.file = file_or_name
135 self.file = file_or_name
135 else:
136 else:
136 self.file = open(file_or_name, mode)
137 self.file = open(file_or_name, mode)
137 self.channel = channel
138 self.channel = channel
138 self.ostream = getattr(sys, channel)
139 self.ostream = getattr(sys, channel)
139 setattr(sys, channel, self)
140 setattr(sys, channel, self)
140 self._closed = False
141 self._closed = False
141
142
142 def close(self):
143 def close(self):
143 """Close the file and restore the channel."""
144 """Close the file and restore the channel."""
144 self.flush()
145 self.flush()
145 setattr(sys, self.channel, self.ostream)
146 setattr(sys, self.channel, self.ostream)
146 self.file.close()
147 self.file.close()
147 self._closed = True
148 self._closed = True
148
149
149 def write(self, data):
150 def write(self, data):
150 """Write data to both channels."""
151 """Write data to both channels."""
151 self.file.write(data)
152 self.file.write(data)
152 self.ostream.write(data)
153 self.ostream.write(data)
153 self.ostream.flush()
154 self.ostream.flush()
154
155
155 def flush(self):
156 def flush(self):
156 """Flush both channels."""
157 """Flush both channels."""
157 self.file.flush()
158 self.file.flush()
158 self.ostream.flush()
159 self.ostream.flush()
159
160
160 def __del__(self):
161 def __del__(self):
161 if not self._closed:
162 if not self._closed:
162 self.close()
163 self.close()
163
164
164
165
165 def ask_yes_no(prompt, default=None, interrupt=None):
166 def ask_yes_no(prompt, default=None, interrupt=None):
166 """Asks a question and returns a boolean (y/n) answer.
167 """Asks a question and returns a boolean (y/n) answer.
167
168
168 If default is given (one of 'y','n'), it is used if the user input is
169 If default is given (one of 'y','n'), it is used if the user input is
169 empty. If interrupt is given (one of 'y','n'), it is used if the user
170 empty. If interrupt is given (one of 'y','n'), it is used if the user
170 presses Ctrl-C. Otherwise the question is repeated until an answer is
171 presses Ctrl-C. Otherwise the question is repeated until an answer is
171 given.
172 given.
172
173
173 An EOF is treated as the default answer. If there is no default, an
174 An EOF is treated as the default answer. If there is no default, an
174 exception is raised to prevent infinite loops.
175 exception is raised to prevent infinite loops.
175
176
176 Valid answers are: y/yes/n/no (match is not case sensitive)."""
177 Valid answers are: y/yes/n/no (match is not case sensitive)."""
177
178
178 answers = {'y':True,'n':False,'yes':True,'no':False}
179 answers = {'y':True,'n':False,'yes':True,'no':False}
179 ans = None
180 ans = None
180 while ans not in answers.keys():
181 while ans not in answers.keys():
181 try:
182 try:
182 ans = input(prompt+' ').lower()
183 ans = input(prompt+' ').lower()
183 if not ans: # response was an empty string
184 if not ans: # response was an empty string
184 ans = default
185 ans = default
185 except KeyboardInterrupt:
186 except KeyboardInterrupt:
186 if interrupt:
187 if interrupt:
187 ans = interrupt
188 ans = interrupt
188 except EOFError:
189 except EOFError:
189 if default in answers.keys():
190 if default in answers.keys():
190 ans = default
191 ans = default
191 print()
192 print()
192 else:
193 else:
193 raise
194 raise
194
195
195 return answers[ans]
196 return answers[ans]
196
197
197
198
198 def temp_pyfile(src, ext='.py'):
199 def temp_pyfile(src, ext='.py'):
199 """Make a temporary python file, return filename and filehandle.
200 """Make a temporary python file, return filename and filehandle.
200
201
201 Parameters
202 Parameters
202 ----------
203 ----------
203 src : string or list of strings (no need for ending newlines if list)
204 src : string or list of strings (no need for ending newlines if list)
204 Source code to be written to the file.
205 Source code to be written to the file.
205
206
206 ext : optional, string
207 ext : optional, string
207 Extension for the generated file.
208 Extension for the generated file.
208
209
209 Returns
210 Returns
210 -------
211 -------
211 (filename, open filehandle)
212 (filename, open filehandle)
212 It is the caller's responsibility to close the open file and unlink it.
213 It is the caller's responsibility to close the open file and unlink it.
213 """
214 """
214 fname = tempfile.mkstemp(ext)[1]
215 fname = tempfile.mkstemp(ext)[1]
215 f = open(fname,'w')
216 f = open(fname,'w')
216 f.write(src)
217 f.write(src)
217 f.flush()
218 f.flush()
218 return fname, f
219 return fname, f
219
220
221 @contextmanager
222 def atomic_writing(path, mode='w', encoding='utf-8', **kwargs):
223 tmp_file = path + '.tmp-write'
224 if 'b' in mode:
225 encoding = None
226
227 with open(tmp_file, mode, encoding=encoding, **kwargs) as f:
228 yield f
229
230 # Written successfully, now rename it
231
232 if os.name == 'nt' and os.path.exists(path):
233 # Rename over existing file doesn't work on Windows
234 os.remove(path)
235
236 os.rename(tmp_file, path)
237
220
238
221 def raw_print(*args, **kw):
239 def raw_print(*args, **kw):
222 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
240 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
223
241
224 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
242 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
225 file=sys.__stdout__)
243 file=sys.__stdout__)
226 sys.__stdout__.flush()
244 sys.__stdout__.flush()
227
245
228
246
229 def raw_print_err(*args, **kw):
247 def raw_print_err(*args, **kw):
230 """Raw print to sys.__stderr__, otherwise identical interface to print()."""
248 """Raw print to sys.__stderr__, otherwise identical interface to print()."""
231
249
232 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
250 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
233 file=sys.__stderr__)
251 file=sys.__stderr__)
234 sys.__stderr__.flush()
252 sys.__stderr__.flush()
235
253
236
254
237 # Short aliases for quick debugging, do NOT use these in production code.
255 # Short aliases for quick debugging, do NOT use these in production code.
238 rprint = raw_print
256 rprint = raw_print
239 rprinte = raw_print_err
257 rprinte = raw_print_err
240
258
241 def unicode_std_stream(stream='stdout'):
259 def unicode_std_stream(stream='stdout'):
242 u"""Get a wrapper to write unicode to stdout/stderr as UTF-8.
260 u"""Get a wrapper to write unicode to stdout/stderr as UTF-8.
243
261
244 This ignores environment variables and default encodings, to reliably write
262 This ignores environment variables and default encodings, to reliably write
245 unicode to stdout or stderr.
263 unicode to stdout or stderr.
246
264
247 ::
265 ::
248
266
249 unicode_std_stream().write(u'ł@e¶ŧ←')
267 unicode_std_stream().write(u'ł@e¶ŧ←')
250 """
268 """
251 assert stream in ('stdout', 'stderr')
269 assert stream in ('stdout', 'stderr')
252 stream = getattr(sys, stream)
270 stream = getattr(sys, stream)
253 if PY3:
271 if PY3:
254 try:
272 try:
255 stream_b = stream.buffer
273 stream_b = stream.buffer
256 except AttributeError:
274 except AttributeError:
257 # sys.stdout has been replaced - use it directly
275 # sys.stdout has been replaced - use it directly
258 return stream
276 return stream
259 else:
277 else:
260 stream_b = stream
278 stream_b = stream
261
279
262 return codecs.getwriter('utf-8')(stream_b)
280 return codecs.getwriter('utf-8')(stream_b)
General Comments 0
You need to be logged in to leave comments. Login now