io.py
345 lines
| 10.7 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2498 | # encoding: utf-8 | ||
""" | ||||
IO related utilities. | ||||
""" | ||||
#----------------------------------------------------------------------------- | ||||
Matthias BUSSONNIER
|
r5390 | # Copyright (C) 2008-2011 The IPython Development Team | ||
Brian Granger
|
r2498 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r2856 | from __future__ import print_function | ||
Thomas Kluyver
|
r13690 | from __future__ import absolute_import | ||
Brian Granger
|
r2498 | |||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Thomas Kluyver
|
r13690 | import codecs | ||
Thomas Kluyver
|
r17557 | from contextlib import contextmanager | ||
Thomas Kluyver
|
r17570 | import io | ||
Brandon Parsons
|
r6653 | import os | ||
Thomas Kluyver
|
r17831 | import shutil | ||
import stat | ||||
Brian Granger
|
r2498 | import sys | ||
import tempfile | ||||
MinRK
|
r12223 | from .capture import CapturedIO, capture_output | ||
Thomas Kluyver
|
r13690 | from .py3compat import string_types, input, PY3 | ||
Brian Granger
|
r2498 | |||
#----------------------------------------------------------------------------- | ||||
# Code | ||||
#----------------------------------------------------------------------------- | ||||
class IOStream: | ||||
MinRK
|
r3800 | def __init__(self,stream, fallback=None): | ||
Brian Granger
|
r2498 | if not hasattr(stream,'write') or not hasattr(stream,'flush'): | ||
MinRK
|
r3800 | if fallback is not None: | ||
stream = fallback | ||||
else: | ||||
raise ValueError("fallback required, but not specified") | ||||
Brian Granger
|
r2498 | self.stream = stream | ||
self._swrite = stream.write | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3800 | # clone all methods not overridden: | ||
def clone(meth): | ||||
return not hasattr(self, meth) and not meth.startswith('_') | ||||
for meth in filter(clone, dir(stream)): | ||||
setattr(self, meth, getattr(stream, meth)) | ||||
Bernardo B. Marques
|
r4872 | |||
Thomas Kluyver
|
r13528 | def __repr__(self): | ||
cls = self.__class__ | ||||
tpl = '{mod}.{cls}({args})' | ||||
return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream) | ||||
Brian Granger
|
r2498 | def write(self,data): | ||
try: | ||||
self._swrite(data) | ||||
except: | ||||
try: | ||||
# print handles some unicode issues which may trip a plain | ||||
Fernando Perez
|
r2856 | # write() call. Emulate write() by using an empty end | ||
# argument. | ||||
print(data, end='', file=self.stream) | ||||
Brian Granger
|
r2498 | except: | ||
# if we get here, something is seriously broken. | ||||
Fernando Perez
|
r2856 | print('ERROR - failed to write data to stream:', self.stream, | ||
file=sys.stderr) | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3800 | def writelines(self, lines): | ||
Thomas Kluyver
|
r13353 | if isinstance(lines, string_types): | ||
MinRK
|
r3800 | lines = [lines] | ||
for line in lines: | ||||
self.write(line) | ||||
Brian Granger
|
r2498 | |||
Brian Granger
|
r2504 | # This class used to have a writeln method, but regular files and streams | ||
# in Python don't have this method. We need to keep this completely | ||||
# compatible so we removed it. | ||||
MinRK
|
r3800 | @property | ||
def closed(self): | ||||
return self.stream.closed | ||||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2498 | def close(self): | ||
pass | ||||
Brandon Parsons
|
r6652 | # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr | ||
Doug Blank
|
r15154 | devnull = open(os.devnull, 'w') | ||
Brandon Parsons
|
r6652 | stdin = IOStream(sys.stdin, fallback=devnull) | ||
stdout = IOStream(sys.stdout, fallback=devnull) | ||||
stderr = IOStream(sys.stderr, fallback=devnull) | ||||
Brian Granger
|
r2498 | |||
class IOTerm: | ||||
""" Term holds the file or file-like objects for handling I/O operations. | ||||
These are normally just sys.stdin, sys.stdout and sys.stderr but for | ||||
Windows they can can replaced to allow editing the strings before they are | ||||
displayed.""" | ||||
# In the future, having IPython channel all its I/O operations through | ||||
# this class will make it easier to embed it into other environments which | ||||
# are not a normal terminal (such as a GUI-based shell) | ||||
MinRK
|
r3800 | def __init__(self, stdin=None, stdout=None, stderr=None): | ||
Brandon Parsons
|
r6652 | mymodule = sys.modules[__name__] | ||
self.stdin = IOStream(stdin, mymodule.stdin) | ||||
self.stdout = IOStream(stdout, mymodule.stdout) | ||||
self.stderr = IOStream(stderr, mymodule.stderr) | ||||
Brian Granger
|
r2498 | |||
class Tee(object): | ||||
"""A class to duplicate an output stream to stdout/err. | ||||
This works in a manner very similar to the Unix 'tee' command. | ||||
When the object is closed or deleted, it closes the original file given to | ||||
it for duplication. | ||||
""" | ||||
# Inspired by: | ||||
# http://mail.python.org/pipermail/python-list/2007-May/442737.html | ||||
Thomas Kluyver
|
r4763 | def __init__(self, file_or_name, mode="w", channel='stdout'): | ||
Brian Granger
|
r2498 | """Construct a new Tee object. | ||
Parameters | ||||
---------- | ||||
file_or_name : filename or open filehandle (writable) | ||||
File that will be duplicated | ||||
mode : optional, valid mode for open(). | ||||
If a filename was give, open with this mode. | ||||
Bernardo B. Marques
|
r4872 | channel : str, one of ['stdout', 'stderr'] | ||
Brian Granger
|
r2498 | """ | ||
if channel not in ['stdout', 'stderr']: | ||||
raise ValueError('Invalid channel spec %s' % channel) | ||||
Bernardo B. Marques
|
r4872 | |||
Thomas Kluyver
|
r4763 | if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'): | ||
Brian Granger
|
r2498 | self.file = file_or_name | ||
else: | ||||
self.file = open(file_or_name, mode) | ||||
self.channel = channel | ||||
self.ostream = getattr(sys, channel) | ||||
setattr(sys, channel, self) | ||||
self._closed = False | ||||
def close(self): | ||||
"""Close the file and restore the channel.""" | ||||
self.flush() | ||||
setattr(sys, self.channel, self.ostream) | ||||
self.file.close() | ||||
self._closed = True | ||||
def write(self, data): | ||||
"""Write data to both channels.""" | ||||
self.file.write(data) | ||||
self.ostream.write(data) | ||||
self.ostream.flush() | ||||
def flush(self): | ||||
"""Flush both channels.""" | ||||
self.file.flush() | ||||
self.ostream.flush() | ||||
def __del__(self): | ||||
if not self._closed: | ||||
self.close() | ||||
Paul Ivanov
|
r13609 | def ask_yes_no(prompt, default=None, interrupt=None): | ||
Brian Granger
|
r2498 | """Asks a question and returns a boolean (y/n) answer. | ||
If default is given (one of 'y','n'), it is used if the user input is | ||||
Paul Ivanov
|
r13609 | empty. If interrupt is given (one of 'y','n'), it is used if the user | ||
presses Ctrl-C. Otherwise the question is repeated until an answer is | ||||
given. | ||||
Brian Granger
|
r2498 | |||
An EOF is treated as the default answer. If there is no default, an | ||||
exception is raised to prevent infinite loops. | ||||
Valid answers are: y/yes/n/no (match is not case sensitive).""" | ||||
answers = {'y':True,'n':False,'yes':True,'no':False} | ||||
ans = None | ||||
while ans not in answers.keys(): | ||||
try: | ||||
Thomas Kluyver
|
r13355 | ans = input(prompt+' ').lower() | ||
Brian Granger
|
r2498 | if not ans: # response was an empty string | ||
ans = default | ||||
except KeyboardInterrupt: | ||||
Paul Ivanov
|
r13609 | if interrupt: | ||
ans = interrupt | ||||
Brian Granger
|
r2498 | except EOFError: | ||
if default in answers.keys(): | ||||
ans = default | ||||
Thomas Spura
|
r4430 | print() | ||
Brian Granger
|
r2498 | else: | ||
raise | ||||
return answers[ans] | ||||
def temp_pyfile(src, ext='.py'): | ||||
"""Make a temporary python file, return filename and filehandle. | ||||
Parameters | ||||
---------- | ||||
src : string or list of strings (no need for ending newlines if list) | ||||
Source code to be written to the file. | ||||
ext : optional, string | ||||
Extension for the generated file. | ||||
Returns | ||||
------- | ||||
(filename, open filehandle) | ||||
It is the caller's responsibility to close the open file and unlink it. | ||||
""" | ||||
fname = tempfile.mkstemp(ext)[1] | ||||
f = open(fname,'w') | ||||
f.write(src) | ||||
f.flush() | ||||
return fname, f | ||||
Thomas Kluyver
|
r17831 | def _copy_metadata(src, dst): | ||
"""Copy the set of metadata we want for atomic_writing. | ||||
Permission bits and flags. We'd like to copy file ownership as well, but we | ||||
can't do that. | ||||
""" | ||||
shutil.copymode(src, dst) | ||||
st = os.stat(src) | ||||
if hasattr(os, 'chflags') and hasattr(st, 'st_flags'): | ||||
Thomas Kluyver
|
r17920 | os.chflags(dst, st.st_flags) | ||
Thomas Kluyver
|
r17831 | |||
Thomas Kluyver
|
r17557 | @contextmanager | ||
Thomas Kluyver
|
r17570 | def atomic_writing(path, text=True, encoding='utf-8', **kwargs): | ||
"""Context manager to write to a file only if the entire write is successful. | ||||
This works by creating a temporary file in the same directory, and renaming | ||||
Thomas Kluyver
|
r18098 | it over the old file if the context is exited without an error. If other | ||
file names are hard linked to the target file, this relationship will not be | ||||
preserved. | ||||
Thomas Kluyver
|
r17570 | |||
On Windows, there is a small chink in the atomicity: the target file is | ||||
deleted before renaming the temporary file over it. This appears to be | ||||
unavoidable. | ||||
Parameters | ||||
---------- | ||||
path : str | ||||
The target file to write to. | ||||
text : bool, optional | ||||
Whether to open the file in text mode (i.e. to write unicode). Default is | ||||
True. | ||||
encoding : str, optional | ||||
The encoding to use for files opened in text mode. Default is UTF-8. | ||||
**kwargs | ||||
Passed to :func:`io.open`. | ||||
""" | ||||
Thomas Kluyver
|
r18075 | # realpath doesn't work on Windows: http://bugs.python.org/issue9949 | ||
# Luckily, we only need to resolve the file itself being a symlink, not | ||||
# any of its directories, so this will suffice: | ||||
if os.path.islink(path): | ||||
path = os.path.join(os.path.dirname(path), os.readlink(path)) | ||||
Thomas Kluyver
|
r17570 | dirname, basename = os.path.split(path) | ||
handle, tmp_path = tempfile.mkstemp(prefix=basename, dir=dirname, text=text) | ||||
if text: | ||||
fileobj = io.open(handle, 'w', encoding=encoding, **kwargs) | ||||
else: | ||||
fileobj = io.open(handle, 'wb', **kwargs) | ||||
try: | ||||
yield fileobj | ||||
except: | ||||
fileobj.close() | ||||
os.remove(tmp_path) | ||||
raise | ||||
Thomas Kluyver
|
r17595 | # Flush to disk | ||
fileobj.flush() | ||||
os.fsync(fileobj.fileno()) | ||||
# Written successfully, now rename it | ||||
fileobj.close() | ||||
Thomas Kluyver
|
r17831 | # Copy permission bits, access time, etc. | ||
try: | ||||
_copy_metadata(path, tmp_path) | ||||
except OSError: | ||||
# e.g. the file didn't already exist. Ignore any failure to copy metadata | ||||
pass | ||||
Thomas Kluyver
|
r17595 | if os.name == 'nt' and os.path.exists(path): | ||
# Rename over existing file doesn't work on Windows | ||||
os.remove(path) | ||||
Thomas Kluyver
|
r17570 | |||
Thomas Kluyver
|
r17595 | os.rename(tmp_path, path) | ||
Thomas Kluyver
|
r17557 | |||
Brian Granger
|
r2498 | |||
Fernando Perez
|
r2874 | def raw_print(*args, **kw): | ||
"""Raw print to sys.__stdout__, otherwise identical interface to print().""" | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r2856 | print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'), | ||
file=sys.__stdout__) | ||||
sys.__stdout__.flush() | ||||
Fernando Perez
|
r2874 | def raw_print_err(*args, **kw): | ||
"""Raw print to sys.__stderr__, otherwise identical interface to print().""" | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r2856 | print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'), | ||
file=sys.__stderr__) | ||||
Fernando Perez
|
r2838 | sys.__stderr__.flush() | ||
Fernando Perez
|
r2874 | |||
# Short aliases for quick debugging, do NOT use these in production code. | ||||
rprint = raw_print | ||||
rprinte = raw_print_err | ||||
Thomas Kluyver
|
r13690 | |||
def unicode_std_stream(stream='stdout'): | ||||
Thomas Kluyver
|
r13802 | u"""Get a wrapper to write unicode to stdout/stderr as UTF-8. | ||
Thomas Kluyver
|
r13690 | |||
This ignores environment variables and default encodings, to reliably write | ||||
unicode to stdout or stderr. | ||||
:: | ||||
unicode_std_stream().write(u'ł@e¶ŧ←') | ||||
""" | ||||
assert stream in ('stdout', 'stderr') | ||||
stream = getattr(sys, stream) | ||||
if PY3: | ||||
try: | ||||
stream_b = stream.buffer | ||||
except AttributeError: | ||||
# sys.stdout has been replaced - use it directly | ||||
return stream | ||||
else: | ||||
stream_b = stream | ||||
return codecs.getwriter('utf-8')(stream_b) | ||||